Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
75 commits
Select commit Hold shift + click to select a range
938bf8b
Added the stream protos, commands, and API surface.
moetemp Sep 14, 2026
4e9314b
Added the stream log store and kept the scavenger off it.
moetemp Sep 14, 2026
2ea6176
Added the CHASM stream component with floor-aware retention.
moetemp Sep 14, 2026
cd3307e
Added the stream service RPCs and the frontend wiring.
moetemp Sep 14, 2026
d014cf3
Let a workflow publish and subscribe to its stream with commands.
moetemp Sep 14, 2026
53e99e9
Delivered consumed ranges through Workflow Tasks, routed across hosts.
moetemp Sep 14, 2026
27ae8a3
Added the functional tests and the SDK validation server host.
moetemp Sep 14, 2026
03a12b1
Formatted the stream proto and the cross-host test.
moetemp Sep 16, 2026
083ad12
Regenerated the execution store wrappers.
moetemp Sep 16, 2026
9682147
Restored the v1.10 and v0.10 schema manifests.
moetemp Sep 16, 2026
197f878
Added the stream_log schema as a new version per database.
moetemp Sep 16, 2026
fac8b43
Made the publish cost measurement opt-in.
moetemp Sep 16, 2026
8f255ca
Fixed the MySQL stream log read bounds.
moetemp Sep 16, 2026
d56c986
Regenerated the stream state proto.
moetemp Sep 16, 2026
989b303
Re-pinned api-go to the branch head.
moetemp Sep 16, 2026
c9afe90
Regenerated the getproto import map.
moetemp Sep 16, 2026
8bb1ad0
Tidied the mixed brain test module.
moetemp Sep 16, 2026
04d7060
Fixed the build-tests target for the mixed brain module.
moetemp Sep 16, 2026
e33d2d4
Made the otel handler test tolerate empty exemplar slices.
moetemp Sep 17, 2026
ed1d612
Cleared the stale test log between local test runs.
moetemp Sep 17, 2026
95719ed
Required protobuf payloads from the system Nexus endpoint.
moetemp Sep 17, 2026
ed762e1
Re-pinned api-go to the documented stream protos.
moetemp Sep 18, 2026
40f54b0
Removed the unused stream log store and its schema.
moetemp Sep 18, 2026
bf40768
Routed the command-path subscription through the stream service.
moetemp Sep 18, 2026
dee9c68
Failed the workflow task when a stream range cannot be served.
moetemp Sep 19, 2026
524dcec
Failed the workflow task on a refused stream command.
moetemp Sep 19, 2026
73fccdf
Proved that a failed workflow task publishes nothing.
moetemp Sep 19, 2026
18a053d
Regenerated the stream protos after the comment edits.
moetemp Sep 19, 2026
ee89886
Treated a stream message with no kind as data.
moetemp Sep 19, 2026
cf37d21
Dropped the implicit head pin from the RPC append handlers.
moetemp Sep 19, 2026
31ab971
Moved the stream limits to dynamic config and budgeted owned streams.
moetemp Sep 19, 2026
774751d
Keyed consumer pins by run and released them when the run closes.
moetemp Sep 19, 2026
d3602ee
Made retention and deletion respect an active consumer.
moetemp Sep 19, 2026
98320c4
Stopped a filtered stream read from skipping unexamined offsets.
moetemp Sep 19, 2026
b6c8b0c
Validated stream ids, names, offsets and page sizes on the frontend.
moetemp Sep 19, 2026
fb3567d
Wrapped long lines, tagged every handler and rewrote stale comments.
moetemp Sep 19, 2026
edfba28
Cleared the linter findings in the stream code.
moetemp Sep 19, 2026
bedc962
Carried the record vocabulary and producer identity through the server.
moetemp Sep 21, 2026
3f3f269
Created the owned stream when a workflow subscribes to it by name.
moetemp Sep 21, 2026
cf095d2
Applied the repo formatter to the stream service proto and race test.
moetemp Sep 21, 2026
ed16b23
Imported the api stream package as streampb wherever both stream pack…
moetemp Sep 21, 2026
032c382
Guarded the stream test env's cancel list against concurrent producers.
moetemp Sep 21, 2026
c0b383d
Declared authorization scope, quotas and redirection for the stream s…
moetemp Sep 21, 2026
16af6ea
Carried the recorded stream ranges on non-sticky query tasks.
moetemp Sep 21, 2026
3f66c5b
Shortened the stream metadata table.
moetemp Sep 21, 2026
125e9d3
Carried stream subscriptions through a workflow reset.
moetemp Sep 21, 2026
19b4452
Characterized a stream under namespace failover in both replication m…
moetemp Sep 21, 2026
6ef2a01
Carried stream subscriptions through a workflow reset.
moetemp Sep 25, 2026
b8254a7
Characterized a stream under namespace failover in both modes.
moetemp Sep 25, 2026
ec800d0
Tied the series to the original branch head.
moetemp Sep 25, 2026
1160593
Merge branch 'moe/AI-198-srv-5-delivery-replay' into moe/AI-198-srv-6…
moetemp Sep 25, 2026
c172b53
Let a reset run publish to the stream it inherited.
moetemp Sep 25, 2026
3ddf6f9
Merge branch 'moe/AI-198-srv-5-delivery-replay' into moe/AI-198-srv-6…
moetemp Sep 25, 2026
ec8ecfb
Merge branch 'moe/AI-198-srv-5-delivery-replay' into moe/AI-198-srv-6…
moetemp Sep 25, 2026
d90098d
Merge branch 'moe/AI-198-srv-5-delivery-replay' into moe/AI-198-srv-6…
moetemp Sep 25, 2026
6f7dc1c
Read the appended event's range on the reset path.
moetemp Sep 25, 2026
6c83fde
Merge branch 'moe/AI-198-srv-5-delivery-replay' into moe/AI-198-srv-6…
moetemp Sep 25, 2026
f8cf8bf
Merge branch 'moe/AI-198-srv-5-delivery-replay' into moe/AI-198-srv-6…
moetemp Sep 26, 2026
ad1b208
Merge branch 'moe/AI-198-srv-5-delivery-replay' into moe/AI-198-srv-6…
moetemp Sep 26, 2026
4acd2b0
Merge branch 'moe/AI-198-srv-5-delivery-replay' into moe/AI-198-srv-6…
moetemp Sep 28, 2026
3640606
Merge branch 'moe/AI-198-srv-5-delivery-replay' into moe/AI-198-srv-6…
moetemp Sep 28, 2026
9f572ef
Merge branch 'moe/AI-198-srv-5-delivery-replay' into moe/AI-198-srv-6…
moetemp Sep 28, 2026
5ccb475
Merge branch 'moe/AI-198-srv-5-delivery-replay' into moe/AI-198-srv-6…
moetemp Sep 29, 2026
c220c39
Merge branch 'moe/AI-198-srv-5-delivery-replay' into moe/AI-198-srv-6…
moetemp Sep 30, 2026
217eed9
Redelivered the reset-point task's stream input to the reset run.
moetemp Sep 30, 2026
9fff65b
Merge branch 'moe/AI-198-srv-5-delivery-replay' into moe/AI-198-srv-6…
moetemp Sep 30, 2026
b89a8b6
Merge branch 'moe/AI-198-srv-5-delivery-replay' into moe/AI-198-srv-6…
moetemp Oct 1, 2026
0da41f7
Kept a seeded batch's age and counted its bytes as held.
moetemp Oct 1, 2026
ffbe615
Merge branch 'moe/AI-198-srv-5-delivery-replay' into moe/AI-198-srv-6…
moetemp Oct 1, 2026
38b3e30
Merge branch 'moe/AI-198-srv-5-delivery-replay' into moe/AI-198-srv-6…
moetemp Oct 2, 2026
01ce482
Merge branch 'moe/AI-198-srv-5-delivery-replay' into moe/AI-198-srv-6…
moetemp Oct 2, 2026
48e0516
Merge branch 'moe/AI-198-srv-5-delivery-replay' into moe/AI-198-srv-6…
moetemp Oct 2, 2026
02178a4
Merge branch 'moe/AI-198-srv-5-delivery-replay' into moe/AI-198-srv-6…
moetemp Oct 2, 2026
21a5cff
Merge branch 'moe/AI-198-srv-5-delivery-replay' into moe/AI-198-srv-6…
moetemp Oct 3, 2026
20f1a5a
Merge branch 'moe/AI-198-srv-5-delivery-replay' into moe/AI-198-srv-6…
moetemp Oct 3, 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)
}
9 changes: 9 additions & 0 deletions chasm/lib/stream/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,13 @@
// re-supply the budget cuts short is refused as a whole rather than marked:
// paging it needs a short flag on the poll response, or on the last
// StreamSlice, that this package does not have on the wire yet.
//
// # Reset
//
// A reset run keeps its subscriptions. A range recorded before the reset
// point is re-supplied from the run reset from; the range the reset-point task
// had been given is delivered again to the reset run's first task, from a
// stream of its own that begins with a copy of that input (see [Stream.Seed]).
// Outside consumers are not told that a reset happened: reporting it as a
// record needs a record shape on the wire, which this package does not have.
package stream
78 changes: 78 additions & 0 deletions chasm/lib/stream/seed.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package stream

