Skip to content

Commit 655c71c

Browse files
committed
Shared connection support + test coverage
Port of 1edffdd
1 parent f1428b9 commit 655c71c

16 files changed

Lines changed: 1106 additions & 38 deletions

smallrye-reactive-messaging-rabbitmq-og/src/main/java/io/smallrye/reactive/messaging/rabbitmq/og/ConnectionHolder.java

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
import java.io.IOException;
77
import java.time.Duration;
8+
import java.util.Set;
9+
import java.util.concurrent.ConcurrentHashMap;
810
import java.util.concurrent.TimeoutException;
911
import java.util.concurrent.atomic.AtomicBoolean;
1012
import java.util.function.Consumer;
@@ -25,11 +27,13 @@ public class ConnectionHolder {
2527

2628
private final ConnectionFactory factory;
2729
private final String channelName;
30+
private final String connectionName;
2831
private final Vertx vertx;
2932
private final Context context;
3033
private final AtomicBoolean connected = new AtomicBoolean(false);
3134
private final int reconnectAttempts;
3235
private final int reconnectInterval;
36+
private final Set<String> channels = ConcurrentHashMap.newKeySet();
3337

3438
private volatile Connection connection;
3539
private Consumer<Connection> onConnectionEstablished;
@@ -38,22 +42,22 @@ public ConnectionHolder(
3842
ConnectionFactory factory,
3943
String channelName,
4044
io.vertx.mutiny.core.Vertx mutinyVertx) {
41-
this(factory, channelName, mutinyVertx, 0, 10);
45+
this(factory, channelName, channelName, mutinyVertx, 0, 10);
4246
}
4347

4448
public ConnectionHolder(
4549
ConnectionFactory factory,
4650
String channelName,
51+
String connectionName,
4752
io.vertx.mutiny.core.Vertx mutinyVertx,
4853
int reconnectAttempts,
4954
int reconnectInterval) {
5055
this.factory = factory;
5156
this.channelName = channelName;
57+
this.connectionName = connectionName;
5258
this.vertx = mutinyVertx.getDelegate();
5359
this.reconnectAttempts = reconnectAttempts;
5460
this.reconnectInterval = reconnectInterval;
55-
// We need event loop context for each connection, so the duplicated context will be running on different event
56-
// loop threads, but that's fine as long as we use the same context for all operations related to this connection
5761
this.context = ((VertxInternal) vertx).createEventLoopContext();
5862
}
5963

@@ -74,7 +78,7 @@ public Uni<Connection> connect() {
7478
try {
7579
log.establishingConnection(channelName);
7680

77-
Connection conn = factory.newConnection();
81+
Connection conn = factory.newConnection(connectionName);
7882

7983
// Set connection before recovery listeners so createChannel() works in recovery callbacks
8084
connection = conn;
@@ -182,4 +186,18 @@ public void close() {
182186
}
183187
}
184188
}
189+
190+
public Set<String> channels() {
191+
return channels;
192+
}
193+
194+
public ConnectionHolder retain(String channel) {
195+
channels.add(channel);
196+
return this;
197+
}
198+
199+
public boolean release(String channel) {
200+
channels.remove(channel);
201+
return channels.isEmpty();
202+
}
185203
}

smallrye-reactive-messaging-rabbitmq-og/src/main/java/io/smallrye/reactive/messaging/rabbitmq/og/IncomingRabbitMQMessage.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,10 @@ public IncomingRabbitMQMetadata getRabbitMQMetadata() {
137137
return rabbitMQMetadata;
138138
}
139139

