3232import com .bloomberg .bmq .impl .infr .msg .StatusCategory ;
3333import com .bloomberg .bmq .impl .infr .net .ConnectionOptions ;
3434import com .bloomberg .bmq .impl .infr .proto .AuthenticationEventBuilder ;
35+ import com .bloomberg .bmq .impl .infr .proto .EventImpl ;
3536import com .bloomberg .bmq .impl .infr .proto .Protocol ;
3637import com .bloomberg .bmq .impl .infr .proto .RequestManager ;
3738import com .bloomberg .bmq .impl .infr .proto .SchemaEventBuilder ;
4748import java .lang .invoke .MethodHandles ;
4849import java .nio .ByteBuffer ;
4950import java .util .concurrent .CompletableFuture ;
51+ import java .util .concurrent .CountDownLatch ;
5052import java .util .concurrent .Executors ;
53+ import java .util .concurrent .LinkedBlockingQueue ;
5154import java .util .concurrent .ScheduledExecutorService ;
5255import java .util .concurrent .ScheduledFuture ;
5356import 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