import (
"time"

commonpb "go.temporal.io/api/common/v1"
"go.temporal.io/api/serviceerror"
"go.temporal.io/server/chasm"
streamlib "go.temporal.io/server/chasm/lib/stream/gen/streampb/v1"
"google.golang.org/protobuf/proto"
)

// Seed copies the records of another stream's window [from, to), read with
// ReadBatches, into this one, which has to begin at from and hold nothing yet.
// The records keep their offsets, so a history that names them stays true. No
// producer entry comes along: the copy is this stream's own past, not an
// append anyone could retry.
//
// Made for a reset. The run reset from keeps its stream, and what it held past
// the inherited cursor is the input the reset-point task had been given; the
// run that re-runs that task needs the same input in a stream of its own.
func (s *Stream) Seed(
mctx chasm.MutableContext,
blobs []*commonpb.DataBlob,
starts []int64,
from int64,
to int64,
) error {
if s.State.BaseOffset != from || s.State.HeadOffset != from || len(s.Batches) != 0 {
return serviceerror.NewInternalf(
"a stream holding [%d,%d) cannot be seeded from offset %d",
s.State.BaseOffset, s.State.HeadOffset, from)
}
if s.Batches == nil {
s.Batches = make(chasm.Map[int64, *commonpb.DataBlob])
}
for i, blob := range blobs {
var batch streamlib.StreamRecordBatch
if err := proto.Unmarshal(blob.GetData(), &batch); err != nil {
return err
}
// A batch straddling the start is trimmed, so the copy begins exactly
// at the inherited offset and is keyed there.
var kept []*streamlib.StreamRecord
for j, record := range batch.GetRecords() {
if offset := starts[i] + int64(j); offset >= from && offset < to {
kept = append(kept, record)
}
}
if len(kept) == 0 {
continue
}
first := max(starts[i], from)
if first != s.State.HeadOffset {
return serviceerror.NewInternalf(
"seeding a stream at %d with a batch starting at %d", s.State.HeadOffset, first)
}
// The copy keeps the source batch's age: the records are as old as
// they were, whichever stream holds them.
var appendedAt time.Time
if batch.GetAppendedAt() != nil {
appendedAt = batch.GetAppendedAt().AsTime()
}
encoded, err := marshalBatch(kept, appendedAt)
if err != nil {
return err
}
s.Batches[first] = chasm.NewDataField(mctx, encoded)
s.State.HeadOffset = first + int64(len(kept))
s.State.AppendedBytes += int64(len(encoded.Data))
s.State.HeldBytes += int64(len(encoded.Data))
}
if s.State.HeadOffset != to {
return serviceerror.NewInternalf(
"seeding stopped at offset %d, short of %d", s.State.HeadOffset, to)
}
return nil
}
81 changes: 81 additions & 0 deletions chasm/lib/stream/seed_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package stream

