Skip to content

Commit 906acbb

Browse files
authored
test(hiroz-go): make reply-correlation test deterministic (gate, not stall) (#258)
1 parent ffcc459 commit 906acbb

1 file changed

Lines changed: 39 additions & 13 deletions

File tree

crates/hiroz-go/interop_tests/service_test.go

Lines changed: 39 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
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

Comments
 (0)