140+
public java.util.Optional<String> getCorrelationId() {
141+
return java.util.Optional.ofNullable(rabbitMQMetadata.getCorrelationId());
142+
}
143+
140144
@Override
141145
public Supplier<CompletionStage<Void>> getAck() {
142146
return () -> ackHandler.handle(this);

smallrye-reactive-messaging-rabbitmq-og/src/main/java/io/smallrye/reactive/messaging/rabbitmq/og/RabbitMQConnector.java

Lines changed: 47 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
import static io.smallrye.reactive.messaging.annotations.ConnectorAttribute.Direction.INCOMING;
44
import static io.smallrye.reactive.messaging.annotations.ConnectorAttribute.Direction.INCOMING_AND_OUTGOING;
55
import static io.smallrye.reactive.messaging.annotations.ConnectorAttribute.Direction.OUTGOING;
6+
import static io.smallrye.reactive.messaging.rabbitmq.og.i18n.RabbitMQExceptions.ex;
67

78
import java.util.List;
89
import java.util.Map;
10+
import java.util.concurrent.ConcurrentHashMap;
911
import java.util.concurrent.CopyOnWriteArrayList;
1012
import java.util.concurrent.Flow;
1113

@@ -33,6 +35,7 @@
3335
import io.smallrye.reactive.messaging.health.HealthReport;
3436
import io.smallrye.reactive.messaging.health.HealthReporter;
3537
import io.smallrye.reactive.messaging.providers.connectors.ExecutionHolder;
38+
import io.smallrye.reactive.messaging.rabbitmq.og.internals.RabbitMQClientHelper;
3639
import io.vertx.core.Vertx;
3740

3841
@ApplicationScoped
@@ -57,6 +60,7 @@
5760
@ConnectorAttribute(name = "reconnect-interval", direction = INCOMING_AND_OUTGOING, description = "The interval (in seconds) between two reconnection attempts", type = "int", alias = "rabbitmq-reconnect-interval", defaultValue = "10")
5861
@ConnectorAttribute(name = "network-recovery-interval", direction = INCOMING_AND_OUTGOING, description = "How long (ms) will automatic recovery wait before attempting to reconnect", type = "int", defaultValue = "5000")
5962
@ConnectorAttribute(name = "user", direction = INCOMING_AND_OUTGOING, description = "The user name to use when connecting to the broker", type = "string", defaultValue = "guest")
63+
@ConnectorAttribute(name = "shared-connection-name", direction = INCOMING_AND_OUTGOING, description = "Optional identifier allowing multiple channels to share the same RabbitMQ connection when set to the same value", type = "string")
6064
@ConnectorAttribute(name = "include-properties", direction = INCOMING_AND_OUTGOING, description = "Whether to include properties when a broker message is passed on the event bus", type = "boolean", defaultValue = "false")
6165
@ConnectorAttribute(name = "requested-channel-max", direction = INCOMING_AND_OUTGOING, description = "The initially requested maximum channel number", type = "int", defaultValue = "2047")
6266
@ConnectorAttribute(name = "requested-heartbeat", direction = INCOMING_AND_OUTGOING, description = "The initially requested heartbeat interval (seconds), zero for none", type = "int", defaultValue = "60")
@@ -161,6 +165,8 @@ public class RabbitMQConnector implements InboundConnector, OutboundConnector, H
161165

162166
private final List<io.smallrye.reactive.messaging.rabbitmq.og.internals.IncomingRabbitMQChannel> incomings = new CopyOnWriteArrayList<>();
163167
private final List<io.smallrye.reactive.messaging.rabbitmq.og.internals.OutgoingRabbitMQChannel> outgoings = new CopyOnWriteArrayList<>();
168+
private final Map<String, ConnectionHolder> connections = new ConcurrentHashMap<>();
169+
private final Map<String, String> connectionFingerprints = new ConcurrentHashMap<>();
164170

165171
RabbitMQConnector() {
166172
// used for proxies
@@ -170,13 +176,7 @@ public class RabbitMQConnector implements InboundConnector, OutboundConnector, H
170176
public Flow.Publisher<? extends Message<?>> getPublisher(Config config) {
171177
RabbitMQConnectorIncomingConfiguration ic = new RabbitMQConnectorIncomingConfiguration(config);
172178

173-
// Create ConnectionFactory
174-
ConnectionFactory factory = io.smallrye.reactive.messaging.rabbitmq.og.internals.RabbitMQClientHelper
175-
.createConnectionFactory(ic, connectionFactories, credentialsProviders, configCustomizers);
176-
177-
// Create ConnectionHolder
178-
ConnectionHolder holder = new ConnectionHolder(factory, ic.getChannel(),
179-
executionHolder.vertx(), ic.getReconnectAttempts(), ic.getReconnectInterval());
179+
ConnectionHolder holder = getOrCreateConnectionHolder(ic);
180180

181181
// Create IncomingRabbitMQChannel
182182
io.smallrye.reactive.messaging.rabbitmq.og.internals.IncomingRabbitMQChannel channel = new io.smallrye.reactive.messaging.rabbitmq.og.internals.IncomingRabbitMQChannel(
@@ -192,13 +192,7 @@ public Flow.Publisher<? extends Message<?>> getPublisher(Config config) {
192192
public Flow.Subscriber<? extends Message<?>> getSubscriber(Config config) {
193193
RabbitMQConnectorOutgoingConfiguration oc = new RabbitMQConnectorOutgoingConfiguration(config);
194194

195-
// Create ConnectionFactory
196-
ConnectionFactory factory = io.smallrye.reactive.messaging.rabbitmq.og.internals.RabbitMQClientHelper
197-
.createConnectionFactory(oc, connectionFactories, credentialsProviders, configCustomizers);
198-
199-
// Create ConnectionHolder
200-
ConnectionHolder holder = new ConnectionHolder(factory, oc.getChannel(),
201-
executionHolder.vertx(), oc.getReconnectAttempts(), oc.getReconnectInterval());
195+
ConnectionHolder holder = getOrCreateConnectionHolder(oc);
202196

203197
// Create OutgoingRabbitMQChannel
204198
io.smallrye.reactive.messaging.rabbitmq.og.internals.OutgoingRabbitMQChannel channel = new io.smallrye.reactive.messaging.rabbitmq.og.internals.OutgoingRabbitMQChannel(
@@ -297,6 +291,45 @@ public void terminate(
297291
}
298292
}
299293
outgoings.clear();
294+
295+
for (ConnectionHolder holder : connections.values()) {
296+
holder.close();
297+
}
298+
connections.clear();
299+
connectionFingerprints.clear();
300+
}
301+
302+
public ConnectionHolder getOrCreateConnectionHolder(RabbitMQConnectorCommonConfiguration config) {
303+
String channel = config.getChannel();
304+
ConnectionFactory factory = RabbitMQClientHelper
305+
.createConnectionFactory(config, connectionFactories, credentialsProviders, configCustomizers);
306+
String connectionName = RabbitMQClientHelper.resolveConnectionName(config);
307+
String fingerprint = RabbitMQClientHelper.computeConnectionFingerprint(factory);
308+
String existing = connectionFingerprints.putIfAbsent(connectionName, fingerprint);
309+
if (existing != null && !existing.equals(fingerprint)) {
310+
throw ex.illegalStateSharedConnectionConfigMismatch(connectionName);
311+
}
312+
return connections.compute(fingerprint,
313+
(key, current) -> {
314+
if (current == null) {
315+
current = new ConnectionHolder(factory, channel, connectionName,
316+
executionHolder.vertx(), config.getReconnectAttempts(), config.getReconnectInterval());
317+
}
318+
return current.retain(channel);
319+
});
320+
}
321+
322+
public void releaseClient(String channel) {
323+
for (var e : connections.entrySet()) {
324+
ConnectionHolder holder = e.getValue();
325+
if (holder.channels().contains(channel)) {
326+
if (connections.computeIfPresent(e.getKey(), (k, c) -> c.release(channel) ? null : c) == null) {
327+
connectionFingerprints.values().remove(e.getKey());
328+
holder.close();
329+
}
330+
return;
331+
}
332+
}
300333
}
301334

302335
public Vertx vertx() {

smallrye-reactive-messaging-rabbitmq-og/src/main/java/io/smallrye/reactive/messaging/rabbitmq/og/i18n/RabbitMQExceptions.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,7 @@ public interface RabbitMQExceptions {
5050

5151
@Message(id = 19012, value = "RabbitMQ channel is closed")
5252
IllegalStateException illegalStateChannelClosed();
53+
54+
@Message(id = 19013, value = "Shared connection '%s' has mismatched configuration; ensure all channels using the same shared-connection-name have identical connection settings")
55+
IllegalStateException illegalStateSharedConnectionConfigMismatch(String sharedConnectionName);
5356
}

smallrye-reactive-messaging-rabbitmq-og/src/main/java/io/smallrye/reactive/messaging/rabbitmq/og/internals/IncomingRabbitMQChannel.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import io.smallrye.reactive.messaging.rabbitmq.og.tracing.RabbitMQOpenTelemetryInstrumenter;
3838
import io.smallrye.reactive.messaging.rabbitmq.og.tracing.RabbitMQTrace;
3939
import io.vertx.core.Context;
40+
import io.vertx.core.internal.VertxInternal;
4041

4142
/**
4243
* Incoming RabbitMQ channel that consumes messages from a queue.
@@ -49,6 +50,7 @@ public class IncomingRabbitMQChannel {
4950
private final Instance<java.util.Map<String, ?>> configMaps;
5051
private final RabbitMQOpenTelemetryInstrumenter instrumenter;
5152

53+
private final Context incomingContext;
5254
private final AtomicBoolean subscribed = new AtomicBoolean(false);
5355
private final AtomicInteger outstandingMessages = new AtomicInteger(0);
5456
private final AtomicReference<Channel> channelRef = new AtomicReference<>();
@@ -65,10 +67,10 @@ public IncomingRabbitMQChannel(
6567
Instance<java.util.Map<String, ?>> configMaps,
6668
Instance<OpenTelemetry> openTelemetryInstance) {
6769

68-
System.out.println("Creating IncomingRabbitMQChannel for channel: " + configuration.getChannel());
6970
this.connectionHolder = connectionHolder;
7071
this.configuration = configuration;
7172
this.configMaps = configMaps;
73+
this.incomingContext = ((VertxInternal) connectionHolder.getVertx()).createEventLoopContext();
7274

7375
// Initialize tracing if enabled
7476
if (configuration.getTracingEnabled()) {
@@ -94,7 +96,7 @@ public Multi<? extends Message<?>> getStream() {
9496
private Multi<? extends Message<?>> createStream() {
9597
// Determine if broadcast mode is needed
9698
boolean broadcast = configuration.getBroadcast();
97-
Context context = connectionHolder.getContext();
99+
Context context = incomingContext;
98100

99101
Multi<Message<?>> messageStream;
100102

@@ -190,7 +192,7 @@ private void setupConsumerOnConnection(
190192
Channel channel = connectionHolder.createChannel();
191193
channelRef.set(channel);
192194

193-
Context context = connectionHolder.getContext();
195+
Context context = incomingContext;
194196

195197
// Set up QoS for backpressure (prefetch count)
196198
// Note: In auto-ack mode, messages are immediately acknowledged upon delivery,

smallrye-reactive-messaging-rabbitmq-og/src/main/java/io/smallrye/reactive/messaging/rabbitmq/og/internals/OutgoingRabbitMQChannel.java

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import io.smallrye.reactive.messaging.rabbitmq.og.tracing.RabbitMQOpenTelemetryInstrumenter;
3030
import io.smallrye.reactive.messaging.rabbitmq.og.tracing.RabbitMQTrace;
3131
import io.vertx.core.Context;
32+
import io.vertx.core.internal.VertxInternal;
3233

3334
/**
3435
* Outgoing RabbitMQ channel that publishes messages to an exchange.
@@ -45,8 +46,8 @@ public class OutgoingRabbitMQChannel implements Subscriber<Message<?>> {
4546
private final AtomicBoolean completed = new AtomicBoolean(false);
4647
private final AtomicLong inflightMessages = new AtomicLong(0);
4748

49+
private final Context outgoingContext;
4850
private Channel channel;
49-
private Context context;
5051
private Subscription subscription;
5152

5253
private final boolean publisherConfirms;
@@ -68,6 +69,7 @@ public OutgoingRabbitMQChannel(
6869
this.connectionHolder = connectionHolder;
6970
this.configuration = configuration;
7071
this.configMaps = configMaps;
72+
this.outgoingContext = ((VertxInternal) connectionHolder.getVertx()).createEventLoopContext();
7173

7274
// Initialize tracing if enabled
7375
if (configuration.getTracingEnabled()) {
@@ -92,7 +94,6 @@ private void initialize() {
9294
try {
9395
// Create channel for publishing
9496
channel = connectionHolder.createChannel();
95-
context = connectionHolder.getContext();
9697

9798
// Set up topology
9899
setupTopology();
@@ -128,11 +129,11 @@ private void setupTopology() throws IOException {
128129

129130
private void setupConfirmListeners() throws IOException {
130131
ConfirmCallback ackCallback = (sequenceNumber, multiple) -> {
131-
context.runOnContext(v -> handleConfirm(sequenceNumber, multiple, true));
132+
outgoingContext.runOnContext(v -> handleConfirm(sequenceNumber, multiple, true));
132133
};
133134

134135
ConfirmCallback nackCallback = (sequenceNumber, multiple) -> {
135-
context.runOnContext(v -> handleConfirm(sequenceNumber, multiple, false));
136+
outgoingContext.runOnContext(v -> handleConfirm(sequenceNumber, multiple, false));
136137
};
137138

138139
channel.addConfirmListener(ackCallback, nackCallback);
@@ -221,7 +222,7 @@ private Uni<Void> publishWithRetry(Message<?> message, int remainingAttempts) {
221222
throw new RuntimeException("Failed to publish message", e);
222223
}
223224
})
224-
.runSubscriptionOn(command -> context.runOnContext(x -> command.run()))
225+
.runSubscriptionOn(command -> outgoingContext.runOnContext(x -> command.run()))
225226
.onFailure().retry()
226227
.withBackOff(Duration.ofSeconds(retryInterval))
227228
.atMost(remainingAttempts);

smallrye-reactive-messaging-rabbitmq-og/src/main/java/io/smallrye/reactive/messaging/rabbitmq/og/internals/RabbitMQClientHelper.java

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@
88

99
import java.io.FileInputStream;
1010
import java.io.IOException;
11+
import java.nio.charset.StandardCharsets;
1112
import java.security.KeyManagementException;
1213
import java.security.KeyStore;
1314
import java.security.KeyStoreException;
15+
import java.security.MessageDigest;
1416
import java.security.NoSuchAlgorithmException;
1517
import java.security.cert.CertificateException;
1618
import java.time.Duration;
@@ -87,9 +89,7 @@ static ConnectionFactory getConnectionFactory(
8789
RabbitMQConnectorCommonConfiguration config,
8890
Instance<CredentialsProvider> credentialsProviders) {
8991

90-
String connectionName = String.format("%s (%s)",
91-
config.getChannel(),
92-
config instanceof RabbitMQConnectorIncomingConfiguration ? "Incoming" : "Outgoing");
92+
String connectionName = resolveConnectionName(config);
9393

9494
Address[] addresses = config.getAddresses()
9595
.map(Address::parseAddresses)
@@ -195,6 +195,42 @@ private static SSLContext createSSLContext(RabbitMQConnectorCommonConfiguration
195195
}
196196
}
197197

198+
public static String resolveConnectionName(RabbitMQConnectorCommonConfiguration config) {
199+
return config.getSharedConnectionName()
200+
.orElseGet(() -> String.format("%s (%s)",
201+
config.getChannel(),
202+
config instanceof RabbitMQConnectorIncomingConfiguration ? "Incoming" : "Outgoing"));
203+
}
204+
205+
public static String computeConnectionFingerprint(ConnectionFactory factory) {
206+
String raw = factory.getHost()
207+
+ ":" + factory.getPort()
208+
+ ":" + factory.getVirtualHost()
209+
+ ":" + factory.getUsername()
210+
+ ":" + factory.isAutomaticRecoveryEnabled()
211+
+ ":" + factory.getConnectionTimeout()
212+
+ ":" + factory.getHandshakeTimeout()
213+
+ ":" + factory.getRequestedChannelMax()
214+
+ ":" + factory.getRequestedHeartbeat()
215+
+ ":" + factory.getNetworkRecoveryInterval()
216+
+ ":" + factory.isSSL();
217+
return sha256(raw);
218+
}
219+
220+
private static String sha256(String value) {
221+
try {
222+
MessageDigest digest = MessageDigest.getInstance("SHA-256");
223+
byte[] hash = digest.digest(value.getBytes(StandardCharsets.UTF_8));
224+
StringBuilder hex = new StringBuilder(hash.length * 2);
225+
for (byte b : hash) {
226+
hex.append(String.format("%02x", b));
227+
}
228+
return hex.toString();
229+
} catch (NoSuchAlgorithmException e) {
230+
throw new IllegalStateException("Unable to compute SHA-256 hash", e);
231+
}
232+
}
233+
198234
public static String serverQueueName(String name) {
199235
if (name.equals("(server.auto)")) {
200236
return "";

0 commit comments

Comments
 (0)