import (
"testing"
"time"

"github.com/stretchr/testify/require"
commonpb "go.temporal.io/api/common/v1"
"go.temporal.io/api/serviceerror"
"go.temporal.io/server/chasm"
streamlib "go.temporal.io/server/chasm/lib/stream/gen/streampb/v1"
)

func bodiesOf(records []*streamlib.StreamRecord) []string {
out := make([]string, len(records))
for i, r := range records {
out[i] = string(r.GetBody().GetData())
}
return out
}

// A copy that begins inside a batch trims it and keys what it kept at the
// copy's start, so the seeded stream begins exactly at the inherited offset.
func TestSeedTrimsTheBatchStraddlingTheStart(t *testing.T) {
// A clock on the source, so its batches carry an age the copy must keep.
written := time.Unix(1_700_000_000, 0).UTC()
mctx := &chasm.MockMutableContext{MockContext: chasm.MockContext{
HandleNow: func(chasm.Component) time.Time { return written },
}}
source := newTestStream(t)
source.Batches = make(chasm.Map[int64, *commonpb.DataBlob])
for _, batch := range [][]string{{"a", "b", "c"}, {"d", "e"}} {
_, err := source.AddMessages(mctx, AddMessagesRequest{Records: msgs(batch...)})
require.NoError(t, err)
}
blobs, starts, err := source.ReadBatches(nil, 1, 4, 0)
require.NoError(t, err)

target := &Stream{State: &streamlib.StreamState{BaseOffset: 1, HeadOffset: 1}}
require.NoError(t, target.Seed(nil, blobs, starts, 1, 4))
require.Equal(t, int64(4), target.State.HeadOffset)
require.Equal(t, []int64{1, 3}, target.batchStarts())
require.Positive(t, target.State.AppendedBytes, "the copy counts against the budget")
require.Equal(t, target.State.AppendedBytes, target.State.HeldBytes, "and is held")
require.Empty(t, target.State.Producers)
appendedAt, ok, err := target.batchAppendedAt(nil, 1)
require.NoError(t, err)
require.True(t, ok, "the copy keeps the source batch's age")
require.Equal(t, written, appendedAt)

copied, copiedStarts, err := target.ReadBatches(nil, 1, 4, 0)
require.NoError(t, err)
records, next, err := CollectRecords(copied, copiedStarts, 1, 4, 10, nil)
require.NoError(t, err)
require.Equal(t, int64(4), next)
require.Equal(t, []string{"b", "c", "d"}, bodiesOf(records))
require.Equal(t, []int64{1, 2, 3}, []int64{
records[0].GetOffset(), records[1].GetOffset(), records[2].GetOffset()})
}

