99 "os"
1010 "os/exec"
1111 "strings"
12+ "sync"
1213 "testing"
1314 "time"
1415
@@ -361,20 +362,40 @@ func TestGoServiceReplyCorrelationAfterTimeout(t *testing.T) {
361362
362363 svc := & example_interfaces.AddTwoInts {}
363364
364- // Stall is keyed on request content, not call ordering: only req1 ({A:1})
365- // is delayed, so it outlasts call #1's short timeout regardless of how many
366- // times or in what order the callback fires. req2 ({A:7}) always replies
367- // immediately. Keying on a call counter would be order-fragile — a
368- // redelivered or retried query before call #1 would shift the stall onto
369- // the wrong request and flake the "expect timeout" assertion.
370- server , err := node .CreateServiceServer ("add_two_ints" ).
365+ // Use a service name unique to this test. The interop environment runs many
366+ // nodes — other tests in this file, and ROS 2 nodes — that expose the
367+ // conventional "add_two_ints" service on the shared domain. If this test
368+ // used that name, one of those other servers could answer call #1's query
369+ // immediately, so call #1 would get a reply (Sum=2) instead of timing out —
370+ // the actual cause of this test's historical flakiness. A distinct name
371+ // guarantees this test's server is the ONLY responder, so call #1's outcome
372+ // is controlled entirely by the release gate below.
373+ const svcName = "reply_corr_add_two_ints"
374+
375+ // req1's reply is gated on an explicit release, not a fixed-duration stall.
376+ // The server blocks the {A:1} request until the test releases it, which
377+ // happens only AFTER call #1 has already timed out — so no reply can exist
378+ // during call #1's window and it times out deterministically (a fixed
379+ // time.Sleep stall would instead depend on scheduling: call #1's timeout is a
380+ // recv_timeout on the reply channel, and a starved timeout thread could wake
381+ // only after a stalled reply had already landed and return it). req2 ({A:7})
382+ // is never gated and replies immediately.
383+ release := make (chan struct {})
384+ var releaseOnce sync.Once
385+ // releaseReply is idempotent and deferred, so the server goroutine blocked on
386+ // <-release can never wedge the test (and server.Close()) if an assertion
387+ // fails before the explicit release below.
388+ releaseReply := func () { releaseOnce .Do (func () { close (release ) }) }
389+ defer releaseReply ()
390+
391+ server , err := node .CreateServiceServer (svcName ).
371392 Build (svc , func (reqBytes []byte ) ([]byte , error ) {
372393 var req example_interfaces.AddTwoIntsRequest
373394 if err := req .DeserializeCDR (reqBytes ); err != nil {
374395 return nil , err
375396 }
376397 if req .A == 1 {
377- time . Sleep ( 500 * time . Millisecond )
398+ <- release
378399 }
379400 resp := & example_interfaces.AddTwoIntsResponse {Sum : req .A + req .B }
380401 return resp .SerializeCDR ()
@@ -384,7 +405,7 @@ func TestGoServiceReplyCorrelationAfterTimeout(t *testing.T) {
384405 }
385406 defer server .Close ()
386407
387- client , err := node .CreateServiceClient ("add_two_ints" ).Build (svc )
408+ client , err := node .CreateServiceClient (svcName ).Build (svc )
388409 if err != nil {
389410 t .Fatalf ("Failed to create service client: %v" , err )
390411 }
@@ -394,15 +415,20 @@ func TestGoServiceReplyCorrelationAfterTimeout(t *testing.T) {
394415 t .Fatalf ("service not ready: %v" , err )
395416 }
396417
397- // Call #1: short timeout, server stalls 500ms → must time out.
418+ // Call #1: this test's server (the only responder for svcName) is blocked on
419+ // <-release and cannot reply, so this times out no matter how the runner
420+ // schedules threads.
398421 req1 := & example_interfaces.AddTwoIntsRequest {A : 1 , B : 1 } // would be 2
399422 var resp1 example_interfaces.AddTwoIntsResponse
400- if err := hiroz .CallTypedWithTimeout (client , req1 , & resp1 , 200 * time .Millisecond ); err == nil {
423+ if err := hiroz .CallTypedWithTimeout (client , req1 , & resp1 , 500 * time .Millisecond ); err == nil {
401424 t .Fatalf ("call #1 expected a timeout, but it returned Sum=%d" , resp1 .Sum )
402425 }
403426
404- // Let call #1's late reply (Sum=2) arrive and sit in the shared channel.
405- time .Sleep (500 * time .Millisecond )
427+ // Release req1's reply now, after call #1 has timed out, so it arrives late.
428+ // Give it time to reach the client and (on the pre-fix shared-channel bug)
429+ // sit where call #2 would wrongly pick it up.
430+ releaseReply ()
431+ time .Sleep (200 * time .Millisecond )
406432
407433 // Call #2: distinct args, generous timeout. Must get its own reply (7+8=15),
408434 // not the stale reply from call #1 (2).
0 commit comments