Skip to content

feat(go): add context variants of blocking calls - #410

Draft
YuanYuYuan wants to merge 30 commits into
feat/c-go-group-choicefrom
feat/go-context-variants
Draft

YuanYuYuan wants to merge 30 commits into
feat/c-go-group-choicefrom
feat/go-context-variants

Conversation

@YuanYuYuan

@YuanYuYuan YuanYuYuan commented Oct 26, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

The Go binding gets context variants for service calls, service waits, goal sends and subscribers. It also fixes three Go documentation examples that name functions that do not exist. No error code value changes.

Important

This PR needs the fix that allocates pointer message types in the typed subscribers to merge first. It uses the newMessage helper that fix adds. This branch does not build without it.

Key Changes

  • CallTypedContext makes a service call that ends when the context ends. A context deadline sets the call timeout, up to the 10 second query timeout of the raw service client. A later deadline returns ErrTimeout after about 10 seconds.
  • ServiceClient.WaitForServiceContext waits for a server until the context ends.
  • ActionClient.SendGoalContext and GetTypedResultContext add context to goal send and result wait.
  • BuildWithTypedCallbackContext and SubscriberWithChannelContext bind a subscriber to a context. The second also closes its channel.
  • Each context call registers with the client before it reads the C handle. Close refuses new calls first and then waits for the registered ones.
  • The same gate covers GetResultWithContext and its wrappers GetResult, GetTypedResult and GetTypedResultContext. SendGoal and Cancel do not use it.
  • Close waits for the C call, not for the context. The C call of a result wait times out after 30 seconds. The C call of a goal send times out after 10 seconds. SendGoalContext also stays registered while it runs the goal's SerializeCDR. A slow serializer therefore holds Close for its own time as well. A serializer that calls Close deadlocks. A GetResult call without a deadline holds Close for at most 30 seconds.
  • Known limit, not new in this PR: GoalHandle.Close is not gated. Do not close a goal handle while a result wait on it is running.
  • SendGoalContext releases its registration when SerializeCDR panics, and sends nothing when the context ends while it serialises. A panic no longer leaves Close waiting.
  • Close ends a running WaitForServiceContext within one 250 ms slice.
  • SendGoalContext and CallTypedContext serialise the message before they start the worker and copy the bytes. The worker owns the copy. A caller can reuse or change the message after a cancelled call, even when its SerializeCDR returns storage that the message owns. The Message interface does not forbid that. The generated SerializeCDR returns a fresh buffer. SendGoal and the plain service calls are unchanged, because they send the bytes before they return.
  • WaitForServiceContext checks the context and Close again after each C wait. A server that appears while the context ends, or while Close starts, no longer gives success.
  • A C timeout that returns after the context deadline has passed gives context.DeadlineExceeded, not ErrTimeout.
  • When the context ends while a result is ready, the call returns ctx.Err(). select picks at random when both channels are ready, so SendGoalContext, GetResultWithContext and CallTypedContext check the context again in the result branch. SendGoalContext closes a handle that arrives after the context ended. CallTypedContext checks the context before it runs SerializeCDR.
  • SubscriberWithChannelContext waits for running callbacks before it closes the channel.
  • SubscriberWithChannelContext allocates the message with newMessage[T](). A pointer type such as *std_msgs.String has a nil zero value, and DeserializeCDR on it panics, so the subscriber dropped every message. BuildWithTypedCallbackContext gets the same fix through BuildWithTypedCallback.
  • docs/bindings/go.md drops client.Call, client.CallWithTimeout, BuildTypedActionClient and SendTypedGoal. None of them exists in the package.
  • docs/bindings/go.md gains a "Context variants" section.

Before and after