func TestSeedRefusesAStreamThatIsNotEmptyAtTheStart(t *testing.T) {
source := newTestStream(t)
_, err := source.AddMessages(nil, AddMessagesRequest{Records: msgs("a", "b")})
require.NoError(t, err)
blobs, starts, err := source.ReadBatches(nil, 0, 2, 0)
require.NoError(t, err)

var internal *serviceerror.Internal
occupied := newTestStream(t)
_, err = occupied.AddMessages(nil, AddMessagesRequest{Records: msgs("x")})
require.NoError(t, err)
require.ErrorAs(t, occupied.Seed(nil, blobs, starts, 0, 2), &internal)

elsewhere := &Stream{State: &streamlib.StreamState{BaseOffset: 5, HeadOffset: 5}}
require.ErrorAs(t, elsewhere.Seed(nil, blobs, starts, 0, 2), &internal)

// A window that does not reach the end it was asked for is refused rather
// than left as a stream shorter than its history claims.
short := newTestStream(t)
require.ErrorAs(t, short.Seed(nil, blobs, starts, 0, 3), &internal)
}
19 changes: 15 additions & 4 deletions chasm/lib/stream/stream.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,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 @@ -97,6 +103,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 @@ -105,10 +114,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
44 changes: 44 additions & 0 deletions chasm/lib/workflow/stream_admission_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,50 @@ func TestSubscriptionsPerWorkflowAreBounded(t *testing.T) {
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.
created, createErr := w.ownStreamFrom(ctx, DefaultStreamName, 100, limits)
require.NoError(t, createErr)
require.True(t, created)

_, 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)
}

// An activity id may contain a slash, so the reserved key escapes it. Without
// that two different (activity, name) pairs would land on one stream.
func TestActivityStreamKeysDoNotCollide(t *testing.T) {
Expand Down
7 changes: 7 additions & 0 deletions chasm/lib/workflow/stream_commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,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
Loading
Loading