Skip to content

Commit 2544b55

Browse files
committed
Fix[TcpBrokerConnection]: desync of bmqEvents
Signed-off-by: Evgeny Malygin <emalygin@bloomberg.net>
1 parent d37b5df commit 2544b55

2 files changed

Lines changed: 125 additions & 13 deletions

File tree

bmq-sdk/src/main/java/com/bloomberg/bmq/impl/TcpBrokerConnection.java

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -287,15 +287,18 @@ private WriteStatus authenticate(AuthnCredential credential) throws IOException
287287

288288
@Override
289289
public void handleAuthenticationResponse() {
290-
if (onAuthenticationTimeoutFuture.isDone()) {
291-
logger.warn("Authentication timeout expired");
292-
return;
293-
}
290+
// Always drain the event that triggered this handler to keep 'bmqEvents'
291+
// in sync, even if the timeout already expired. Otherwise a stale event
292+
// would be left in the queue and misread by a later BlazingMQ event.
294293
EventImpl bmqEv = bmqEvents.poll();
295294
if (bmqEv == null) {
296295
logger.error("No BlazingMQ events");
297296
return;
298297
}
298+
if (onAuthenticationTimeoutFuture.isDone()) {
299+
logger.warn("Authentication timeout expired");
300+
return;
301+
}
299302
if (bmqEv.type() != EventType.AUTHENTICATION) {
300303
logger.error("Unexpected BlazingMQ event: {}", bmqEv);
301304
return;
@@ -495,15 +498,18 @@ public void doDisconnectChannel() {
495498

496499
@Override
497500
public void handleNegotiationResponse() {
498-
if (onNegotiationTimeoutFuture.isDone()) {
499-
logger.warn("Negotiation timeout expired");
500-
return;
501-
}
501+
// Always drain the event that triggered this handler to keep 'bmqEvents'
502+
// in sync, even if the timeout already expired. Otherwise a stale event
503+
// would be left in the queue and misread by a later BlazingMQ event.
502504
EventImpl bmqEv = bmqEvents.poll();
503505
if (bmqEv == null) {
504506
logger.error("No BlazingMQ events");
505507
return;
506508
}
509+
if (onNegotiationTimeoutFuture.isDone()) {
510+
logger.warn("Negotiation timeout expired");
511+
return;
512+
}
507513
if (bmqEv.type() != EventType.CONTROL) {
508514
logger.error("Unexpected BlazingMQ event: {}", bmqEv);
509515
return;

bmq-sdk/src/test/java/com/bloomberg/bmq/impl/TcpBrokerConnectionReauthTest.java

Lines changed: 111 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import com.bloomberg.bmq.impl.infr.msg.StatusCategory;
3333
import com.bloomberg.bmq.impl.infr.net.ConnectionOptions;
3434
import com.bloomberg.bmq.impl.infr.proto.AuthenticationEventBuilder;
35+
import com.bloomberg.bmq.impl.infr.proto.EventImpl;
3536
import com.bloomberg.bmq.impl.infr.proto.Protocol;
3637
import com.bloomberg.bmq.impl.infr.proto.RequestManager;
3738
import com.bloomberg.bmq.impl.infr.proto.SchemaEventBuilder;
@@ -47,7 +48,9 @@
4748
import java.lang.invoke.MethodHandles;
4849
import java.nio.ByteBuffer;
4950
import java.util.concurrent.CompletableFuture;
51+
import java.util.concurrent.CountDownLatch;
5052
import java.util.concurrent.Executors;
53+
import java.util.concurrent.LinkedBlockingQueue;
5154
import java.util.concurrent.ScheduledExecutorService;
5255
import java.util.concurrent.ScheduledFuture;
5356
import java.util.concurrent.TimeUnit;
@@ -133,10 +136,7 @@ private ByteBuffer[] buildAuthResponse(StatusCategory category, Integer lifetime
133136
return builder.build();
134137
}
135138

136-
private void sendNegotiationResponse(TestTcpConnection conn) throws IOException {
137-
ByteBuffer[] negoRequest = conn.nextWriteRequest();
138-
assertNotNull(negoRequest);
139-
139+
private ByteBuffer[] buildNegotiationResponse() throws IOException {
140140
NegotiationMessageChoice negMsg = new NegotiationMessageChoice();
141141
negMsg.makeBrokerResponse();
142142
BrokerResponse brokerResponse = negMsg.brokerResponse();
@@ -152,7 +152,13 @@ private void sendNegotiationResponse(TestTcpConnection conn) throws IOException
152152

153153
SchemaEventBuilder builder = new SchemaEventBuilder();
154154
builder.setMessage(negMsg);
155-
conn.sendResponse(builder.build());
155+
return builder.build();
156+
}
157+
158+
private void sendNegotiationResponse(TestTcpConnection conn) throws IOException {
159+
ByteBuffer[] negoRequest = conn.nextWriteRequest();
160+
assertNotNull(negoRequest);
161+
conn.sendResponse(buildNegotiationResponse());
156162
}
157163

158164
private CompletableFuture<StartStatus> startConnection(TcpBrokerConnection connection) {
@@ -400,4 +406,104 @@ void testReauthCancelledOnChannelDown() throws Exception {
400406

401407
assertTrue(reauthFuture.isCancelled());
402408
}
409+
410+
@SuppressWarnings("unchecked")
411+
private LinkedBlockingQueue<EventImpl> getBmqEvents(TcpBrokerConnection connection) {
412+
return (LinkedBlockingQueue<EventImpl>)
413+
TestHelpers.getInternalState(connection, "bmqEvents");
414+
}
415+
416+
// Parks the single scheduler thread inside a task until the returned latch is released.
417+
// While parked, tasks the SDK submits (e.g. BMQ_EVENT inputs from incoming frames) queue
418+
// up behind it without running. This lets a test deliver several frames through the real
419+
// read path and control exactly when the FSM processes them -- reproducing broker frames
420+
// that arrive back-to-back on the same scheduler tick.
421+
private CountDownLatch blockScheduler() throws InterruptedException {
422+
CountDownLatch release = new CountDownLatch(1);
423+
CountDownLatch parked = new CountDownLatch(1);
424+
scheduler.execute(
425+
() -> {
426+
parked.countDown();
427+
try {
428+
release.await(5, TimeUnit.SECONDS);
429+
} catch (InterruptedException e) {
430+
Thread.currentThread().interrupt();
431+
}
432+
});
433+
assertTrue(parked.await(5, TimeUnit.SECONDS), "scheduler did not park");
434+
return release;
435+
}
436+
437+
// End-to-end: two negotiation responses arriving back-to-back while the FSM is in
438+
// NEGOTIATING. The first drains its event and cancels the negotiation timeout (making the
439+
// future isDone()); the second BMQ_EVENT is then processed while still in NEGOTIATING with
440+
// the timeout already done. If that path early-returns without draining, the event is
441+
// stranded and 'bmqEvents' desyncs (off-by-one) -- later polls return the stale event.
442+
@Test
443+
void testNegotiationBurstDoesNotDesyncBmqEvents() throws Exception {
444+
for (int i = 0; i < 25; i++) {
445+
TcpBrokerConnection connection = createConnection(null);
446+
TestTcpConnection testConn = connectionFactory.getTestConnection();
447+
448+
CompletableFuture<StartStatus> startFuture = startConnection(connection);
449+
450+
// The negotiation request is written as the FSM enters NEGOTIATING, so its arrival
451+
// means the timeout is armed and the connection awaits a negotiation response.
452+
assertNotNull(testConn.nextWriteRequest(), "iteration " + i);
453+
454+
LinkedBlockingQueue<EventImpl> bmqEvents = getBmqEvents(connection);
455+
456+
CountDownLatch release = blockScheduler();
457+
testConn.sendResponse(buildNegotiationResponse());
458+
testConn.sendResponse(buildNegotiationResponse());
459+
// Both events reached the queue through the real read path, none processed yet.
460+
assertEquals(2, bmqEvents.size(), "iteration " + i);
461+
release.countDown();
462+
463+
// Reaching CONNECTED guarantees both BMQ_EVENT tasks (queued before the
464+
// NEGOTIATION_RESPONSE transition) have already run on the scheduler.
465+
assertEquals(
466+
StartStatus.SUCCESS, startFuture.get(5, TimeUnit.SECONDS), "iteration " + i);
467+
assertEquals(0, bmqEvents.size(), "bmqEvents desynced on iteration " + i);
468+
}
469+
}
470+
471+
// End-to-end counterpart for the AUTHENTICATING phase: two authentication responses arriving
472+
// back-to-back while the FSM is in AUTHENTICATING.
473+
@Test
474+
void testAuthenticationBurstDoesNotDesyncBmqEvents() throws Exception {
475+
AuthnCredentialCb cb =
476+
() ->
477+
AuthnCredential.builder()
478+
.setMechanism("OAUTH2")
479+
.setData("token".getBytes())
480+
.build();
481+
for (int i = 0; i < 25; i++) {
482+
TcpBrokerConnection connection = createConnection(cb);
483+
TestTcpConnection testConn = connectionFactory.getTestConnection();
484+
485+
CompletableFuture<StartStatus> startFuture = startConnection(connection);
486+
487+
// The authentication request is written as the FSM enters AUTHENTICATING.
488+
assertNotNull(testConn.nextWriteRequest(), "iteration " + i);
489+
490+
LinkedBlockingQueue<EventImpl> bmqEvents = getBmqEvents(connection);
491+
492+
CountDownLatch release = blockScheduler();
493+
testConn.sendResponse(buildAuthResponse(StatusCategory.E_SUCCESS, null));
494+
testConn.sendResponse(buildAuthResponse(StatusCategory.E_SUCCESS, null));
495+
assertEquals(2, bmqEvents.size(), "iteration " + i);
496+
release.countDown();
497+
498+
// The first auth response drives the FSM to NEGOTIATING, which writes the
499+
// negotiation request; its arrival means the second BMQ_EVENT has already run too.
500+
assertNotNull(testConn.nextWriteRequest(), "iteration " + i);
501+
assertEquals(0, bmqEvents.size(), "bmqEvents desynced on iteration " + i);
502+
503+
// Complete the handshake so the connection ends each iteration in a clean state.
504+
testConn.sendResponse(buildNegotiationResponse());
505+
assertEquals(
506+
StartStatus.SUCCESS, startFuture.get(5, TimeUnit.SECONDS), "iteration " + i);
507+
}
508+
}
403509
}

0 commit comments

Comments
 (0)