Repository navigation
feat(durable): send, list, and subscribe to durable messages - #343
Conversation
Bundles the hosting contract that adds durable messages and the execution client token, and regenerates the client. Co-authored-by: Cursor <cursoragent@cursor.com>
Adds client.durable.messages: send_message and list on the application and project routes, and subscribe, which follows an execution's realtime channel and reads back what it missed after every reconnect. Durable#start returns the client_token of the execution it creates. Co-authored-by: Cursor <cursoragent@cursor.com>
subnetmarco
left a comment
There was a problem hiding this comment.
Client-only, and the send/list/subscribe surface matches JS and Python, including the conflict codes. One issue: DurableMessageStream uses the same seq <= last_seq drop as the other SDKs, so a broadcast the server loses or delivers out of order is never delivered on an open subscription. The server-side fix is on #1826; once that payload change lands, this stream needs the matching catch-up.
A broadcast now carries prev_seq, the seq of the execution's previous publish. A subscription that has not delivered it reads the list before handing on the new message, so a lost or reordered broadcast is no longer skipped. A catch-up page that delivers nothing new also ends the read. Co-authored-by: Cursor <cursoragent@cursor.com>
|
Done in 0e82287, matching the server change on Kong/volcano-hosting#1826.
|
Co-authored-by: Cursor <cursoragent@cursor.com> # Conflicts: # lib/volcano/generated/.openapi-generator/FILES # lib/volcano/generated/README.md # lib/volcano/generated/lib/volcano-generated.rb # lib/volcano/generated/lib/volcano-generated/api/system_api.rb # lib/volcano/generated/lib/volcano-generated/models/capability.rb # openapi/openapi.yaml # spec/volcano_generation_spec.rb
AI review: no P0-P2 findingsAdds client.durable.messages (send_message, list, subscribe) on a new internal durable realtime channel type, exposes client_token on DurableExecution, and regenerates the OpenAPI client. The hand-written stream, catch-up, session-binding and realtime lifecycle code is carefully built and extensively tested, and no defects were found; the main risk is the larger change to shared realtime connection behavior and reliance on an unverified Hosting contract. Not checked: The AI review read the diff, the code, and the PR's text; it ran no tests or builds and read nothing outside this repository.
Human review: needed
The AI review found no P0-P2 problems at 6add532, so the code needs no second review for bugs. Decide the points above: approve to accept them, or request changes. Suggested reviewer: |
…ch-up A broadcast now carries the seq of the publish before it. Live messages are held while a catch-up reads, and behind any message still waiting on its predecessor, so a missed publish can no longer be skipped. A failed or short catch-up is retried with backoff from half a second to 30 seconds, messages the connection dropped for lack of room are read back, and removing or disconnecting the channel ends the subscription. Co-authored-by: Cursor <cursoragent@cursor.com>
…e catch-ups, and read off the reactor Co-authored-by: Cursor <cursoragent@cursor.com>
Bind each subscription to the session that opened it, as Postgres delivery does. Signing out, signing in again, or setting another session closes the stream before its next read or delivery and raises SessionChangedError; a token refresh keeps it. Co-authored-by: Cursor <cursoragent@cursor.com>
A typed call without one always raised ArgumentError at runtime. Co-authored-by: Cursor <cursoragent@cursor.com>
idempotency_key and client_token travel as headers, so a non-ASCII or control character now raises an ArgumentError naming the field before the request is built. Co-authored-by: Cursor <cursoragent@cursor.com>
…ivering status Co-authored-by: Cursor <cursoragent@cursor.com>
subnetmarco
left a comment
There was a problem hiding this comment.
Second pass at 6eb894f. The gap fix matches JS and Python:
liveholds a message whoseprev_seqis ahead, catches up in the background, and releases the held messages in order.read_pagesstops at a page that delivers nothing new.
I re-read durable_messages.rb, the session binding, durable_channel.rb and the realtime lifecycle changes against hosting#1826. Nothing blocking:
-
On the AI review's question about
leave_durable_channel: dropping the connection when a leave isn't confirmed seems right to me.- A leave only fails when the socket is already in trouble, so a reconnect is usually coming anyway.
- Dropping guarantees the server-side subscription is gone after a session change.
- The durable channel is removed from
@channelsfirst, so the reconnect doesn't bring it back.
-
Each SDK handles a resubscribe the server refuses differently:
- Ruby:
restore_subscriptionsreports the error, and the channel staysrestoration_needed?. Sincereconnect_needed?checks that, the loop retries it about every 20 s and reports toon_erroreach time. Meanwhile the subscription delivers nothing and never ends. - JS logs once and removes the channel.
- Python goes silent (see Kong/volcano-sdk-python#391).
A refused durable subscribe is usually permanent: the visibility changed, or the execution is gone after retention. So ending the subscription and reporting once, like JS, would be better here. Not blocking, but worth aligning across the three.
- Ruby:
Co-authored-by: Cursor <cursoragent@cursor.com>
|
Done in 5986c5f. A durable resubscribe the server refuses now ends the subscription and reports once, like JS.
Rubocop, both Steep targets, the full suite (2173 examples, 100% branch coverage) and |
… its answer Co-authored-by: Cursor <cursoragent@cursor.com>
…fore its answer Co-authored-by: Cursor <cursoragent@cursor.com>
…iting for the lock Co-authored-by: Cursor <cursoragent@cursor.com>
A publication the stream cannot read is still one the execution published. It now starts a catch-up, as a dropped one does, instead of being skipped until the next gap. Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Sync the contract with Hosting: send, publish and message data accept and return an explicit null. Co-authored-by: Cursor <cursoragent@cursor.com>
… its frame is written Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Change
A Ruby application can send messages to a running durable execution, list them, and follow what it publishes. Server support is in volcano-hosting#1826. Ruby has no durable authoring API, so waiting and publishing stay in JavaScript and Python.
client.durable.messages.send_message(function, execution_id, name, data, client_token:, idempotency_key:). It's namedsend_messageso it doesn't shadowObject#send.client.durable.messages.list(...)takesdirection:,name:,after:andlimit:. Passingproject_id:uses the project route.client.durable.messages.subscribe(...) { |message| ... }follows the execution's realtime channel and lists from the lastseqafter every reconnect, so each published message arrives once, in order.Durable#startreturns theclient_tokenof the execution it creates.data:field for the client token; the adapter stays internal.docs/functions.mdand the README are updated. The OpenAPI client is regenerated.Verification
bundle exec rake quality:quality:auditandquality:generatedquality:lint: 410 files, no offensesquality:types: RBS validate and both Steep targets, no errorsquality:spec: 2,122 examples, 0 failures, with 100% line coverage (5,684/5,684) and 100% branch coverage (1,579/1,579)quality:defectsandquality:package, which builds the gem and smoke-tests it in an isolated installCross-language impact
Classify this change: public facade / wire contract / shared behavior
tests/sdk-contract/features/durable-messages.feature, with Ruby step definitions and support, in volcano-hosting#1827.docs/functions.mdand the README here; JavaScript and Python in their PRs;docs/public/functions/durable-messages.mdin Hosting.