Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
938bf8b
Added the stream protos, commands, and API surface.
moedash Sep 14, 2026
4e9314b
Added the stream log store and kept the scavenger off it.
moedash Sep 14, 2026
2ea6176
Added the CHASM stream component with floor-aware retention.
moedash Sep 14, 2026
cd3307e
Added the stream service RPCs and the frontend wiring.
moedash Sep 14, 2026
d014cf3
Let a workflow publish and subscribe to its stream with commands.
moedash Sep 14, 2026
53e99e9
Delivered consumed ranges through Workflow Tasks, routed across hosts.
moedash Sep 14, 2026
27ae8a3
Added the functional tests and the SDK validation server host.
moedash Sep 14, 2026
03a12b1
Formatted the stream proto and the cross-host test.
moedash Sep 16, 2026
083ad12
Regenerated the execution store wrappers.
moedash Sep 16, 2026
9682147
Restored the v1.10 and v0.10 schema manifests.
moedash Sep 16, 2026
197f878
Added the stream_log schema as a new version per database.
moedash Sep 16, 2026
fac8b43
Made the publish cost measurement opt-in.
moedash Sep 16, 2026
8f255ca
Fixed the MySQL stream log read bounds.
moedash Sep 16, 2026
d56c986
Regenerated the stream state proto.
moedash Sep 16, 2026
989b303
Re-pinned api-go to the branch head.
moedash Sep 16, 2026
c9afe90
Regenerated the getproto import map.
moedash Sep 16, 2026
8bb1ad0
Tidied the mixed brain test module.
moedash Sep 16, 2026
04d7060
Fixed the build-tests target for the mixed brain module.
moedash Sep 16, 2026
e33d2d4
Made the otel handler test tolerate empty exemplar slices.
moedash Sep 17, 2026
ed1d612
Cleared the stale test log between local test runs.
moedash Sep 17, 2026
95719ed
Required protobuf payloads from the system Nexus endpoint.
moedash Sep 17, 2026
ed762e1
Re-pinned api-go to the documented stream protos.
moedash Sep 18, 2026
40f54b0
Removed the unused stream log store and its schema.
moedash Sep 18, 2026
bf40768
Routed the command-path subscription through the stream service.
moedash Sep 18, 2026
dee9c68
Failed the workflow task when a stream range cannot be served.
moedash Sep 19, 2026
524dcec
Failed the workflow task on a refused stream command.
moedash Sep 19, 2026
73fccdf
Proved that a failed workflow task publishes nothing.
moedash Sep 19, 2026
18a053d
Regenerated the stream protos after the comment edits.
moedash Sep 19, 2026
ee89886
Treated a stream message with no kind as data.
moedash Sep 19, 2026
cf37d21
Dropped the implicit head pin from the RPC append handlers.
moedash Sep 19, 2026
31ab971
Moved the stream limits to dynamic config and budgeted owned streams.
moedash Sep 19, 2026
774751d
Keyed consumer pins by run and released them when the run closes.
moedash Sep 19, 2026
d3602ee
Made retention and deletion respect an active consumer.
moedash Sep 19, 2026
98320c4
Stopped a filtered stream read from skipping unexamined offsets.
moedash Sep 19, 2026
b6c8b0c
Validated stream ids, names, offsets and page sizes on the frontend.
moedash Sep 19, 2026
fb3567d
Wrapped long lines, tagged every handler and rewrote stale comments.
moedash Sep 19, 2026
edfba28
Cleared the linter findings in the stream code.
moedash Sep 19, 2026
bedc962
Carried the record vocabulary and producer identity through the server.
moedash Sep 21, 2026
3f3f269
Created the owned stream when a workflow subscribes to it by name.
moedash Sep 21, 2026
cf095d2
Applied the repo formatter to the stream service proto and race test.
moedash Sep 21, 2026
ed16b23
Imported the api stream package as streampb wherever both stream pack…
moedash Sep 21, 2026
032c382
Guarded the stream test env's cancel list against concurrent producers.
moedash Sep 21, 2026
c0b383d
Declared authorization scope, quotas and redirection for the stream s…
moedash Sep 21, 2026
16af6ea
Carried the recorded stream ranges on non-sticky query tasks.
moedash Sep 21, 2026
3f66c5b
Shortened the stream metadata table.
moedash Sep 21, 2026
125e9d3
Carried stream subscriptions through a workflow reset.
moedash Sep 21, 2026
19b4452
Characterized a stream under namespace failover in both replication m…
moedash Sep 21, 2026
6ef2a01
Carried stream subscriptions through a workflow reset.
moedash Sep 25, 2026
b8254a7
Characterized a stream under namespace failover in both modes.
moedash Sep 25, 2026
ec800d0
Tied the series to the original branch head.
moedash Sep 25, 2026
1160593
Merge branch 'moe/AI-198-srv-5-delivery-replay' into moe/AI-198-srv-6…
moedash Sep 25, 2026
c172b53
Let a reset run publish to the stream it inherited.
moedash Sep 25, 2026
3ddf6f9
Merge branch 'moe/AI-198-srv-5-delivery-replay' into moe/AI-198-srv-6…
moedash Sep 25, 2026
ec8ecfb
Merge branch 'moe/AI-198-srv-5-delivery-replay' into moe/AI-198-srv-6…
moedash Sep 25, 2026
d90098d
Merge branch 'moe/AI-198-srv-5-delivery-replay' into moe/AI-198-srv-6…
moedash Sep 25, 2026
6f7dc1c
Read the appended event's range on the reset path.
moedash Sep 25, 2026
6c83fde
Merge branch 'moe/AI-198-srv-5-delivery-replay' into moe/AI-198-srv-6…
moedash Sep 25, 2026
f8cf8bf
Merge branch 'moe/AI-198-srv-5-delivery-replay' into moe/AI-198-srv-6…
moedash Sep 26, 2026
ad1b208
Merge branch 'moe/AI-198-srv-5-delivery-replay' into moe/AI-198-srv-6…
moedash Sep 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions chasm/lib/stream/cursor.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,3 +139,23 @@ func (c *Cursor) AdvanceKnownHead(_ chasm.MutableContext, head int64) {
func (c *Cursor) StartOffset() int64 {
return c.State.StartOffset
}

// Restore puts the cursor where a completed task's event says it stood, for a
// run being rebuilt from its history. It only moves forward: the events are
// applied in order, and a range folded in earlier is never taken back.
func (c *Cursor) Restore(_ chasm.MutableContext, offset int64) {
if offset > c.State.Offset {
c.State.Offset = offset
}
if offset > c.State.KnownHead {
c.State.KnownHead = offset
}
}

// MarkExternal says the stream lives in another execution, which the subscribe
// event alone cannot tell a rebuilt run, and carries over the frontier that
// stream last pushed at the run this one was rebuilt from.
func (c *Cursor) MarkExternal(mctx chasm.MutableContext, knownHead int64) {
c.State.External = true
c.AdvanceKnownHead(mctx, knownHead)
}
19 changes: 15 additions & 4 deletions chasm/lib/stream/stream.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ type NewStreamRequest struct {
// child of the root, so an attached stream carries none and is found
// through its owner instead of through ListStreams.
Attached bool

// StartOffset is where the stream's offsets begin. Zero for a new stream.
// A run created by a reset inherits a cursor whose position is written in
// its History, so the stream it goes on reading has to continue that
// offset space rather than start over at zero.
StartOffset int64
}

type AddMessagesRequest struct {
Expand Down Expand Up @@ -98,6 +104,9 @@ type AddMessagesResult struct {
}

func NewStream(ctx chasm.MutableContext, req NewStreamRequest) (*Stream, error) {
if req.StartOffset < 0 {
return nil, serviceerror.NewInvalidArgument("start offset cannot be negative")
}
visibility := chasm.NewEmptyField[*chasm.Visibility]()
if !req.Attached {
visibility = chasm.NewComponentField(ctx, chasm.NewVisibility(ctx))
Expand All @@ -106,10 +115,12 @@ func NewStream(ctx chasm.MutableContext, req NewStreamRequest) (*Stream, error)
Visibility: visibility,
Batches: make(chasm.Map[int64, *commonpb.DataBlob]),
State: &streamlib.StreamState{
Lifecycle: req.Lifecycle,
Budget: req.Budget,
Producers: make(map[string]*streamlib.ProducerCursor),
Consumers: make(map[string]*streamlib.ConsumerCursor),
HeadOffset: req.StartOffset,
BaseOffset: req.StartOffset,
Lifecycle: req.Lifecycle,
Budget: req.Budget,
Producers: make(map[string]*streamlib.ProducerCursor),
Consumers: make(map[string]*streamlib.ConsumerCursor),
},
}, nil
}
Expand Down
42 changes: 42 additions & 0 deletions chasm/lib/workflow/stream_admission_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,3 +162,45 @@ func TestSubscriptionsPerWorkflowAreBounded(t *testing.T) {
// not refused for room.
require.NoError(t, subscribe("a"))
}

// A reset run's stream continues the base run's offset space, so it is born
// with a head offset well above zero while holding nothing. Measuring the
// budget against that head would put it over the moment it exists, and every
// publish on the reset run would be refused.
func TestAnInheritedStreamIsNotBornOverItsItemBudget(t *testing.T) {
ctx := newStreamBudgetTestContext()
w := &Workflow{}
limits := stream.Limits{
MaxOwnedStreamsPerWorkflow: 10,
MaxConsumersPerStream: 10,
OwnedStreamMaxItems: 3,
OwnedStreamMaxBytes: 1 << 20,
}

// The inherited cursor stands far past the item budget.
require.NoError(t, w.ownStreamFrom(ctx, DefaultStreamName, 100, limits))

_, err := w.AppendToOwnedStream(ctx, DefaultStreamName, stream.AddMessagesRequest{
Records: budgetTestRecords(8),
Limits: limits,
})
require.NoError(t, err, "the stream holds nothing, so its whole budget is free")

owned := w.Streams[DefaultStreamName].Get(ctx)
require.Equal(t, int64(101), owned.State.GetHeadOffset())

// Still bounded against what it holds.
for range 2 {
_, err = w.AppendToOwnedStream(ctx, DefaultStreamName, stream.AddMessagesRequest{
Records: budgetTestRecords(8),
Limits: limits,
})
require.NoError(t, err)
}
_, err = w.AppendToOwnedStream(ctx, DefaultStreamName, stream.AddMessagesRequest{
Records: budgetTestRecords(8),
Limits: limits,
})
var exhausted *serviceerror.ResourceExhausted
require.ErrorAs(t, err, &exhausted)
}
7 changes: 7 additions & 0 deletions chasm/lib/workflow/stream_commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,13 @@ func (w *Workflow) RecordStreamRecordsAppended(
// streamNamed returns the workflow's stream of that name, creating it on first
// use. Implicit creation is deliberate: a workflow publishing to its own output
// should not have to coordinate with anyone about who creates it.
//
// The stream belongs to this run. After a reset the new run publishes to and
// subscribes on streams of its own, created here on first use, and the run it
// was reset from keeps the records its own history refers to. A stream the
// reset run inherited a subscription to is created by the reset itself, at the
// offset that subscription stood at, so it is already here by the time a
// command names it.
func (w *Workflow) streamNamed(
ctx chasm.MutableContext,
name string,
Expand Down
156 changes: 156 additions & 0 deletions chasm/lib/workflow/stream_cursor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -242,3 +242,159 @@ func TestConsumerOutrunByTruncationIsToldSo(t *testing.T) {
require.Equal(t, int64(3), state.GetBaseOffset(),
"the floor moved, which is what the consumer has to find out about")
}

func newStreamCursorTestContextForRun(runID string) chasm.MutableContext {
return &chasm.MockMutableContext{
MockContext: chasm.MockContext{
HandleExecutionKey: func() chasm.ExecutionKey {
return chasm.ExecutionKey{NamespaceID: "ns-1", BusinessID: "wf-1", RunID: runID}
},
},
}
}

func subscribedEvent(streamID string, startOffset int64) *historypb.HistoryEvent {
return &historypb.HistoryEvent{
EventType: enumspb.EVENT_TYPE_WORKFLOW_STREAM_SUBSCRIBED,
Attributes: &historypb.HistoryEvent_WorkflowStreamSubscribedEventAttributes{
WorkflowStreamSubscribedEventAttributes: &historypb.WorkflowStreamSubscribedEventAttributes{
StreamId: streamID,
StartOffset: startOffset,
},
},
}
}

// A run rebuilt from its history, which is what a reset produces, gets its
// cursors back from the events alone: the subscribe event places the cursor
// and each completed task's recorded range moves it.
func TestRebuildRecreatesTheCursorFromItsEvents(t *testing.T) {
ctx := newStreamCursorTestContext()
w := &Workflow{}

require.NoError(t, streamSubscribedEvent{}.Apply(ctx, w, subscribedEvent("inputs", 1)))
cursor := w.StreamCursors["inputs"].Get(ctx)
require.Equal(t, int64(1), cursor.StartOffset())
require.Equal(t, int64(1), cursor.Offset())
require.False(t, cursor.IsExternal(), "the event cannot say where the stream lives")

require.NoError(t, w.ApplyConsumedStreamRanges(ctx, []*streampb.StreamRange{
{StreamId: "inputs", FromOffset: 1, ToOffset: 4},
{StreamId: "out-of-band", FromOffset: 3, ToOffset: 9},
}))
require.Equal(t, int64(4), cursor.Offset())
require.Equal(t, int64(1), cursor.StartOffset(), "where reading began does not move")
outOfBand, ok := w.StreamCursors["out-of-band"]
require.True(t, ok, "a recorded range proves a subscription the events never mentioned")
require.Equal(t, int64(3), outOfBand.Get(ctx).StartOffset())
require.Equal(t, int64(9), outOfBand.Get(ctx).Offset())

// The same subscribe event applied twice, as a resubscribe would leave in
// History, must not rewind the cursor.
require.NoError(t, streamSubscribedEvent{}.Apply(ctx, w, subscribedEvent("inputs", 1)))
require.Equal(t, int64(4), w.StreamCursors["inputs"].Get(ctx).Offset())
}

// A reset run's cursor on a stream the base run owned gets a stream of the
// reset run's own, starting where the cursor stands, so the ranges below it
// stay in the base run and everything from here on is the reset run's.
func TestResetRunInheritsAnOwnedStreamAtItsCursor(t *testing.T) {
baseCtx := newStreamCursorTestContextForRun("base-run")
base := &Workflow{}
base.Streams = chasm.Map[string, *stream.Stream]{
DefaultStreamName: chasm.NewComponentField(baseCtx, newAttachedStream(t, baseCtx, 4)),
}
_, err := base.SubscribeToOwnedStream(baseCtx, DefaultStreamName, 0, stream.DefaultLimits())
require.NoError(t, err)

resetCtx := newStreamCursorTestContextForRun("reset-run")
reset := &Workflow{}
require.NoError(t,
streamSubscribedEvent{}.Apply(resetCtx, reset, subscribedEvent(DefaultStreamName, 0)))
require.NoError(t, reset.ApplyConsumedStreamRanges(resetCtx, []*streampb.StreamRange{
{StreamId: DefaultStreamName, FromOffset: 0, ToOffset: 2},
}))

require.NoError(t, reset.InheritStreamsOnReset(resetCtx, base, baseCtx, stream.DefaultLimits()))

cursor := reset.StreamCursors[DefaultStreamName].Get(resetCtx)
require.False(t, cursor.IsExternal())
require.Equal(t, int64(2), cursor.Offset())

own := reset.OwnedStream(resetCtx, DefaultStreamName)
require.NotNil(t, own, "the reset run reads and writes a stream of its own")
state, err := own.Snapshot(resetCtx, struct{}{})
require.NoError(t, err)
require.Equal(t, int64(2), state.GetBaseOffset(), "the stream continues the offset space")
require.Equal(t, int64(2), state.GetHeadOffset())
require.NotNil(t, state.GetBudget(), "an owned stream is budgeted like one created by a publish")
pin := state.GetConsumers()[streamConsumerID(DefaultStreamName)]
require.NotNil(t, pin, "the reset run pins its own stream")
require.Equal(t, "reset-run", pin.GetRunId())
require.Equal(t, int64(2), pin.GetReplayFloor())

// The base run's stream is untouched: it still holds what the reset run's
// history refers to.
baseState, err := base.OwnedStream(baseCtx, DefaultStreamName).Snapshot(baseCtx, struct{}{})
require.NoError(t, err)
require.Equal(t, int64(0), baseState.GetBaseOffset())
require.Equal(t, int64(4), baseState.GetHeadOffset())

// A publish on the reset run lands after the inherited position.
result, err := own.AddMessages(resetCtx, stream.AddMessagesRequest{
Records: []*streamlib.StreamRecord{{Kind: streampb.STREAM_RECORD_KIND_DATA}},
Limits: stream.DefaultLimits(),
})
require.NoError(t, err)
require.Equal(t, int64(2), result.FirstOffset)
}

// A reset run's cursor on a stream in another execution stays on that stream
// and learns what the base run knew of its frontier.
func TestResetRunInheritsAnExternalCursorAsExternal(t *testing.T) {
baseCtx := newStreamCursorTestContextForRun("base-run")
base := &Workflow{}
_, err := base.SubscribeToExternalStream(baseCtx, ExternalStreamSubscription{
StreamID: "shared", StartOffset: 0, KnownHead: 7,
})
require.NoError(t, err)

resetCtx := newStreamCursorTestContextForRun("reset-run")
reset := &Workflow{}
require.NoError(t, streamSubscribedEvent{}.Apply(resetCtx, reset, subscribedEvent("shared", 0)))
require.NoError(t, reset.ApplyConsumedStreamRanges(resetCtx, []*streampb.StreamRange{
{StreamId: "shared", FromOffset: 0, ToOffset: 3},
}))

require.NoError(t, reset.InheritStreamsOnReset(resetCtx, base, baseCtx, stream.DefaultLimits()))

cursor := reset.StreamCursors["shared"].Get(resetCtx)
require.True(t, cursor.IsExternal())
require.Equal(t, int64(3), cursor.Offset())
require.Equal(t, int64(7), cursor.KnownHead(), "the frontier the base run last knew carries over")
require.Nil(t, reset.OwnedStream(resetCtx, "shared"), "no stream of its own for an external one")
}

// A subscription made through the service leaves no event, so a reset run whose
// history records nothing for it is given the cursor from the base run, where
// that subscription began.
func TestResetRunCarriesASubscriptionTheEventsNeverMentioned(t *testing.T) {
baseCtx := newStreamCursorTestContextForRun("base-run")
base := &Workflow{}
base.Streams = chasm.Map[string, *stream.Stream]{
"inputs": chasm.NewComponentField(baseCtx, newAttachedStream(t, baseCtx, 4)),
}
_, err := base.SubscribeToOwnedStream(baseCtx, "inputs", 1, stream.DefaultLimits())
require.NoError(t, err)

resetCtx := newStreamCursorTestContextForRun("reset-run")
reset := &Workflow{}
require.NoError(t, reset.InheritStreamsOnReset(resetCtx, base, baseCtx, stream.DefaultLimits()))

cursor := reset.StreamCursors["inputs"].Get(resetCtx)
require.Equal(t, int64(1), cursor.StartOffset())
require.Equal(t, int64(1), cursor.Offset())
state, err := reset.OwnedStream(resetCtx, "inputs").Snapshot(resetCtx, struct{}{})
require.NoError(t, err)
require.Equal(t, int64(1), state.GetBaseOffset())
}
Loading
Loading