before after
service call with a context none CallTypedContext
service wait with a context none WaitForServiceContext
goal send with a context none SendGoalContext
subscriber bound to a context none two variants
context cancel on a service call not possible context.Canceled, not ErrTimeout
goal changed after a cancelled SendGoalContext the send could read the changed goal the send uses the goal as it was at the call
goal whose SerializeCDR returns its own storage, changed in place after a cancelled SendGoalContext the send could read the changed bytes the send reads its own copy
request whose SerializeCDR returns its own storage, changed in place after a cancelled CallTypedContext the call could read the changed bytes the call reads its own copy
WaitForServiceContext when the context ends during a C wait that finds the server returns success returns context.Canceled
WaitForServiceContext when Close starts during a C wait that finds the server returns success returns the closed-client error
context deadline on a service call not possible context.DeadlineExceeded, not ErrTimeout, for a deadline up to 10 seconds
context deadline later than 10 seconds not possible ErrTimeout after about 10 seconds, as the docs state
SerializeCDR panics in SendGoalContext not possible the registration is released and Close does not wait
server does not answer in time ErrTimeout ErrTimeout, unchanged
existing error code values -1 to -17, -100 unchanged
SubscriberWithChannelContext[*T] delivery no message arrives, because the receiver is nil the message arrives

GetResultWithContext exists in 0.2.1. Its registration with the client now happens before the handle read. A call that starts during Close returns an error and does not reach the C layer.

What fails without this

The tests ran with Go 1.26.4 against a debug build of the C library. The new tests use stubs of the C calls and order the goroutines with channels, not sleeps. Three were measured at 167b2677e, the commit that adds them, one at 63d0c88b4 and one at af6493983. Both ran on a scratch merge with the nil-receiver fix.

test head with the tests but without the fixes head with the fixes
TestCallTypedContextTimeoutAfterDeadlineIsDeadlineExceeded fails: returns ErrTimeout passes
TestSubscriberWithChannelContextLateCallbackIsSafe panics: send on closed channel, which stops the package run passes
TestSendGoalContextSerialisesBeforeReturn fails: the goal was not serialised when the call returned, and the send read the changed goal passes
TestSendGoalContextOwnsSerialisedBytes fails: the send read [9 2] where the goal was [1 2] at the call passes
TestCallTypedContextOwnsSerialisedBytes fails: the call read [9 2] where the request was [1 2] at the call passes
TestWaitForServiceContextCancelDuringWaitWinsOverReady fails: returns no error passes
TestWaitForServiceContextCloseDuringWaitWinsOverReady fails: returns no error passes
the other subscriber and SendGoalContext lifetime tests pass: they pin behaviour that already holds pass
TestCallTypedContextRefusedOnceCloseStarted, TestSendGoalContextRefusedOnceCloseStarted, TestCloseEndsWaitForServiceContext, TestWaitSliceMs not run: they name the new gate pass

TestFinishSendGoalEndedContextClosesHandle fails when finishSendGoal does not close the late handle. The other TestFinish* tests have no failing baseline. They call the helpers that hold the result-branch check, and select order is random, so no test fails reliably without the check. TestCallTypedContextCancelledSkipsSerializer does fail without the check before SerializeCDR.

The last row has no failing baseline. The gate tests do not compile against the old code. The old code lets a call register after Close has started waiting. That is a race between two goroutines, and the tests order it with channels, not with sleeps.

The delivery tests for pointer types are in subscriber_context_delivery_test.go. They use a hand-written message type that decodes into its receiver, with a raw-callback control. They were run on the new commit merged with the nil-receiver fix, because this branch alone does not build.

test new test commit merged with the fix, without the channel fix with the channel fix
TestCtxDeliveryRawCallbackControl passes passes
TestSubscriberWithChannelContextDeliversForPointerType fails: "no message received" passes
TestBuildWithTypedCallbackContextDeliversForPointerType passes: the nil-receiver fix already covers BuildWithTypedCallback passes

The third row has no failing baseline. It pins the delivery path of a function that this branch did not need to change.

Evidence

