Repository navigation
Conversation
A linked channel lives in the owner's mutable state and the owner is its listener by construction, so a notification is one write on the owner, the one that schedules its task, with no subscribe event and no registration race. The independent kind stays for fan-out to many listeners.
This was referenced Oct 2, 2026
…srv-8-linked-channel
On a server with the linked kind every workflow-owned external stream uses it, so the independent subscribe path could not be exercised live. With channel.linkedKindEnabled off the public channel calls ignore workflow_execution and reach the independent channel of the name, as before the linked kind existed. The channels a native stream drives in its owner's state are not affected.
…srv-8-linked-channel
The public requests address a linked channel's owner as an Execution. The frontend and the notification conversion map it to the workflow run, and the tests name the owner that way.
…srv-8-linked-channel
Owner
Author
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR adds the second kind of notification channel, one linked to a workflow and held in that workflow's own mutable state.
What changed?
channel.Channelcomponent as a child of the Workflow component, keyed by channel name inWorkflow.LinkedChannels. The owning run is its listener by construction: no subscribe command, no event and no registration.ChannelStategainslinked, the owner's pending notification and the counter its scheduled task carries. The fan-out and idle tasks are guarded off for this kind, since the owner is reached in the write that accepts the notification and the channel dies with the run. The transaction close reads the linked channels' state to find a pending notification, which costs only runs that hold a linked channel; every other run answers from the map's length.NotifyChannelwithworkflow_executionset routes by workflow id to the owner's shard, an empty run id meaning the chain's current run as for a Signal. It reads the owner first: a counter above the latest joins the ring and the owner takes it as pending, while a repeat the owner holds, pending or on a task that has not started, and that every callback listener holds too, writes nothing. The transaction close schedules the Workflow Task through the hook the subscribed kind uses, andTakeChannelNotificationsputs the linked pendings on the scheduled event beside the subscribed ones, each withlinked_tonaming the owner and the receiving run.listener_countanswers the owner plus the callback listeners.linked_to.PollChannelwithworkflow_executionlong-polls the owner's execution over the linked ring withafter_counter.DescribeChannelanswerskind,linked_to, the listeners and the retained count; a name nobody has notified yet on a running workflow answersCHANNEL_KIND_LINKEDwith nothing in it rather thanNotFound, which is the probe a client uses for the linked kind, and a closed or absent workflow answersNotFound.RegisterChannelListenerandUnregisterChannelListenerwithworkflow_executionact on the owner's state. The independent kind answersCHANNEL_KIND_INDEPENDENTand is otherwise unchanged.ChannelServicegetsNotifyLinkedChannel,RegisterLinkedChannelListener,UnregisterLinkedChannelListener,PollLinkedChannelandDescribeLinkedChannel, taking the same request messages withworkflow_executionon the input and routed by its workflow id. The frontend branches onworkflow_executionand validates it the way a Signal target is validated. The internalNotificationcarrieslinked_to, so the ring, the scheduled event and the callback post agree.channel.maxLinkedChannelsPerWorkflowbounds how many linked channels one run holds and refuses a notify or registration that would create one past it withResourceExhausted;channel.linkedRetainedNotificationsbounds the linked ring, smaller than an independent channel's since it lives in the run's state;channel.maxListeners, the metadata cap and the notify rate apply as before. The channel counters gain akindlabel,independentorlinked.tests/linked_channel_test.godrives the linked kind with the raw client, and unit tests cover the owner-side state inchasm/lib/channelandchasm/lib/workflow.channel.linkedKindEnabled(default on). Off, the five channel calls ignoreworkflow_executionand reach the independent channel of that name, as before this kind existed, soDescribeChannelon an untouched name with an owner answersNotFoundand a client's probe lands on the independent kind. The channels a native stream drives in its owner's state are not affected. It exists so the independent subscribe path can be exercised live on a server that has the linked kind.Part of AI-198 (epic AI-37).
Why?
Max's review of the channel proposal asked for a channel linked to the listener's own mutable state, and the measurement backed it: with one listener the independent channel costs more writes than a Signal, all of them the channel execution's own. A linked channel makes a notification one write on the owner, the same write that schedules its task, with no subscription event and no registration race, so the task that opens a workflow's first reader can stay retained. The independent kind stays for fan-out to many listeners.
How did you test it?
go build ./...is clean, and golangci-lint and theerrortypevet are clean on the touched packages. Unit tests ran for./chasm/lib/channel/...,./chasm/lib/workflow/...,./service/frontend/...and./service/history/workflow/..., covering the owner's fold rules, the take onto the scheduled event withlinked_to, the linked ring bound, the callback handoff and the per-run limit. The functional tests ran with-tags test_dep: a notify carries on the owner's next scheduled event withlinked_toand the owner as the listener, an untouched name describes as linked with nothing in it while the independent namesake staysNotFound, the fold while pending and into an unstarted task leaves the owner's state-transition count unchanged, a repeat after the task ran schedules again, continue-as-new is followed by workflow id with an empty successor andNotFoundon the closed run, poll with a filter, a page and a long poll, a callback listener posted withlinked_toand a late one handed the latest, describe, notify and poll on a closed chain and on an absent workflow answerNotFound, and the limits hold. The independent channel suite and the stream suite pass.