This branch alone does not compile without the newMessage helper from the nil-receiver fix. The rows below ran on a scratch merge of this branch with that fix. The merge has one textual conflict in the import block of subscriber_typed.go, which takes both import lists. The merge commit is not published.

All rows were measured with this branch at 9cc789a51 as the merge parent, and with the nil-receiver fix as the other merge parent.

what command result
go vet with the fix go vet ./hiroz/ clean
gofmt with the fix gofmt -l ./hiroz no file listed
go test -race with the fix go test -race -v -count=1 ./hiroz/ 123 passed, 0 failed, no data race
the context tests, 20 repeats, with the fix go test -race -count=20 -run 'TestFinish|CancelledSkipsSerializer|SendGoalContext|CallTypedContext' ./hiroz/ ok, 0 failed
TestFinishSendGoalEndedContextClosesHandle without the Close call in finishSendGoal go test -count=1 -run TestFinishSendGoal ./hiroz/ fails: the handle was not closed
TestCallTypedContextCancelledSkipsSerializer without the check before SerializeCDR go test -count=1 -run CancelledSkipsSerializer ./hiroz/ fails: the serializer ran for a cancelled call

Each row is one run on Go 1.26.4 with a debug build of the C library. The red result of TestSendGoalContextPanicInSerializeReleasesClient was measured at cd18bbded, the commit that adds it: that test failed. TestSendGoalContextEndedDuringSerializeSendsNothing passed before the fix and only pins the behaviour. The red result of TestSendGoalContextOwnsSerialisedBytes was measured at 63d0c88b4: that test failed and the other tests in the package passed. The red result of TestCallTypedContextOwnsSerialisedBytes was measured at af6493983: that test failed and the other tests in the package passed. The red results of the three earlier new tests were measured at 167b2677e. The other red results were measured at earlier commits of this branch. None of the red results is re-run at the final head. go test ./... also lists the example packages as failed to set up. Their generated message packages are absent in that tree. GOARCH=386 CGO_ENABLED=0 go vet does not apply, because the package needs cgo. Neither run is a result for this change, and neither was re-run at the final head. The documentation sentences about Close and its time bounds were checked by reading the code. No test checks the goal handle cleanup after Close.

Breaking Changes

None. The change adds functions and unexported fields. Existing signatures, error codes and error messages stay as they were. A call on a closed client keeps its error text.

A context call read the C handle before it registered with Close, so Close could free the handle first. Registration now happens first, under the same lock that Close uses to refuse new calls. Close also ends a running wait for a service, a past deadline no longer becomes a huge C timeout, and the channel variant waits for running callbacks before it closes the channel.
The stub variable is read by the call goroutine and written by test cleanup. The race detector reported it.
The race build checks pointer conversions. A one-byte block converted to the C subscriber type failed that check.
GetResult, GetTypedResult and GetTypedResultContext register with the gate through GetResultWithContext. Only SendGoal and Cancel do not. Document the goal handle cleanup after Close as a known limit.
The C call of a result wait times out after 30 seconds, so Close waits at most that long. Document that GoalHandle.Close is not gated.
@github-actions

github-actions Bot commented Oct 26, 2026 •

Copy link
Copy Markdown
PR Preview Action v1.8.1

QR code for preview link

🚀 View preview at
https://ZettaScaleLabs.github.io/hiroz/pr-preview/pr-410/

Built to branch gh-pages at 2026-10-30 20:35 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The prerequisite allocator is absent, and goal serialization and service waits retain cancellation-related correctness issues.

3 open issues
What changed in this PR

This PR adds context-aware operations to the Go bindings, allowing cancellation of service calls, action waits, and subscriber lifetimes while coordinating client shutdown.

Changes:

  • Adds context variants for services, actions, and typed subscribers.
  • Introduces an in-flight call gate and lifecycle regression tests.
  • Corrects documentation examples and explains cancellation and shutdown limits.
File Description
docs/​bindings/​go.md Corrects examples and documents context variants.
crates/​hiroz-go/​hiroz/​subscriber.go Adds a replaceable subscriber destruction hook for tests.
crates/​hiroz-go/​hiroz/​subscriber_typed.go Adds context-bound callback and channel subscribers.
crates/​hiroz-go/​hiroz/​subscriber_context_test.go Tests subscriber cancellation and channel shutdown.
crates/​hiroz-go/​hiroz/​subscriber_context_delivery_test.go Tests delivery with pointer message types.
crates/​hiroz-go/​hiroz/​service.go Adds context-aware calls, waits, and shutdown coordination.
crates/​hiroz-go/​hiroz/​service_typed.go Adds typed service calls with context support.
crates/​hiroz-go/​hiroz/​service_context_test.go Tests cancellation, deadlines, and service errors.
crates/​hiroz-go/​hiroz/​lifecycle_helpers_test.go Adds a bounded lifecycle polling helper.
crates/​hiroz-go/​hiroz/​inflight.go Adds registration and shutdown gating.
crates/​hiroz-go/​hiroz/​inflight_test.go Tests shutdown admission and service wait slices.
crates/​hiroz-go/​hiroz/​action.go Adds context-aware goal sending and gates result waits.
crates/​hiroz-go/​hiroz/​action_typed.go Adds typed result retrieval with context support.
crates/​hiroz-go/​hiroz/​action_context_test.go Tests goal cancellation and late-handle cleanup.

Review effort: Balanced

Comment thread crates/hiroz-go/hiroz/action.go Resolved
Comment thread crates/hiroz-go/hiroz/service.go Resolved

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Cancellation handling, panic-safe registration cleanup, and propagation of longer service deadlines remain unresolved.

3 open issues
2 previously missed issues

In code that hasn't changed since last review

Medium severity Propagate context timeout to the underlying Zenoh query

crates/​hiroz-go/​hiroz/​service.go:217

A 30-second context deadline cannot receive a response arriving after 15 seconds: the underlying querier has a fixed 10-second timeout (crates/hiroz/src/node.rs:827), so the call returns ErrTimeout early. RawServiceClient.call_raw applies the supplied timeout only to rx.recv_timeout, not to the query. Propagate the selected timeout to the Zenoh query as well so longer context deadlines work as documented.

Medium severity Honor cancellation before deserializing successful responses

crates/​hiroz-go/​hiroz/​service.go:243

If cancellation occurs before the worker publishes a successful response, both channels can be ready and select can choose the response. Because this context check only runs on errors, CallTypedContext can deserialize the response and return success instead of the documented context error. Check ctx.Err() regardless of r.err, and add coverage for cancellation followed by a successful C response.

2 resolved since last review

Review effort: Balanced

Comment thread crates/hiroz-go/hiroz/action.go Resolved
Comment thread crates/hiroz-go/hiroz/action.go Resolved

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Cancellation checks are incomplete, allowing serialization after cancellation and races that return successful responses or goal handles instead of context errors while skipping required handle cleanup.

4 open issues
1 resolved since last review

Review effort: Balanced

Comment thread crates/hiroz-go/hiroz/service.go Resolved
Comment thread crates/hiroz-go/hiroz/service_typed.go Resolved
select picks at random when ctx.Done and the result channel are both ready, so a call could return a result or a goal handle after its context ended. The result branch now checks ctx again. CallTypedContext checks ctx before it runs the serializer.
The handle in the ended-context test has no C handle, so a second Close returns nil whether or not the first ran. Read the close-once guard instead: it is spent only when finishSendGoal called Close. The live-context test checks the guard is unspent.
SendGoalContext stays registered while it runs the goal's SerializeCDR, so a slow serializer holds ActionClient.Close longer than the 10 second C timeout, and a serializer that calls Close deadlocks.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Human assessment is needed because an unpublished critical callback–teardown race can let consumers release resources before deserialization finishes, violating the promised callback drain.

No open issues 🎉

4 resolved since last review

Review effort: Balanced

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants