diff --git a/chasm/lib/activity/activity.go b/chasm/lib/activity/activity.go index ccd9ef8ef3d..451fcd3fb5c 100644 --- a/chasm/lib/activity/activity.go +++ b/chasm/lib/activity/activity.go @@ -17,6 +17,7 @@ import ( "go.temporal.io/server/chasm" "go.temporal.io/server/chasm/lib/activity/gen/activitypb/v1" "go.temporal.io/server/chasm/lib/callback" + "go.temporal.io/server/chasm/lib/stream" "go.temporal.io/server/common" "go.temporal.io/server/common/contextutil" "go.temporal.io/server/common/metrics" @@ -74,6 +75,11 @@ type Activity struct { // Callbacks holds completion callbacks to be invoked when this standalone activity reaches a terminal state. Nil // for workflow-embedded activities as the workflow handles its own callbacks. Callbacks chasm.Map[string, *callback.Callback] + + // Streams this activity owns, keyed by stream name. One map per execution + // rather than per attempt, so a retry keeps writing to the stream its + // earlier attempts wrote to. + Streams chasm.Map[string, *stream.Stream] } // WithToken wraps a request with its deserialized task token. diff --git a/chasm/lib/activity/streams.go b/chasm/lib/activity/streams.go new file mode 100644 index 00000000000..f1cf80c3e5b --- /dev/null +++ b/chasm/lib/activity/streams.go @@ -0,0 +1,44 @@ +package activity + +import ( + "go.temporal.io/server/chasm" + "go.temporal.io/server/chasm/lib/stream" +) + +var _ stream.Owner = (*Activity)(nil) + +func (a *Activity) ownedStreams() stream.OwnedStreams { + return stream.OwnedStreams{Streams: &a.Streams, Kind: "activity"} +} + +// OwnedStream returns the stream under key, or nil when nothing has created it. +func (a *Activity) OwnedStream(ctx chasm.Context, key string) *stream.Stream { + return a.ownedStreams().Get(ctx, key) +} + +// OwnedStreamEnded reports that the activity reached a terminal status. A +// failed attempt that will be retried is not the end: the next attempt writes +// to the same stream. +func (a *Activity) OwnedStreamEnded(_ chasm.Context, _ string) bool { + return a.isTerminal() +} + +// AppendToOwnedStream appends on behalf of a writer outside the execution, +// which for an activity is every writer: an activity publishes over the +// stream service, never through commands. +// +// Refused once the activity is terminal. A worker still running an attempt +// the server already timed out would otherwise keep adding to a stream its +// readers were told had ended. +func (a *Activity) AppendToOwnedStream( + mctx chasm.MutableContext, + key string, + req stream.AddMessagesRequest, +) (stream.AddMessagesResult, error) { + if a.isTerminal() { + return stream.AddMessagesResult{}, stream.Refusal(stream.ReasonStreamClosed, + "activity execution closed with status %v, so its streams take no more records", + InternalStatusToAPIStatus(a.GetStatus())) + } + return a.ownedStreams().Append(mctx, key, req) +} diff --git a/chasm/lib/activity/streams_test.go b/chasm/lib/activity/streams_test.go new file mode 100644 index 00000000000..7d515d21878 --- /dev/null +++ b/chasm/lib/activity/streams_test.go @@ -0,0 +1,104 @@ +package activity + +import ( + "testing" + + "github.com/stretchr/testify/require" + commonpb "go.temporal.io/api/common/v1" + "go.temporal.io/api/serviceerror" + streampb "go.temporal.io/api/stream/v1" + "go.temporal.io/server/chasm" + "go.temporal.io/server/chasm/lib/activity/gen/activitypb/v1" + "go.temporal.io/server/chasm/lib/stream" + streamlib "go.temporal.io/server/chasm/lib/stream/gen/streampb/v1" +) + +func streamRecords(bodies ...string) []*streamlib.StreamRecord { + out := make([]*streamlib.StreamRecord, len(bodies)) + for i, b := range bodies { + out[i] = &streamlib.StreamRecord{ + Body: &commonpb.Payload{Data: []byte(b)}, + Kind: streampb.STREAM_RECORD_KIND_DATA, + } + } + return out +} + +func activityInStatus(status activitypb.ActivityExecutionStatus) *Activity { + return &Activity{ActivityState: &activitypb.ActivityState{Status: status}} +} + +// A standalone activity owns a map of named streams, created on first write, +// and a retry scheduled after a failed attempt keeps writing to the same one. +func TestActivityOwnsStreamsAcrossAttempts(t *testing.T) { + ctx := &chasm.MockMutableContext{} + a := activityInStatus(activitypb.ACTIVITY_EXECUTION_STATUS_STARTED) + + first, err := a.AppendToOwnedStream(ctx, stream.DefaultStreamName, stream.AddMessagesRequest{ + Records: streamRecords("attempt one"), + }) + require.NoError(t, err) + require.Equal(t, int64(0), first.FirstOffset) + require.NotNil(t, a.OwnedStream(ctx, stream.DefaultStreamName)) + require.Nil(t, a.OwnedStream(ctx, "progress"), "a name nothing wrote to has no stream") + + // The attempt failed and a retry is scheduled. That is not the end of the + // activity, so the stream is not ended either. + a.Status = activitypb.ACTIVITY_EXECUTION_STATUS_SCHEDULED + require.False(t, a.OwnedStreamEnded(ctx, stream.DefaultStreamName)) + + a.Status = activitypb.ACTIVITY_EXECUTION_STATUS_STARTED + second, err := a.AppendToOwnedStream(ctx, stream.DefaultStreamName, stream.AddMessagesRequest{ + Records: streamRecords("attempt two"), + }) + require.NoError(t, err) + require.Equal(t, int64(1), second.FirstOffset, "the retry continues the same log") +} + +// Once the activity is terminal its streams are ended for readers, and an +// append from a worker still running an attempt the server gave up on is +// refused rather than added after readers were told the stream was over. +func TestActivityStreamsEndAtTerminalStatus(t *testing.T) { + for _, status := range []activitypb.ActivityExecutionStatus{ + activitypb.ACTIVITY_EXECUTION_STATUS_COMPLETED, + activitypb.ACTIVITY_EXECUTION_STATUS_FAILED, + activitypb.ACTIVITY_EXECUTION_STATUS_CANCELED, + activitypb.ACTIVITY_EXECUTION_STATUS_TERMINATED, + activitypb.ACTIVITY_EXECUTION_STATUS_TIMED_OUT, + } { + t.Run(status.String(), func(t *testing.T) { + ctx := &chasm.MockMutableContext{} + a := activityInStatus(status) + require.True(t, a.OwnedStreamEnded(ctx, stream.DefaultStreamName)) + + _, err := a.AppendToOwnedStream(ctx, stream.DefaultStreamName, stream.AddMessagesRequest{ + Records: streamRecords("too late"), + }) + var precondition *serviceerror.FailedPrecondition + require.ErrorAs(t, err, &precondition) + require.Equal(t, stream.ReasonStreamClosed, stream.ReasonOf(err.Error()), + "an SDK maps it to its closed-stream error by the token") + require.Nil(t, a.OwnedStream(ctx, stream.DefaultStreamName), + "a refused append must not leave a stream behind") + }) + } +} + +// The count bound and the shared byte budget are the same code a workflow +// uses, so an activity cannot grow its state past what a workflow may. +func TestActivityStreamsAreBoundedLikeAWorkflowsAre(t *testing.T) { + ctx := &chasm.MockMutableContext{} + a := activityInStatus(activitypb.ACTIVITY_EXECUTION_STATUS_STARTED) + limits := stream.Limits{MaxOwnedStreamsPerWorkflow: 2} + + for _, name := range []string{"a", "b"} { + _, err := a.AppendToOwnedStream(ctx, name, stream.AddMessagesRequest{ + Records: streamRecords("x"), Limits: limits, + }) + require.NoError(t, err) + } + _, err := a.AppendToOwnedStream(ctx, "c", stream.AddMessagesRequest{ + Records: streamRecords("x"), Limits: limits, + }) + require.ErrorContains(t, err, "activity already owns 2 streams") +} diff --git a/chasm/lib/stream/config.go b/chasm/lib/stream/config.go index f4d10083133..ed535d2c1c5 100644 --- a/chasm/lib/stream/config.go +++ b/chasm/lib/stream/config.go @@ -95,13 +95,32 @@ const ( // the same reason: each batch is a node, and many small ones cost state // that the byte budget alone does not see. OwnedStreamMaxItems = 10_000 + + // OwnedStreamsMaxBytesPerWorkflow bounds every stream one execution owns + // taken together. The per-stream budget multiplied by the stream count + // comes to far more than limit.mutableStateSize.error, so without this an + // outside writer can name enough streams to terminate the execution while + // every single stream stays inside its own budget. Half the error limit, + // which leaves the rest of mutable state its own room. + OwnedStreamsMaxBytesPerWorkflow = 4 << 20 ) var ( + // EnabledSetting gates the whole feature. Off by default: registering + // StreamService on the frontend otherwise turns a large new surface on in + // every deployment the moment it ships, and an operator needs a way to take + // it back without a rollback. + EnabledSetting = dynamicconfig.NewNamespaceBoolSetting( + "stream.enabled", + false, + `Whether the stream service and the workflow stream commands are available to a +namespace. Off by default.`, + ) MaxConsumeItemsPerTaskSetting = dynamicconfig.NewNamespaceIntSetting( "stream.maxConsumeItemsPerTask", MaxConsumeItemsPerTask, - `Most stream records one workflow task carries per subscription.`, + `Most stream records one workflow task carries per subscription. Clamped to 1000, +which is the largest page a single stream read returns.`, ) MaxConsumeBytesPerTaskSetting = dynamicconfig.NewNamespaceIntSetting( "stream.maxConsumeBytesPerTask", @@ -144,20 +163,66 @@ under limit.mutableStateSize.error, which would otherwise terminate the workflow OwnedStreamMaxItems, `Message budget of a stream a workflow owns. Appends past it are refused.`, ) + OwnedStreamsMaxBytesPerWorkflowSetting = dynamicconfig.NewNamespaceIntSetting( + "stream.ownedStreamsMaxBytesPerWorkflow", + OwnedStreamsMaxBytesPerWorkflow, + `Byte budget of every stream one workflow execution owns, taken together. Appends +past it are refused. Keep it under limit.mutableStateSize.error, which would otherwise +terminate the workflow.`, + ) RetentionRecheckIntervalSetting = dynamicconfig.NewGlobalDurationSetting( "stream.retentionRecheckInterval", time.Minute, `How long a closed stream past its retention waits before asking again whether the consumers holding it are still running.`, ) + CreateWaitRecheckIntervalSetting = dynamicconfig.NewGlobalDurationSetting( + "stream.createWaitRecheckInterval", + 250*time.Millisecond, + `How often a blocking poll on a standalone stream id that names nothing yet asks +again whether the stream has been created, until the poll's wait expires.`, + ) + + // The rates below are enforced by the stream service per namespace and + // answered with ResourceExhausted when exceeded. Zero means unlimited: a + // rate is something an operator may want off, and the enablement setting + // already covers turning the feature off. + AppendRecordsPerSecondSetting = dynamicconfig.NewNamespaceIntSetting( + "stream.appendRecordsPerSecond", + AppendRecordsPerSecond, + `Most stream records a namespace may append per second through the stream service. +A burst of one full batch is always admitted. Zero means unlimited.`, + ) + AppendBytesPerSecondSetting = dynamicconfig.NewNamespaceIntSetting( + "stream.appendBytesPerSecond", + AppendBytesPerSecond, + `Most stream record bytes a namespace may append per second through the stream +service. A burst of one full batch is always admitted. Zero means unlimited.`, + ) + PollsPerSecondSetting = dynamicconfig.NewNamespaceIntSetting( + "stream.pollsPerSecond", + PollsPerSecond, + `Most stream polls a namespace may make per second, blocking or not. Zero means +unlimited.`, + ) +) + +// Default rates. Generous: they exist so a runaway producer or reader can be +// contained by config, not to shape ordinary traffic. +const ( + AppendRecordsPerSecond = 100_000 + AppendBytesPerSecond = 256 << 20 + PollsPerSecond = 10_000 ) // Config holds the settings as live property functions. type Config struct { + Enabled dynamicconfig.BoolPropertyFnWithNamespaceFilter // The id length limit shared with workflow ids. A stream id becomes an // execution's business id, and a stream name a key in mutable state. MaxIDLength dynamicconfig.IntPropertyFn RetentionRecheckInterval dynamicconfig.DurationPropertyFn + CreateWaitRecheckInterval dynamicconfig.DurationPropertyFn MaxConsumeItemsPerTask dynamicconfig.IntPropertyFnWithNamespaceFilter MaxConsumeBytesPerTask dynamicconfig.IntPropertyFnWithNamespaceFilter MaxProducersPerStream dynamicconfig.IntPropertyFnWithNamespaceFilter @@ -167,12 +232,21 @@ type Config struct { MaxOwnedStreamsPerWorkflow dynamicconfig.IntPropertyFnWithNamespaceFilter OwnedStreamMaxBytes dynamicconfig.IntPropertyFnWithNamespaceFilter OwnedStreamMaxItems dynamicconfig.IntPropertyFnWithNamespaceFilter + // Bounds every stream one execution owns taken together, which the + // per-stream budget cannot do. + OwnedStreamsMaxBytesPerWorkflow dynamicconfig.IntPropertyFnWithNamespaceFilter + + AppendRecordsPerSecond dynamicconfig.IntPropertyFnWithNamespaceFilter + AppendBytesPerSecond dynamicconfig.IntPropertyFnWithNamespaceFilter + PollsPerSecond dynamicconfig.IntPropertyFnWithNamespaceFilter } func NewConfig(dc *dynamicconfig.Collection) *Config { return &Config{ + Enabled: EnabledSetting.Get(dc), MaxIDLength: dynamicconfig.MaxIDLengthLimit.Get(dc), RetentionRecheckInterval: RetentionRecheckIntervalSetting.Get(dc), + CreateWaitRecheckInterval: CreateWaitRecheckIntervalSetting.Get(dc), MaxConsumeItemsPerTask: MaxConsumeItemsPerTaskSetting.Get(dc), MaxConsumeBytesPerTask: MaxConsumeBytesPerTaskSetting.Get(dc), MaxProducersPerStream: MaxProducersPerStreamSetting.Get(dc), @@ -182,6 +256,12 @@ func NewConfig(dc *dynamicconfig.Collection) *Config { MaxOwnedStreamsPerWorkflow: MaxOwnedStreamsPerWorkflowSetting.Get(dc), OwnedStreamMaxBytes: OwnedStreamMaxBytesSetting.Get(dc), OwnedStreamMaxItems: OwnedStreamMaxItemsSetting.Get(dc), + + OwnedStreamsMaxBytesPerWorkflow: OwnedStreamsMaxBytesPerWorkflowSetting.Get(dc), + + AppendRecordsPerSecond: AppendRecordsPerSecondSetting.Get(dc), + AppendBytesPerSecond: AppendBytesPerSecondSetting.Get(dc), + PollsPerSecond: PollsPerSecondSetting.Get(dc), } } @@ -197,6 +277,8 @@ type Limits struct { MaxOwnedStreamsPerWorkflow int OwnedStreamMaxBytes int OwnedStreamMaxItems int + + OwnedStreamsMaxBytesPerWorkflow int } // LimitsFor resolves the limits for a namespace. A nil Config, which is what @@ -215,9 +297,21 @@ func (c *Config) LimitsFor(namespaceName string) Limits { MaxOwnedStreamsPerWorkflow: c.MaxOwnedStreamsPerWorkflow(namespaceName), OwnedStreamMaxBytes: c.OwnedStreamMaxBytes(namespaceName), OwnedStreamMaxItems: c.OwnedStreamMaxItems(namespaceName), + + OwnedStreamsMaxBytesPerWorkflow: c.OwnedStreamsMaxBytesPerWorkflow(namespaceName), }.withDefaults() } +// EnabledFor reports whether a namespace may use streams. A nil Config, which +// is what component code driven without a service gets, reads as enabled: the +// gate is a deployment switch, and a unit test is not a deployment. +func (c *Config) EnabledFor(namespaceName string) bool { + if c == nil || c.Enabled == nil { + return true + } + return c.Enabled(namespaceName) +} + // DefaultLimits is the constant set above. func DefaultLimits() Limits { return Limits{}.withDefaults() @@ -236,6 +330,10 @@ func (l Limits) withDefaults() Limits { } } fill(&l.MaxConsumeItemsPerTask, MaxConsumeItemsPerTask) + // A slice is built from one stream read, which serves at most a page, so a + // larger setting than that cannot take effect. Clamped here rather than + // left to disagree with what delivery does. + l.MaxConsumeItemsPerTask = min(l.MaxConsumeItemsPerTask, DefaultMaxMessagesPerPoll) fill(&l.MaxConsumeBytesPerTask, MaxConsumeBytesPerTask) fill(&l.MaxProducersPerStream, MaxProducersPerStream) fill(&l.MaxConsumersPerStream, MaxConsumersPerStream) @@ -244,5 +342,6 @@ func (l Limits) withDefaults() Limits { fill(&l.MaxOwnedStreamsPerWorkflow, MaxOwnedStreamsPerWorkflow) fill(&l.OwnedStreamMaxBytes, OwnedStreamMaxBytes) fill(&l.OwnedStreamMaxItems, OwnedStreamMaxItems) + fill(&l.OwnedStreamsMaxBytesPerWorkflow, OwnedStreamsMaxBytesPerWorkflow) return l } diff --git a/chasm/lib/stream/doc.go b/chasm/lib/stream/doc.go index 3061485000c..307114bd5d1 100644 --- a/chasm/lib/stream/doc.go +++ b/chasm/lib/stream/doc.go @@ -22,6 +22,9 @@ // starts at. // - STREAM_CLOSED: an append on a stream that has been sealed. Its records // stay readable; nothing more goes in. +// - STREAM_POLICY_MISMATCH: a create naming a stream that already exists +// with a different lifecycle. A create that repeats the existing +// lifecycle is an idempotent retry and answers AlreadyExists instead. // // [Refusal] builds one and [ReasonOf] reads one back. // diff --git a/chasm/lib/stream/gen/streampb/v1/namespace.go b/chasm/lib/stream/gen/streampb/v1/namespace.go new file mode 100644 index 00000000000..dd465ef065e --- /dev/null +++ b/chasm/lib/stream/gen/streampb/v1/namespace.go @@ -0,0 +1,75 @@ +package streampb + +// Every RPC here carries its namespace inside `frontend_request` rather than at +// the top level, because the top-level field is the resolved namespace id the +// frontend fills in before routing. +// +// The server's interceptors find a request's namespace by asserting it to +// `interceptor.NamespaceNameGetter`, which wants `GetNamespace() string` on the +// request itself. Without these the assertion falls through to the id getter, +// which at the frontend is still empty, so namespace rate limits, request +// validation, the authorization target, redirection and the long-poll deadline +// all resolve to the empty namespace and silently do nothing. +// +// The generator has no way to express "read it from this nested field", so the +// methods are written here, next to the generated types they belong to. + +func (x *CreateStreamRequest) GetNamespace() string { + return x.GetFrontendRequest().GetNamespace() +} + +func (x *AddMessagesRequest) GetNamespace() string { + return x.GetFrontendRequest().GetNamespace() +} + +func (x *FinishWritingRequest) GetNamespace() string { + return x.GetFrontendRequest().GetNamespace() +} + +func (x *SubscribeWorkflowRequest) GetNamespace() string { + return x.GetFrontendRequest().GetNamespace() +} + +func (x *PollMessagesRequest) GetNamespace() string { + return x.GetFrontendRequest().GetNamespace() +} + +func (x *DescribeStreamRequest) GetNamespace() string { + return x.GetFrontendRequest().GetNamespace() +} + +func (x *PollWorkflowMessagesRequest) GetNamespace() string { + return x.GetFrontendRequest().GetNamespace() +} + +func (x *DescribeWorkflowStreamRequest) GetNamespace() string { + return x.GetFrontendRequest().GetNamespace() +} + +func (x *AddWorkflowMessagesRequest) GetNamespace() string { + return x.GetFrontendRequest().GetNamespace() +} + +func (x *RegisterStreamConsumerRequest) GetNamespace() string { + return x.GetFrontendRequest().GetNamespace() +} + +func (x *AdvanceConsumerHeadRequest) GetNamespace() string { + return x.GetFrontendRequest().GetNamespace() +} + +func (x *CloseStreamRequest) GetNamespace() string { + return x.GetFrontendRequest().GetNamespace() +} + +func (x *TruncateStreamRequest) GetNamespace() string { + return x.GetFrontendRequest().GetNamespace() +} + +func (x *ListStreamsRequest) GetNamespace() string { + return x.GetFrontendRequest().GetNamespace() +} + +func (x *DeleteStreamRequest) GetNamespace() string { + return x.GetFrontendRequest().GetNamespace() +} diff --git a/chasm/lib/stream/gen/streampb/v1/namespace_test.go b/chasm/lib/stream/gen/streampb/v1/namespace_test.go new file mode 100644 index 00000000000..b88bcaf856c --- /dev/null +++ b/chasm/lib/stream/gen/streampb/v1/namespace_test.go @@ -0,0 +1,38 @@ +package streampb + +import ( + "testing" + + "go.temporal.io/server/common/rpc/interceptor" + "google.golang.org/protobuf/reflect/protoregistry" +) + +// Every request carrying a frontend_request must expose its namespace to the +// interceptors. Driven off the descriptor rather than a hand-written list so a +// new RPC fails here instead of silently opting out of namespace rate limits, +// validation, authorization and redirection. +func TestEveryRoutedRequestExposesNamespace(t *testing.T) { + fd := File_temporal_server_chasm_lib_stream_proto_v1_request_response_proto + messages := fd.Messages() + + checked := 0 + for i := 0; i < messages.Len(); i++ { + md := messages.Get(i) + if md.Fields().ByName("frontend_request") == nil { + continue + } + mt, err := protoregistry.GlobalTypes.FindMessageByName(md.FullName()) + if err != nil { + t.Fatalf("%s is not registered: %v", md.FullName(), err) + } + msg := mt.New().Interface() + if _, ok := msg.(interceptor.NamespaceNameGetter); !ok { + t.Errorf("%s has a frontend_request but no GetNamespace; add it in namespace.go", md.FullName()) + } + checked++ + } + + if checked == 0 { + t.Fatal("found no routed requests, so this test is not checking anything") + } +} diff --git a/chasm/lib/stream/gen/streampb/v1/request_response.go-helpers.pb.go b/chasm/lib/stream/gen/streampb/v1/request_response.go-helpers.pb.go new file mode 100644 index 00000000000..ae21fe1d337 --- /dev/null +++ b/chasm/lib/stream/gen/streampb/v1/request_response.go-helpers.pb.go @@ -0,0 +1,2211 @@ +// Code generated by protoc-gen-go-helpers. DO NOT EDIT. +package streampb + +import ( + "fmt" + + "google.golang.org/protobuf/proto" +) + +// Marshal an object of type CreateStreamInput to the protobuf v3 wire format +func (val *CreateStreamInput) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type CreateStreamInput from the protobuf v3 wire format +func (val *CreateStreamInput) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *CreateStreamInput) Size() int { + return proto.Size(val) +} + +// Equal returns whether two CreateStreamInput values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *CreateStreamInput) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *CreateStreamInput + switch t := that.(type) { + case *CreateStreamInput: + that1 = t + case CreateStreamInput: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type CreateStreamOutput to the protobuf v3 wire format +func (val *CreateStreamOutput) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type CreateStreamOutput from the protobuf v3 wire format +func (val *CreateStreamOutput) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *CreateStreamOutput) Size() int { + return proto.Size(val) +} + +// Equal returns whether two CreateStreamOutput values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *CreateStreamOutput) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *CreateStreamOutput + switch t := that.(type) { + case *CreateStreamOutput: + that1 = t + case CreateStreamOutput: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type AddMessagesInput to the protobuf v3 wire format +func (val *AddMessagesInput) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type AddMessagesInput from the protobuf v3 wire format +func (val *AddMessagesInput) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *AddMessagesInput) Size() int { + return proto.Size(val) +} + +// Equal returns whether two AddMessagesInput values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *AddMessagesInput) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *AddMessagesInput + switch t := that.(type) { + case *AddMessagesInput: + that1 = t + case AddMessagesInput: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type AddMessagesOutput to the protobuf v3 wire format +func (val *AddMessagesOutput) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type AddMessagesOutput from the protobuf v3 wire format +func (val *AddMessagesOutput) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *AddMessagesOutput) Size() int { + return proto.Size(val) +} + +// Equal returns whether two AddMessagesOutput values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *AddMessagesOutput) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *AddMessagesOutput + switch t := that.(type) { + case *AddMessagesOutput: + that1 = t + case AddMessagesOutput: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type FinishWritingInput to the protobuf v3 wire format +func (val *FinishWritingInput) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type FinishWritingInput from the protobuf v3 wire format +func (val *FinishWritingInput) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *FinishWritingInput) Size() int { + return proto.Size(val) +} + +// Equal returns whether two FinishWritingInput values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *FinishWritingInput) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *FinishWritingInput + switch t := that.(type) { + case *FinishWritingInput: + that1 = t + case FinishWritingInput: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type FinishWritingOutput to the protobuf v3 wire format +func (val *FinishWritingOutput) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type FinishWritingOutput from the protobuf v3 wire format +func (val *FinishWritingOutput) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *FinishWritingOutput) Size() int { + return proto.Size(val) +} + +// Equal returns whether two FinishWritingOutput values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *FinishWritingOutput) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *FinishWritingOutput + switch t := that.(type) { + case *FinishWritingOutput: + that1 = t + case FinishWritingOutput: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type SubscribeWorkflowInput to the protobuf v3 wire format +func (val *SubscribeWorkflowInput) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type SubscribeWorkflowInput from the protobuf v3 wire format +func (val *SubscribeWorkflowInput) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *SubscribeWorkflowInput) Size() int { + return proto.Size(val) +} + +// Equal returns whether two SubscribeWorkflowInput values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *SubscribeWorkflowInput) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *SubscribeWorkflowInput + switch t := that.(type) { + case *SubscribeWorkflowInput: + that1 = t + case SubscribeWorkflowInput: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type SubscribeWorkflowOutput to the protobuf v3 wire format +func (val *SubscribeWorkflowOutput) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type SubscribeWorkflowOutput from the protobuf v3 wire format +func (val *SubscribeWorkflowOutput) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *SubscribeWorkflowOutput) Size() int { + return proto.Size(val) +} + +// Equal returns whether two SubscribeWorkflowOutput values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *SubscribeWorkflowOutput) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *SubscribeWorkflowOutput + switch t := that.(type) { + case *SubscribeWorkflowOutput: + that1 = t + case SubscribeWorkflowOutput: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type PollMessagesInput to the protobuf v3 wire format +func (val *PollMessagesInput) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type PollMessagesInput from the protobuf v3 wire format +func (val *PollMessagesInput) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *PollMessagesInput) Size() int { + return proto.Size(val) +} + +// Equal returns whether two PollMessagesInput values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *PollMessagesInput) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *PollMessagesInput + switch t := that.(type) { + case *PollMessagesInput: + that1 = t + case PollMessagesInput: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type PollMessagesOutput to the protobuf v3 wire format +func (val *PollMessagesOutput) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type PollMessagesOutput from the protobuf v3 wire format +func (val *PollMessagesOutput) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *PollMessagesOutput) Size() int { + return proto.Size(val) +} + +// Equal returns whether two PollMessagesOutput values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *PollMessagesOutput) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *PollMessagesOutput + switch t := that.(type) { + case *PollMessagesOutput: + that1 = t + case PollMessagesOutput: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type DescribeStreamInput to the protobuf v3 wire format +func (val *DescribeStreamInput) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type DescribeStreamInput from the protobuf v3 wire format +func (val *DescribeStreamInput) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *DescribeStreamInput) Size() int { + return proto.Size(val) +} + +// Equal returns whether two DescribeStreamInput values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *DescribeStreamInput) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *DescribeStreamInput + switch t := that.(type) { + case *DescribeStreamInput: + that1 = t + case DescribeStreamInput: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type StreamOwner to the protobuf v3 wire format +func (val *StreamOwner) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type StreamOwner from the protobuf v3 wire format +func (val *StreamOwner) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *StreamOwner) Size() int { + return proto.Size(val) +} + +// Equal returns whether two StreamOwner values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *StreamOwner) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *StreamOwner + switch t := that.(type) { + case *StreamOwner: + that1 = t + case StreamOwner: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type PollWorkflowMessagesInput to the protobuf v3 wire format +func (val *PollWorkflowMessagesInput) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type PollWorkflowMessagesInput from the protobuf v3 wire format +func (val *PollWorkflowMessagesInput) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *PollWorkflowMessagesInput) Size() int { + return proto.Size(val) +} + +// Equal returns whether two PollWorkflowMessagesInput values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *PollWorkflowMessagesInput) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *PollWorkflowMessagesInput + switch t := that.(type) { + case *PollWorkflowMessagesInput: + that1 = t + case PollWorkflowMessagesInput: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type DescribeWorkflowStreamInput to the protobuf v3 wire format +func (val *DescribeWorkflowStreamInput) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type DescribeWorkflowStreamInput from the protobuf v3 wire format +func (val *DescribeWorkflowStreamInput) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *DescribeWorkflowStreamInput) Size() int { + return proto.Size(val) +} + +// Equal returns whether two DescribeWorkflowStreamInput values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *DescribeWorkflowStreamInput) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *DescribeWorkflowStreamInput + switch t := that.(type) { + case *DescribeWorkflowStreamInput: + that1 = t + case DescribeWorkflowStreamInput: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type AddWorkflowMessagesInput to the protobuf v3 wire format +func (val *AddWorkflowMessagesInput) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type AddWorkflowMessagesInput from the protobuf v3 wire format +func (val *AddWorkflowMessagesInput) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *AddWorkflowMessagesInput) Size() int { + return proto.Size(val) +} + +// Equal returns whether two AddWorkflowMessagesInput values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *AddWorkflowMessagesInput) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *AddWorkflowMessagesInput + switch t := that.(type) { + case *AddWorkflowMessagesInput: + that1 = t + case AddWorkflowMessagesInput: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type DescribeStreamOutput to the protobuf v3 wire format +func (val *DescribeStreamOutput) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type DescribeStreamOutput from the protobuf v3 wire format +func (val *DescribeStreamOutput) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *DescribeStreamOutput) Size() int { + return proto.Size(val) +} + +// Equal returns whether two DescribeStreamOutput values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *DescribeStreamOutput) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *DescribeStreamOutput + switch t := that.(type) { + case *DescribeStreamOutput: + that1 = t + case DescribeStreamOutput: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type CloseStreamInput to the protobuf v3 wire format +func (val *CloseStreamInput) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type CloseStreamInput from the protobuf v3 wire format +func (val *CloseStreamInput) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *CloseStreamInput) Size() int { + return proto.Size(val) +} + +// Equal returns whether two CloseStreamInput values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *CloseStreamInput) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *CloseStreamInput + switch t := that.(type) { + case *CloseStreamInput: + that1 = t + case CloseStreamInput: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type CloseStreamOutput to the protobuf v3 wire format +func (val *CloseStreamOutput) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type CloseStreamOutput from the protobuf v3 wire format +func (val *CloseStreamOutput) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *CloseStreamOutput) Size() int { + return proto.Size(val) +} + +// Equal returns whether two CloseStreamOutput values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *CloseStreamOutput) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *CloseStreamOutput + switch t := that.(type) { + case *CloseStreamOutput: + that1 = t + case CloseStreamOutput: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type TruncateStreamInput to the protobuf v3 wire format +func (val *TruncateStreamInput) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type TruncateStreamInput from the protobuf v3 wire format +func (val *TruncateStreamInput) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *TruncateStreamInput) Size() int { + return proto.Size(val) +} + +// Equal returns whether two TruncateStreamInput values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *TruncateStreamInput) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *TruncateStreamInput + switch t := that.(type) { + case *TruncateStreamInput: + that1 = t + case TruncateStreamInput: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type TruncateStreamOutput to the protobuf v3 wire format +func (val *TruncateStreamOutput) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type TruncateStreamOutput from the protobuf v3 wire format +func (val *TruncateStreamOutput) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *TruncateStreamOutput) Size() int { + return proto.Size(val) +} + +// Equal returns whether two TruncateStreamOutput values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *TruncateStreamOutput) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *TruncateStreamOutput + switch t := that.(type) { + case *TruncateStreamOutput: + that1 = t + case TruncateStreamOutput: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type DeleteStreamInput to the protobuf v3 wire format +func (val *DeleteStreamInput) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type DeleteStreamInput from the protobuf v3 wire format +func (val *DeleteStreamInput) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *DeleteStreamInput) Size() int { + return proto.Size(val) +} + +// Equal returns whether two DeleteStreamInput values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *DeleteStreamInput) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *DeleteStreamInput + switch t := that.(type) { + case *DeleteStreamInput: + that1 = t + case DeleteStreamInput: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type DeleteStreamOutput to the protobuf v3 wire format +func (val *DeleteStreamOutput) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type DeleteStreamOutput from the protobuf v3 wire format +func (val *DeleteStreamOutput) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *DeleteStreamOutput) Size() int { + return proto.Size(val) +} + +// Equal returns whether two DeleteStreamOutput values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *DeleteStreamOutput) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *DeleteStreamOutput + switch t := that.(type) { + case *DeleteStreamOutput: + that1 = t + case DeleteStreamOutput: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type CreateStreamRequest to the protobuf v3 wire format +func (val *CreateStreamRequest) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type CreateStreamRequest from the protobuf v3 wire format +func (val *CreateStreamRequest) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *CreateStreamRequest) Size() int { + return proto.Size(val) +} + +// Equal returns whether two CreateStreamRequest values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *CreateStreamRequest) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *CreateStreamRequest + switch t := that.(type) { + case *CreateStreamRequest: + that1 = t + case CreateStreamRequest: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type CreateStreamResponse to the protobuf v3 wire format +func (val *CreateStreamResponse) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type CreateStreamResponse from the protobuf v3 wire format +func (val *CreateStreamResponse) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *CreateStreamResponse) Size() int { + return proto.Size(val) +} + +// Equal returns whether two CreateStreamResponse values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *CreateStreamResponse) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *CreateStreamResponse + switch t := that.(type) { + case *CreateStreamResponse: + that1 = t + case CreateStreamResponse: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type AddMessagesRequest to the protobuf v3 wire format +func (val *AddMessagesRequest) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type AddMessagesRequest from the protobuf v3 wire format +func (val *AddMessagesRequest) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *AddMessagesRequest) Size() int { + return proto.Size(val) +} + +// Equal returns whether two AddMessagesRequest values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *AddMessagesRequest) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *AddMessagesRequest + switch t := that.(type) { + case *AddMessagesRequest: + that1 = t + case AddMessagesRequest: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type AddMessagesResponse to the protobuf v3 wire format +func (val *AddMessagesResponse) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type AddMessagesResponse from the protobuf v3 wire format +func (val *AddMessagesResponse) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *AddMessagesResponse) Size() int { + return proto.Size(val) +} + +// Equal returns whether two AddMessagesResponse values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *AddMessagesResponse) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *AddMessagesResponse + switch t := that.(type) { + case *AddMessagesResponse: + that1 = t + case AddMessagesResponse: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type FinishWritingRequest to the protobuf v3 wire format +func (val *FinishWritingRequest) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type FinishWritingRequest from the protobuf v3 wire format +func (val *FinishWritingRequest) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *FinishWritingRequest) Size() int { + return proto.Size(val) +} + +// Equal returns whether two FinishWritingRequest values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *FinishWritingRequest) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *FinishWritingRequest + switch t := that.(type) { + case *FinishWritingRequest: + that1 = t + case FinishWritingRequest: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type FinishWritingResponse to the protobuf v3 wire format +func (val *FinishWritingResponse) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type FinishWritingResponse from the protobuf v3 wire format +func (val *FinishWritingResponse) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *FinishWritingResponse) Size() int { + return proto.Size(val) +} + +// Equal returns whether two FinishWritingResponse values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *FinishWritingResponse) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *FinishWritingResponse + switch t := that.(type) { + case *FinishWritingResponse: + that1 = t + case FinishWritingResponse: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type SubscribeWorkflowRequest to the protobuf v3 wire format +func (val *SubscribeWorkflowRequest) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type SubscribeWorkflowRequest from the protobuf v3 wire format +func (val *SubscribeWorkflowRequest) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *SubscribeWorkflowRequest) Size() int { + return proto.Size(val) +} + +// Equal returns whether two SubscribeWorkflowRequest values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *SubscribeWorkflowRequest) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *SubscribeWorkflowRequest + switch t := that.(type) { + case *SubscribeWorkflowRequest: + that1 = t + case SubscribeWorkflowRequest: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type SubscribeWorkflowResponse to the protobuf v3 wire format +func (val *SubscribeWorkflowResponse) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type SubscribeWorkflowResponse from the protobuf v3 wire format +func (val *SubscribeWorkflowResponse) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *SubscribeWorkflowResponse) Size() int { + return proto.Size(val) +} + +// Equal returns whether two SubscribeWorkflowResponse values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *SubscribeWorkflowResponse) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *SubscribeWorkflowResponse + switch t := that.(type) { + case *SubscribeWorkflowResponse: + that1 = t + case SubscribeWorkflowResponse: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type PollMessagesRequest to the protobuf v3 wire format +func (val *PollMessagesRequest) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type PollMessagesRequest from the protobuf v3 wire format +func (val *PollMessagesRequest) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *PollMessagesRequest) Size() int { + return proto.Size(val) +} + +// Equal returns whether two PollMessagesRequest values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *PollMessagesRequest) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *PollMessagesRequest + switch t := that.(type) { + case *PollMessagesRequest: + that1 = t + case PollMessagesRequest: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type PollMessagesResponse to the protobuf v3 wire format +func (val *PollMessagesResponse) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type PollMessagesResponse from the protobuf v3 wire format +func (val *PollMessagesResponse) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *PollMessagesResponse) Size() int { + return proto.Size(val) +} + +// Equal returns whether two PollMessagesResponse values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *PollMessagesResponse) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *PollMessagesResponse + switch t := that.(type) { + case *PollMessagesResponse: + that1 = t + case PollMessagesResponse: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type DescribeStreamRequest to the protobuf v3 wire format +func (val *DescribeStreamRequest) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type DescribeStreamRequest from the protobuf v3 wire format +func (val *DescribeStreamRequest) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *DescribeStreamRequest) Size() int { + return proto.Size(val) +} + +// Equal returns whether two DescribeStreamRequest values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *DescribeStreamRequest) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *DescribeStreamRequest + switch t := that.(type) { + case *DescribeStreamRequest: + that1 = t + case DescribeStreamRequest: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type DescribeStreamResponse to the protobuf v3 wire format +func (val *DescribeStreamResponse) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type DescribeStreamResponse from the protobuf v3 wire format +func (val *DescribeStreamResponse) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *DescribeStreamResponse) Size() int { + return proto.Size(val) +} + +// Equal returns whether two DescribeStreamResponse values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *DescribeStreamResponse) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *DescribeStreamResponse + switch t := that.(type) { + case *DescribeStreamResponse: + that1 = t + case DescribeStreamResponse: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type PollWorkflowMessagesRequest to the protobuf v3 wire format +func (val *PollWorkflowMessagesRequest) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type PollWorkflowMessagesRequest from the protobuf v3 wire format +func (val *PollWorkflowMessagesRequest) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *PollWorkflowMessagesRequest) Size() int { + return proto.Size(val) +} + +// Equal returns whether two PollWorkflowMessagesRequest values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *PollWorkflowMessagesRequest) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *PollWorkflowMessagesRequest + switch t := that.(type) { + case *PollWorkflowMessagesRequest: + that1 = t + case PollWorkflowMessagesRequest: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type PollWorkflowMessagesResponse to the protobuf v3 wire format +func (val *PollWorkflowMessagesResponse) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type PollWorkflowMessagesResponse from the protobuf v3 wire format +func (val *PollWorkflowMessagesResponse) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *PollWorkflowMessagesResponse) Size() int { + return proto.Size(val) +} + +// Equal returns whether two PollWorkflowMessagesResponse values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *PollWorkflowMessagesResponse) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *PollWorkflowMessagesResponse + switch t := that.(type) { + case *PollWorkflowMessagesResponse: + that1 = t + case PollWorkflowMessagesResponse: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type DescribeWorkflowStreamRequest to the protobuf v3 wire format +func (val *DescribeWorkflowStreamRequest) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type DescribeWorkflowStreamRequest from the protobuf v3 wire format +func (val *DescribeWorkflowStreamRequest) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *DescribeWorkflowStreamRequest) Size() int { + return proto.Size(val) +} + +// Equal returns whether two DescribeWorkflowStreamRequest values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *DescribeWorkflowStreamRequest) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *DescribeWorkflowStreamRequest + switch t := that.(type) { + case *DescribeWorkflowStreamRequest: + that1 = t + case DescribeWorkflowStreamRequest: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type DescribeWorkflowStreamResponse to the protobuf v3 wire format +func (val *DescribeWorkflowStreamResponse) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type DescribeWorkflowStreamResponse from the protobuf v3 wire format +func (val *DescribeWorkflowStreamResponse) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *DescribeWorkflowStreamResponse) Size() int { + return proto.Size(val) +} + +// Equal returns whether two DescribeWorkflowStreamResponse values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *DescribeWorkflowStreamResponse) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *DescribeWorkflowStreamResponse + switch t := that.(type) { + case *DescribeWorkflowStreamResponse: + that1 = t + case DescribeWorkflowStreamResponse: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type RegisterStreamConsumerInput to the protobuf v3 wire format +func (val *RegisterStreamConsumerInput) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type RegisterStreamConsumerInput from the protobuf v3 wire format +func (val *RegisterStreamConsumerInput) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *RegisterStreamConsumerInput) Size() int { + return proto.Size(val) +} + +// Equal returns whether two RegisterStreamConsumerInput values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *RegisterStreamConsumerInput) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *RegisterStreamConsumerInput + switch t := that.(type) { + case *RegisterStreamConsumerInput: + that1 = t + case RegisterStreamConsumerInput: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type RegisterStreamConsumerOutput to the protobuf v3 wire format +func (val *RegisterStreamConsumerOutput) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type RegisterStreamConsumerOutput from the protobuf v3 wire format +func (val *RegisterStreamConsumerOutput) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *RegisterStreamConsumerOutput) Size() int { + return proto.Size(val) +} + +// Equal returns whether two RegisterStreamConsumerOutput values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *RegisterStreamConsumerOutput) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *RegisterStreamConsumerOutput + switch t := that.(type) { + case *RegisterStreamConsumerOutput: + that1 = t + case RegisterStreamConsumerOutput: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type AdvanceConsumerHeadInput to the protobuf v3 wire format +func (val *AdvanceConsumerHeadInput) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type AdvanceConsumerHeadInput from the protobuf v3 wire format +func (val *AdvanceConsumerHeadInput) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *AdvanceConsumerHeadInput) Size() int { + return proto.Size(val) +} + +// Equal returns whether two AdvanceConsumerHeadInput values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *AdvanceConsumerHeadInput) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *AdvanceConsumerHeadInput + switch t := that.(type) { + case *AdvanceConsumerHeadInput: + that1 = t + case AdvanceConsumerHeadInput: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type AdvanceConsumerHeadOutput to the protobuf v3 wire format +func (val *AdvanceConsumerHeadOutput) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type AdvanceConsumerHeadOutput from the protobuf v3 wire format +func (val *AdvanceConsumerHeadOutput) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *AdvanceConsumerHeadOutput) Size() int { + return proto.Size(val) +} + +// Equal returns whether two AdvanceConsumerHeadOutput values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *AdvanceConsumerHeadOutput) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *AdvanceConsumerHeadOutput + switch t := that.(type) { + case *AdvanceConsumerHeadOutput: + that1 = t + case AdvanceConsumerHeadOutput: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type AddWorkflowMessagesRequest to the protobuf v3 wire format +func (val *AddWorkflowMessagesRequest) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type AddWorkflowMessagesRequest from the protobuf v3 wire format +func (val *AddWorkflowMessagesRequest) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *AddWorkflowMessagesRequest) Size() int { + return proto.Size(val) +} + +// Equal returns whether two AddWorkflowMessagesRequest values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *AddWorkflowMessagesRequest) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *AddWorkflowMessagesRequest + switch t := that.(type) { + case *AddWorkflowMessagesRequest: + that1 = t + case AddWorkflowMessagesRequest: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type AddWorkflowMessagesResponse to the protobuf v3 wire format +func (val *AddWorkflowMessagesResponse) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type AddWorkflowMessagesResponse from the protobuf v3 wire format +func (val *AddWorkflowMessagesResponse) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *AddWorkflowMessagesResponse) Size() int { + return proto.Size(val) +} + +// Equal returns whether two AddWorkflowMessagesResponse values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *AddWorkflowMessagesResponse) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *AddWorkflowMessagesResponse + switch t := that.(type) { + case *AddWorkflowMessagesResponse: + that1 = t + case AddWorkflowMessagesResponse: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type RegisterStreamConsumerRequest to the protobuf v3 wire format +func (val *RegisterStreamConsumerRequest) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type RegisterStreamConsumerRequest from the protobuf v3 wire format +func (val *RegisterStreamConsumerRequest) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *RegisterStreamConsumerRequest) Size() int { + return proto.Size(val) +} + +// Equal returns whether two RegisterStreamConsumerRequest values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *RegisterStreamConsumerRequest) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *RegisterStreamConsumerRequest + switch t := that.(type) { + case *RegisterStreamConsumerRequest: + that1 = t + case RegisterStreamConsumerRequest: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type RegisterStreamConsumerResponse to the protobuf v3 wire format +func (val *RegisterStreamConsumerResponse) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type RegisterStreamConsumerResponse from the protobuf v3 wire format +func (val *RegisterStreamConsumerResponse) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *RegisterStreamConsumerResponse) Size() int { + return proto.Size(val) +} + +// Equal returns whether two RegisterStreamConsumerResponse values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *RegisterStreamConsumerResponse) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *RegisterStreamConsumerResponse + switch t := that.(type) { + case *RegisterStreamConsumerResponse: + that1 = t + case RegisterStreamConsumerResponse: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type AdvanceConsumerHeadRequest to the protobuf v3 wire format +func (val *AdvanceConsumerHeadRequest) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type AdvanceConsumerHeadRequest from the protobuf v3 wire format +func (val *AdvanceConsumerHeadRequest) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *AdvanceConsumerHeadRequest) Size() int { + return proto.Size(val) +} + +// Equal returns whether two AdvanceConsumerHeadRequest values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *AdvanceConsumerHeadRequest) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *AdvanceConsumerHeadRequest + switch t := that.(type) { + case *AdvanceConsumerHeadRequest: + that1 = t + case AdvanceConsumerHeadRequest: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type AdvanceConsumerHeadResponse to the protobuf v3 wire format +func (val *AdvanceConsumerHeadResponse) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type AdvanceConsumerHeadResponse from the protobuf v3 wire format +func (val *AdvanceConsumerHeadResponse) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *AdvanceConsumerHeadResponse) Size() int { + return proto.Size(val) +} + +// Equal returns whether two AdvanceConsumerHeadResponse values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *AdvanceConsumerHeadResponse) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *AdvanceConsumerHeadResponse + switch t := that.(type) { + case *AdvanceConsumerHeadResponse: + that1 = t + case AdvanceConsumerHeadResponse: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type CloseStreamRequest to the protobuf v3 wire format +func (val *CloseStreamRequest) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type CloseStreamRequest from the protobuf v3 wire format +func (val *CloseStreamRequest) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *CloseStreamRequest) Size() int { + return proto.Size(val) +} + +// Equal returns whether two CloseStreamRequest values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *CloseStreamRequest) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *CloseStreamRequest + switch t := that.(type) { + case *CloseStreamRequest: + that1 = t + case CloseStreamRequest: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type CloseStreamResponse to the protobuf v3 wire format +func (val *CloseStreamResponse) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type CloseStreamResponse from the protobuf v3 wire format +func (val *CloseStreamResponse) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *CloseStreamResponse) Size() int { + return proto.Size(val) +} + +// Equal returns whether two CloseStreamResponse values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *CloseStreamResponse) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *CloseStreamResponse + switch t := that.(type) { + case *CloseStreamResponse: + that1 = t + case CloseStreamResponse: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type TruncateStreamRequest to the protobuf v3 wire format +func (val *TruncateStreamRequest) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type TruncateStreamRequest from the protobuf v3 wire format +func (val *TruncateStreamRequest) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *TruncateStreamRequest) Size() int { + return proto.Size(val) +} + +// Equal returns whether two TruncateStreamRequest values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *TruncateStreamRequest) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *TruncateStreamRequest + switch t := that.(type) { + case *TruncateStreamRequest: + that1 = t + case TruncateStreamRequest: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type TruncateStreamResponse to the protobuf v3 wire format +func (val *TruncateStreamResponse) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type TruncateStreamResponse from the protobuf v3 wire format +func (val *TruncateStreamResponse) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *TruncateStreamResponse) Size() int { + return proto.Size(val) +} + +// Equal returns whether two TruncateStreamResponse values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *TruncateStreamResponse) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *TruncateStreamResponse + switch t := that.(type) { + case *TruncateStreamResponse: + that1 = t + case TruncateStreamResponse: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type ListStreamsInput to the protobuf v3 wire format +func (val *ListStreamsInput) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type ListStreamsInput from the protobuf v3 wire format +func (val *ListStreamsInput) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *ListStreamsInput) Size() int { + return proto.Size(val) +} + +// Equal returns whether two ListStreamsInput values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *ListStreamsInput) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *ListStreamsInput + switch t := that.(type) { + case *ListStreamsInput: + that1 = t + case ListStreamsInput: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type StreamListEntry to the protobuf v3 wire format +func (val *StreamListEntry) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type StreamListEntry from the protobuf v3 wire format +func (val *StreamListEntry) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *StreamListEntry) Size() int { + return proto.Size(val) +} + +// Equal returns whether two StreamListEntry values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *StreamListEntry) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *StreamListEntry + switch t := that.(type) { + case *StreamListEntry: + that1 = t + case StreamListEntry: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type ListStreamsOutput to the protobuf v3 wire format +func (val *ListStreamsOutput) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type ListStreamsOutput from the protobuf v3 wire format +func (val *ListStreamsOutput) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *ListStreamsOutput) Size() int { + return proto.Size(val) +} + +// Equal returns whether two ListStreamsOutput values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *ListStreamsOutput) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *ListStreamsOutput + switch t := that.(type) { + case *ListStreamsOutput: + that1 = t + case ListStreamsOutput: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type ListStreamsRequest to the protobuf v3 wire format +func (val *ListStreamsRequest) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type ListStreamsRequest from the protobuf v3 wire format +func (val *ListStreamsRequest) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *ListStreamsRequest) Size() int { + return proto.Size(val) +} + +// Equal returns whether two ListStreamsRequest values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *ListStreamsRequest) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *ListStreamsRequest + switch t := that.(type) { + case *ListStreamsRequest: + that1 = t + case ListStreamsRequest: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type ListStreamsResponse to the protobuf v3 wire format +func (val *ListStreamsResponse) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type ListStreamsResponse from the protobuf v3 wire format +func (val *ListStreamsResponse) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *ListStreamsResponse) Size() int { + return proto.Size(val) +} + +// Equal returns whether two ListStreamsResponse values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *ListStreamsResponse) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *ListStreamsResponse + switch t := that.(type) { + case *ListStreamsResponse: + that1 = t + case ListStreamsResponse: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type DeleteStreamRequest to the protobuf v3 wire format +func (val *DeleteStreamRequest) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type DeleteStreamRequest from the protobuf v3 wire format +func (val *DeleteStreamRequest) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *DeleteStreamRequest) Size() int { + return proto.Size(val) +} + +// Equal returns whether two DeleteStreamRequest values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *DeleteStreamRequest) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *DeleteStreamRequest + switch t := that.(type) { + case *DeleteStreamRequest: + that1 = t + case DeleteStreamRequest: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type DeleteStreamResponse to the protobuf v3 wire format +func (val *DeleteStreamResponse) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type DeleteStreamResponse from the protobuf v3 wire format +func (val *DeleteStreamResponse) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *DeleteStreamResponse) Size() int { + return proto.Size(val) +} + +// Equal returns whether two DeleteStreamResponse values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *DeleteStreamResponse) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *DeleteStreamResponse + switch t := that.(type) { + case *DeleteStreamResponse: + that1 = t + case DeleteStreamResponse: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +var ( + StreamOwnerKind_shorthandValue = map[string]int32{ + "Unspecified": 0, + "Workflow": 1, + "Activity": 2, + "WorkflowActivity": 3, + } +) + +// StreamOwnerKindFromString parses a StreamOwnerKind value from either the protojson +// canonical SCREAMING_CASE enum or the traditional temporal PascalCase enum to StreamOwnerKind +func StreamOwnerKindFromString(s string) (StreamOwnerKind, error) { + if v, ok := StreamOwnerKind_value[s]; ok { + return StreamOwnerKind(v), nil + } else if v, ok := StreamOwnerKind_shorthandValue[s]; ok { + return StreamOwnerKind(v), nil + } + return StreamOwnerKind(0), fmt.Errorf("%s is not a valid StreamOwnerKind", s) +} diff --git a/chasm/lib/stream/gen/streampb/v1/request_response.pb.go b/chasm/lib/stream/gen/streampb/v1/request_response.pb.go new file mode 100644 index 00000000000..e2e6f6ac2c9 --- /dev/null +++ b/chasm/lib/stream/gen/streampb/v1/request_response.pb.go @@ -0,0 +1,3911 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// plugins: +// protoc-gen-go +// protoc +// source: temporal/server/chasm/lib/stream/proto/v1/request_response.proto + +package streampb + +import ( + reflect "reflect" + "strconv" + sync "sync" + unsafe "unsafe" + + v11 "go.temporal.io/api/common/v1" + v1 "go.temporal.io/api/stream/v1" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// The kind of execution an owned stream lives in. +type StreamOwnerKind int32 + +const ( + STREAM_OWNER_KIND_UNSPECIFIED StreamOwnerKind = 0 + STREAM_OWNER_KIND_WORKFLOW StreamOwnerKind = 1 + // A standalone activity, which is an execution of its own. + STREAM_OWNER_KIND_ACTIVITY StreamOwnerKind = 2 + // An activity a workflow scheduled. It is not an execution of its own, so + // its streams are reached through the workflow and routed on it. + STREAM_OWNER_KIND_WORKFLOW_ACTIVITY StreamOwnerKind = 3 +) + +// Enum value maps for StreamOwnerKind. +var ( + StreamOwnerKind_name = map[int32]string{ + 0: "STREAM_OWNER_KIND_UNSPECIFIED", + 1: "STREAM_OWNER_KIND_WORKFLOW", + 2: "STREAM_OWNER_KIND_ACTIVITY", + 3: "STREAM_OWNER_KIND_WORKFLOW_ACTIVITY", + } + StreamOwnerKind_value = map[string]int32{ + "STREAM_OWNER_KIND_UNSPECIFIED": 0, + "STREAM_OWNER_KIND_WORKFLOW": 1, + "STREAM_OWNER_KIND_ACTIVITY": 2, + "STREAM_OWNER_KIND_WORKFLOW_ACTIVITY": 3, + } +) + +func (x StreamOwnerKind) Enum() *StreamOwnerKind { + p := new(StreamOwnerKind) + *p = x + return p +} + +func (x StreamOwnerKind) String() string { + switch x { + case STREAM_OWNER_KIND_UNSPECIFIED: + return "Unspecified" + case STREAM_OWNER_KIND_WORKFLOW: + return "Workflow" + case STREAM_OWNER_KIND_ACTIVITY: + return "Activity" + case STREAM_OWNER_KIND_WORKFLOW_ACTIVITY: + return "WorkflowActivity" + default: + return strconv.Itoa(int(x)) + } + +} + +func (StreamOwnerKind) Descriptor() protoreflect.EnumDescriptor { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_enumTypes[0].Descriptor() +} + +func (StreamOwnerKind) Type() protoreflect.EnumType { + return &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_enumTypes[0] +} + +func (x StreamOwnerKind) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use StreamOwnerKind.Descriptor instead. +func (StreamOwnerKind) EnumDescriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{0} +} + +type CreateStreamInput struct { + state protoimpl.MessageState `protogen:"open.v1"` + Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"` + StreamId string `protobuf:"bytes,2,opt,name=stream_id,json=streamId,proto3" json:"stream_id,omitempty"` + Lifecycle *StreamLifecycle `protobuf:"bytes,3,opt,name=lifecycle,proto3" json:"lifecycle,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateStreamInput) Reset() { + *x = CreateStreamInput{} + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateStreamInput) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateStreamInput) ProtoMessage() {} + +func (x *CreateStreamInput) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateStreamInput.ProtoReflect.Descriptor instead. +func (*CreateStreamInput) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{0} +} + +func (x *CreateStreamInput) GetNamespace() string { + if x != nil { + return x.Namespace + } + return "" +} + +func (x *CreateStreamInput) GetStreamId() string { + if x != nil { + return x.StreamId + } + return "" +} + +func (x *CreateStreamInput) GetLifecycle() *StreamLifecycle { + if x != nil { + return x.Lifecycle + } + return nil +} + +type CreateStreamOutput struct { + state protoimpl.MessageState `protogen:"open.v1"` + RunId string `protobuf:"bytes,1,opt,name=run_id,json=runId,proto3" json:"run_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateStreamOutput) Reset() { + *x = CreateStreamOutput{} + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateStreamOutput) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateStreamOutput) ProtoMessage() {} + +func (x *CreateStreamOutput) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateStreamOutput.ProtoReflect.Descriptor instead. +func (*CreateStreamOutput) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{1} +} + +func (x *CreateStreamOutput) GetRunId() string { + if x != nil { + return x.RunId + } + return "" +} + +type AddMessagesInput struct { + state protoimpl.MessageState `protogen:"open.v1"` + Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"` + StreamId string `protobuf:"bytes,2,opt,name=stream_id,json=streamId,proto3" json:"stream_id,omitempty"` + // Optional. Supplying it skips resolving the stream's current run, which is + // a persistence lookup on every call. CreateStream returns it. + RunId string `protobuf:"bytes,9,opt,name=run_id,json=runId,proto3" json:"run_id,omitempty"` + Records []*StreamRecord `protobuf:"bytes,3,rep,name=records,proto3" json:"records,omitempty"` + // Idempotency, all optional. Supply a producer identity and sequence, or an + // expected offset, or neither and accept at-least-once. + ProducerId string `protobuf:"bytes,4,opt,name=producer_id,json=producerId,proto3" json:"producer_id,omitempty"` + Sequence int64 `protobuf:"varint,5,opt,name=sequence,proto3" json:"sequence,omitempty"` + // Guarded by use_expected_offset because proto3 optional is not supported + // by this repo's helper generator. + ExpectedOffset int64 `protobuf:"varint,6,opt,name=expected_offset,json=expectedOffset,proto3" json:"expected_offset,omitempty"` + UseExpectedOffset bool `protobuf:"varint,8,opt,name=use_expected_offset,json=useExpectedOffset,proto3" json:"use_expected_offset,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AddMessagesInput) Reset() { + *x = AddMessagesInput{} + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AddMessagesInput) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AddMessagesInput) ProtoMessage() {} + +func (x *AddMessagesInput) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AddMessagesInput.ProtoReflect.Descriptor instead. +func (*AddMessagesInput) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{2} +} + +func (x *AddMessagesInput) GetNamespace() string { + if x != nil { + return x.Namespace + } + return "" +} + +func (x *AddMessagesInput) GetStreamId() string { + if x != nil { + return x.StreamId + } + return "" +} + +func (x *AddMessagesInput) GetRunId() string { + if x != nil { + return x.RunId + } + return "" +} + +func (x *AddMessagesInput) GetRecords() []*StreamRecord { + if x != nil { + return x.Records + } + return nil +} + +func (x *AddMessagesInput) GetProducerId() string { + if x != nil { + return x.ProducerId + } + return "" +} + +func (x *AddMessagesInput) GetSequence() int64 { + if x != nil { + return x.Sequence + } + return 0 +} + +func (x *AddMessagesInput) GetExpectedOffset() int64 { + if x != nil { + return x.ExpectedOffset + } + return 0 +} + +func (x *AddMessagesInput) GetUseExpectedOffset() bool { + if x != nil { + return x.UseExpectedOffset + } + return false +} + +type AddMessagesOutput struct { + state protoimpl.MessageState `protogen:"open.v1"` + FirstOffset int64 `protobuf:"varint,1,opt,name=first_offset,json=firstOffset,proto3" json:"first_offset,omitempty"` + NextOffset int64 `protobuf:"varint,2,opt,name=next_offset,json=nextOffset,proto3" json:"next_offset,omitempty"` + Count int64 `protobuf:"varint,3,opt,name=count,proto3" json:"count,omitempty"` + // True when a retry matched a recorded sequence and nothing was appended. + Deduplicated bool `protobuf:"varint,4,opt,name=deduplicated,proto3" json:"deduplicated,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AddMessagesOutput) Reset() { + *x = AddMessagesOutput{} + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AddMessagesOutput) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AddMessagesOutput) ProtoMessage() {} + +func (x *AddMessagesOutput) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AddMessagesOutput.ProtoReflect.Descriptor instead. +func (*AddMessagesOutput) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{3} +} + +func (x *AddMessagesOutput) GetFirstOffset() int64 { + if x != nil { + return x.FirstOffset + } + return 0 +} + +func (x *AddMessagesOutput) GetNextOffset() int64 { + if x != nil { + return x.NextOffset + } + return 0 +} + +func (x *AddMessagesOutput) GetCount() int64 { + if x != nil { + return x.Count + } + return 0 +} + +func (x *AddMessagesOutput) GetDeduplicated() bool { + if x != nil { + return x.Deduplicated + } + return false +} + +type FinishWritingInput struct { + state protoimpl.MessageState `protogen:"open.v1"` + Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"` + StreamId string `protobuf:"bytes,2,opt,name=stream_id,json=streamId,proto3" json:"stream_id,omitempty"` + ProducerId string `protobuf:"bytes,3,opt,name=producer_id,json=producerId,proto3" json:"producer_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *FinishWritingInput) Reset() { + *x = FinishWritingInput{} + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *FinishWritingInput) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FinishWritingInput) ProtoMessage() {} + +func (x *FinishWritingInput) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FinishWritingInput.ProtoReflect.Descriptor instead. +func (*FinishWritingInput) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{4} +} + +func (x *FinishWritingInput) GetNamespace() string { + if x != nil { + return x.Namespace + } + return "" +} + +func (x *FinishWritingInput) GetStreamId() string { + if x != nil { + return x.StreamId + } + return "" +} + +func (x *FinishWritingInput) GetProducerId() string { + if x != nil { + return x.ProducerId + } + return "" +} + +type FinishWritingOutput struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *FinishWritingOutput) Reset() { + *x = FinishWritingOutput{} + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *FinishWritingOutput) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FinishWritingOutput) ProtoMessage() {} + +func (x *FinishWritingOutput) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FinishWritingOutput.ProtoReflect.Descriptor instead. +func (*FinishWritingOutput) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{5} +} + +// Registers a Workflow as a consumer of a stream it owns. The cursor lands in +// the Workflow's own state, so from then on each of its Workflow Tasks carries +// the next range and records what it consumed. +type SubscribeWorkflowInput struct { + state protoimpl.MessageState `protogen:"open.v1"` + Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"` + WorkflowId string `protobuf:"bytes,2,opt,name=workflow_id,json=workflowId,proto3" json:"workflow_id,omitempty"` + // Optional. Pins to one run, so a caller that has continued as new is not + // silently redirected to the successor's stream, which starts empty and at + // offset zero. Empty means whichever run is current. + OwnerRunId string `protobuf:"bytes,6,opt,name=owner_run_id,json=ownerRunId,proto3" json:"owner_run_id,omitempty"` + // Name of the stream within the Workflow, for a stream it owns. + StreamName string `protobuf:"bytes,3,opt,name=stream_name,json=streamName,proto3" json:"stream_name,omitempty"` + // Id of a standalone stream in another execution. Exactly one of this and + // stream_name is set. + StreamId string `protobuf:"bytes,5,opt,name=stream_id,json=streamId,proto3" json:"stream_id,omitempty"` + // Where to start, as an absolute offset. Read only when start_position is + // unset. A negative value is refused. + StartOffset int64 `protobuf:"varint,4,opt,name=start_offset,json=startOffset,proto3" json:"start_offset,omitempty"` + // Where to start. Resolved here rather than at delivery, so the first + // recorded range starts from a fact instead of a reading. Refused alongside a + // non-zero start_offset. + StartPosition *v1.StreamStartPosition `protobuf:"bytes,7,opt,name=start_position,json=startPosition,proto3" json:"start_position,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SubscribeWorkflowInput) Reset() { + *x = SubscribeWorkflowInput{} + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SubscribeWorkflowInput) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SubscribeWorkflowInput) ProtoMessage() {} + +func (x *SubscribeWorkflowInput) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SubscribeWorkflowInput.ProtoReflect.Descriptor instead. +func (*SubscribeWorkflowInput) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{6} +} + +func (x *SubscribeWorkflowInput) GetNamespace() string { + if x != nil { + return x.Namespace + } + return "" +} + +func (x *SubscribeWorkflowInput) GetWorkflowId() string { + if x != nil { + return x.WorkflowId + } + return "" +} + +func (x *SubscribeWorkflowInput) GetOwnerRunId() string { + if x != nil { + return x.OwnerRunId + } + return "" +} + +func (x *SubscribeWorkflowInput) GetStreamName() string { + if x != nil { + return x.StreamName + } + return "" +} + +func (x *SubscribeWorkflowInput) GetStreamId() string { + if x != nil { + return x.StreamId + } + return "" +} + +func (x *SubscribeWorkflowInput) GetStartOffset() int64 { + if x != nil { + return x.StartOffset + } + return 0 +} + +func (x *SubscribeWorkflowInput) GetStartPosition() *v1.StreamStartPosition { + if x != nil { + return x.StartPosition + } + return nil +} + +type SubscribeWorkflowOutput struct { + state protoimpl.MessageState `protogen:"open.v1"` + StartOffset int64 `protobuf:"varint,1,opt,name=start_offset,json=startOffset,proto3" json:"start_offset,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SubscribeWorkflowOutput) Reset() { + *x = SubscribeWorkflowOutput{} + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SubscribeWorkflowOutput) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SubscribeWorkflowOutput) ProtoMessage() {} + +func (x *SubscribeWorkflowOutput) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SubscribeWorkflowOutput.ProtoReflect.Descriptor instead. +func (*SubscribeWorkflowOutput) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{7} +} + +func (x *SubscribeWorkflowOutput) GetStartOffset() int64 { + if x != nil { + return x.StartOffset + } + return 0 +} + +type PollMessagesInput struct { + state protoimpl.MessageState `protogen:"open.v1"` + Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"` + StreamId string `protobuf:"bytes,2,opt,name=stream_id,json=streamId,proto3" json:"stream_id,omitempty"` + // Optional, as on AddMessagesInput. + RunId string `protobuf:"bytes,7,opt,name=run_id,json=runId,proto3" json:"run_id,omitempty"` + FromOffset int64 `protobuf:"varint,3,opt,name=from_offset,json=fromOffset,proto3" json:"from_offset,omitempty"` + // Where a first poll starts, when the reader has no offset yet. Resolved in + // the same read that serves the records, so it cannot race with truncation, + // and next_offset carries the result for the polls after it. Refused + // alongside a non-zero from_offset. + StartPosition *v1.StreamStartPosition `protobuf:"bytes,8,opt,name=start_position,json=startPosition,proto3" json:"start_position,omitempty"` + MaxMessages int32 `protobuf:"varint,4,opt,name=max_messages,json=maxMessages,proto3" json:"max_messages,omitempty"` + // Filters by exact topic. Offsets are assigned over the unfiltered stream, so + // next_offset advances past filtered-out records too. + Topics []string `protobuf:"bytes,5,rep,name=topics,proto3" json:"topics,omitempty"` + // When set and the reader is caught up, block until something arrives, the + // stream closes, or the server's long-poll timeout elapses. A timeout returns + // an empty response rather than an error, so the caller simply polls again. + WaitNewMessages bool `protobuf:"varint,6,opt,name=wait_new_messages,json=waitNewMessages,proto3" json:"wait_new_messages,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PollMessagesInput) Reset() { + *x = PollMessagesInput{} + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PollMessagesInput) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PollMessagesInput) ProtoMessage() {} + +func (x *PollMessagesInput) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PollMessagesInput.ProtoReflect.Descriptor instead. +func (*PollMessagesInput) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{8} +} + +func (x *PollMessagesInput) GetNamespace() string { + if x != nil { + return x.Namespace + } + return "" +} + +func (x *PollMessagesInput) GetStreamId() string { + if x != nil { + return x.StreamId + } + return "" +} + +func (x *PollMessagesInput) GetRunId() string { + if x != nil { + return x.RunId + } + return "" +} + +func (x *PollMessagesInput) GetFromOffset() int64 { + if x != nil { + return x.FromOffset + } + return 0 +} + +func (x *PollMessagesInput) GetStartPosition() *v1.StreamStartPosition { + if x != nil { + return x.StartPosition + } + return nil +} + +func (x *PollMessagesInput) GetMaxMessages() int32 { + if x != nil { + return x.MaxMessages + } + return 0 +} + +func (x *PollMessagesInput) GetTopics() []string { + if x != nil { + return x.Topics + } + return nil +} + +func (x *PollMessagesInput) GetWaitNewMessages() bool { + if x != nil { + return x.WaitNewMessages + } + return false +} + +type PollMessagesOutput struct { + state protoimpl.MessageState `protogen:"open.v1"` + Records []*StreamRecord `protobuf:"bytes,1,rep,name=records,proto3" json:"records,omitempty"` + NextOffset int64 `protobuf:"varint,2,opt,name=next_offset,json=nextOffset,proto3" json:"next_offset,omitempty"` + HeadOffset int64 `protobuf:"varint,3,opt,name=head_offset,json=headOffset,proto3" json:"head_offset,omitempty"` + Closed bool `protobuf:"varint,4,opt,name=closed,proto3" json:"closed,omitempty"` + CloseReason *v11.Payload `protobuf:"bytes,5,opt,name=close_reason,json=closeReason,proto3" json:"close_reason,omitempty"` + // The execution holding the stream. A workflow task slice built from this + // read names the run it came from. + RunId string `protobuf:"bytes,6,opt,name=run_id,json=runId,proto3" json:"run_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PollMessagesOutput) Reset() { + *x = PollMessagesOutput{} + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PollMessagesOutput) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PollMessagesOutput) ProtoMessage() {} + +func (x *PollMessagesOutput) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PollMessagesOutput.ProtoReflect.Descriptor instead. +func (*PollMessagesOutput) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{9} +} + +func (x *PollMessagesOutput) GetRecords() []*StreamRecord { + if x != nil { + return x.Records + } + return nil +} + +func (x *PollMessagesOutput) GetNextOffset() int64 { + if x != nil { + return x.NextOffset + } + return 0 +} + +func (x *PollMessagesOutput) GetHeadOffset() int64 { + if x != nil { + return x.HeadOffset + } + return 0 +} + +func (x *PollMessagesOutput) GetClosed() bool { + if x != nil { + return x.Closed + } + return false +} + +func (x *PollMessagesOutput) GetCloseReason() *v11.Payload { + if x != nil { + return x.CloseReason + } + return nil +} + +func (x *PollMessagesOutput) GetRunId() string { + if x != nil { + return x.RunId + } + return "" +} + +type DescribeStreamInput struct { + state protoimpl.MessageState `protogen:"open.v1"` + Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"` + StreamId string `protobuf:"bytes,2,opt,name=stream_id,json=streamId,proto3" json:"stream_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DescribeStreamInput) Reset() { + *x = DescribeStreamInput{} + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DescribeStreamInput) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DescribeStreamInput) ProtoMessage() {} + +func (x *DescribeStreamInput) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DescribeStreamInput.ProtoReflect.Descriptor instead. +func (*DescribeStreamInput) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{10} +} + +func (x *DescribeStreamInput) GetNamespace() string { + if x != nil { + return x.Namespace + } + return "" +} + +func (x *DescribeStreamInput) GetStreamId() string { + if x != nil { + return x.StreamId + } + return "" +} + +// The execution that owns a stream. An attached stream has no id of its own, +// so it is named by this and a stream name, and routed on id. +type StreamOwner struct { + state protoimpl.MessageState `protogen:"open.v1"` + Kind StreamOwnerKind `protobuf:"varint,1,opt,name=kind,proto3,enum=temporal.server.chasm.lib.stream.proto.v1.StreamOwnerKind" json:"kind,omitempty"` + // The workflow id for WORKFLOW and WORKFLOW_ACTIVITY, the activity id for + // ACTIVITY. + Id string `protobuf:"bytes,2,opt,name=id,proto3" json:"id,omitempty"` + // Optional. Pins to one run of the execution named by id, so a caller is not + // silently redirected to a later run's stream, which starts empty and at + // offset zero. Empty means whichever run is current. + RunId string `protobuf:"bytes,3,opt,name=run_id,json=runId,proto3" json:"run_id,omitempty"` + // The activity within the workflow, for WORKFLOW_ACTIVITY only. + ActivityId string `protobuf:"bytes,4,opt,name=activity_id,json=activityId,proto3" json:"activity_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StreamOwner) Reset() { + *x = StreamOwner{} + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StreamOwner) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StreamOwner) ProtoMessage() {} + +func (x *StreamOwner) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StreamOwner.ProtoReflect.Descriptor instead. +func (*StreamOwner) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{11} +} + +func (x *StreamOwner) GetKind() StreamOwnerKind { + if x != nil { + return x.Kind + } + return STREAM_OWNER_KIND_UNSPECIFIED +} + +func (x *StreamOwner) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *StreamOwner) GetRunId() string { + if x != nil { + return x.RunId + } + return "" +} + +func (x *StreamOwner) GetActivityId() string { + if x != nil { + return x.ActivityId + } + return "" +} + +// A stream an execution owns lives inside it, so it has no standalone id to +// address it by. It is named by its owner and its name instead, and routed on +// the owner. +type PollWorkflowMessagesInput struct { + state protoimpl.MessageState `protogen:"open.v1"` + Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"` + Owner *StreamOwner `protobuf:"bytes,9,opt,name=owner,proto3" json:"owner,omitempty"` + // Shorthand for an owner of kind WORKFLOW, and refused alongside owner. + WorkflowId string `protobuf:"bytes,2,opt,name=workflow_id,json=workflowId,proto3" json:"workflow_id,omitempty"` + OwnerRunId string `protobuf:"bytes,8,opt,name=owner_run_id,json=ownerRunId,proto3" json:"owner_run_id,omitempty"` + // Empty means the owner's default output stream. + StreamName string `protobuf:"bytes,3,opt,name=stream_name,json=streamName,proto3" json:"stream_name,omitempty"` + FromOffset int64 `protobuf:"varint,4,opt,name=from_offset,json=fromOffset,proto3" json:"from_offset,omitempty"` + // As on PollMessagesInput. + StartPosition *v1.StreamStartPosition `protobuf:"bytes,10,opt,name=start_position,json=startPosition,proto3" json:"start_position,omitempty"` + MaxMessages int32 `protobuf:"varint,5,opt,name=max_messages,json=maxMessages,proto3" json:"max_messages,omitempty"` + // Filters as on PollMessagesInput. + Topics []string `protobuf:"bytes,6,rep,name=topics,proto3" json:"topics,omitempty"` + WaitNewMessages bool `protobuf:"varint,7,opt,name=wait_new_messages,json=waitNewMessages,proto3" json:"wait_new_messages,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PollWorkflowMessagesInput) Reset() { + *x = PollWorkflowMessagesInput{} + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PollWorkflowMessagesInput) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PollWorkflowMessagesInput) ProtoMessage() {} + +func (x *PollWorkflowMessagesInput) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[12] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PollWorkflowMessagesInput.ProtoReflect.Descriptor instead. +func (*PollWorkflowMessagesInput) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{12} +} + +func (x *PollWorkflowMessagesInput) GetNamespace() string { + if x != nil { + return x.Namespace + } + return "" +} + +func (x *PollWorkflowMessagesInput) GetOwner() *StreamOwner { + if x != nil { + return x.Owner + } + return nil +} + +func (x *PollWorkflowMessagesInput) GetWorkflowId() string { + if x != nil { + return x.WorkflowId + } + return "" +} + +func (x *PollWorkflowMessagesInput) GetOwnerRunId() string { + if x != nil { + return x.OwnerRunId + } + return "" +} + +func (x *PollWorkflowMessagesInput) GetStreamName() string { + if x != nil { + return x.StreamName + } + return "" +} + +func (x *PollWorkflowMessagesInput) GetFromOffset() int64 { + if x != nil { + return x.FromOffset + } + return 0 +} + +func (x *PollWorkflowMessagesInput) GetStartPosition() *v1.StreamStartPosition { + if x != nil { + return x.StartPosition + } + return nil +} + +func (x *PollWorkflowMessagesInput) GetMaxMessages() int32 { + if x != nil { + return x.MaxMessages + } + return 0 +} + +func (x *PollWorkflowMessagesInput) GetTopics() []string { + if x != nil { + return x.Topics + } + return nil +} + +func (x *PollWorkflowMessagesInput) GetWaitNewMessages() bool { + if x != nil { + return x.WaitNewMessages + } + return false +} + +type DescribeWorkflowStreamInput struct { + state protoimpl.MessageState `protogen:"open.v1"` + Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"` + Owner *StreamOwner `protobuf:"bytes,5,opt,name=owner,proto3" json:"owner,omitempty"` + // Shorthand for an owner of kind WORKFLOW, and refused alongside owner. + WorkflowId string `protobuf:"bytes,2,opt,name=workflow_id,json=workflowId,proto3" json:"workflow_id,omitempty"` + OwnerRunId string `protobuf:"bytes,4,opt,name=owner_run_id,json=ownerRunId,proto3" json:"owner_run_id,omitempty"` + StreamName string `protobuf:"bytes,3,opt,name=stream_name,json=streamName,proto3" json:"stream_name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DescribeWorkflowStreamInput) Reset() { + *x = DescribeWorkflowStreamInput{} + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DescribeWorkflowStreamInput) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DescribeWorkflowStreamInput) ProtoMessage() {} + +func (x *DescribeWorkflowStreamInput) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[13] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DescribeWorkflowStreamInput.ProtoReflect.Descriptor instead. +func (*DescribeWorkflowStreamInput) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{13} +} + +func (x *DescribeWorkflowStreamInput) GetNamespace() string { + if x != nil { + return x.Namespace + } + return "" +} + +func (x *DescribeWorkflowStreamInput) GetOwner() *StreamOwner { + if x != nil { + return x.Owner + } + return nil +} + +func (x *DescribeWorkflowStreamInput) GetWorkflowId() string { + if x != nil { + return x.WorkflowId + } + return "" +} + +func (x *DescribeWorkflowStreamInput) GetOwnerRunId() string { + if x != nil { + return x.OwnerRunId + } + return "" +} + +func (x *DescribeWorkflowStreamInput) GetStreamName() string { + if x != nil { + return x.StreamName + } + return "" +} + +// Appending to an owned stream from outside its owner. A workflow's own +// publishes ride its Workflow Task instead; an activity has no other path. +type AddWorkflowMessagesInput struct { + state protoimpl.MessageState `protogen:"open.v1"` + Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"` + Owner *StreamOwner `protobuf:"bytes,8,opt,name=owner,proto3" json:"owner,omitempty"` + // Shorthand for an owner of kind WORKFLOW, and refused alongside owner. + WorkflowId string `protobuf:"bytes,2,opt,name=workflow_id,json=workflowId,proto3" json:"workflow_id,omitempty"` + OwnerRunId string `protobuf:"bytes,7,opt,name=owner_run_id,json=ownerRunId,proto3" json:"owner_run_id,omitempty"` + // Empty means the owner's default output stream. + StreamName string `protobuf:"bytes,3,opt,name=stream_name,json=streamName,proto3" json:"stream_name,omitempty"` + Records []*StreamRecord `protobuf:"bytes,4,rep,name=records,proto3" json:"records,omitempty"` + // Optional idempotency, as on AddMessagesInput. + ProducerId string `protobuf:"bytes,5,opt,name=producer_id,json=producerId,proto3" json:"producer_id,omitempty"` + Sequence int64 `protobuf:"varint,6,opt,name=sequence,proto3" json:"sequence,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AddWorkflowMessagesInput) Reset() { + *x = AddWorkflowMessagesInput{} + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AddWorkflowMessagesInput) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AddWorkflowMessagesInput) ProtoMessage() {} + +func (x *AddWorkflowMessagesInput) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[14] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AddWorkflowMessagesInput.ProtoReflect.Descriptor instead. +func (*AddWorkflowMessagesInput) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{14} +} + +func (x *AddWorkflowMessagesInput) GetNamespace() string { + if x != nil { + return x.Namespace + } + return "" +} + +func (x *AddWorkflowMessagesInput) GetOwner() *StreamOwner { + if x != nil { + return x.Owner + } + return nil +} + +func (x *AddWorkflowMessagesInput) GetWorkflowId() string { + if x != nil { + return x.WorkflowId + } + return "" +} + +func (x *AddWorkflowMessagesInput) GetOwnerRunId() string { + if x != nil { + return x.OwnerRunId + } + return "" +} + +func (x *AddWorkflowMessagesInput) GetStreamName() string { + if x != nil { + return x.StreamName + } + return "" +} + +func (x *AddWorkflowMessagesInput) GetRecords() []*StreamRecord { + if x != nil { + return x.Records + } + return nil +} + +func (x *AddWorkflowMessagesInput) GetProducerId() string { + if x != nil { + return x.ProducerId + } + return "" +} + +func (x *AddWorkflowMessagesInput) GetSequence() int64 { + if x != nil { + return x.Sequence + } + return 0 +} + +type DescribeStreamOutput struct { + state protoimpl.MessageState `protogen:"open.v1"` + State *StreamState `protobuf:"bytes,1,opt,name=state,proto3" json:"state,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DescribeStreamOutput) Reset() { + *x = DescribeStreamOutput{} + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DescribeStreamOutput) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DescribeStreamOutput) ProtoMessage() {} + +func (x *DescribeStreamOutput) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[15] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DescribeStreamOutput.ProtoReflect.Descriptor instead. +func (*DescribeStreamOutput) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{15} +} + +func (x *DescribeStreamOutput) GetState() *StreamState { + if x != nil { + return x.State + } + return nil +} + +type CloseStreamInput struct { + state protoimpl.MessageState `protogen:"open.v1"` + Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"` + StreamId string `protobuf:"bytes,2,opt,name=stream_id,json=streamId,proto3" json:"stream_id,omitempty"` + Reason *v11.Payload `protobuf:"bytes,3,opt,name=reason,proto3" json:"reason,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CloseStreamInput) Reset() { + *x = CloseStreamInput{} + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CloseStreamInput) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CloseStreamInput) ProtoMessage() {} + +func (x *CloseStreamInput) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[16] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CloseStreamInput.ProtoReflect.Descriptor instead. +func (*CloseStreamInput) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{16} +} + +func (x *CloseStreamInput) GetNamespace() string { + if x != nil { + return x.Namespace + } + return "" +} + +func (x *CloseStreamInput) GetStreamId() string { + if x != nil { + return x.StreamId + } + return "" +} + +func (x *CloseStreamInput) GetReason() *v11.Payload { + if x != nil { + return x.Reason + } + return nil +} + +type CloseStreamOutput struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CloseStreamOutput) Reset() { + *x = CloseStreamOutput{} + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CloseStreamOutput) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CloseStreamOutput) ProtoMessage() {} + +func (x *CloseStreamOutput) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[17] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CloseStreamOutput.ProtoReflect.Descriptor instead. +func (*CloseStreamOutput) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{17} +} + +type TruncateStreamInput struct { + state protoimpl.MessageState `protogen:"open.v1"` + Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"` + StreamId string `protobuf:"bytes,2,opt,name=stream_id,json=streamId,proto3" json:"stream_id,omitempty"` + NewBaseOffset int64 `protobuf:"varint,3,opt,name=new_base_offset,json=newBaseOffset,proto3" json:"new_base_offset,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TruncateStreamInput) Reset() { + *x = TruncateStreamInput{} + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[18] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TruncateStreamInput) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TruncateStreamInput) ProtoMessage() {} + +func (x *TruncateStreamInput) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[18] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TruncateStreamInput.ProtoReflect.Descriptor instead. +func (*TruncateStreamInput) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{18} +} + +func (x *TruncateStreamInput) GetNamespace() string { + if x != nil { + return x.Namespace + } + return "" +} + +func (x *TruncateStreamInput) GetStreamId() string { + if x != nil { + return x.StreamId + } + return "" +} + +func (x *TruncateStreamInput) GetNewBaseOffset() int64 { + if x != nil { + return x.NewBaseOffset + } + return 0 +} + +type TruncateStreamOutput struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TruncateStreamOutput) Reset() { + *x = TruncateStreamOutput{} + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TruncateStreamOutput) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TruncateStreamOutput) ProtoMessage() {} + +func (x *TruncateStreamOutput) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[19] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TruncateStreamOutput.ProtoReflect.Descriptor instead. +func (*TruncateStreamOutput) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{19} +} + +type DeleteStreamInput struct { + state protoimpl.MessageState `protogen:"open.v1"` + Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"` + StreamId string `protobuf:"bytes,2,opt,name=stream_id,json=streamId,proto3" json:"stream_id,omitempty"` + // Delete even while a workflow consumer is active. Without it the call is + // refused, because the consumer's History depends on ranges the deletion + // takes with it. + Force bool `protobuf:"varint,3,opt,name=force,proto3" json:"force,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteStreamInput) Reset() { + *x = DeleteStreamInput{} + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[20] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteStreamInput) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteStreamInput) ProtoMessage() {} + +func (x *DeleteStreamInput) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[20] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteStreamInput.ProtoReflect.Descriptor instead. +func (*DeleteStreamInput) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{20} +} + +func (x *DeleteStreamInput) GetNamespace() string { + if x != nil { + return x.Namespace + } + return "" +} + +func (x *DeleteStreamInput) GetStreamId() string { + if x != nil { + return x.StreamId + } + return "" +} + +func (x *DeleteStreamInput) GetForce() bool { + if x != nil { + return x.Force + } + return false +} + +type DeleteStreamOutput struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteStreamOutput) Reset() { + *x = DeleteStreamOutput{} + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[21] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteStreamOutput) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteStreamOutput) ProtoMessage() {} + +func (x *DeleteStreamOutput) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[21] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteStreamOutput.ProtoReflect.Descriptor instead. +func (*DeleteStreamOutput) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{21} +} + +type CreateStreamRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + NamespaceId string `protobuf:"bytes,1,opt,name=namespace_id,json=namespaceId,proto3" json:"namespace_id,omitempty"` + FrontendRequest *CreateStreamInput `protobuf:"bytes,2,opt,name=frontend_request,json=frontendRequest,proto3" json:"frontend_request,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateStreamRequest) Reset() { + *x = CreateStreamRequest{} + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[22] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateStreamRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateStreamRequest) ProtoMessage() {} + +func (x *CreateStreamRequest) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[22] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateStreamRequest.ProtoReflect.Descriptor instead. +func (*CreateStreamRequest) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{22} +} + +func (x *CreateStreamRequest) GetNamespaceId() string { + if x != nil { + return x.NamespaceId + } + return "" +} + +func (x *CreateStreamRequest) GetFrontendRequest() *CreateStreamInput { + if x != nil { + return x.FrontendRequest + } + return nil +} + +type CreateStreamResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + FrontendResponse *CreateStreamOutput `protobuf:"bytes,1,opt,name=frontend_response,json=frontendResponse,proto3" json:"frontend_response,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateStreamResponse) Reset() { + *x = CreateStreamResponse{} + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[23] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateStreamResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateStreamResponse) ProtoMessage() {} + +func (x *CreateStreamResponse) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[23] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateStreamResponse.ProtoReflect.Descriptor instead. +func (*CreateStreamResponse) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{23} +} + +func (x *CreateStreamResponse) GetFrontendResponse() *CreateStreamOutput { + if x != nil { + return x.FrontendResponse + } + return nil +} + +type AddMessagesRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + NamespaceId string `protobuf:"bytes,1,opt,name=namespace_id,json=namespaceId,proto3" json:"namespace_id,omitempty"` + FrontendRequest *AddMessagesInput `protobuf:"bytes,2,opt,name=frontend_request,json=frontendRequest,proto3" json:"frontend_request,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AddMessagesRequest) Reset() { + *x = AddMessagesRequest{} + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[24] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AddMessagesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AddMessagesRequest) ProtoMessage() {} + +func (x *AddMessagesRequest) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[24] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AddMessagesRequest.ProtoReflect.Descriptor instead. +func (*AddMessagesRequest) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{24} +} + +func (x *AddMessagesRequest) GetNamespaceId() string { + if x != nil { + return x.NamespaceId + } + return "" +} + +func (x *AddMessagesRequest) GetFrontendRequest() *AddMessagesInput { + if x != nil { + return x.FrontendRequest + } + return nil +} + +type AddMessagesResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + FrontendResponse *AddMessagesOutput `protobuf:"bytes,1,opt,name=frontend_response,json=frontendResponse,proto3" json:"frontend_response,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AddMessagesResponse) Reset() { + *x = AddMessagesResponse{} + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[25] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AddMessagesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AddMessagesResponse) ProtoMessage() {} + +func (x *AddMessagesResponse) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[25] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AddMessagesResponse.ProtoReflect.Descriptor instead. +func (*AddMessagesResponse) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{25} +} + +func (x *AddMessagesResponse) GetFrontendResponse() *AddMessagesOutput { + if x != nil { + return x.FrontendResponse + } + return nil +} + +type FinishWritingRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + NamespaceId string `protobuf:"bytes,1,opt,name=namespace_id,json=namespaceId,proto3" json:"namespace_id,omitempty"` + FrontendRequest *FinishWritingInput `protobuf:"bytes,2,opt,name=frontend_request,json=frontendRequest,proto3" json:"frontend_request,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *FinishWritingRequest) Reset() { + *x = FinishWritingRequest{} + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[26] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *FinishWritingRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FinishWritingRequest) ProtoMessage() {} + +func (x *FinishWritingRequest) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[26] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FinishWritingRequest.ProtoReflect.Descriptor instead. +func (*FinishWritingRequest) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{26} +} + +func (x *FinishWritingRequest) GetNamespaceId() string { + if x != nil { + return x.NamespaceId + } + return "" +} + +func (x *FinishWritingRequest) GetFrontendRequest() *FinishWritingInput { + if x != nil { + return x.FrontendRequest + } + return nil +} + +type FinishWritingResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + FrontendResponse *FinishWritingOutput `protobuf:"bytes,1,opt,name=frontend_response,json=frontendResponse,proto3" json:"frontend_response,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *FinishWritingResponse) Reset() { + *x = FinishWritingResponse{} + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[27] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *FinishWritingResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FinishWritingResponse) ProtoMessage() {} + +func (x *FinishWritingResponse) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[27] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FinishWritingResponse.ProtoReflect.Descriptor instead. +func (*FinishWritingResponse) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{27} +} + +func (x *FinishWritingResponse) GetFrontendResponse() *FinishWritingOutput { + if x != nil { + return x.FrontendResponse + } + return nil +} + +type SubscribeWorkflowRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + NamespaceId string `protobuf:"bytes,1,opt,name=namespace_id,json=namespaceId,proto3" json:"namespace_id,omitempty"` + FrontendRequest *SubscribeWorkflowInput `protobuf:"bytes,2,opt,name=frontend_request,json=frontendRequest,proto3" json:"frontend_request,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SubscribeWorkflowRequest) Reset() { + *x = SubscribeWorkflowRequest{} + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[28] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SubscribeWorkflowRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SubscribeWorkflowRequest) ProtoMessage() {} + +func (x *SubscribeWorkflowRequest) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[28] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SubscribeWorkflowRequest.ProtoReflect.Descriptor instead. +func (*SubscribeWorkflowRequest) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{28} +} + +func (x *SubscribeWorkflowRequest) GetNamespaceId() string { + if x != nil { + return x.NamespaceId + } + return "" +} + +func (x *SubscribeWorkflowRequest) GetFrontendRequest() *SubscribeWorkflowInput { + if x != nil { + return x.FrontendRequest + } + return nil +} + +type SubscribeWorkflowResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + FrontendResponse *SubscribeWorkflowOutput `protobuf:"bytes,1,opt,name=frontend_response,json=frontendResponse,proto3" json:"frontend_response,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SubscribeWorkflowResponse) Reset() { + *x = SubscribeWorkflowResponse{} + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[29] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SubscribeWorkflowResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SubscribeWorkflowResponse) ProtoMessage() {} + +func (x *SubscribeWorkflowResponse) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[29] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SubscribeWorkflowResponse.ProtoReflect.Descriptor instead. +func (*SubscribeWorkflowResponse) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{29} +} + +func (x *SubscribeWorkflowResponse) GetFrontendResponse() *SubscribeWorkflowOutput { + if x != nil { + return x.FrontendResponse + } + return nil +} + +type PollMessagesRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + NamespaceId string `protobuf:"bytes,1,opt,name=namespace_id,json=namespaceId,proto3" json:"namespace_id,omitempty"` + FrontendRequest *PollMessagesInput `protobuf:"bytes,2,opt,name=frontend_request,json=frontendRequest,proto3" json:"frontend_request,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PollMessagesRequest) Reset() { + *x = PollMessagesRequest{} + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[30] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PollMessagesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PollMessagesRequest) ProtoMessage() {} + +func (x *PollMessagesRequest) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[30] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PollMessagesRequest.ProtoReflect.Descriptor instead. +func (*PollMessagesRequest) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{30} +} + +func (x *PollMessagesRequest) GetNamespaceId() string { + if x != nil { + return x.NamespaceId + } + return "" +} + +func (x *PollMessagesRequest) GetFrontendRequest() *PollMessagesInput { + if x != nil { + return x.FrontendRequest + } + return nil +} + +type PollMessagesResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + FrontendResponse *PollMessagesOutput `protobuf:"bytes,1,opt,name=frontend_response,json=frontendResponse,proto3" json:"frontend_response,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PollMessagesResponse) Reset() { + *x = PollMessagesResponse{} + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[31] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PollMessagesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PollMessagesResponse) ProtoMessage() {} + +func (x *PollMessagesResponse) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[31] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PollMessagesResponse.ProtoReflect.Descriptor instead. +func (*PollMessagesResponse) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{31} +} + +func (x *PollMessagesResponse) GetFrontendResponse() *PollMessagesOutput { + if x != nil { + return x.FrontendResponse + } + return nil +} + +type DescribeStreamRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + NamespaceId string `protobuf:"bytes,1,opt,name=namespace_id,json=namespaceId,proto3" json:"namespace_id,omitempty"` + FrontendRequest *DescribeStreamInput `protobuf:"bytes,2,opt,name=frontend_request,json=frontendRequest,proto3" json:"frontend_request,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DescribeStreamRequest) Reset() { + *x = DescribeStreamRequest{} + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[32] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DescribeStreamRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DescribeStreamRequest) ProtoMessage() {} + +func (x *DescribeStreamRequest) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[32] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DescribeStreamRequest.ProtoReflect.Descriptor instead. +func (*DescribeStreamRequest) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{32} +} + +func (x *DescribeStreamRequest) GetNamespaceId() string { + if x != nil { + return x.NamespaceId + } + return "" +} + +func (x *DescribeStreamRequest) GetFrontendRequest() *DescribeStreamInput { + if x != nil { + return x.FrontendRequest + } + return nil +} + +type DescribeStreamResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + FrontendResponse *DescribeStreamOutput `protobuf:"bytes,1,opt,name=frontend_response,json=frontendResponse,proto3" json:"frontend_response,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DescribeStreamResponse) Reset() { + *x = DescribeStreamResponse{} + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[33] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DescribeStreamResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DescribeStreamResponse) ProtoMessage() {} + +func (x *DescribeStreamResponse) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[33] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DescribeStreamResponse.ProtoReflect.Descriptor instead. +func (*DescribeStreamResponse) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{33} +} + +func (x *DescribeStreamResponse) GetFrontendResponse() *DescribeStreamOutput { + if x != nil { + return x.FrontendResponse + } + return nil +} + +type PollWorkflowMessagesRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + NamespaceId string `protobuf:"bytes,1,opt,name=namespace_id,json=namespaceId,proto3" json:"namespace_id,omitempty"` + FrontendRequest *PollWorkflowMessagesInput `protobuf:"bytes,2,opt,name=frontend_request,json=frontendRequest,proto3" json:"frontend_request,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PollWorkflowMessagesRequest) Reset() { + *x = PollWorkflowMessagesRequest{} + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[34] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PollWorkflowMessagesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PollWorkflowMessagesRequest) ProtoMessage() {} + +func (x *PollWorkflowMessagesRequest) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[34] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PollWorkflowMessagesRequest.ProtoReflect.Descriptor instead. +func (*PollWorkflowMessagesRequest) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{34} +} + +func (x *PollWorkflowMessagesRequest) GetNamespaceId() string { + if x != nil { + return x.NamespaceId + } + return "" +} + +func (x *PollWorkflowMessagesRequest) GetFrontendRequest() *PollWorkflowMessagesInput { + if x != nil { + return x.FrontendRequest + } + return nil +} + +type PollWorkflowMessagesResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + FrontendResponse *PollMessagesOutput `protobuf:"bytes,1,opt,name=frontend_response,json=frontendResponse,proto3" json:"frontend_response,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PollWorkflowMessagesResponse) Reset() { + *x = PollWorkflowMessagesResponse{} + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[35] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PollWorkflowMessagesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PollWorkflowMessagesResponse) ProtoMessage() {} + +func (x *PollWorkflowMessagesResponse) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[35] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PollWorkflowMessagesResponse.ProtoReflect.Descriptor instead. +func (*PollWorkflowMessagesResponse) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{35} +} + +func (x *PollWorkflowMessagesResponse) GetFrontendResponse() *PollMessagesOutput { + if x != nil { + return x.FrontendResponse + } + return nil +} + +type DescribeWorkflowStreamRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + NamespaceId string `protobuf:"bytes,1,opt,name=namespace_id,json=namespaceId,proto3" json:"namespace_id,omitempty"` + FrontendRequest *DescribeWorkflowStreamInput `protobuf:"bytes,2,opt,name=frontend_request,json=frontendRequest,proto3" json:"frontend_request,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DescribeWorkflowStreamRequest) Reset() { + *x = DescribeWorkflowStreamRequest{} + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[36] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DescribeWorkflowStreamRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DescribeWorkflowStreamRequest) ProtoMessage() {} + +func (x *DescribeWorkflowStreamRequest) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[36] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DescribeWorkflowStreamRequest.ProtoReflect.Descriptor instead. +func (*DescribeWorkflowStreamRequest) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{36} +} + +func (x *DescribeWorkflowStreamRequest) GetNamespaceId() string { + if x != nil { + return x.NamespaceId + } + return "" +} + +func (x *DescribeWorkflowStreamRequest) GetFrontendRequest() *DescribeWorkflowStreamInput { + if x != nil { + return x.FrontendRequest + } + return nil +} + +type DescribeWorkflowStreamResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + FrontendResponse *DescribeStreamOutput `protobuf:"bytes,1,opt,name=frontend_response,json=frontendResponse,proto3" json:"frontend_response,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DescribeWorkflowStreamResponse) Reset() { + *x = DescribeWorkflowStreamResponse{} + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[37] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DescribeWorkflowStreamResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DescribeWorkflowStreamResponse) ProtoMessage() {} + +func (x *DescribeWorkflowStreamResponse) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[37] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DescribeWorkflowStreamResponse.ProtoReflect.Descriptor instead. +func (*DescribeWorkflowStreamResponse) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{37} +} + +func (x *DescribeWorkflowStreamResponse) GetFrontendResponse() *DescribeStreamOutput { + if x != nil { + return x.FrontendResponse + } + return nil +} + +// Registering a consumer on a stream in another execution. Split out from +// SubscribeWorkflow because the two halves live on different shards: the pin +// goes on the stream, the cursor goes on the consuming workflow, and a handler +// can only reach the shard it was routed to. +type RegisterStreamConsumerInput struct { + state protoimpl.MessageState `protogen:"open.v1"` + Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"` + StreamId string `protobuf:"bytes,2,opt,name=stream_id,json=streamId,proto3" json:"stream_id,omitempty"` + // The workflow that will consume. Together with the run it names the pin, + // so a later run of the same workflow id registers fresh rather than + // inheriting a closed run's floor. + ConsumerWorkflowId string `protobuf:"bytes,3,opt,name=consumer_workflow_id,json=consumerWorkflowId,proto3" json:"consumer_workflow_id,omitempty"` + ConsumerRunId string `protobuf:"bytes,5,opt,name=consumer_run_id,json=consumerRunId,proto3" json:"consumer_run_id,omitempty"` + // An absolute offset, read only when start_position is unset. A negative + // value is refused. + StartOffset int64 `protobuf:"varint,4,opt,name=start_offset,json=startOffset,proto3" json:"start_offset,omitempty"` + // Resolved here, where the frontier is, and returned as start_offset on the + // output so the cursor records a fact. + StartPosition *v1.StreamStartPosition `protobuf:"bytes,6,opt,name=start_position,json=startPosition,proto3" json:"start_position,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RegisterStreamConsumerInput) Reset() { + *x = RegisterStreamConsumerInput{} + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[38] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RegisterStreamConsumerInput) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RegisterStreamConsumerInput) ProtoMessage() {} + +func (x *RegisterStreamConsumerInput) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[38] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RegisterStreamConsumerInput.ProtoReflect.Descriptor instead. +func (*RegisterStreamConsumerInput) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{38} +} + +func (x *RegisterStreamConsumerInput) GetNamespace() string { + if x != nil { + return x.Namespace + } + return "" +} + +func (x *RegisterStreamConsumerInput) GetStreamId() string { + if x != nil { + return x.StreamId + } + return "" +} + +func (x *RegisterStreamConsumerInput) GetConsumerWorkflowId() string { + if x != nil { + return x.ConsumerWorkflowId + } + return "" +} + +func (x *RegisterStreamConsumerInput) GetConsumerRunId() string { + if x != nil { + return x.ConsumerRunId + } + return "" +} + +func (x *RegisterStreamConsumerInput) GetStartOffset() int64 { + if x != nil { + return x.StartOffset + } + return 0 +} + +func (x *RegisterStreamConsumerInput) GetStartPosition() *v1.StreamStartPosition { + if x != nil { + return x.StartPosition + } + return nil +} + +type RegisterStreamConsumerOutput struct { + state protoimpl.MessageState `protogen:"open.v1"` + StartOffset int64 `protobuf:"varint,1,opt,name=start_offset,json=startOffset,proto3" json:"start_offset,omitempty"` + // The frontier at registration, so the cursor starts with a known head + // instead of waiting for the first push. + KnownHead int64 `protobuf:"varint,4,opt,name=known_head,json=knownHead,proto3" json:"known_head,omitempty"` + // No execution in this namespace holds a stream with that id, so nothing + // was registered. An answer rather than a NotFound because the caller acts + // on it: a subscribe command falls back to a stream of the workflow's own by + // that name. Every other NotFound, from a registry miss to a shard that has + // moved, stays an error, since binding the workflow to different data on one + // of those would be silent and permanent. + StreamAbsent bool `protobuf:"varint,5,opt,name=stream_absent,json=streamAbsent,proto3" json:"stream_absent,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RegisterStreamConsumerOutput) Reset() { + *x = RegisterStreamConsumerOutput{} + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[39] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RegisterStreamConsumerOutput) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RegisterStreamConsumerOutput) ProtoMessage() {} + +func (x *RegisterStreamConsumerOutput) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[39] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RegisterStreamConsumerOutput.ProtoReflect.Descriptor instead. +func (*RegisterStreamConsumerOutput) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{39} +} + +func (x *RegisterStreamConsumerOutput) GetStartOffset() int64 { + if x != nil { + return x.StartOffset + } + return 0 +} + +func (x *RegisterStreamConsumerOutput) GetKnownHead() int64 { + if x != nil { + return x.KnownHead + } + return 0 +} + +func (x *RegisterStreamConsumerOutput) GetStreamAbsent() bool { + if x != nil { + return x.StreamAbsent + } + return false +} + +// Telling one consumer that the frontier moved. Routed to the consumer, which +// is not where the stream lives. +type AdvanceConsumerHeadInput struct { + state protoimpl.MessageState `protogen:"open.v1"` + Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"` + WorkflowId string `protobuf:"bytes,2,opt,name=workflow_id,json=workflowId,proto3" json:"workflow_id,omitempty"` + // The run that subscribed. A successor from continue-as-new holds no cursor + // for this stream, so pushing the frontier at it would land nowhere. + OwnerRunId string `protobuf:"bytes,5,opt,name=owner_run_id,json=ownerRunId,proto3" json:"owner_run_id,omitempty"` + StreamId string `protobuf:"bytes,3,opt,name=stream_id,json=streamId,proto3" json:"stream_id,omitempty"` + HeadOffset int64 `protobuf:"varint,4,opt,name=head_offset,json=headOffset,proto3" json:"head_offset,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AdvanceConsumerHeadInput) Reset() { + *x = AdvanceConsumerHeadInput{} + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[40] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AdvanceConsumerHeadInput) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AdvanceConsumerHeadInput) ProtoMessage() {} + +func (x *AdvanceConsumerHeadInput) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[40] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AdvanceConsumerHeadInput.ProtoReflect.Descriptor instead. +func (*AdvanceConsumerHeadInput) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{40} +} + +func (x *AdvanceConsumerHeadInput) GetNamespace() string { + if x != nil { + return x.Namespace + } + return "" +} + +func (x *AdvanceConsumerHeadInput) GetWorkflowId() string { + if x != nil { + return x.WorkflowId + } + return "" +} + +func (x *AdvanceConsumerHeadInput) GetOwnerRunId() string { + if x != nil { + return x.OwnerRunId + } + return "" +} + +func (x *AdvanceConsumerHeadInput) GetStreamId() string { + if x != nil { + return x.StreamId + } + return "" +} + +func (x *AdvanceConsumerHeadInput) GetHeadOffset() int64 { + if x != nil { + return x.HeadOffset + } + return 0 +} + +type AdvanceConsumerHeadOutput struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The run that held the pin is closed and no current run of the workflow + // consumes the stream, so the stream can release the pin. + ConsumerClosed bool `protobuf:"varint,1,opt,name=consumer_closed,json=consumerClosed,proto3" json:"consumer_closed,omitempty"` + // The run that held the pin is closed but a successor carries the + // subscription, so the stream re-keys the pin to it, with the floor the + // successor's cursor started from. + SuccessorRunId string `protobuf:"bytes,2,opt,name=successor_run_id,json=successorRunId,proto3" json:"successor_run_id,omitempty"` + SuccessorStartOffset int64 `protobuf:"varint,3,opt,name=successor_start_offset,json=successorStartOffset,proto3" json:"successor_start_offset,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AdvanceConsumerHeadOutput) Reset() { + *x = AdvanceConsumerHeadOutput{} + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[41] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AdvanceConsumerHeadOutput) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AdvanceConsumerHeadOutput) ProtoMessage() {} + +func (x *AdvanceConsumerHeadOutput) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[41] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AdvanceConsumerHeadOutput.ProtoReflect.Descriptor instead. +func (*AdvanceConsumerHeadOutput) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{41} +} + +func (x *AdvanceConsumerHeadOutput) GetConsumerClosed() bool { + if x != nil { + return x.ConsumerClosed + } + return false +} + +func (x *AdvanceConsumerHeadOutput) GetSuccessorRunId() string { + if x != nil { + return x.SuccessorRunId + } + return "" +} + +func (x *AdvanceConsumerHeadOutput) GetSuccessorStartOffset() int64 { + if x != nil { + return x.SuccessorStartOffset + } + return 0 +} + +type AddWorkflowMessagesRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + NamespaceId string `protobuf:"bytes,1,opt,name=namespace_id,json=namespaceId,proto3" json:"namespace_id,omitempty"` + FrontendRequest *AddWorkflowMessagesInput `protobuf:"bytes,2,opt,name=frontend_request,json=frontendRequest,proto3" json:"frontend_request,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AddWorkflowMessagesRequest) Reset() { + *x = AddWorkflowMessagesRequest{} + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[42] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AddWorkflowMessagesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AddWorkflowMessagesRequest) ProtoMessage() {} + +func (x *AddWorkflowMessagesRequest) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[42] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AddWorkflowMessagesRequest.ProtoReflect.Descriptor instead. +func (*AddWorkflowMessagesRequest) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{42} +} + +func (x *AddWorkflowMessagesRequest) GetNamespaceId() string { + if x != nil { + return x.NamespaceId + } + return "" +} + +func (x *AddWorkflowMessagesRequest) GetFrontendRequest() *AddWorkflowMessagesInput { + if x != nil { + return x.FrontendRequest + } + return nil +} + +type AddWorkflowMessagesResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + FrontendResponse *AddMessagesOutput `protobuf:"bytes,1,opt,name=frontend_response,json=frontendResponse,proto3" json:"frontend_response,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AddWorkflowMessagesResponse) Reset() { + *x = AddWorkflowMessagesResponse{} + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[43] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AddWorkflowMessagesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AddWorkflowMessagesResponse) ProtoMessage() {} + +func (x *AddWorkflowMessagesResponse) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[43] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AddWorkflowMessagesResponse.ProtoReflect.Descriptor instead. +func (*AddWorkflowMessagesResponse) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{43} +} + +func (x *AddWorkflowMessagesResponse) GetFrontendResponse() *AddMessagesOutput { + if x != nil { + return x.FrontendResponse + } + return nil +} + +type RegisterStreamConsumerRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + NamespaceId string `protobuf:"bytes,1,opt,name=namespace_id,json=namespaceId,proto3" json:"namespace_id,omitempty"` + FrontendRequest *RegisterStreamConsumerInput `protobuf:"bytes,2,opt,name=frontend_request,json=frontendRequest,proto3" json:"frontend_request,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RegisterStreamConsumerRequest) Reset() { + *x = RegisterStreamConsumerRequest{} + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[44] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RegisterStreamConsumerRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RegisterStreamConsumerRequest) ProtoMessage() {} + +func (x *RegisterStreamConsumerRequest) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[44] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RegisterStreamConsumerRequest.ProtoReflect.Descriptor instead. +func (*RegisterStreamConsumerRequest) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{44} +} + +func (x *RegisterStreamConsumerRequest) GetNamespaceId() string { + if x != nil { + return x.NamespaceId + } + return "" +} + +func (x *RegisterStreamConsumerRequest) GetFrontendRequest() *RegisterStreamConsumerInput { + if x != nil { + return x.FrontendRequest + } + return nil +} + +type RegisterStreamConsumerResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + FrontendResponse *RegisterStreamConsumerOutput `protobuf:"bytes,1,opt,name=frontend_response,json=frontendResponse,proto3" json:"frontend_response,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RegisterStreamConsumerResponse) Reset() { + *x = RegisterStreamConsumerResponse{} + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[45] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RegisterStreamConsumerResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RegisterStreamConsumerResponse) ProtoMessage() {} + +func (x *RegisterStreamConsumerResponse) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[45] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RegisterStreamConsumerResponse.ProtoReflect.Descriptor instead. +func (*RegisterStreamConsumerResponse) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{45} +} + +func (x *RegisterStreamConsumerResponse) GetFrontendResponse() *RegisterStreamConsumerOutput { + if x != nil { + return x.FrontendResponse + } + return nil +} + +type AdvanceConsumerHeadRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + NamespaceId string `protobuf:"bytes,1,opt,name=namespace_id,json=namespaceId,proto3" json:"namespace_id,omitempty"` + FrontendRequest *AdvanceConsumerHeadInput `protobuf:"bytes,2,opt,name=frontend_request,json=frontendRequest,proto3" json:"frontend_request,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AdvanceConsumerHeadRequest) Reset() { + *x = AdvanceConsumerHeadRequest{} + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[46] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AdvanceConsumerHeadRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AdvanceConsumerHeadRequest) ProtoMessage() {} + +func (x *AdvanceConsumerHeadRequest) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[46] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AdvanceConsumerHeadRequest.ProtoReflect.Descriptor instead. +func (*AdvanceConsumerHeadRequest) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{46} +} + +func (x *AdvanceConsumerHeadRequest) GetNamespaceId() string { + if x != nil { + return x.NamespaceId + } + return "" +} + +func (x *AdvanceConsumerHeadRequest) GetFrontendRequest() *AdvanceConsumerHeadInput { + if x != nil { + return x.FrontendRequest + } + return nil +} + +type AdvanceConsumerHeadResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + FrontendResponse *AdvanceConsumerHeadOutput `protobuf:"bytes,1,opt,name=frontend_response,json=frontendResponse,proto3" json:"frontend_response,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AdvanceConsumerHeadResponse) Reset() { + *x = AdvanceConsumerHeadResponse{} + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[47] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AdvanceConsumerHeadResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AdvanceConsumerHeadResponse) ProtoMessage() {} + +func (x *AdvanceConsumerHeadResponse) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[47] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AdvanceConsumerHeadResponse.ProtoReflect.Descriptor instead. +func (*AdvanceConsumerHeadResponse) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{47} +} + +func (x *AdvanceConsumerHeadResponse) GetFrontendResponse() *AdvanceConsumerHeadOutput { + if x != nil { + return x.FrontendResponse + } + return nil +} + +type CloseStreamRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + NamespaceId string `protobuf:"bytes,1,opt,name=namespace_id,json=namespaceId,proto3" json:"namespace_id,omitempty"` + FrontendRequest *CloseStreamInput `protobuf:"bytes,2,opt,name=frontend_request,json=frontendRequest,proto3" json:"frontend_request,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CloseStreamRequest) Reset() { + *x = CloseStreamRequest{} + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[48] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CloseStreamRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CloseStreamRequest) ProtoMessage() {} + +func (x *CloseStreamRequest) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[48] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CloseStreamRequest.ProtoReflect.Descriptor instead. +func (*CloseStreamRequest) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{48} +} + +func (x *CloseStreamRequest) GetNamespaceId() string { + if x != nil { + return x.NamespaceId + } + return "" +} + +func (x *CloseStreamRequest) GetFrontendRequest() *CloseStreamInput { + if x != nil { + return x.FrontendRequest + } + return nil +} + +type CloseStreamResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + FrontendResponse *CloseStreamOutput `protobuf:"bytes,1,opt,name=frontend_response,json=frontendResponse,proto3" json:"frontend_response,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CloseStreamResponse) Reset() { + *x = CloseStreamResponse{} + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[49] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CloseStreamResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CloseStreamResponse) ProtoMessage() {} + +func (x *CloseStreamResponse) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[49] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CloseStreamResponse.ProtoReflect.Descriptor instead. +func (*CloseStreamResponse) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{49} +} + +func (x *CloseStreamResponse) GetFrontendResponse() *CloseStreamOutput { + if x != nil { + return x.FrontendResponse + } + return nil +} + +type TruncateStreamRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + NamespaceId string `protobuf:"bytes,1,opt,name=namespace_id,json=namespaceId,proto3" json:"namespace_id,omitempty"` + FrontendRequest *TruncateStreamInput `protobuf:"bytes,2,opt,name=frontend_request,json=frontendRequest,proto3" json:"frontend_request,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TruncateStreamRequest) Reset() { + *x = TruncateStreamRequest{} + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[50] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TruncateStreamRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TruncateStreamRequest) ProtoMessage() {} + +func (x *TruncateStreamRequest) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[50] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TruncateStreamRequest.ProtoReflect.Descriptor instead. +func (*TruncateStreamRequest) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{50} +} + +func (x *TruncateStreamRequest) GetNamespaceId() string { + if x != nil { + return x.NamespaceId + } + return "" +} + +func (x *TruncateStreamRequest) GetFrontendRequest() *TruncateStreamInput { + if x != nil { + return x.FrontendRequest + } + return nil +} + +type TruncateStreamResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + FrontendResponse *TruncateStreamOutput `protobuf:"bytes,1,opt,name=frontend_response,json=frontendResponse,proto3" json:"frontend_response,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TruncateStreamResponse) Reset() { + *x = TruncateStreamResponse{} + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[51] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TruncateStreamResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TruncateStreamResponse) ProtoMessage() {} + +func (x *TruncateStreamResponse) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[51] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TruncateStreamResponse.ProtoReflect.Descriptor instead. +func (*TruncateStreamResponse) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{51} +} + +func (x *TruncateStreamResponse) GetFrontendResponse() *TruncateStreamOutput { + if x != nil { + return x.FrontendResponse + } + return nil +} + +type ListStreamsInput struct { + state protoimpl.MessageState `protogen:"open.v1"` + Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"` + PageSize int32 `protobuf:"varint,2,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` + NextPageToken []byte `protobuf:"bytes,3,opt,name=next_page_token,json=nextPageToken,proto3" json:"next_page_token,omitempty"` + Query string `protobuf:"bytes,4,opt,name=query,proto3" json:"query,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListStreamsInput) Reset() { + *x = ListStreamsInput{} + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[52] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListStreamsInput) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListStreamsInput) ProtoMessage() {} + +func (x *ListStreamsInput) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[52] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListStreamsInput.ProtoReflect.Descriptor instead. +func (*ListStreamsInput) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{52} +} + +func (x *ListStreamsInput) GetNamespace() string { + if x != nil { + return x.Namespace + } + return "" +} + +func (x *ListStreamsInput) GetPageSize() int32 { + if x != nil { + return x.PageSize + } + return 0 +} + +func (x *ListStreamsInput) GetNextPageToken() []byte { + if x != nil { + return x.NextPageToken + } + return nil +} + +func (x *ListStreamsInput) GetQuery() string { + if x != nil { + return x.Query + } + return "" +} + +type StreamListEntry struct { + state protoimpl.MessageState `protogen:"open.v1"` + StreamId string `protobuf:"bytes,1,opt,name=stream_id,json=streamId,proto3" json:"stream_id,omitempty"` + RunId string `protobuf:"bytes,2,opt,name=run_id,json=runId,proto3" json:"run_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StreamListEntry) Reset() { + *x = StreamListEntry{} + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[53] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StreamListEntry) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StreamListEntry) ProtoMessage() {} + +func (x *StreamListEntry) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[53] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StreamListEntry.ProtoReflect.Descriptor instead. +func (*StreamListEntry) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{53} +} + +func (x *StreamListEntry) GetStreamId() string { + if x != nil { + return x.StreamId + } + return "" +} + +func (x *StreamListEntry) GetRunId() string { + if x != nil { + return x.RunId + } + return "" +} + +type ListStreamsOutput struct { + state protoimpl.MessageState `protogen:"open.v1"` + Streams []*StreamListEntry `protobuf:"bytes,1,rep,name=streams,proto3" json:"streams,omitempty"` + NextPageToken []byte `protobuf:"bytes,2,opt,name=next_page_token,json=nextPageToken,proto3" json:"next_page_token,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListStreamsOutput) Reset() { + *x = ListStreamsOutput{} + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[54] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListStreamsOutput) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListStreamsOutput) ProtoMessage() {} + +func (x *ListStreamsOutput) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[54] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListStreamsOutput.ProtoReflect.Descriptor instead. +func (*ListStreamsOutput) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{54} +} + +func (x *ListStreamsOutput) GetStreams() []*StreamListEntry { + if x != nil { + return x.Streams + } + return nil +} + +func (x *ListStreamsOutput) GetNextPageToken() []byte { + if x != nil { + return x.NextPageToken + } + return nil +} + +type ListStreamsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + NamespaceId string `protobuf:"bytes,1,opt,name=namespace_id,json=namespaceId,proto3" json:"namespace_id,omitempty"` + FrontendRequest *ListStreamsInput `protobuf:"bytes,2,opt,name=frontend_request,json=frontendRequest,proto3" json:"frontend_request,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListStreamsRequest) Reset() { + *x = ListStreamsRequest{} + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[55] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListStreamsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListStreamsRequest) ProtoMessage() {} + +func (x *ListStreamsRequest) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[55] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListStreamsRequest.ProtoReflect.Descriptor instead. +func (*ListStreamsRequest) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{55} +} + +func (x *ListStreamsRequest) GetNamespaceId() string { + if x != nil { + return x.NamespaceId + } + return "" +} + +func (x *ListStreamsRequest) GetFrontendRequest() *ListStreamsInput { + if x != nil { + return x.FrontendRequest + } + return nil +} + +type ListStreamsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + FrontendResponse *ListStreamsOutput `protobuf:"bytes,1,opt,name=frontend_response,json=frontendResponse,proto3" json:"frontend_response,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListStreamsResponse) Reset() { + *x = ListStreamsResponse{} + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[56] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListStreamsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListStreamsResponse) ProtoMessage() {} + +func (x *ListStreamsResponse) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[56] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListStreamsResponse.ProtoReflect.Descriptor instead. +func (*ListStreamsResponse) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{56} +} + +func (x *ListStreamsResponse) GetFrontendResponse() *ListStreamsOutput { + if x != nil { + return x.FrontendResponse + } + return nil +} + +type DeleteStreamRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + NamespaceId string `protobuf:"bytes,1,opt,name=namespace_id,json=namespaceId,proto3" json:"namespace_id,omitempty"` + FrontendRequest *DeleteStreamInput `protobuf:"bytes,2,opt,name=frontend_request,json=frontendRequest,proto3" json:"frontend_request,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteStreamRequest) Reset() { + *x = DeleteStreamRequest{} + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[57] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteStreamRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteStreamRequest) ProtoMessage() {} + +func (x *DeleteStreamRequest) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[57] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteStreamRequest.ProtoReflect.Descriptor instead. +func (*DeleteStreamRequest) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{57} +} + +func (x *DeleteStreamRequest) GetNamespaceId() string { + if x != nil { + return x.NamespaceId + } + return "" +} + +func (x *DeleteStreamRequest) GetFrontendRequest() *DeleteStreamInput { + if x != nil { + return x.FrontendRequest + } + return nil +} + +type DeleteStreamResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + FrontendResponse *DeleteStreamOutput `protobuf:"bytes,1,opt,name=frontend_response,json=frontendResponse,proto3" json:"frontend_response,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteStreamResponse) Reset() { + *x = DeleteStreamResponse{} + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[58] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteStreamResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteStreamResponse) ProtoMessage() {} + +func (x *DeleteStreamResponse) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[58] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteStreamResponse.ProtoReflect.Descriptor instead. +func (*DeleteStreamResponse) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{58} +} + +func (x *DeleteStreamResponse) GetFrontendResponse() *DeleteStreamOutput { + if x != nil { + return x.FrontendResponse + } + return nil +} + +var File_temporal_server_chasm_lib_stream_proto_v1_request_response_proto protoreflect.FileDescriptor + +const file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDesc = "" + + "\n" + + "@temporal/server/chasm/lib/stream/proto/v1/request_response.proto\x12)temporal.server.chasm.lib.stream.proto.v1\x1a7temporal/server/chasm/lib/stream/proto/v1/message.proto\x1a.temporal.server.chasm.lib.stream.proto.v1.FinishWritingOutputR\x10frontendResponse\"\xab\x01\n" + + "\x18SubscribeWorkflowRequest\x12!\n" + + "\fnamespace_id\x18\x01 \x01(\tR\vnamespaceId\x12l\n" + + "\x10frontend_request\x18\x02 \x01(\v2A.temporal.server.chasm.lib.stream.proto.v1.SubscribeWorkflowInputR\x0ffrontendRequest\"\x8c\x01\n" + + "\x19SubscribeWorkflowResponse\x12o\n" + + "\x11frontend_response\x18\x01 \x01(\v2B.temporal.server.chasm.lib.stream.proto.v1.SubscribeWorkflowOutputR\x10frontendResponse\"\xa1\x01\n" + + "\x13PollMessagesRequest\x12!\n" + + "\fnamespace_id\x18\x01 \x01(\tR\vnamespaceId\x12g\n" + + "\x10frontend_request\x18\x02 \x01(\v2<.temporal.server.chasm.lib.stream.proto.v1.PollMessagesInputR\x0ffrontendRequest\"\x82\x01\n" + + "\x14PollMessagesResponse\x12j\n" + + "\x11frontend_response\x18\x01 \x01(\v2=.temporal.server.chasm.lib.stream.proto.v1.PollMessagesOutputR\x10frontendResponse\"\xa5\x01\n" + + "\x15DescribeStreamRequest\x12!\n" + + "\fnamespace_id\x18\x01 \x01(\tR\vnamespaceId\x12i\n" + + "\x10frontend_request\x18\x02 \x01(\v2>.temporal.server.chasm.lib.stream.proto.v1.DescribeStreamInputR\x0ffrontendRequest\"\x86\x01\n" + + "\x16DescribeStreamResponse\x12l\n" + + "\x11frontend_response\x18\x01 \x01(\v2?.temporal.server.chasm.lib.stream.proto.v1.DescribeStreamOutputR\x10frontendResponse\"\xb1\x01\n" + + "\x1bPollWorkflowMessagesRequest\x12!\n" + + "\fnamespace_id\x18\x01 \x01(\tR\vnamespaceId\x12o\n" + + "\x10frontend_request\x18\x02 \x01(\v2D.temporal.server.chasm.lib.stream.proto.v1.PollWorkflowMessagesInputR\x0ffrontendRequest\"\x8a\x01\n" + + "\x1cPollWorkflowMessagesResponse\x12j\n" + + "\x11frontend_response\x18\x01 \x01(\v2=.temporal.server.chasm.lib.stream.proto.v1.PollMessagesOutputR\x10frontendResponse\"\xb5\x01\n" + + "\x1dDescribeWorkflowStreamRequest\x12!\n" + + "\fnamespace_id\x18\x01 \x01(\tR\vnamespaceId\x12q\n" + + "\x10frontend_request\x18\x02 \x01(\v2F.temporal.server.chasm.lib.stream.proto.v1.DescribeWorkflowStreamInputR\x0ffrontendRequest\"\x8e\x01\n" + + "\x1eDescribeWorkflowStreamResponse\x12l\n" + + "\x11frontend_response\x18\x01 \x01(\v2?.temporal.server.chasm.lib.stream.proto.v1.DescribeStreamOutputR\x10frontendResponse\"\xa9\x02\n" + + "\x1bRegisterStreamConsumerInput\x12\x1c\n" + + "\tnamespace\x18\x01 \x01(\tR\tnamespace\x12\x1b\n" + + "\tstream_id\x18\x02 \x01(\tR\bstreamId\x120\n" + + "\x14consumer_workflow_id\x18\x03 \x01(\tR\x12consumerWorkflowId\x12&\n" + + "\x0fconsumer_run_id\x18\x05 \x01(\tR\rconsumerRunId\x12!\n" + + "\fstart_offset\x18\x04 \x01(\x03R\vstartOffset\x12R\n" + + "\x0estart_position\x18\x06 \x01(\v2+.temporal.api.stream.v1.StreamStartPositionR\rstartPosition\"\x91\x01\n" + + "\x1cRegisterStreamConsumerOutput\x12!\n" + + "\fstart_offset\x18\x01 \x01(\x03R\vstartOffset\x12\x1d\n" + + "\n" + + "known_head\x18\x04 \x01(\x03R\tknownHead\x12#\n" + + "\rstream_absent\x18\x05 \x01(\bR\fstreamAbsentJ\x04\b\x02\x10\x03J\x04\b\x03\x10\x04\"\xb9\x01\n" + + "\x18AdvanceConsumerHeadInput\x12\x1c\n" + + "\tnamespace\x18\x01 \x01(\tR\tnamespace\x12\x1f\n" + + "\vworkflow_id\x18\x02 \x01(\tR\n" + + "workflowId\x12 \n" + + "\fowner_run_id\x18\x05 \x01(\tR\n" + + "ownerRunId\x12\x1b\n" + + "\tstream_id\x18\x03 \x01(\tR\bstreamId\x12\x1f\n" + + "\vhead_offset\x18\x04 \x01(\x03R\n" + + "headOffset\"\xa4\x01\n" + + "\x19AdvanceConsumerHeadOutput\x12'\n" + + "\x0fconsumer_closed\x18\x01 \x01(\bR\x0econsumerClosed\x12(\n" + + "\x10successor_run_id\x18\x02 \x01(\tR\x0esuccessorRunId\x124\n" + + "\x16successor_start_offset\x18\x03 \x01(\x03R\x14successorStartOffset\"\xaf\x01\n" + + "\x1aAddWorkflowMessagesRequest\x12!\n" + + "\fnamespace_id\x18\x01 \x01(\tR\vnamespaceId\x12n\n" + + "\x10frontend_request\x18\x02 \x01(\v2C.temporal.server.chasm.lib.stream.proto.v1.AddWorkflowMessagesInputR\x0ffrontendRequest\"\x88\x01\n" + + "\x1bAddWorkflowMessagesResponse\x12i\n" + + "\x11frontend_response\x18\x01 \x01(\v2<.temporal.server.chasm.lib.stream.proto.v1.AddMessagesOutputR\x10frontendResponse\"\xb5\x01\n" + + "\x1dRegisterStreamConsumerRequest\x12!\n" + + "\fnamespace_id\x18\x01 \x01(\tR\vnamespaceId\x12q\n" + + "\x10frontend_request\x18\x02 \x01(\v2F.temporal.server.chasm.lib.stream.proto.v1.RegisterStreamConsumerInputR\x0ffrontendRequest\"\x96\x01\n" + + "\x1eRegisterStreamConsumerResponse\x12t\n" + + "\x11frontend_response\x18\x01 \x01(\v2G.temporal.server.chasm.lib.stream.proto.v1.RegisterStreamConsumerOutputR\x10frontendResponse\"\xaf\x01\n" + + "\x1aAdvanceConsumerHeadRequest\x12!\n" + + "\fnamespace_id\x18\x01 \x01(\tR\vnamespaceId\x12n\n" + + "\x10frontend_request\x18\x02 \x01(\v2C.temporal.server.chasm.lib.stream.proto.v1.AdvanceConsumerHeadInputR\x0ffrontendRequest\"\x90\x01\n" + + "\x1bAdvanceConsumerHeadResponse\x12q\n" + + "\x11frontend_response\x18\x01 \x01(\v2D.temporal.server.chasm.lib.stream.proto.v1.AdvanceConsumerHeadOutputR\x10frontendResponse\"\x9f\x01\n" + + "\x12CloseStreamRequest\x12!\n" + + "\fnamespace_id\x18\x01 \x01(\tR\vnamespaceId\x12f\n" + + "\x10frontend_request\x18\x02 \x01(\v2;.temporal.server.chasm.lib.stream.proto.v1.CloseStreamInputR\x0ffrontendRequest\"\x80\x01\n" + + "\x13CloseStreamResponse\x12i\n" + + "\x11frontend_response\x18\x01 \x01(\v2<.temporal.server.chasm.lib.stream.proto.v1.CloseStreamOutputR\x10frontendResponse\"\xa5\x01\n" + + "\x15TruncateStreamRequest\x12!\n" + + "\fnamespace_id\x18\x01 \x01(\tR\vnamespaceId\x12i\n" + + "\x10frontend_request\x18\x02 \x01(\v2>.temporal.server.chasm.lib.stream.proto.v1.TruncateStreamInputR\x0ffrontendRequest\"\x86\x01\n" + + "\x16TruncateStreamResponse\x12l\n" + + "\x11frontend_response\x18\x01 \x01(\v2?.temporal.server.chasm.lib.stream.proto.v1.TruncateStreamOutputR\x10frontendResponse\"\x8b\x01\n" + + "\x10ListStreamsInput\x12\x1c\n" + + "\tnamespace\x18\x01 \x01(\tR\tnamespace\x12\x1b\n" + + "\tpage_size\x18\x02 \x01(\x05R\bpageSize\x12&\n" + + "\x0fnext_page_token\x18\x03 \x01(\fR\rnextPageToken\x12\x14\n" + + "\x05query\x18\x04 \x01(\tR\x05query\"E\n" + + "\x0fStreamListEntry\x12\x1b\n" + + "\tstream_id\x18\x01 \x01(\tR\bstreamId\x12\x15\n" + + "\x06run_id\x18\x02 \x01(\tR\x05runId\"\x91\x01\n" + + "\x11ListStreamsOutput\x12T\n" + + "\astreams\x18\x01 \x03(\v2:.temporal.server.chasm.lib.stream.proto.v1.StreamListEntryR\astreams\x12&\n" + + "\x0fnext_page_token\x18\x02 \x01(\fR\rnextPageToken\"\x9f\x01\n" + + "\x12ListStreamsRequest\x12!\n" + + "\fnamespace_id\x18\x01 \x01(\tR\vnamespaceId\x12f\n" + + "\x10frontend_request\x18\x02 \x01(\v2;.temporal.server.chasm.lib.stream.proto.v1.ListStreamsInputR\x0ffrontendRequest\"\x80\x01\n" + + "\x13ListStreamsResponse\x12i\n" + + "\x11frontend_response\x18\x01 \x01(\v2<.temporal.server.chasm.lib.stream.proto.v1.ListStreamsOutputR\x10frontendResponse\"\xa1\x01\n" + + "\x13DeleteStreamRequest\x12!\n" + + "\fnamespace_id\x18\x01 \x01(\tR\vnamespaceId\x12g\n" + + "\x10frontend_request\x18\x02 \x01(\v2<.temporal.server.chasm.lib.stream.proto.v1.DeleteStreamInputR\x0ffrontendRequest\"\x82\x01\n" + + "\x14DeleteStreamResponse\x12j\n" + + "\x11frontend_response\x18\x01 \x01(\v2=.temporal.server.chasm.lib.stream.proto.v1.DeleteStreamOutputR\x10frontendResponse*\x9d\x01\n" + + "\x0fStreamOwnerKind\x12!\n" + + "\x1dSTREAM_OWNER_KIND_UNSPECIFIED\x10\x00\x12\x1e\n" + + "\x1aSTREAM_OWNER_KIND_WORKFLOW\x10\x01\x12\x1e\n" + + "\x1aSTREAM_OWNER_KIND_ACTIVITY\x10\x02\x12'\n" + + "#STREAM_OWNER_KIND_WORKFLOW_ACTIVITY\x10\x03B>Z temporal.server.chasm.lib.stream.proto.v1.StreamLifecycle + 61, // 1: temporal.server.chasm.lib.stream.proto.v1.AddMessagesInput.records:type_name -> temporal.server.chasm.lib.stream.proto.v1.StreamRecord + 62, // 2: temporal.server.chasm.lib.stream.proto.v1.SubscribeWorkflowInput.start_position:type_name -> temporal.api.stream.v1.StreamStartPosition + 62, // 3: temporal.server.chasm.lib.stream.proto.v1.PollMessagesInput.start_position:type_name -> temporal.api.stream.v1.StreamStartPosition + 61, // 4: temporal.server.chasm.lib.stream.proto.v1.PollMessagesOutput.records:type_name -> temporal.server.chasm.lib.stream.proto.v1.StreamRecord + 63, // 5: temporal.server.chasm.lib.stream.proto.v1.PollMessagesOutput.close_reason:type_name -> temporal.api.common.v1.Payload + 0, // 6: temporal.server.chasm.lib.stream.proto.v1.StreamOwner.kind:type_name -> temporal.server.chasm.lib.stream.proto.v1.StreamOwnerKind + 12, // 7: temporal.server.chasm.lib.stream.proto.v1.PollWorkflowMessagesInput.owner:type_name -> temporal.server.chasm.lib.stream.proto.v1.StreamOwner + 62, // 8: temporal.server.chasm.lib.stream.proto.v1.PollWorkflowMessagesInput.start_position:type_name -> temporal.api.stream.v1.StreamStartPosition + 12, // 9: temporal.server.chasm.lib.stream.proto.v1.DescribeWorkflowStreamInput.owner:type_name -> temporal.server.chasm.lib.stream.proto.v1.StreamOwner + 12, // 10: temporal.server.chasm.lib.stream.proto.v1.AddWorkflowMessagesInput.owner:type_name -> temporal.server.chasm.lib.stream.proto.v1.StreamOwner + 61, // 11: temporal.server.chasm.lib.stream.proto.v1.AddWorkflowMessagesInput.records:type_name -> temporal.server.chasm.lib.stream.proto.v1.StreamRecord + 64, // 12: temporal.server.chasm.lib.stream.proto.v1.DescribeStreamOutput.state:type_name -> temporal.server.chasm.lib.stream.proto.v1.StreamState + 63, // 13: temporal.server.chasm.lib.stream.proto.v1.CloseStreamInput.reason:type_name -> temporal.api.common.v1.Payload + 1, // 14: temporal.server.chasm.lib.stream.proto.v1.CreateStreamRequest.frontend_request:type_name -> temporal.server.chasm.lib.stream.proto.v1.CreateStreamInput + 2, // 15: temporal.server.chasm.lib.stream.proto.v1.CreateStreamResponse.frontend_response:type_name -> temporal.server.chasm.lib.stream.proto.v1.CreateStreamOutput + 3, // 16: temporal.server.chasm.lib.stream.proto.v1.AddMessagesRequest.frontend_request:type_name -> temporal.server.chasm.lib.stream.proto.v1.AddMessagesInput + 4, // 17: temporal.server.chasm.lib.stream.proto.v1.AddMessagesResponse.frontend_response:type_name -> temporal.server.chasm.lib.stream.proto.v1.AddMessagesOutput + 5, // 18: temporal.server.chasm.lib.stream.proto.v1.FinishWritingRequest.frontend_request:type_name -> temporal.server.chasm.lib.stream.proto.v1.FinishWritingInput + 6, // 19: temporal.server.chasm.lib.stream.proto.v1.FinishWritingResponse.frontend_response:type_name -> temporal.server.chasm.lib.stream.proto.v1.FinishWritingOutput + 7, // 20: temporal.server.chasm.lib.stream.proto.v1.SubscribeWorkflowRequest.frontend_request:type_name -> temporal.server.chasm.lib.stream.proto.v1.SubscribeWorkflowInput + 8, // 21: temporal.server.chasm.lib.stream.proto.v1.SubscribeWorkflowResponse.frontend_response:type_name -> temporal.server.chasm.lib.stream.proto.v1.SubscribeWorkflowOutput + 9, // 22: temporal.server.chasm.lib.stream.proto.v1.PollMessagesRequest.frontend_request:type_name -> temporal.server.chasm.lib.stream.proto.v1.PollMessagesInput + 10, // 23: temporal.server.chasm.lib.stream.proto.v1.PollMessagesResponse.frontend_response:type_name -> temporal.server.chasm.lib.stream.proto.v1.PollMessagesOutput + 11, // 24: temporal.server.chasm.lib.stream.proto.v1.DescribeStreamRequest.frontend_request:type_name -> temporal.server.chasm.lib.stream.proto.v1.DescribeStreamInput + 16, // 25: temporal.server.chasm.lib.stream.proto.v1.DescribeStreamResponse.frontend_response:type_name -> temporal.server.chasm.lib.stream.proto.v1.DescribeStreamOutput + 13, // 26: temporal.server.chasm.lib.stream.proto.v1.PollWorkflowMessagesRequest.frontend_request:type_name -> temporal.server.chasm.lib.stream.proto.v1.PollWorkflowMessagesInput + 10, // 27: temporal.server.chasm.lib.stream.proto.v1.PollWorkflowMessagesResponse.frontend_response:type_name -> temporal.server.chasm.lib.stream.proto.v1.PollMessagesOutput + 14, // 28: temporal.server.chasm.lib.stream.proto.v1.DescribeWorkflowStreamRequest.frontend_request:type_name -> temporal.server.chasm.lib.stream.proto.v1.DescribeWorkflowStreamInput + 16, // 29: temporal.server.chasm.lib.stream.proto.v1.DescribeWorkflowStreamResponse.frontend_response:type_name -> temporal.server.chasm.lib.stream.proto.v1.DescribeStreamOutput + 62, // 30: temporal.server.chasm.lib.stream.proto.v1.RegisterStreamConsumerInput.start_position:type_name -> temporal.api.stream.v1.StreamStartPosition + 15, // 31: temporal.server.chasm.lib.stream.proto.v1.AddWorkflowMessagesRequest.frontend_request:type_name -> temporal.server.chasm.lib.stream.proto.v1.AddWorkflowMessagesInput + 4, // 32: temporal.server.chasm.lib.stream.proto.v1.AddWorkflowMessagesResponse.frontend_response:type_name -> temporal.server.chasm.lib.stream.proto.v1.AddMessagesOutput + 39, // 33: temporal.server.chasm.lib.stream.proto.v1.RegisterStreamConsumerRequest.frontend_request:type_name -> temporal.server.chasm.lib.stream.proto.v1.RegisterStreamConsumerInput + 40, // 34: temporal.server.chasm.lib.stream.proto.v1.RegisterStreamConsumerResponse.frontend_response:type_name -> temporal.server.chasm.lib.stream.proto.v1.RegisterStreamConsumerOutput + 41, // 35: temporal.server.chasm.lib.stream.proto.v1.AdvanceConsumerHeadRequest.frontend_request:type_name -> temporal.server.chasm.lib.stream.proto.v1.AdvanceConsumerHeadInput + 42, // 36: temporal.server.chasm.lib.stream.proto.v1.AdvanceConsumerHeadResponse.frontend_response:type_name -> temporal.server.chasm.lib.stream.proto.v1.AdvanceConsumerHeadOutput + 17, // 37: temporal.server.chasm.lib.stream.proto.v1.CloseStreamRequest.frontend_request:type_name -> temporal.server.chasm.lib.stream.proto.v1.CloseStreamInput + 18, // 38: temporal.server.chasm.lib.stream.proto.v1.CloseStreamResponse.frontend_response:type_name -> temporal.server.chasm.lib.stream.proto.v1.CloseStreamOutput + 19, // 39: temporal.server.chasm.lib.stream.proto.v1.TruncateStreamRequest.frontend_request:type_name -> temporal.server.chasm.lib.stream.proto.v1.TruncateStreamInput + 20, // 40: temporal.server.chasm.lib.stream.proto.v1.TruncateStreamResponse.frontend_response:type_name -> temporal.server.chasm.lib.stream.proto.v1.TruncateStreamOutput + 54, // 41: temporal.server.chasm.lib.stream.proto.v1.ListStreamsOutput.streams:type_name -> temporal.server.chasm.lib.stream.proto.v1.StreamListEntry + 53, // 42: temporal.server.chasm.lib.stream.proto.v1.ListStreamsRequest.frontend_request:type_name -> temporal.server.chasm.lib.stream.proto.v1.ListStreamsInput + 55, // 43: temporal.server.chasm.lib.stream.proto.v1.ListStreamsResponse.frontend_response:type_name -> temporal.server.chasm.lib.stream.proto.v1.ListStreamsOutput + 21, // 44: temporal.server.chasm.lib.stream.proto.v1.DeleteStreamRequest.frontend_request:type_name -> temporal.server.chasm.lib.stream.proto.v1.DeleteStreamInput + 22, // 45: temporal.server.chasm.lib.stream.proto.v1.DeleteStreamResponse.frontend_response:type_name -> temporal.server.chasm.lib.stream.proto.v1.DeleteStreamOutput + 46, // [46:46] is the sub-list for method output_type + 46, // [46:46] is the sub-list for method input_type + 46, // [46:46] is the sub-list for extension type_name + 46, // [46:46] is the sub-list for extension extendee + 0, // [0:46] is the sub-list for field type_name +} + +func init() { file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_init() } +func file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_init() { + if File_temporal_server_chasm_lib_stream_proto_v1_request_response_proto != nil { + return + } + file_temporal_server_chasm_lib_stream_proto_v1_message_proto_init() + file_temporal_server_chasm_lib_stream_proto_v1_stream_state_proto_init() + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDesc), len(file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDesc)), + NumEnums: 1, + NumMessages: 59, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_goTypes, + DependencyIndexes: file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_depIdxs, + EnumInfos: file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_enumTypes, + MessageInfos: file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes, + }.Build() + File_temporal_server_chasm_lib_stream_proto_v1_request_response_proto = out.File + file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_goTypes = nil + file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_depIdxs = nil +} diff --git a/chasm/lib/stream/gen/streampb/v1/service.pb.go b/chasm/lib/stream/gen/streampb/v1/service.pb.go new file mode 100644 index 00000000000..908ee46128a --- /dev/null +++ b/chasm/lib/stream/gen/streampb/v1/service.pb.go @@ -0,0 +1,140 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// plugins: +// protoc-gen-go +// protoc +// source: temporal/server/chasm/lib/stream/proto/v1/service.proto + +package streampb + +import ( + reflect "reflect" + unsafe "unsafe" + + _ "go.temporal.io/server/api/common/v1" + _ "go.temporal.io/server/api/routing/v1" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +var File_temporal_server_chasm_lib_stream_proto_v1_service_proto protoreflect.FileDescriptor + +const file_temporal_server_chasm_lib_stream_proto_v1_service_proto_rawDesc = "" + + "\n" + + "7temporal/server/chasm/lib/stream/proto/v1/service.proto\x12)temporal.server.chasm.lib.stream.proto.v1\x1a@temporal/server/chasm/lib/stream/proto/v1/request_response.proto\x1a0temporal/server/api/common/v1/api_category.proto\x1a.temporal/server/api/routing/v1/extension.proto2\xef\x16\n" + + "\rStreamService\x12\xb7\x01\n" + + "\fCreateStream\x12>.temporal.server.chasm.lib.stream.proto.v1.CreateStreamRequest\x1a?.temporal.server.chasm.lib.stream.proto.v1.CreateStreamResponse\"&\x8a\xb5\x18\x02\b\x01\xd2\xc3\x18\x1c\x1a\x1afrontend_request.stream_id\x12\xb4\x01\n" + + "\vAddMessages\x12=.temporal.server.chasm.lib.stream.proto.v1.AddMessagesRequest\x1a>.temporal.server.chasm.lib.stream.proto.v1.AddMessagesResponse\"&\x8a\xb5\x18\x02\b\x01\xd2\xc3\x18\x1c\x1a\x1afrontend_request.stream_id\x12\xba\x01\n" + + "\rFinishWriting\x12?.temporal.server.chasm.lib.stream.proto.v1.FinishWritingRequest\x1a@.temporal.server.chasm.lib.stream.proto.v1.FinishWritingResponse\"&\x8a\xb5\x18\x02\b\x01\xd2\xc3\x18\x1c\x1a\x1afrontend_request.stream_id\x12\xc8\x01\n" + + "\x11SubscribeWorkflow\x12C.temporal.server.chasm.lib.stream.proto.v1.SubscribeWorkflowRequest\x1aD.temporal.server.chasm.lib.stream.proto.v1.SubscribeWorkflowResponse\"(\x8a\xb5\x18\x02\b\x01\xd2\xc3\x18\x1e\x1a\x1cfrontend_request.workflow_id\x12\xb7\x01\n" + + "\fPollMessages\x12>.temporal.server.chasm.lib.stream.proto.v1.PollMessagesRequest\x1a?.temporal.server.chasm.lib.stream.proto.v1.PollMessagesResponse\"&\x8a\xb5\x18\x02\b\x02\xd2\xc3\x18\x1c\x1a\x1afrontend_request.stream_id\x12\xbd\x01\n" + + "\x0eDescribeStream\x12@.temporal.server.chasm.lib.stream.proto.v1.DescribeStreamRequest\x1aA.temporal.server.chasm.lib.stream.proto.v1.DescribeStreamResponse\"&\x8a\xb5\x18\x02\b\x01\xd2\xc3\x18\x1c\x1a\x1afrontend_request.stream_id\x12\xce\x01\n" + + "\x14PollWorkflowMessages\x12F.temporal.server.chasm.lib.stream.proto.v1.PollWorkflowMessagesRequest\x1aG.temporal.server.chasm.lib.stream.proto.v1.PollWorkflowMessagesResponse\"%\x8a\xb5\x18\x02\b\x02\xd2\xc3\x18\x1b\x1a\x19frontend_request.owner.id\x12\xd4\x01\n" + + "\x16DescribeWorkflowStream\x12H.temporal.server.chasm.lib.stream.proto.v1.DescribeWorkflowStreamRequest\x1aI.temporal.server.chasm.lib.stream.proto.v1.DescribeWorkflowStreamResponse\"%\x8a\xb5\x18\x02\b\x01\xd2\xc3\x18\x1b\x1a\x19frontend_request.owner.id\x12\xcb\x01\n" + + "\x13AddWorkflowMessages\x12E.temporal.server.chasm.lib.stream.proto.v1.AddWorkflowMessagesRequest\x1aF.temporal.server.chasm.lib.stream.proto.v1.AddWorkflowMessagesResponse\"%\x8a\xb5\x18\x02\b\x01\xd2\xc3\x18\x1b\x1a\x19frontend_request.owner.id\x12\xd5\x01\n" + + "\x16RegisterStreamConsumer\x12H.temporal.server.chasm.lib.stream.proto.v1.RegisterStreamConsumerRequest\x1aI.temporal.server.chasm.lib.stream.proto.v1.RegisterStreamConsumerResponse\"&\x8a\xb5\x18\x02\b\x01\xd2\xc3\x18\x1c\x1a\x1afrontend_request.stream_id\x12\xce\x01\n" + + "\x13AdvanceConsumerHead\x12E.temporal.server.chasm.lib.stream.proto.v1.AdvanceConsumerHeadRequest\x1aF.temporal.server.chasm.lib.stream.proto.v1.AdvanceConsumerHeadResponse\"(\x8a\xb5\x18\x02\b\x01\xd2\xc3\x18\x1e\x1a\x1cfrontend_request.workflow_id\x12\xb4\x01\n" + + "\vCloseStream\x12=.temporal.server.chasm.lib.stream.proto.v1.CloseStreamRequest\x1a>.temporal.server.chasm.lib.stream.proto.v1.CloseStreamResponse\"&\x8a\xb5\x18\x02\b\x01\xd2\xc3\x18\x1c\x1a\x1afrontend_request.stream_id\x12\xbd\x01\n" + + "\x0eTruncateStream\x12@.temporal.server.chasm.lib.stream.proto.v1.TruncateStreamRequest\x1aA.temporal.server.chasm.lib.stream.proto.v1.TruncateStreamResponse\"&\x8a\xb5\x18\x02\b\x01\xd2\xc3\x18\x1c\x1a\x1afrontend_request.stream_id\x12\x9a\x01\n" + + "\vListStreams\x12=.temporal.server.chasm.lib.stream.proto.v1.ListStreamsRequest\x1a>.temporal.server.chasm.lib.stream.proto.v1.ListStreamsResponse\"\f\x8a\xb5\x18\x02\b\x01\xd2\xc3\x18\x02\b\x01\x12\xb7\x01\n" + + "\fDeleteStream\x12>.temporal.server.chasm.lib.stream.proto.v1.DeleteStreamRequest\x1a?.temporal.server.chasm.lib.stream.proto.v1.DeleteStreamResponse\"&\x8a\xb5\x18\x02\b\x01\xd2\xc3\x18\x1c\x1a\x1afrontend_request.stream_idB>Z temporal.server.chasm.lib.stream.proto.v1.CreateStreamRequest + 1, // 1: temporal.server.chasm.lib.stream.proto.v1.StreamService.AddMessages:input_type -> temporal.server.chasm.lib.stream.proto.v1.AddMessagesRequest + 2, // 2: temporal.server.chasm.lib.stream.proto.v1.StreamService.FinishWriting:input_type -> temporal.server.chasm.lib.stream.proto.v1.FinishWritingRequest + 3, // 3: temporal.server.chasm.lib.stream.proto.v1.StreamService.SubscribeWorkflow:input_type -> temporal.server.chasm.lib.stream.proto.v1.SubscribeWorkflowRequest + 4, // 4: temporal.server.chasm.lib.stream.proto.v1.StreamService.PollMessages:input_type -> temporal.server.chasm.lib.stream.proto.v1.PollMessagesRequest + 5, // 5: temporal.server.chasm.lib.stream.proto.v1.StreamService.DescribeStream:input_type -> temporal.server.chasm.lib.stream.proto.v1.DescribeStreamRequest + 6, // 6: temporal.server.chasm.lib.stream.proto.v1.StreamService.PollWorkflowMessages:input_type -> temporal.server.chasm.lib.stream.proto.v1.PollWorkflowMessagesRequest + 7, // 7: temporal.server.chasm.lib.stream.proto.v1.StreamService.DescribeWorkflowStream:input_type -> temporal.server.chasm.lib.stream.proto.v1.DescribeWorkflowStreamRequest + 8, // 8: temporal.server.chasm.lib.stream.proto.v1.StreamService.AddWorkflowMessages:input_type -> temporal.server.chasm.lib.stream.proto.v1.AddWorkflowMessagesRequest + 9, // 9: temporal.server.chasm.lib.stream.proto.v1.StreamService.RegisterStreamConsumer:input_type -> temporal.server.chasm.lib.stream.proto.v1.RegisterStreamConsumerRequest + 10, // 10: temporal.server.chasm.lib.stream.proto.v1.StreamService.AdvanceConsumerHead:input_type -> temporal.server.chasm.lib.stream.proto.v1.AdvanceConsumerHeadRequest + 11, // 11: temporal.server.chasm.lib.stream.proto.v1.StreamService.CloseStream:input_type -> temporal.server.chasm.lib.stream.proto.v1.CloseStreamRequest + 12, // 12: temporal.server.chasm.lib.stream.proto.v1.StreamService.TruncateStream:input_type -> temporal.server.chasm.lib.stream.proto.v1.TruncateStreamRequest + 13, // 13: temporal.server.chasm.lib.stream.proto.v1.StreamService.ListStreams:input_type -> temporal.server.chasm.lib.stream.proto.v1.ListStreamsRequest + 14, // 14: temporal.server.chasm.lib.stream.proto.v1.StreamService.DeleteStream:input_type -> temporal.server.chasm.lib.stream.proto.v1.DeleteStreamRequest + 15, // 15: temporal.server.chasm.lib.stream.proto.v1.StreamService.CreateStream:output_type -> temporal.server.chasm.lib.stream.proto.v1.CreateStreamResponse + 16, // 16: temporal.server.chasm.lib.stream.proto.v1.StreamService.AddMessages:output_type -> temporal.server.chasm.lib.stream.proto.v1.AddMessagesResponse + 17, // 17: temporal.server.chasm.lib.stream.proto.v1.StreamService.FinishWriting:output_type -> temporal.server.chasm.lib.stream.proto.v1.FinishWritingResponse + 18, // 18: temporal.server.chasm.lib.stream.proto.v1.StreamService.SubscribeWorkflow:output_type -> temporal.server.chasm.lib.stream.proto.v1.SubscribeWorkflowResponse + 19, // 19: temporal.server.chasm.lib.stream.proto.v1.StreamService.PollMessages:output_type -> temporal.server.chasm.lib.stream.proto.v1.PollMessagesResponse + 20, // 20: temporal.server.chasm.lib.stream.proto.v1.StreamService.DescribeStream:output_type -> temporal.server.chasm.lib.stream.proto.v1.DescribeStreamResponse + 21, // 21: temporal.server.chasm.lib.stream.proto.v1.StreamService.PollWorkflowMessages:output_type -> temporal.server.chasm.lib.stream.proto.v1.PollWorkflowMessagesResponse + 22, // 22: temporal.server.chasm.lib.stream.proto.v1.StreamService.DescribeWorkflowStream:output_type -> temporal.server.chasm.lib.stream.proto.v1.DescribeWorkflowStreamResponse + 23, // 23: temporal.server.chasm.lib.stream.proto.v1.StreamService.AddWorkflowMessages:output_type -> temporal.server.chasm.lib.stream.proto.v1.AddWorkflowMessagesResponse + 24, // 24: temporal.server.chasm.lib.stream.proto.v1.StreamService.RegisterStreamConsumer:output_type -> temporal.server.chasm.lib.stream.proto.v1.RegisterStreamConsumerResponse + 25, // 25: temporal.server.chasm.lib.stream.proto.v1.StreamService.AdvanceConsumerHead:output_type -> temporal.server.chasm.lib.stream.proto.v1.AdvanceConsumerHeadResponse + 26, // 26: temporal.server.chasm.lib.stream.proto.v1.StreamService.CloseStream:output_type -> temporal.server.chasm.lib.stream.proto.v1.CloseStreamResponse + 27, // 27: temporal.server.chasm.lib.stream.proto.v1.StreamService.TruncateStream:output_type -> temporal.server.chasm.lib.stream.proto.v1.TruncateStreamResponse + 28, // 28: temporal.server.chasm.lib.stream.proto.v1.StreamService.ListStreams:output_type -> temporal.server.chasm.lib.stream.proto.v1.ListStreamsResponse + 29, // 29: temporal.server.chasm.lib.stream.proto.v1.StreamService.DeleteStream:output_type -> temporal.server.chasm.lib.stream.proto.v1.DeleteStreamResponse + 15, // [15:30] is the sub-list for method output_type + 0, // [0:15] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_temporal_server_chasm_lib_stream_proto_v1_service_proto_init() } +func file_temporal_server_chasm_lib_stream_proto_v1_service_proto_init() { + if File_temporal_server_chasm_lib_stream_proto_v1_service_proto != nil { + return + } + file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_init() + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_temporal_server_chasm_lib_stream_proto_v1_service_proto_rawDesc), len(file_temporal_server_chasm_lib_stream_proto_v1_service_proto_rawDesc)), + NumEnums: 0, + NumMessages: 0, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_temporal_server_chasm_lib_stream_proto_v1_service_proto_goTypes, + DependencyIndexes: file_temporal_server_chasm_lib_stream_proto_v1_service_proto_depIdxs, + }.Build() + File_temporal_server_chasm_lib_stream_proto_v1_service_proto = out.File + file_temporal_server_chasm_lib_stream_proto_v1_service_proto_goTypes = nil + file_temporal_server_chasm_lib_stream_proto_v1_service_proto_depIdxs = nil +} diff --git a/chasm/lib/stream/gen/streampb/v1/service_client.pb.go b/chasm/lib/stream/gen/streampb/v1/service_client.pb.go new file mode 100644 index 00000000000..213969200b4 --- /dev/null +++ b/chasm/lib/stream/gen/streampb/v1/service_client.pb.go @@ -0,0 +1,713 @@ +// Code generated by protoc-gen-go-chasm. DO NOT EDIT. +package streampb + +import ( + "context" + "math/rand" + "time" + + "go.temporal.io/server/client/history" + "go.temporal.io/server/common" + "go.temporal.io/server/common/backoff" + "go.temporal.io/server/common/config" + "go.temporal.io/server/common/dynamicconfig" + "go.temporal.io/server/common/headers" + "go.temporal.io/server/common/log" + "go.temporal.io/server/common/membership" + "go.temporal.io/server/common/metrics" + "go.temporal.io/server/common/primitives" + "go.uber.org/fx" + "google.golang.org/grpc" +) + +// StreamServiceLayeredClient is a client for StreamService. +type StreamServiceLayeredClient struct { + metricsHandler metrics.Handler + numShards int32 + redirector history.Redirector[StreamServiceClient] + retryPolicy backoff.RetryPolicy +} + +// NewStreamServiceLayeredClient initializes a new StreamServiceLayeredClient. +func NewStreamServiceLayeredClient( + lc fx.Lifecycle, + dc *dynamicconfig.Collection, + rpcFactory common.RPCFactory, + monitor membership.Monitor, + config *config.Persistence, + logger log.Logger, + metricsHandler metrics.Handler, +) (StreamServiceClient, error) { + resolver, err := monitor.GetResolver(primitives.HistoryService) + if err != nil { + return nil, err + } + connections := history.NewConnectionPool(resolver, rpcFactory, NewStreamServiceClient, logger, dynamicconfig.HistoryConnectionCloseDelay.Get(dc)) + var redirector history.Redirector[StreamServiceClient] + if dynamicconfig.HistoryClientOwnershipCachingEnabled.Get(dc)() { + redirector = history.NewCachingRedirector( + connections, + resolver, + logger, + dynamicconfig.HistoryClientOwnershipCachingStaleTTL.Get(dc), + ) + } else { + redirector = history.NewBasicRedirector(connections, resolver) + } + client := &StreamServiceLayeredClient{ + metricsHandler: metricsHandler, + redirector: redirector, + numShards: config.NumHistoryShards, + retryPolicy: common.CreateHistoryClientRetryPolicy(dynamicconfig.RetryUnboundedOnSystemResourceExhausted.Get(dc)), + } + lc.Append(fx.StopHook(client.Stop)) + return client, nil +} +func (c *StreamServiceLayeredClient) Stop() { + c.redirector.Close() +} +func (c *StreamServiceLayeredClient) callCreateStreamNoRetry( + ctx context.Context, + request *CreateStreamRequest, + opts ...grpc.CallOption, +) (*CreateStreamResponse, error) { + var response *CreateStreamResponse + var err error + startTime := time.Now().UTC() + // the caller is a namespace, hence the tag below. + caller := headers.GetCallerInfo(ctx).CallerName + metricsHandler := c.metricsHandler.WithTags( + metrics.OperationTag("StreamService.CreateStream"), + metrics.NamespaceTag(caller), + metrics.ServiceRoleTag(metrics.HistoryRoleTagValue), + ) + metrics.ClientRequests.With(metricsHandler).Record(1) + defer func() { + if err != nil { + metrics.ClientFailures.With(metricsHandler).Record(1, metrics.ServiceErrorTypeTag(err)) + } + metrics.ClientLatency.With(metricsHandler).Record(time.Since(startTime)) + }() + shardID := common.WorkflowIDToHistoryShard(request.GetNamespaceId(), request.GetFrontendRequest().GetStreamId(), c.numShards) + op := func(ctx context.Context, client StreamServiceClient) error { + var err error + ctx, cancel := context.WithTimeout(ctx, history.DefaultTimeout) + defer cancel() + response, err = client.CreateStream(ctx, request, opts...) + return err + } + err = c.redirector.Execute(ctx, shardID, op) + return response, err +} +func (c *StreamServiceLayeredClient) CreateStream( + ctx context.Context, + request *CreateStreamRequest, + opts ...grpc.CallOption, +) (*CreateStreamResponse, error) { + call := func(ctx context.Context) (*CreateStreamResponse, error) { + return c.callCreateStreamNoRetry(ctx, request, opts...) + } + return backoff.ThrottleRetryContextWithReturn(ctx, call, c.retryPolicy, common.IsServiceClientTransientError) +} +func (c *StreamServiceLayeredClient) callAddMessagesNoRetry( + ctx context.Context, + request *AddMessagesRequest, + opts ...grpc.CallOption, +) (*AddMessagesResponse, error) { + var response *AddMessagesResponse + var err error + startTime := time.Now().UTC() + // the caller is a namespace, hence the tag below. + caller := headers.GetCallerInfo(ctx).CallerName + metricsHandler := c.metricsHandler.WithTags( + metrics.OperationTag("StreamService.AddMessages"), + metrics.NamespaceTag(caller), + metrics.ServiceRoleTag(metrics.HistoryRoleTagValue), + ) + metrics.ClientRequests.With(metricsHandler).Record(1) + defer func() { + if err != nil { + metrics.ClientFailures.With(metricsHandler).Record(1, metrics.ServiceErrorTypeTag(err)) + } + metrics.ClientLatency.With(metricsHandler).Record(time.Since(startTime)) + }() + shardID := common.WorkflowIDToHistoryShard(request.GetNamespaceId(), request.GetFrontendRequest().GetStreamId(), c.numShards) + op := func(ctx context.Context, client StreamServiceClient) error { + var err error + ctx, cancel := context.WithTimeout(ctx, history.DefaultTimeout) + defer cancel() + response, err = client.AddMessages(ctx, request, opts...) + return err + } + err = c.redirector.Execute(ctx, shardID, op) + return response, err +} +func (c *StreamServiceLayeredClient) AddMessages( + ctx context.Context, + request *AddMessagesRequest, + opts ...grpc.CallOption, +) (*AddMessagesResponse, error) { + call := func(ctx context.Context) (*AddMessagesResponse, error) { + return c.callAddMessagesNoRetry(ctx, request, opts...) + } + return backoff.ThrottleRetryContextWithReturn(ctx, call, c.retryPolicy, common.IsServiceClientTransientError) +} +func (c *StreamServiceLayeredClient) callFinishWritingNoRetry( + ctx context.Context, + request *FinishWritingRequest, + opts ...grpc.CallOption, +) (*FinishWritingResponse, error) { + var response *FinishWritingResponse + var err error + startTime := time.Now().UTC() + // the caller is a namespace, hence the tag below. + caller := headers.GetCallerInfo(ctx).CallerName + metricsHandler := c.metricsHandler.WithTags( + metrics.OperationTag("StreamService.FinishWriting"), + metrics.NamespaceTag(caller), + metrics.ServiceRoleTag(metrics.HistoryRoleTagValue), + ) + metrics.ClientRequests.With(metricsHandler).Record(1) + defer func() { + if err != nil { + metrics.ClientFailures.With(metricsHandler).Record(1, metrics.ServiceErrorTypeTag(err)) + } + metrics.ClientLatency.With(metricsHandler).Record(time.Since(startTime)) + }() + shardID := common.WorkflowIDToHistoryShard(request.GetNamespaceId(), request.GetFrontendRequest().GetStreamId(), c.numShards) + op := func(ctx context.Context, client StreamServiceClient) error { + var err error + ctx, cancel := context.WithTimeout(ctx, history.DefaultTimeout) + defer cancel() + response, err = client.FinishWriting(ctx, request, opts...) + return err + } + err = c.redirector.Execute(ctx, shardID, op) + return response, err +} +func (c *StreamServiceLayeredClient) FinishWriting( + ctx context.Context, + request *FinishWritingRequest, + opts ...grpc.CallOption, +) (*FinishWritingResponse, error) { + call := func(ctx context.Context) (*FinishWritingResponse, error) { + return c.callFinishWritingNoRetry(ctx, request, opts...) + } + return backoff.ThrottleRetryContextWithReturn(ctx, call, c.retryPolicy, common.IsServiceClientTransientError) +} +func (c *StreamServiceLayeredClient) callSubscribeWorkflowNoRetry( + ctx context.Context, + request *SubscribeWorkflowRequest, + opts ...grpc.CallOption, +) (*SubscribeWorkflowResponse, error) { + var response *SubscribeWorkflowResponse + var err error + startTime := time.Now().UTC() + // the caller is a namespace, hence the tag below. + caller := headers.GetCallerInfo(ctx).CallerName + metricsHandler := c.metricsHandler.WithTags( + metrics.OperationTag("StreamService.SubscribeWorkflow"), + metrics.NamespaceTag(caller), + metrics.ServiceRoleTag(metrics.HistoryRoleTagValue), + ) + metrics.ClientRequests.With(metricsHandler).Record(1) + defer func() { + if err != nil { + metrics.ClientFailures.With(metricsHandler).Record(1, metrics.ServiceErrorTypeTag(err)) + } + metrics.ClientLatency.With(metricsHandler).Record(time.Since(startTime)) + }() + shardID := common.WorkflowIDToHistoryShard(request.GetNamespaceId(), request.GetFrontendRequest().GetWorkflowId(), c.numShards) + op := func(ctx context.Context, client StreamServiceClient) error { + var err error + ctx, cancel := context.WithTimeout(ctx, history.DefaultTimeout) + defer cancel() + response, err = client.SubscribeWorkflow(ctx, request, opts...) + return err + } + err = c.redirector.Execute(ctx, shardID, op) + return response, err +} +func (c *StreamServiceLayeredClient) SubscribeWorkflow( + ctx context.Context, + request *SubscribeWorkflowRequest, + opts ...grpc.CallOption, +) (*SubscribeWorkflowResponse, error) { + call := func(ctx context.Context) (*SubscribeWorkflowResponse, error) { + return c.callSubscribeWorkflowNoRetry(ctx, request, opts...) + } + return backoff.ThrottleRetryContextWithReturn(ctx, call, c.retryPolicy, common.IsServiceClientTransientError) +} +func (c *StreamServiceLayeredClient) callPollMessagesNoRetry( + ctx context.Context, + request *PollMessagesRequest, + opts ...grpc.CallOption, +) (*PollMessagesResponse, error) { + var response *PollMessagesResponse + var err error + startTime := time.Now().UTC() + // the caller is a namespace, hence the tag below. + caller := headers.GetCallerInfo(ctx).CallerName + metricsHandler := c.metricsHandler.WithTags( + metrics.OperationTag("StreamService.PollMessages"), + metrics.NamespaceTag(caller), + metrics.ServiceRoleTag(metrics.HistoryRoleTagValue), + ) + metrics.ClientRequests.With(metricsHandler).Record(1) + defer func() { + if err != nil { + metrics.ClientFailures.With(metricsHandler).Record(1, metrics.ServiceErrorTypeTag(err)) + } + metrics.ClientLatency.With(metricsHandler).Record(time.Since(startTime)) + }() + shardID := common.WorkflowIDToHistoryShard(request.GetNamespaceId(), request.GetFrontendRequest().GetStreamId(), c.numShards) + op := func(ctx context.Context, client StreamServiceClient) error { + var err error + ctx, cancel := context.WithTimeout(ctx, history.DefaultTimeout) + defer cancel() + response, err = client.PollMessages(ctx, request, opts...) + return err + } + err = c.redirector.Execute(ctx, shardID, op) + return response, err +} +func (c *StreamServiceLayeredClient) PollMessages( + ctx context.Context, + request *PollMessagesRequest, + opts ...grpc.CallOption, +) (*PollMessagesResponse, error) { + call := func(ctx context.Context) (*PollMessagesResponse, error) { + return c.callPollMessagesNoRetry(ctx, request, opts...) + } + return backoff.ThrottleRetryContextWithReturn(ctx, call, c.retryPolicy, common.IsServiceClientTransientError) +} +func (c *StreamServiceLayeredClient) callDescribeStreamNoRetry( + ctx context.Context, + request *DescribeStreamRequest, + opts ...grpc.CallOption, +) (*DescribeStreamResponse, error) { + var response *DescribeStreamResponse + var err error + startTime := time.Now().UTC() + // the caller is a namespace, hence the tag below. + caller := headers.GetCallerInfo(ctx).CallerName + metricsHandler := c.metricsHandler.WithTags( + metrics.OperationTag("StreamService.DescribeStream"), + metrics.NamespaceTag(caller), + metrics.ServiceRoleTag(metrics.HistoryRoleTagValue), + ) + metrics.ClientRequests.With(metricsHandler).Record(1) + defer func() { + if err != nil { + metrics.ClientFailures.With(metricsHandler).Record(1, metrics.ServiceErrorTypeTag(err)) + } + metrics.ClientLatency.With(metricsHandler).Record(time.Since(startTime)) + }() + shardID := common.WorkflowIDToHistoryShard(request.GetNamespaceId(), request.GetFrontendRequest().GetStreamId(), c.numShards) + op := func(ctx context.Context, client StreamServiceClient) error { + var err error + ctx, cancel := context.WithTimeout(ctx, history.DefaultTimeout) + defer cancel() + response, err = client.DescribeStream(ctx, request, opts...) + return err + } + err = c.redirector.Execute(ctx, shardID, op) + return response, err +} +func (c *StreamServiceLayeredClient) DescribeStream( + ctx context.Context, + request *DescribeStreamRequest, + opts ...grpc.CallOption, +) (*DescribeStreamResponse, error) { + call := func(ctx context.Context) (*DescribeStreamResponse, error) { + return c.callDescribeStreamNoRetry(ctx, request, opts...) + } + return backoff.ThrottleRetryContextWithReturn(ctx, call, c.retryPolicy, common.IsServiceClientTransientError) +} +func (c *StreamServiceLayeredClient) callPollWorkflowMessagesNoRetry( + ctx context.Context, + request *PollWorkflowMessagesRequest, + opts ...grpc.CallOption, +) (*PollWorkflowMessagesResponse, error) { + var response *PollWorkflowMessagesResponse + var err error + startTime := time.Now().UTC() + // the caller is a namespace, hence the tag below. + caller := headers.GetCallerInfo(ctx).CallerName + metricsHandler := c.metricsHandler.WithTags( + metrics.OperationTag("StreamService.PollWorkflowMessages"), + metrics.NamespaceTag(caller), + metrics.ServiceRoleTag(metrics.HistoryRoleTagValue), + ) + metrics.ClientRequests.With(metricsHandler).Record(1) + defer func() { + if err != nil { + metrics.ClientFailures.With(metricsHandler).Record(1, metrics.ServiceErrorTypeTag(err)) + } + metrics.ClientLatency.With(metricsHandler).Record(time.Since(startTime)) + }() + shardID := common.WorkflowIDToHistoryShard(request.GetNamespaceId(), request.GetFrontendRequest().GetOwner().GetId(), c.numShards) + op := func(ctx context.Context, client StreamServiceClient) error { + var err error + ctx, cancel := context.WithTimeout(ctx, history.DefaultTimeout) + defer cancel() + response, err = client.PollWorkflowMessages(ctx, request, opts...) + return err + } + err = c.redirector.Execute(ctx, shardID, op) + return response, err +} +func (c *StreamServiceLayeredClient) PollWorkflowMessages( + ctx context.Context, + request *PollWorkflowMessagesRequest, + opts ...grpc.CallOption, +) (*PollWorkflowMessagesResponse, error) { + call := func(ctx context.Context) (*PollWorkflowMessagesResponse, error) { + return c.callPollWorkflowMessagesNoRetry(ctx, request, opts...) + } + return backoff.ThrottleRetryContextWithReturn(ctx, call, c.retryPolicy, common.IsServiceClientTransientError) +} +func (c *StreamServiceLayeredClient) callDescribeWorkflowStreamNoRetry( + ctx context.Context, + request *DescribeWorkflowStreamRequest, + opts ...grpc.CallOption, +) (*DescribeWorkflowStreamResponse, error) { + var response *DescribeWorkflowStreamResponse + var err error + startTime := time.Now().UTC() + // the caller is a namespace, hence the tag below. + caller := headers.GetCallerInfo(ctx).CallerName + metricsHandler := c.metricsHandler.WithTags( + metrics.OperationTag("StreamService.DescribeWorkflowStream"), + metrics.NamespaceTag(caller), + metrics.ServiceRoleTag(metrics.HistoryRoleTagValue), + ) + metrics.ClientRequests.With(metricsHandler).Record(1) + defer func() { + if err != nil { + metrics.ClientFailures.With(metricsHandler).Record(1, metrics.ServiceErrorTypeTag(err)) + } + metrics.ClientLatency.With(metricsHandler).Record(time.Since(startTime)) + }() + shardID := common.WorkflowIDToHistoryShard(request.GetNamespaceId(), request.GetFrontendRequest().GetOwner().GetId(), c.numShards) + op := func(ctx context.Context, client StreamServiceClient) error { + var err error + ctx, cancel := context.WithTimeout(ctx, history.DefaultTimeout) + defer cancel() + response, err = client.DescribeWorkflowStream(ctx, request, opts...) + return err + } + err = c.redirector.Execute(ctx, shardID, op) + return response, err +} +func (c *StreamServiceLayeredClient) DescribeWorkflowStream( + ctx context.Context, + request *DescribeWorkflowStreamRequest, + opts ...grpc.CallOption, +) (*DescribeWorkflowStreamResponse, error) { + call := func(ctx context.Context) (*DescribeWorkflowStreamResponse, error) { + return c.callDescribeWorkflowStreamNoRetry(ctx, request, opts...) + } + return backoff.ThrottleRetryContextWithReturn(ctx, call, c.retryPolicy, common.IsServiceClientTransientError) +} +func (c *StreamServiceLayeredClient) callAddWorkflowMessagesNoRetry( + ctx context.Context, + request *AddWorkflowMessagesRequest, + opts ...grpc.CallOption, +) (*AddWorkflowMessagesResponse, error) { + var response *AddWorkflowMessagesResponse + var err error + startTime := time.Now().UTC() + // the caller is a namespace, hence the tag below. + caller := headers.GetCallerInfo(ctx).CallerName + metricsHandler := c.metricsHandler.WithTags( + metrics.OperationTag("StreamService.AddWorkflowMessages"), + metrics.NamespaceTag(caller), + metrics.ServiceRoleTag(metrics.HistoryRoleTagValue), + ) + metrics.ClientRequests.With(metricsHandler).Record(1) + defer func() { + if err != nil { + metrics.ClientFailures.With(metricsHandler).Record(1, metrics.ServiceErrorTypeTag(err)) + } + metrics.ClientLatency.With(metricsHandler).Record(time.Since(startTime)) + }() + shardID := common.WorkflowIDToHistoryShard(request.GetNamespaceId(), request.GetFrontendRequest().GetOwner().GetId(), c.numShards) + op := func(ctx context.Context, client StreamServiceClient) error { + var err error + ctx, cancel := context.WithTimeout(ctx, history.DefaultTimeout) + defer cancel() + response, err = client.AddWorkflowMessages(ctx, request, opts...) + return err + } + err = c.redirector.Execute(ctx, shardID, op) + return response, err +} +func (c *StreamServiceLayeredClient) AddWorkflowMessages( + ctx context.Context, + request *AddWorkflowMessagesRequest, + opts ...grpc.CallOption, +) (*AddWorkflowMessagesResponse, error) { + call := func(ctx context.Context) (*AddWorkflowMessagesResponse, error) { + return c.callAddWorkflowMessagesNoRetry(ctx, request, opts...) + } + return backoff.ThrottleRetryContextWithReturn(ctx, call, c.retryPolicy, common.IsServiceClientTransientError) +} +func (c *StreamServiceLayeredClient) callRegisterStreamConsumerNoRetry( + ctx context.Context, + request *RegisterStreamConsumerRequest, + opts ...grpc.CallOption, +) (*RegisterStreamConsumerResponse, error) { + var response *RegisterStreamConsumerResponse + var err error + startTime := time.Now().UTC() + // the caller is a namespace, hence the tag below. + caller := headers.GetCallerInfo(ctx).CallerName + metricsHandler := c.metricsHandler.WithTags( + metrics.OperationTag("StreamService.RegisterStreamConsumer"), + metrics.NamespaceTag(caller), + metrics.ServiceRoleTag(metrics.HistoryRoleTagValue), + ) + metrics.ClientRequests.With(metricsHandler).Record(1) + defer func() { + if err != nil { + metrics.ClientFailures.With(metricsHandler).Record(1, metrics.ServiceErrorTypeTag(err)) + } + metrics.ClientLatency.With(metricsHandler).Record(time.Since(startTime)) + }() + shardID := common.WorkflowIDToHistoryShard(request.GetNamespaceId(), request.GetFrontendRequest().GetStreamId(), c.numShards) + op := func(ctx context.Context, client StreamServiceClient) error { + var err error + ctx, cancel := context.WithTimeout(ctx, history.DefaultTimeout) + defer cancel() + response, err = client.RegisterStreamConsumer(ctx, request, opts...) + return err + } + err = c.redirector.Execute(ctx, shardID, op) + return response, err +} +func (c *StreamServiceLayeredClient) RegisterStreamConsumer( + ctx context.Context, + request *RegisterStreamConsumerRequest, + opts ...grpc.CallOption, +) (*RegisterStreamConsumerResponse, error) { + call := func(ctx context.Context) (*RegisterStreamConsumerResponse, error) { + return c.callRegisterStreamConsumerNoRetry(ctx, request, opts...) + } + return backoff.ThrottleRetryContextWithReturn(ctx, call, c.retryPolicy, common.IsServiceClientTransientError) +} +func (c *StreamServiceLayeredClient) callAdvanceConsumerHeadNoRetry( + ctx context.Context, + request *AdvanceConsumerHeadRequest, + opts ...grpc.CallOption, +) (*AdvanceConsumerHeadResponse, error) { + var response *AdvanceConsumerHeadResponse + var err error + startTime := time.Now().UTC() + // the caller is a namespace, hence the tag below. + caller := headers.GetCallerInfo(ctx).CallerName + metricsHandler := c.metricsHandler.WithTags( + metrics.OperationTag("StreamService.AdvanceConsumerHead"), + metrics.NamespaceTag(caller), + metrics.ServiceRoleTag(metrics.HistoryRoleTagValue), + ) + metrics.ClientRequests.With(metricsHandler).Record(1) + defer func() { + if err != nil { + metrics.ClientFailures.With(metricsHandler).Record(1, metrics.ServiceErrorTypeTag(err)) + } + metrics.ClientLatency.With(metricsHandler).Record(time.Since(startTime)) + }() + shardID := common.WorkflowIDToHistoryShard(request.GetNamespaceId(), request.GetFrontendRequest().GetWorkflowId(), c.numShards) + op := func(ctx context.Context, client StreamServiceClient) error { + var err error + ctx, cancel := context.WithTimeout(ctx, history.DefaultTimeout) + defer cancel() + response, err = client.AdvanceConsumerHead(ctx, request, opts...) + return err + } + err = c.redirector.Execute(ctx, shardID, op) + return response, err +} +func (c *StreamServiceLayeredClient) AdvanceConsumerHead( + ctx context.Context, + request *AdvanceConsumerHeadRequest, + opts ...grpc.CallOption, +) (*AdvanceConsumerHeadResponse, error) { + call := func(ctx context.Context) (*AdvanceConsumerHeadResponse, error) { + return c.callAdvanceConsumerHeadNoRetry(ctx, request, opts...) + } + return backoff.ThrottleRetryContextWithReturn(ctx, call, c.retryPolicy, common.IsServiceClientTransientError) +} +func (c *StreamServiceLayeredClient) callCloseStreamNoRetry( + ctx context.Context, + request *CloseStreamRequest, + opts ...grpc.CallOption, +) (*CloseStreamResponse, error) { + var response *CloseStreamResponse + var err error + startTime := time.Now().UTC() + // the caller is a namespace, hence the tag below. + caller := headers.GetCallerInfo(ctx).CallerName + metricsHandler := c.metricsHandler.WithTags( + metrics.OperationTag("StreamService.CloseStream"), + metrics.NamespaceTag(caller), + metrics.ServiceRoleTag(metrics.HistoryRoleTagValue), + ) + metrics.ClientRequests.With(metricsHandler).Record(1) + defer func() { + if err != nil { + metrics.ClientFailures.With(metricsHandler).Record(1, metrics.ServiceErrorTypeTag(err)) + } + metrics.ClientLatency.With(metricsHandler).Record(time.Since(startTime)) + }() + shardID := common.WorkflowIDToHistoryShard(request.GetNamespaceId(), request.GetFrontendRequest().GetStreamId(), c.numShards) + op := func(ctx context.Context, client StreamServiceClient) error { + var err error + ctx, cancel := context.WithTimeout(ctx, history.DefaultTimeout) + defer cancel() + response, err = client.CloseStream(ctx, request, opts...) + return err + } + err = c.redirector.Execute(ctx, shardID, op) + return response, err +} +func (c *StreamServiceLayeredClient) CloseStream( + ctx context.Context, + request *CloseStreamRequest, + opts ...grpc.CallOption, +) (*CloseStreamResponse, error) { + call := func(ctx context.Context) (*CloseStreamResponse, error) { + return c.callCloseStreamNoRetry(ctx, request, opts...) + } + return backoff.ThrottleRetryContextWithReturn(ctx, call, c.retryPolicy, common.IsServiceClientTransientError) +} +func (c *StreamServiceLayeredClient) callTruncateStreamNoRetry( + ctx context.Context, + request *TruncateStreamRequest, + opts ...grpc.CallOption, +) (*TruncateStreamResponse, error) { + var response *TruncateStreamResponse + var err error + startTime := time.Now().UTC() + // the caller is a namespace, hence the tag below. + caller := headers.GetCallerInfo(ctx).CallerName + metricsHandler := c.metricsHandler.WithTags( + metrics.OperationTag("StreamService.TruncateStream"), + metrics.NamespaceTag(caller), + metrics.ServiceRoleTag(metrics.HistoryRoleTagValue), + ) + metrics.ClientRequests.With(metricsHandler).Record(1) + defer func() { + if err != nil { + metrics.ClientFailures.With(metricsHandler).Record(1, metrics.ServiceErrorTypeTag(err)) + } + metrics.ClientLatency.With(metricsHandler).Record(time.Since(startTime)) + }() + shardID := common.WorkflowIDToHistoryShard(request.GetNamespaceId(), request.GetFrontendRequest().GetStreamId(), c.numShards) + op := func(ctx context.Context, client StreamServiceClient) error { + var err error + ctx, cancel := context.WithTimeout(ctx, history.DefaultTimeout) + defer cancel() + response, err = client.TruncateStream(ctx, request, opts...) + return err + } + err = c.redirector.Execute(ctx, shardID, op) + return response, err +} +func (c *StreamServiceLayeredClient) TruncateStream( + ctx context.Context, + request *TruncateStreamRequest, + opts ...grpc.CallOption, +) (*TruncateStreamResponse, error) { + call := func(ctx context.Context) (*TruncateStreamResponse, error) { + return c.callTruncateStreamNoRetry(ctx, request, opts...) + } + return backoff.ThrottleRetryContextWithReturn(ctx, call, c.retryPolicy, common.IsServiceClientTransientError) +} +func (c *StreamServiceLayeredClient) callListStreamsNoRetry( + ctx context.Context, + request *ListStreamsRequest, + opts ...grpc.CallOption, +) (*ListStreamsResponse, error) { + var response *ListStreamsResponse + var err error + startTime := time.Now().UTC() + // the caller is a namespace, hence the tag below. + caller := headers.GetCallerInfo(ctx).CallerName + metricsHandler := c.metricsHandler.WithTags( + metrics.OperationTag("StreamService.ListStreams"), + metrics.NamespaceTag(caller), + metrics.ServiceRoleTag(metrics.HistoryRoleTagValue), + ) + metrics.ClientRequests.With(metricsHandler).Record(1) + defer func() { + if err != nil { + metrics.ClientFailures.With(metricsHandler).Record(1, metrics.ServiceErrorTypeTag(err)) + } + metrics.ClientLatency.With(metricsHandler).Record(time.Since(startTime)) + }() + shardID := int32(rand.Intn(int(c.numShards)) + 1) + op := func(ctx context.Context, client StreamServiceClient) error { + var err error + ctx, cancel := context.WithTimeout(ctx, history.DefaultTimeout) + defer cancel() + response, err = client.ListStreams(ctx, request, opts...) + return err + } + err = c.redirector.Execute(ctx, shardID, op) + return response, err +} +func (c *StreamServiceLayeredClient) ListStreams( + ctx context.Context, + request *ListStreamsRequest, + opts ...grpc.CallOption, +) (*ListStreamsResponse, error) { + call := func(ctx context.Context) (*ListStreamsResponse, error) { + return c.callListStreamsNoRetry(ctx, request, opts...) + } + return backoff.ThrottleRetryContextWithReturn(ctx, call, c.retryPolicy, common.IsServiceClientTransientError) +} +func (c *StreamServiceLayeredClient) callDeleteStreamNoRetry( + ctx context.Context, + request *DeleteStreamRequest, + opts ...grpc.CallOption, +) (*DeleteStreamResponse, error) { + var response *DeleteStreamResponse + var err error + startTime := time.Now().UTC() + // the caller is a namespace, hence the tag below. + caller := headers.GetCallerInfo(ctx).CallerName + metricsHandler := c.metricsHandler.WithTags( + metrics.OperationTag("StreamService.DeleteStream"), + metrics.NamespaceTag(caller), + metrics.ServiceRoleTag(metrics.HistoryRoleTagValue), + ) + metrics.ClientRequests.With(metricsHandler).Record(1) + defer func() { + if err != nil { + metrics.ClientFailures.With(metricsHandler).Record(1, metrics.ServiceErrorTypeTag(err)) + } + metrics.ClientLatency.With(metricsHandler).Record(time.Since(startTime)) + }() + shardID := common.WorkflowIDToHistoryShard(request.GetNamespaceId(), request.GetFrontendRequest().GetStreamId(), c.numShards) + op := func(ctx context.Context, client StreamServiceClient) error { + var err error + ctx, cancel := context.WithTimeout(ctx, history.DefaultTimeout) + defer cancel() + response, err = client.DeleteStream(ctx, request, opts...) + return err + } + err = c.redirector.Execute(ctx, shardID, op) + return response, err +} +func (c *StreamServiceLayeredClient) DeleteStream( + ctx context.Context, + request *DeleteStreamRequest, + opts ...grpc.CallOption, +) (*DeleteStreamResponse, error) { + call := func(ctx context.Context) (*DeleteStreamResponse, error) { + return c.callDeleteStreamNoRetry(ctx, request, opts...) + } + return backoff.ThrottleRetryContextWithReturn(ctx, call, c.retryPolicy, common.IsServiceClientTransientError) +} diff --git a/chasm/lib/stream/gen/streampb/v1/service_grpc.pb.go b/chasm/lib/stream/gen/streampb/v1/service_grpc.pb.go new file mode 100644 index 00000000000..f5fa6d3bcb1 --- /dev/null +++ b/chasm/lib/stream/gen/streampb/v1/service_grpc.pb.go @@ -0,0 +1,642 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// plugins: +// - protoc-gen-go-grpc +// - protoc +// source: temporal/server/chasm/lib/stream/proto/v1/service.proto + +package streampb + +import ( + context "context" + + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +const ( + StreamService_CreateStream_FullMethodName = "/temporal.server.chasm.lib.stream.proto.v1.StreamService/CreateStream" + StreamService_AddMessages_FullMethodName = "/temporal.server.chasm.lib.stream.proto.v1.StreamService/AddMessages" + StreamService_FinishWriting_FullMethodName = "/temporal.server.chasm.lib.stream.proto.v1.StreamService/FinishWriting" + StreamService_SubscribeWorkflow_FullMethodName = "/temporal.server.chasm.lib.stream.proto.v1.StreamService/SubscribeWorkflow" + StreamService_PollMessages_FullMethodName = "/temporal.server.chasm.lib.stream.proto.v1.StreamService/PollMessages" + StreamService_DescribeStream_FullMethodName = "/temporal.server.chasm.lib.stream.proto.v1.StreamService/DescribeStream" + StreamService_PollWorkflowMessages_FullMethodName = "/temporal.server.chasm.lib.stream.proto.v1.StreamService/PollWorkflowMessages" + StreamService_DescribeWorkflowStream_FullMethodName = "/temporal.server.chasm.lib.stream.proto.v1.StreamService/DescribeWorkflowStream" + StreamService_AddWorkflowMessages_FullMethodName = "/temporal.server.chasm.lib.stream.proto.v1.StreamService/AddWorkflowMessages" + StreamService_RegisterStreamConsumer_FullMethodName = "/temporal.server.chasm.lib.stream.proto.v1.StreamService/RegisterStreamConsumer" + StreamService_AdvanceConsumerHead_FullMethodName = "/temporal.server.chasm.lib.stream.proto.v1.StreamService/AdvanceConsumerHead" + StreamService_CloseStream_FullMethodName = "/temporal.server.chasm.lib.stream.proto.v1.StreamService/CloseStream" + StreamService_TruncateStream_FullMethodName = "/temporal.server.chasm.lib.stream.proto.v1.StreamService/TruncateStream" + StreamService_ListStreams_FullMethodName = "/temporal.server.chasm.lib.stream.proto.v1.StreamService/ListStreams" + StreamService_DeleteStream_FullMethodName = "/temporal.server.chasm.lib.stream.proto.v1.StreamService/DeleteStream" +) + +// StreamServiceClient is the client API for StreamService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type StreamServiceClient interface { + CreateStream(ctx context.Context, in *CreateStreamRequest, opts ...grpc.CallOption) (*CreateStreamResponse, error) + AddMessages(ctx context.Context, in *AddMessagesRequest, opts ...grpc.CallOption) (*AddMessagesResponse, error) + FinishWriting(ctx context.Context, in *FinishWritingRequest, opts ...grpc.CallOption) (*FinishWritingResponse, error) + SubscribeWorkflow(ctx context.Context, in *SubscribeWorkflowRequest, opts ...grpc.CallOption) (*SubscribeWorkflowResponse, error) + PollMessages(ctx context.Context, in *PollMessagesRequest, opts ...grpc.CallOption) (*PollMessagesResponse, error) + DescribeStream(ctx context.Context, in *DescribeStreamRequest, opts ...grpc.CallOption) (*DescribeStreamResponse, error) + // The owned-stream calls are routed on the owner, because the stream has no + // id of its own. A workflow's activity is routed on the workflow. + PollWorkflowMessages(ctx context.Context, in *PollWorkflowMessagesRequest, opts ...grpc.CallOption) (*PollWorkflowMessagesResponse, error) + DescribeWorkflowStream(ctx context.Context, in *DescribeWorkflowStreamRequest, opts ...grpc.CallOption) (*DescribeWorkflowStreamResponse, error) + AddWorkflowMessages(ctx context.Context, in *AddWorkflowMessagesRequest, opts ...grpc.CallOption) (*AddWorkflowMessagesResponse, error) + // Internal. History calls these on itself to reach a shard it does not own, + // which is the only way a step that spans two executions can work on a + // cluster with more than one history host. + RegisterStreamConsumer(ctx context.Context, in *RegisterStreamConsumerRequest, opts ...grpc.CallOption) (*RegisterStreamConsumerResponse, error) + AdvanceConsumerHead(ctx context.Context, in *AdvanceConsumerHeadRequest, opts ...grpc.CallOption) (*AdvanceConsumerHeadResponse, error) + CloseStream(ctx context.Context, in *CloseStreamRequest, opts ...grpc.CallOption) (*CloseStreamResponse, error) + TruncateStream(ctx context.Context, in *TruncateStreamRequest, opts ...grpc.CallOption) (*TruncateStreamResponse, error) + // Served on the frontend only: it queries visibility rather than a stream, + // so there is no business ID to route on and nothing for a shard to answer. + ListStreams(ctx context.Context, in *ListStreamsRequest, opts ...grpc.CallOption) (*ListStreamsResponse, error) + DeleteStream(ctx context.Context, in *DeleteStreamRequest, opts ...grpc.CallOption) (*DeleteStreamResponse, error) +} + +type streamServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewStreamServiceClient(cc grpc.ClientConnInterface) StreamServiceClient { + return &streamServiceClient{cc} +} + +func (c *streamServiceClient) CreateStream(ctx context.Context, in *CreateStreamRequest, opts ...grpc.CallOption) (*CreateStreamResponse, error) { + out := new(CreateStreamResponse) + err := c.cc.Invoke(ctx, StreamService_CreateStream_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *streamServiceClient) AddMessages(ctx context.Context, in *AddMessagesRequest, opts ...grpc.CallOption) (*AddMessagesResponse, error) { + out := new(AddMessagesResponse) + err := c.cc.Invoke(ctx, StreamService_AddMessages_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *streamServiceClient) FinishWriting(ctx context.Context, in *FinishWritingRequest, opts ...grpc.CallOption) (*FinishWritingResponse, error) { + out := new(FinishWritingResponse) + err := c.cc.Invoke(ctx, StreamService_FinishWriting_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *streamServiceClient) SubscribeWorkflow(ctx context.Context, in *SubscribeWorkflowRequest, opts ...grpc.CallOption) (*SubscribeWorkflowResponse, error) { + out := new(SubscribeWorkflowResponse) + err := c.cc.Invoke(ctx, StreamService_SubscribeWorkflow_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *streamServiceClient) PollMessages(ctx context.Context, in *PollMessagesRequest, opts ...grpc.CallOption) (*PollMessagesResponse, error) { + out := new(PollMessagesResponse) + err := c.cc.Invoke(ctx, StreamService_PollMessages_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *streamServiceClient) DescribeStream(ctx context.Context, in *DescribeStreamRequest, opts ...grpc.CallOption) (*DescribeStreamResponse, error) { + out := new(DescribeStreamResponse) + err := c.cc.Invoke(ctx, StreamService_DescribeStream_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *streamServiceClient) PollWorkflowMessages(ctx context.Context, in *PollWorkflowMessagesRequest, opts ...grpc.CallOption) (*PollWorkflowMessagesResponse, error) { + out := new(PollWorkflowMessagesResponse) + err := c.cc.Invoke(ctx, StreamService_PollWorkflowMessages_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *streamServiceClient) DescribeWorkflowStream(ctx context.Context, in *DescribeWorkflowStreamRequest, opts ...grpc.CallOption) (*DescribeWorkflowStreamResponse, error) { + out := new(DescribeWorkflowStreamResponse) + err := c.cc.Invoke(ctx, StreamService_DescribeWorkflowStream_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *streamServiceClient) AddWorkflowMessages(ctx context.Context, in *AddWorkflowMessagesRequest, opts ...grpc.CallOption) (*AddWorkflowMessagesResponse, error) { + out := new(AddWorkflowMessagesResponse) + err := c.cc.Invoke(ctx, StreamService_AddWorkflowMessages_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *streamServiceClient) RegisterStreamConsumer(ctx context.Context, in *RegisterStreamConsumerRequest, opts ...grpc.CallOption) (*RegisterStreamConsumerResponse, error) { + out := new(RegisterStreamConsumerResponse) + err := c.cc.Invoke(ctx, StreamService_RegisterStreamConsumer_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *streamServiceClient) AdvanceConsumerHead(ctx context.Context, in *AdvanceConsumerHeadRequest, opts ...grpc.CallOption) (*AdvanceConsumerHeadResponse, error) { + out := new(AdvanceConsumerHeadResponse) + err := c.cc.Invoke(ctx, StreamService_AdvanceConsumerHead_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *streamServiceClient) CloseStream(ctx context.Context, in *CloseStreamRequest, opts ...grpc.CallOption) (*CloseStreamResponse, error) { + out := new(CloseStreamResponse) + err := c.cc.Invoke(ctx, StreamService_CloseStream_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *streamServiceClient) TruncateStream(ctx context.Context, in *TruncateStreamRequest, opts ...grpc.CallOption) (*TruncateStreamResponse, error) { + out := new(TruncateStreamResponse) + err := c.cc.Invoke(ctx, StreamService_TruncateStream_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *streamServiceClient) ListStreams(ctx context.Context, in *ListStreamsRequest, opts ...grpc.CallOption) (*ListStreamsResponse, error) { + out := new(ListStreamsResponse) + err := c.cc.Invoke(ctx, StreamService_ListStreams_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *streamServiceClient) DeleteStream(ctx context.Context, in *DeleteStreamRequest, opts ...grpc.CallOption) (*DeleteStreamResponse, error) { + out := new(DeleteStreamResponse) + err := c.cc.Invoke(ctx, StreamService_DeleteStream_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// StreamServiceServer is the server API for StreamService service. +// All implementations must embed UnimplementedStreamServiceServer +// for forward compatibility +type StreamServiceServer interface { + CreateStream(context.Context, *CreateStreamRequest) (*CreateStreamResponse, error) + AddMessages(context.Context, *AddMessagesRequest) (*AddMessagesResponse, error) + FinishWriting(context.Context, *FinishWritingRequest) (*FinishWritingResponse, error) + SubscribeWorkflow(context.Context, *SubscribeWorkflowRequest) (*SubscribeWorkflowResponse, error) + PollMessages(context.Context, *PollMessagesRequest) (*PollMessagesResponse, error) + DescribeStream(context.Context, *DescribeStreamRequest) (*DescribeStreamResponse, error) + // The owned-stream calls are routed on the owner, because the stream has no + // id of its own. A workflow's activity is routed on the workflow. + PollWorkflowMessages(context.Context, *PollWorkflowMessagesRequest) (*PollWorkflowMessagesResponse, error) + DescribeWorkflowStream(context.Context, *DescribeWorkflowStreamRequest) (*DescribeWorkflowStreamResponse, error) + AddWorkflowMessages(context.Context, *AddWorkflowMessagesRequest) (*AddWorkflowMessagesResponse, error) + // Internal. History calls these on itself to reach a shard it does not own, + // which is the only way a step that spans two executions can work on a + // cluster with more than one history host. + RegisterStreamConsumer(context.Context, *RegisterStreamConsumerRequest) (*RegisterStreamConsumerResponse, error) + AdvanceConsumerHead(context.Context, *AdvanceConsumerHeadRequest) (*AdvanceConsumerHeadResponse, error) + CloseStream(context.Context, *CloseStreamRequest) (*CloseStreamResponse, error) + TruncateStream(context.Context, *TruncateStreamRequest) (*TruncateStreamResponse, error) + // Served on the frontend only: it queries visibility rather than a stream, + // so there is no business ID to route on and nothing for a shard to answer. + ListStreams(context.Context, *ListStreamsRequest) (*ListStreamsResponse, error) + DeleteStream(context.Context, *DeleteStreamRequest) (*DeleteStreamResponse, error) + mustEmbedUnimplementedStreamServiceServer() +} + +// UnimplementedStreamServiceServer must be embedded to have forward compatible implementations. +type UnimplementedStreamServiceServer struct { +} + +func (UnimplementedStreamServiceServer) CreateStream(context.Context, *CreateStreamRequest) (*CreateStreamResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method CreateStream not implemented") +} +func (UnimplementedStreamServiceServer) AddMessages(context.Context, *AddMessagesRequest) (*AddMessagesResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method AddMessages not implemented") +} +func (UnimplementedStreamServiceServer) FinishWriting(context.Context, *FinishWritingRequest) (*FinishWritingResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method FinishWriting not implemented") +} +func (UnimplementedStreamServiceServer) SubscribeWorkflow(context.Context, *SubscribeWorkflowRequest) (*SubscribeWorkflowResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method SubscribeWorkflow not implemented") +} +func (UnimplementedStreamServiceServer) PollMessages(context.Context, *PollMessagesRequest) (*PollMessagesResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method PollMessages not implemented") +} +func (UnimplementedStreamServiceServer) DescribeStream(context.Context, *DescribeStreamRequest) (*DescribeStreamResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method DescribeStream not implemented") +} +func (UnimplementedStreamServiceServer) PollWorkflowMessages(context.Context, *PollWorkflowMessagesRequest) (*PollWorkflowMessagesResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method PollWorkflowMessages not implemented") +} +func (UnimplementedStreamServiceServer) DescribeWorkflowStream(context.Context, *DescribeWorkflowStreamRequest) (*DescribeWorkflowStreamResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method DescribeWorkflowStream not implemented") +} +func (UnimplementedStreamServiceServer) AddWorkflowMessages(context.Context, *AddWorkflowMessagesRequest) (*AddWorkflowMessagesResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method AddWorkflowMessages not implemented") +} +func (UnimplementedStreamServiceServer) RegisterStreamConsumer(context.Context, *RegisterStreamConsumerRequest) (*RegisterStreamConsumerResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method RegisterStreamConsumer not implemented") +} +func (UnimplementedStreamServiceServer) AdvanceConsumerHead(context.Context, *AdvanceConsumerHeadRequest) (*AdvanceConsumerHeadResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method AdvanceConsumerHead not implemented") +} +func (UnimplementedStreamServiceServer) CloseStream(context.Context, *CloseStreamRequest) (*CloseStreamResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method CloseStream not implemented") +} +func (UnimplementedStreamServiceServer) TruncateStream(context.Context, *TruncateStreamRequest) (*TruncateStreamResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method TruncateStream not implemented") +} +func (UnimplementedStreamServiceServer) ListStreams(context.Context, *ListStreamsRequest) (*ListStreamsResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ListStreams not implemented") +} +func (UnimplementedStreamServiceServer) DeleteStream(context.Context, *DeleteStreamRequest) (*DeleteStreamResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method DeleteStream not implemented") +} +func (UnimplementedStreamServiceServer) mustEmbedUnimplementedStreamServiceServer() {} + +// UnsafeStreamServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to StreamServiceServer will +// result in compilation errors. +type UnsafeStreamServiceServer interface { + mustEmbedUnimplementedStreamServiceServer() +} + +func RegisterStreamServiceServer(s grpc.ServiceRegistrar, srv StreamServiceServer) { + s.RegisterService(&StreamService_ServiceDesc, srv) +} + +func _StreamService_CreateStream_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CreateStreamRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StreamServiceServer).CreateStream(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: StreamService_CreateStream_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StreamServiceServer).CreateStream(ctx, req.(*CreateStreamRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _StreamService_AddMessages_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(AddMessagesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StreamServiceServer).AddMessages(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: StreamService_AddMessages_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StreamServiceServer).AddMessages(ctx, req.(*AddMessagesRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _StreamService_FinishWriting_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(FinishWritingRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StreamServiceServer).FinishWriting(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: StreamService_FinishWriting_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StreamServiceServer).FinishWriting(ctx, req.(*FinishWritingRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _StreamService_SubscribeWorkflow_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SubscribeWorkflowRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StreamServiceServer).SubscribeWorkflow(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: StreamService_SubscribeWorkflow_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StreamServiceServer).SubscribeWorkflow(ctx, req.(*SubscribeWorkflowRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _StreamService_PollMessages_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(PollMessagesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StreamServiceServer).PollMessages(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: StreamService_PollMessages_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StreamServiceServer).PollMessages(ctx, req.(*PollMessagesRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _StreamService_DescribeStream_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DescribeStreamRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StreamServiceServer).DescribeStream(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: StreamService_DescribeStream_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StreamServiceServer).DescribeStream(ctx, req.(*DescribeStreamRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _StreamService_PollWorkflowMessages_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(PollWorkflowMessagesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StreamServiceServer).PollWorkflowMessages(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: StreamService_PollWorkflowMessages_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StreamServiceServer).PollWorkflowMessages(ctx, req.(*PollWorkflowMessagesRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _StreamService_DescribeWorkflowStream_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DescribeWorkflowStreamRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StreamServiceServer).DescribeWorkflowStream(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: StreamService_DescribeWorkflowStream_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StreamServiceServer).DescribeWorkflowStream(ctx, req.(*DescribeWorkflowStreamRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _StreamService_AddWorkflowMessages_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(AddWorkflowMessagesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StreamServiceServer).AddWorkflowMessages(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: StreamService_AddWorkflowMessages_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StreamServiceServer).AddWorkflowMessages(ctx, req.(*AddWorkflowMessagesRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _StreamService_RegisterStreamConsumer_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RegisterStreamConsumerRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StreamServiceServer).RegisterStreamConsumer(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: StreamService_RegisterStreamConsumer_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StreamServiceServer).RegisterStreamConsumer(ctx, req.(*RegisterStreamConsumerRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _StreamService_AdvanceConsumerHead_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(AdvanceConsumerHeadRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StreamServiceServer).AdvanceConsumerHead(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: StreamService_AdvanceConsumerHead_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StreamServiceServer).AdvanceConsumerHead(ctx, req.(*AdvanceConsumerHeadRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _StreamService_CloseStream_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CloseStreamRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StreamServiceServer).CloseStream(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: StreamService_CloseStream_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StreamServiceServer).CloseStream(ctx, req.(*CloseStreamRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _StreamService_TruncateStream_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(TruncateStreamRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StreamServiceServer).TruncateStream(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: StreamService_TruncateStream_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StreamServiceServer).TruncateStream(ctx, req.(*TruncateStreamRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _StreamService_ListStreams_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListStreamsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StreamServiceServer).ListStreams(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: StreamService_ListStreams_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StreamServiceServer).ListStreams(ctx, req.(*ListStreamsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _StreamService_DeleteStream_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DeleteStreamRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StreamServiceServer).DeleteStream(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: StreamService_DeleteStream_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StreamServiceServer).DeleteStream(ctx, req.(*DeleteStreamRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// StreamService_ServiceDesc is the grpc.ServiceDesc for StreamService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var StreamService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "temporal.server.chasm.lib.stream.proto.v1.StreamService", + HandlerType: (*StreamServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "CreateStream", + Handler: _StreamService_CreateStream_Handler, + }, + { + MethodName: "AddMessages", + Handler: _StreamService_AddMessages_Handler, + }, + { + MethodName: "FinishWriting", + Handler: _StreamService_FinishWriting_Handler, + }, + { + MethodName: "SubscribeWorkflow", + Handler: _StreamService_SubscribeWorkflow_Handler, + }, + { + MethodName: "PollMessages", + Handler: _StreamService_PollMessages_Handler, + }, + { + MethodName: "DescribeStream", + Handler: _StreamService_DescribeStream_Handler, + }, + { + MethodName: "PollWorkflowMessages", + Handler: _StreamService_PollWorkflowMessages_Handler, + }, + { + MethodName: "DescribeWorkflowStream", + Handler: _StreamService_DescribeWorkflowStream_Handler, + }, + { + MethodName: "AddWorkflowMessages", + Handler: _StreamService_AddWorkflowMessages_Handler, + }, + { + MethodName: "RegisterStreamConsumer", + Handler: _StreamService_RegisterStreamConsumer_Handler, + }, + { + MethodName: "AdvanceConsumerHead", + Handler: _StreamService_AdvanceConsumerHead_Handler, + }, + { + MethodName: "CloseStream", + Handler: _StreamService_CloseStream_Handler, + }, + { + MethodName: "TruncateStream", + Handler: _StreamService_TruncateStream_Handler, + }, + { + MethodName: "ListStreams", + Handler: _StreamService_ListStreams_Handler, + }, + { + MethodName: "DeleteStream", + Handler: _StreamService_DeleteStream_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "temporal/server/chasm/lib/stream/proto/v1/service.proto", +} diff --git a/chasm/lib/stream/owned.go b/chasm/lib/stream/owned.go new file mode 100644 index 00000000000..492d45f40ff --- /dev/null +++ b/chasm/lib/stream/owned.go @@ -0,0 +1,137 @@ +package stream + +import ( + "go.temporal.io/api/serviceerror" + "go.temporal.io/server/chasm" + streamlib "go.temporal.io/server/chasm/lib/stream/gen/streampb/v1" +) + +// DefaultStreamName is the stream an owner's writer or reader addresses when it +// names none. +const DefaultStreamName = "output" + +// Owner is an execution that holds streams by key. An attached stream has no +// id of its own, so every read and write from outside the execution goes +// through one of these, whatever kind of execution it is. +type Owner interface { + // OwnedStream returns the stream under key, or nil when nothing has + // created it yet. + OwnedStream(ctx chasm.Context, key string) *Stream + + // OwnedStreamEnded reports that nothing more can be appended under key + // because the owner itself is finished, whether or not the stream was + // closed. A reader tailing it is released instead of parked forever. + OwnedStreamEnded(ctx chasm.Context, key string) bool + + // AppendToOwnedStream appends on behalf of a writer outside the execution, + // creating the stream on first write. + AppendToOwnedStream( + mctx chasm.MutableContext, key string, req AddMessagesRequest, + ) (AddMessagesResult, error) +} + +// OwnedStreams is the map of streams one execution owns. Every kind of owner +// keeps one, so the create path and the shared budget are written once. +type OwnedStreams struct { + Streams *chasm.Map[string, *Stream] + + // Kind names the owner in refusals, so a caller learns which execution + // ran out of room. + Kind string +} + +// Named returns the stream under key, creating it on first use. Implicit +// creation is deliberate: an owner publishing to its own output should not +// have to coordinate with anyone about who creates it. +func (o OwnedStreams) Named( + ctx chasm.MutableContext, + key string, + limits Limits, +) (*Stream, error) { + if *o.Streams == nil { + *o.Streams = make(chasm.Map[string, *Stream]) + } + if field, ok := (*o.Streams)[key]; ok { + return field.Get(ctx), nil + } + limits = limits.withDefaults() + + // Checked only on the create path, so an existing stream is never refused + // for room. The key arrives from the caller and every distinct one adds a + // component to this execution's mutable state, so without a bound an + // outside writer can grow that state until the size limit ends the owner. + if len(*o.Streams) >= limits.MaxOwnedStreamsPerWorkflow { + return nil, serviceerror.NewFailedPreconditionf( + "%s already owns %d streams, the limit", o.Kind, limits.MaxOwnedStreamsPerWorkflow) + } + + // Budgeted, because the batches live in this execution's mutable state and + // the size limit on that ends the execution instead of refusing. + created, err := NewStream(ctx, NewStreamRequest{ + Attached: true, + Budget: &streamlib.StreamBudget{ + MaxItems: int64(limits.OwnedStreamMaxItems), + MaxBytes: int64(limits.OwnedStreamMaxBytes), + }, + }) + if err != nil { + return nil, err + } + (*o.Streams)[key] = chasm.NewComponentField(ctx, created) + return created, nil +} + +// Get returns the stream under key, or nil when nothing has created it. +func (o OwnedStreams) Get(ctx chasm.Context, key string) *Stream { + field, ok := (*o.Streams)[key] + if !ok { + return nil + } + return field.Get(ctx) +} + +// SiblingBytes is what every other stream of this owner holds. +// +// The per-stream budget bounds one stream, and an outside writer can name as +// many as MaxOwnedStreamsPerWorkflow allows. Their sum is what the execution +// size limit sees, so an append has to be measured against the sum. +// +// Skipped for the common case of a single stream, where the sum is the stream +// itself and reading the others would load state nothing else needs. +func (o OwnedStreams) SiblingBytes(ctx chasm.Context, key string) int64 { + if len(*o.Streams) < 2 { + return 0 + } + var total int64 + for other, field := range *o.Streams { + if other == key { + continue + } + total += field.Get(ctx).State.GetAppendedBytes() + } + return total +} + +// Append appends under key, creating the stream on first write, and measures +// the batch against the owner's shared budget as well as the stream's own. +func (o OwnedStreams) Append( + mctx chasm.MutableContext, + key string, + req AddMessagesRequest, +) (AddMessagesResult, error) { + s, err := o.Named(mctx, key, req.Limits) + if err != nil { + return AddMessagesResult{}, err + } + req.SiblingBytes = o.SiblingBytes(mctx, key) + return s.AddMessages(mctx, req) +} + +// CheckStreamName refuses a name too long to become a key in mutable state. +func CheckStreamName(name string) error { + if len(name) > MaxStreamNameLength { + return serviceerror.NewInvalidArgumentf( + "stream name is %d characters, over the %d limit", len(name), MaxStreamNameLength) + } + return nil +} diff --git a/chasm/lib/stream/proto/v1/request_response.proto b/chasm/lib/stream/proto/v1/request_response.proto new file mode 100644 index 00000000000..24b242745e8 --- /dev/null +++ b/chasm/lib/stream/proto/v1/request_response.proto @@ -0,0 +1,430 @@ +syntax = "proto3"; + +package temporal.server.chasm.lib.stream.proto.v1; + +import "chasm/lib/stream/proto/v1/message.proto"; +import "chasm/lib/stream/proto/v1/stream_state.proto"; +import "temporal/api/common/v1/message.proto"; +import "temporal/api/stream/v1/message.proto"; + +option go_package = "go.temporal.io/server/chasm/lib/stream/gen/streampb;streampb"; + +// The frontend-facing shapes are defined here rather than in the public API +// because streams have no public API yet. Keeping them in a nested +// frontend_request mirrors the other CHASM libraries, so promoting them later +// is a package move rather than a redesign. + +message CreateStreamInput { + string namespace = 1; + string stream_id = 2; + StreamLifecycle lifecycle = 3; +} + +message CreateStreamOutput { + string run_id = 1; +} + +message AddMessagesInput { + string namespace = 1; + string stream_id = 2; + // Optional. Supplying it skips resolving the stream's current run, which is + // a persistence lookup on every call. CreateStream returns it. + string run_id = 9; + repeated StreamRecord records = 3; + + // Idempotency, all optional. Supply a producer identity and sequence, or an + // expected offset, or neither and accept at-least-once. + string producer_id = 4; + int64 sequence = 5; + // Guarded by use_expected_offset because proto3 optional is not supported + // by this repo's helper generator. + int64 expected_offset = 6; + bool use_expected_offset = 8; + + reserved 7; +} + +message AddMessagesOutput { + int64 first_offset = 1; + int64 next_offset = 2; + int64 count = 3; + // True when a retry matched a recorded sequence and nothing was appended. + bool deduplicated = 4; +} + +message FinishWritingInput { + string namespace = 1; + string stream_id = 2; + string producer_id = 3; +} + +message FinishWritingOutput {} + +// Registers a Workflow as a consumer of a stream it owns. The cursor lands in +// the Workflow's own state, so from then on each of its Workflow Tasks carries +// the next range and records what it consumed. +message SubscribeWorkflowInput { + string namespace = 1; + string workflow_id = 2; + // Optional. Pins to one run, so a caller that has continued as new is not + // silently redirected to the successor's stream, which starts empty and at + // offset zero. Empty means whichever run is current. + string owner_run_id = 6; + + // Name of the stream within the Workflow, for a stream it owns. + string stream_name = 3; + // Id of a standalone stream in another execution. Exactly one of this and + // stream_name is set. + string stream_id = 5; + // Where to start, as an absolute offset. Read only when start_position is + // unset. A negative value is refused. + int64 start_offset = 4; + // Where to start. Resolved here rather than at delivery, so the first + // recorded range starts from a fact instead of a reading. Refused alongside a + // non-zero start_offset. + temporal.api.stream.v1.StreamStartPosition start_position = 7; +} + +message SubscribeWorkflowOutput { + int64 start_offset = 1; +} + +message PollMessagesInput { + string namespace = 1; + string stream_id = 2; + // Optional, as on AddMessagesInput. + string run_id = 7; + int64 from_offset = 3; + // Where a first poll starts, when the reader has no offset yet. Resolved in + // the same read that serves the records, so it cannot race with truncation, + // and next_offset carries the result for the polls after it. Refused + // alongside a non-zero from_offset. + temporal.api.stream.v1.StreamStartPosition start_position = 8; + int32 max_messages = 4; + // Filters by exact topic. Offsets are assigned over the unfiltered stream, so + // next_offset advances past filtered-out records too. + repeated string topics = 5; + + // When set and the reader is caught up, block until something arrives, the + // stream closes, or the server's long-poll timeout elapses. A timeout returns + // an empty response rather than an error, so the caller simply polls again. + bool wait_new_messages = 6; +} + +message PollMessagesOutput { + repeated StreamRecord records = 1; + int64 next_offset = 2; + int64 head_offset = 3; + bool closed = 4; + temporal.api.common.v1.Payload close_reason = 5; + // The execution holding the stream. A workflow task slice built from this + // read names the run it came from. + string run_id = 6; +} + +message DescribeStreamInput { + string namespace = 1; + string stream_id = 2; +} + +// The kind of execution an owned stream lives in. +enum StreamOwnerKind { + STREAM_OWNER_KIND_UNSPECIFIED = 0; + STREAM_OWNER_KIND_WORKFLOW = 1; + // A standalone activity, which is an execution of its own. + STREAM_OWNER_KIND_ACTIVITY = 2; + // An activity a workflow scheduled. It is not an execution of its own, so + // its streams are reached through the workflow and routed on it. + STREAM_OWNER_KIND_WORKFLOW_ACTIVITY = 3; +} + +// The execution that owns a stream. An attached stream has no id of its own, +// so it is named by this and a stream name, and routed on id. +message StreamOwner { + StreamOwnerKind kind = 1; + // The workflow id for WORKFLOW and WORKFLOW_ACTIVITY, the activity id for + // ACTIVITY. + string id = 2; + // Optional. Pins to one run of the execution named by id, so a caller is not + // silently redirected to a later run's stream, which starts empty and at + // offset zero. Empty means whichever run is current. + string run_id = 3; + // The activity within the workflow, for WORKFLOW_ACTIVITY only. + string activity_id = 4; +} + +// A stream an execution owns lives inside it, so it has no standalone id to +// address it by. It is named by its owner and its name instead, and routed on +// the owner. +message PollWorkflowMessagesInput { + string namespace = 1; + StreamOwner owner = 9; + // Shorthand for an owner of kind WORKFLOW, and refused alongside owner. + string workflow_id = 2; + string owner_run_id = 8; + + // Empty means the owner's default output stream. + string stream_name = 3; + int64 from_offset = 4; + // As on PollMessagesInput. + temporal.api.stream.v1.StreamStartPosition start_position = 10; + int32 max_messages = 5; + // Filters as on PollMessagesInput. + repeated string topics = 6; + bool wait_new_messages = 7; +} + +message DescribeWorkflowStreamInput { + string namespace = 1; + StreamOwner owner = 5; + // Shorthand for an owner of kind WORKFLOW, and refused alongside owner. + string workflow_id = 2; + string owner_run_id = 4; + + string stream_name = 3; +} + +// Appending to an owned stream from outside its owner. A workflow's own +// publishes ride its Workflow Task instead; an activity has no other path. +message AddWorkflowMessagesInput { + string namespace = 1; + StreamOwner owner = 8; + // Shorthand for an owner of kind WORKFLOW, and refused alongside owner. + string workflow_id = 2; + string owner_run_id = 7; + + // Empty means the owner's default output stream. + string stream_name = 3; + repeated StreamRecord records = 4; + // Optional idempotency, as on AddMessagesInput. + string producer_id = 5; + int64 sequence = 6; +} + +message DescribeStreamOutput { + StreamState state = 1; +} + +message CloseStreamInput { + string namespace = 1; + string stream_id = 2; + temporal.api.common.v1.Payload reason = 3; +} + +message CloseStreamOutput {} + +message TruncateStreamInput { + string namespace = 1; + string stream_id = 2; + int64 new_base_offset = 3; +} + +message TruncateStreamOutput {} + +message DeleteStreamInput { + string namespace = 1; + string stream_id = 2; + // Delete even while a workflow consumer is active. Without it the call is + // refused, because the consumer's History depends on ranges the deletion + // takes with it. + bool force = 3; +} + +message DeleteStreamOutput {} + +message CreateStreamRequest { + string namespace_id = 1; + CreateStreamInput frontend_request = 2; +} +message CreateStreamResponse { + CreateStreamOutput frontend_response = 1; +} + +message AddMessagesRequest { + string namespace_id = 1; + AddMessagesInput frontend_request = 2; +} +message AddMessagesResponse { + AddMessagesOutput frontend_response = 1; +} + +message FinishWritingRequest { + string namespace_id = 1; + FinishWritingInput frontend_request = 2; +} +message FinishWritingResponse { + FinishWritingOutput frontend_response = 1; +} + +message SubscribeWorkflowRequest { + string namespace_id = 1; + SubscribeWorkflowInput frontend_request = 2; +} +message SubscribeWorkflowResponse { + SubscribeWorkflowOutput frontend_response = 1; +} + +message PollMessagesRequest { + string namespace_id = 1; + PollMessagesInput frontend_request = 2; +} +message PollMessagesResponse { + PollMessagesOutput frontend_response = 1; +} + +message DescribeStreamRequest { + string namespace_id = 1; + DescribeStreamInput frontend_request = 2; +} +message DescribeStreamResponse { + DescribeStreamOutput frontend_response = 1; +} + +message PollWorkflowMessagesRequest { + string namespace_id = 1; + PollWorkflowMessagesInput frontend_request = 2; +} +message PollWorkflowMessagesResponse { + PollMessagesOutput frontend_response = 1; +} + +message DescribeWorkflowStreamRequest { + string namespace_id = 1; + DescribeWorkflowStreamInput frontend_request = 2; +} +message DescribeWorkflowStreamResponse { + DescribeStreamOutput frontend_response = 1; +} + +// Registering a consumer on a stream in another execution. Split out from +// SubscribeWorkflow because the two halves live on different shards: the pin +// goes on the stream, the cursor goes on the consuming workflow, and a handler +// can only reach the shard it was routed to. +message RegisterStreamConsumerInput { + string namespace = 1; + string stream_id = 2; + // The workflow that will consume. Together with the run it names the pin, + // so a later run of the same workflow id registers fresh rather than + // inheriting a closed run's floor. + string consumer_workflow_id = 3; + string consumer_run_id = 5; + // An absolute offset, read only when start_position is unset. A negative + // value is refused. + int64 start_offset = 4; + // Resolved here, where the frontier is, and returned as start_offset on the + // output so the cursor records a fact. + temporal.api.stream.v1.StreamStartPosition start_position = 6; +} + +message RegisterStreamConsumerOutput { + reserved 2, 3; + + int64 start_offset = 1; + // The frontier at registration, so the cursor starts with a known head + // instead of waiting for the first push. + int64 known_head = 4; + + // No execution in this namespace holds a stream with that id, so nothing + // was registered. An answer rather than a NotFound because the caller acts + // on it: a subscribe command falls back to a stream of the workflow's own by + // that name. Every other NotFound, from a registry miss to a shard that has + // moved, stays an error, since binding the workflow to different data on one + // of those would be silent and permanent. + bool stream_absent = 5; +} + +// Telling one consumer that the frontier moved. Routed to the consumer, which +// is not where the stream lives. +message AdvanceConsumerHeadInput { + string namespace = 1; + string workflow_id = 2; + // The run that subscribed. A successor from continue-as-new holds no cursor + // for this stream, so pushing the frontier at it would land nowhere. + string owner_run_id = 5; + string stream_id = 3; + int64 head_offset = 4; +} + +message AdvanceConsumerHeadOutput { + // The run that held the pin is closed and no current run of the workflow + // consumes the stream, so the stream can release the pin. + bool consumer_closed = 1; + // The run that held the pin is closed but a successor carries the + // subscription, so the stream re-keys the pin to it, with the floor the + // successor's cursor started from. + string successor_run_id = 2; + int64 successor_start_offset = 3; +} + +message AddWorkflowMessagesRequest { + string namespace_id = 1; + AddWorkflowMessagesInput frontend_request = 2; +} +message AddWorkflowMessagesResponse { + AddMessagesOutput frontend_response = 1; +} + +message RegisterStreamConsumerRequest { + string namespace_id = 1; + RegisterStreamConsumerInput frontend_request = 2; +} +message RegisterStreamConsumerResponse { + RegisterStreamConsumerOutput frontend_response = 1; +} + +message AdvanceConsumerHeadRequest { + string namespace_id = 1; + AdvanceConsumerHeadInput frontend_request = 2; +} +message AdvanceConsumerHeadResponse { + AdvanceConsumerHeadOutput frontend_response = 1; +} + +message CloseStreamRequest { + string namespace_id = 1; + CloseStreamInput frontend_request = 2; +} +message CloseStreamResponse { + CloseStreamOutput frontend_response = 1; +} + +message TruncateStreamRequest { + string namespace_id = 1; + TruncateStreamInput frontend_request = 2; +} +message TruncateStreamResponse { + TruncateStreamOutput frontend_response = 1; +} + +message ListStreamsInput { + string namespace = 1; + int32 page_size = 2; + bytes next_page_token = 3; + string query = 4; +} + +message StreamListEntry { + string stream_id = 1; + string run_id = 2; +} + +message ListStreamsOutput { + repeated StreamListEntry streams = 1; + bytes next_page_token = 2; +} + +message ListStreamsRequest { + string namespace_id = 1; + ListStreamsInput frontend_request = 2; +} +message ListStreamsResponse { + ListStreamsOutput frontend_response = 1; +} + +message DeleteStreamRequest { + string namespace_id = 1; + DeleteStreamInput frontend_request = 2; +} +message DeleteStreamResponse { + DeleteStreamOutput frontend_response = 1; +} diff --git a/chasm/lib/stream/proto/v1/service.proto b/chasm/lib/stream/proto/v1/service.proto new file mode 100644 index 00000000000..fa2850d1abd --- /dev/null +++ b/chasm/lib/stream/proto/v1/service.proto @@ -0,0 +1,93 @@ +syntax = "proto3"; + +package temporal.server.chasm.lib.stream.proto.v1; + +import "chasm/lib/stream/proto/v1/request_response.proto"; +import "temporal/server/api/common/v1/api_category.proto"; +import "temporal/server/api/routing/v1/extension.proto"; + +option go_package = "go.temporal.io/server/chasm/lib/stream/gen/streampb;streampb"; + +service StreamService { + rpc CreateStream(CreateStreamRequest) returns (CreateStreamResponse) { + option (temporal.server.api.routing.v1.routing).business_id = "frontend_request.stream_id"; + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + rpc AddMessages(AddMessagesRequest) returns (AddMessagesResponse) { + option (temporal.server.api.routing.v1.routing).business_id = "frontend_request.stream_id"; + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + rpc FinishWriting(FinishWritingRequest) returns (FinishWritingResponse) { + option (temporal.server.api.routing.v1.routing).business_id = "frontend_request.stream_id"; + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + rpc SubscribeWorkflow(SubscribeWorkflowRequest) returns (SubscribeWorkflowResponse) { + option (temporal.server.api.routing.v1.routing).business_id = "frontend_request.workflow_id"; + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + rpc PollMessages(PollMessagesRequest) returns (PollMessagesResponse) { + option (temporal.server.api.routing.v1.routing).business_id = "frontend_request.stream_id"; + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_LONG_POLL; + } + + rpc DescribeStream(DescribeStreamRequest) returns (DescribeStreamResponse) { + option (temporal.server.api.routing.v1.routing).business_id = "frontend_request.stream_id"; + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // The owned-stream calls are routed on the owner, because the stream has no + // id of its own. A workflow's activity is routed on the workflow. + rpc PollWorkflowMessages(PollWorkflowMessagesRequest) returns (PollWorkflowMessagesResponse) { + option (temporal.server.api.routing.v1.routing).business_id = "frontend_request.owner.id"; + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_LONG_POLL; + } + + rpc DescribeWorkflowStream(DescribeWorkflowStreamRequest) returns (DescribeWorkflowStreamResponse) { + option (temporal.server.api.routing.v1.routing).business_id = "frontend_request.owner.id"; + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + rpc AddWorkflowMessages(AddWorkflowMessagesRequest) returns (AddWorkflowMessagesResponse) { + option (temporal.server.api.routing.v1.routing).business_id = "frontend_request.owner.id"; + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // Internal. History calls these on itself to reach a shard it does not own, + // which is the only way a step that spans two executions can work on a + // cluster with more than one history host. + rpc RegisterStreamConsumer(RegisterStreamConsumerRequest) returns (RegisterStreamConsumerResponse) { + option (temporal.server.api.routing.v1.routing).business_id = "frontend_request.stream_id"; + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + rpc AdvanceConsumerHead(AdvanceConsumerHeadRequest) returns (AdvanceConsumerHeadResponse) { + option (temporal.server.api.routing.v1.routing).business_id = "frontend_request.workflow_id"; + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + rpc CloseStream(CloseStreamRequest) returns (CloseStreamResponse) { + option (temporal.server.api.routing.v1.routing).business_id = "frontend_request.stream_id"; + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + rpc TruncateStream(TruncateStreamRequest) returns (TruncateStreamResponse) { + option (temporal.server.api.routing.v1.routing).business_id = "frontend_request.stream_id"; + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // Served on the frontend only: it queries visibility rather than a stream, + // so there is no business ID to route on and nothing for a shard to answer. + rpc ListStreams(ListStreamsRequest) returns (ListStreamsResponse) { + option (temporal.server.api.routing.v1.routing).random = true; + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + rpc DeleteStream(DeleteStreamRequest) returns (DeleteStreamResponse) { + option (temporal.server.api.routing.v1.routing).business_id = "frontend_request.stream_id"; + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } +} diff --git a/chasm/lib/stream/reason.go b/chasm/lib/stream/reason.go index 7fe1139c041..06f8c78c09b 100644 --- a/chasm/lib/stream/reason.go +++ b/chasm/lib/stream/reason.go @@ -13,6 +13,7 @@ const ( ReasonProducerStaleSequence = "STREAM_PRODUCER_STALE_SEQUENCE" ReasonCursorBelowFloor = "STREAM_CURSOR_BELOW_FLOOR" ReasonStreamClosed = "STREAM_CLOSED" + ReasonPolicyMismatch = "STREAM_POLICY_MISMATCH" ) const reasonSeparator = ": " @@ -33,7 +34,7 @@ func ReasonOf(message string) string { } switch token { case ReasonProducerConflict, ReasonProducerStaleSequence, ReasonCursorBelowFloor, - ReasonStreamClosed: + ReasonStreamClosed, ReasonPolicyMismatch: return token default: return "" diff --git a/chasm/lib/stream/service/age_task.go b/chasm/lib/stream/service/age_task.go new file mode 100644 index 00000000000..5c143beda01 --- /dev/null +++ b/chasm/lib/stream/service/age_task.go @@ -0,0 +1,44 @@ +package service + +import ( + "go.temporal.io/server/chasm" + "go.temporal.io/server/chasm/lib/stream" + streampb "go.temporal.io/server/chasm/lib/stream/gen/streampb/v1" +) + +// ageTaskHandler reclaims what has aged past an open stream's retention. +// +// A pure task: it reads the oldest batches for their age and moves the floor +// in one transition, with nothing to say to anyone else. The component decides +// whether to arm the next check and when, so the handler only supplies the +// cadence it re-arms on. +type ageTaskHandler struct { + chasm.PureTaskHandlerBase + + config *stream.Config +} + +func newAgeTaskHandler(config *stream.Config) *ageTaskHandler { + return &ageTaskHandler{config: config} +} + +// Validate lets the check run while it is the one outstanding and the stream +// is open. A closed stream's retention counts down to deletion instead, and a +// check whose flag is already down was superseded. +func (h *ageTaskHandler) Validate( + _ chasm.Context, + s *stream.Stream, + _ chasm.TaskInvocation, + _ *streampb.StreamAgeTask, +) (bool, error) { + return s.State.GetAgeTaskPending() && !s.State.GetClosed(), nil +} + +func (h *ageTaskHandler) Execute( + mctx chasm.MutableContext, + s *stream.Stream, + _ chasm.TaskAttributes, + _ *streampb.StreamAgeTask, +) error { + return s.RunAgeCheck(mctx, h.config.RetentionRecheckInterval()) +} diff --git a/chasm/lib/stream/service/frontend.go b/chasm/lib/stream/service/frontend.go new file mode 100644 index 00000000000..5f66e8a8045 --- /dev/null +++ b/chasm/lib/stream/service/frontend.go @@ -0,0 +1,543 @@ +package service + +import ( + "context" + "strings" + + "go.temporal.io/api/serviceerror" + streampb "go.temporal.io/api/stream/v1" + "go.temporal.io/server/chasm" + "go.temporal.io/server/chasm/lib/stream" + streamlib "go.temporal.io/server/chasm/lib/stream/gen/streampb/v1" + "go.temporal.io/server/common" + "go.temporal.io/server/common/log" + "go.temporal.io/server/common/namespace" + "go.temporal.io/server/common/searchattribute/sadefs" + "google.golang.org/protobuf/types/known/durationpb" + "google.golang.org/protobuf/types/known/emptypb" +) + +// FrontendHandler serves StreamService on the frontend. It resolves the +// namespace name to an ID, checks what can be checked without the stream, and +// forwards to the history shard that owns the stream; the layered client does +// the routing from the business ID. +// +// The checks are the ones the workflow handler makes for its own ids. A stream +// id becomes an execution's business id and a stream name a key in mutable +// state, so neither may be longer than an id is allowed to be, and an offset +// or page size that history would only clamp or refuse later is refused here +// before the request is routed. +type FrontendHandler struct { + streamlib.UnimplementedStreamServiceServer + + client streamlib.StreamServiceClient + namespaceRegistry namespace.Registry + logger log.Logger + config *stream.Config +} + +func NewFrontendHandler( + client streamlib.StreamServiceClient, + namespaceRegistry namespace.Registry, + logger log.Logger, + config *stream.Config, +) *FrontendHandler { + return &FrontendHandler{ + client: client, + namespaceRegistry: namespaceRegistry, + logger: logger, + config: config, + } +} + +// RedirectableMethods lists the stream RPCs a cell forwards to the namespace's +// active cell when it is not that cell itself, each with the response it +// answers with, for the redirection interceptor. +// +// The stream lives in the active cell's mutable state, so a call served where +// it happens to land would write to a copy nothing reads or read one that +// stops at the last replication. The two calls History makes on itself are not +// listed: they never reach a frontend legitimately, and this handler answers +// them with Unimplemented wherever they land. +func RedirectableMethods() map[string]func() any { + return map[string]func() any{ + streamlib.StreamService_CreateStream_FullMethodName: func() any { + return &streamlib.CreateStreamResponse{} + }, + streamlib.StreamService_AddMessages_FullMethodName: func() any { + return &streamlib.AddMessagesResponse{} + }, + streamlib.StreamService_FinishWriting_FullMethodName: func() any { + return &streamlib.FinishWritingResponse{} + }, + streamlib.StreamService_SubscribeWorkflow_FullMethodName: func() any { + return &streamlib.SubscribeWorkflowResponse{} + }, + streamlib.StreamService_PollMessages_FullMethodName: func() any { + return &streamlib.PollMessagesResponse{} + }, + streamlib.StreamService_DescribeStream_FullMethodName: func() any { + return &streamlib.DescribeStreamResponse{} + }, + streamlib.StreamService_PollWorkflowMessages_FullMethodName: func() any { + return &streamlib.PollWorkflowMessagesResponse{} + }, + streamlib.StreamService_DescribeWorkflowStream_FullMethodName: func() any { + return &streamlib.DescribeWorkflowStreamResponse{} + }, + streamlib.StreamService_AddWorkflowMessages_FullMethodName: func() any { + return &streamlib.AddWorkflowMessagesResponse{} + }, + streamlib.StreamService_CloseStream_FullMethodName: func() any { + return &streamlib.CloseStreamResponse{} + }, + streamlib.StreamService_TruncateStream_FullMethodName: func() any { + return &streamlib.TruncateStreamResponse{} + }, + streamlib.StreamService_ListStreams_FullMethodName: func() any { + return &streamlib.ListStreamsResponse{} + }, + streamlib.StreamService_DeleteStream_FullMethodName: func() any { + return &streamlib.DeleteStreamResponse{} + }, + } +} + +// namespaceID resolves the namespace and refuses the call when streams are off +// for it. +// +// Every RPC goes through here, so the gate covers the whole surface. It is off +// by default: this registers a large new service on the public frontend, and +// an operator needs a switch for it that is not a rollback. +func (h *FrontendHandler) namespaceID(name string) (string, error) { + if name == "" { + return "", serviceerror.NewInvalidArgument("namespace is required") + } + if !h.config.EnabledFor(name) { + return "", serviceerror.NewUnimplementedf( + "streams are not enabled for namespace: %s", name) + } + id, err := h.namespaceRegistry.GetNamespaceID(namespace.Name(name)) + if err != nil { + return "", err + } + return id.String(), nil +} + +// checkLifecycle settles the lifecycle a caller asked for. +// +// Retention left unset means the namespace's own workflow retention rather +// than forever: a closed stream with no retention schedules no deletion task, +// so its execution and its batches stay in the database for good. Everything +// else in the system gets a retention bound from its namespace, and a stream +// should not be the exception because the caller left a field empty. +func (h *FrontendHandler) checkLifecycle( + namespaceName string, + lifecycle *streamlib.StreamLifecycle, +) (*streamlib.StreamLifecycle, error) { + ns, err := h.namespaceRegistry.GetNamespace(namespace.Name(namespaceName)) + if err != nil { + return nil, err + } + if lifecycle == nil { + lifecycle = &streamlib.StreamLifecycle{} + } + lifecycle = common.CloneProto(lifecycle) + + if lifecycle.GetMaxItems() < 0 { + return nil, serviceerror.NewInvalidArgumentf( + "max items cannot be negative, got %d", lifecycle.GetMaxItems()) + } + if lifecycle.GetMaxBytes() < 0 { + return nil, serviceerror.NewInvalidArgumentf( + "max bytes cannot be negative, got %d", lifecycle.GetMaxBytes()) + } + + retention := lifecycle.GetRetention().AsDuration() + switch { + case lifecycle.GetRetention() == nil: + retention = ns.Retention() + case retention <= 0: + return nil, serviceerror.NewInvalidArgumentf( + "retention must be positive, got %v", retention) + case retention > ns.Retention(): + return nil, serviceerror.NewInvalidArgumentf( + "retention of %v is over the namespace's retention of %v", retention, ns.Retention()) + default: + // A positive retention within the namespace's is kept as requested. + } + lifecycle.Retention = durationpb.New(retention) + return lifecycle, nil +} + +// checkID refuses an id or name longer than the namespace's id limit. Empty is +// allowed here: some fields mean the default when empty and the ones that do +// not are checked by their handler. +func (h *FrontendHandler) checkID(field, value string) error { + if len(value) > h.config.MaxIDLength() { + return serviceerror.NewInvalidArgumentf( + "%s is %d characters, over the %d limit", field, len(value), h.config.MaxIDLength()) + } + return nil +} + +// ownerOf settles which execution an owned-stream call names. A call that +// sets only the workflow fields means that workflow; everything the history +// side routes and resolves on is then the owner alone. +func (h *FrontendHandler) ownerOf( + owner *streamlib.StreamOwner, + workflowID, runID string, +) (*streamlib.StreamOwner, error) { + if owner == nil { + owner = &streamlib.StreamOwner{ + Kind: streamlib.STREAM_OWNER_KIND_WORKFLOW, + Id: workflowID, + RunId: runID, + } + } else if workflowID != "" || runID != "" { + return nil, serviceerror.NewInvalidArgument( + "set either owner or the workflow id and owner run id, not both") + } + if err := checkOwner(owner); err != nil { + return nil, err + } + if err := h.checkID("owner id", owner.GetId()); err != nil { + return nil, err + } + if err := h.checkID("owner run id", owner.GetRunId()); err != nil { + return nil, err + } + if err := h.checkID("owner activity id", owner.GetActivityId()); err != nil { + return nil, err + } + return owner, nil +} + +func checkOffset(field string, value int64) error { + if value < 0 { + return serviceerror.NewInvalidArgumentf("%s cannot be negative, got %d", field, value) + } + return nil +} + +// checkFirstPoll refuses a start position the history side could not resolve, +// or one sent by a reader that already has an offset. +func checkFirstPoll(pos *streampb.StreamStartPosition, from int64) error { + if pos == nil { + return nil + } + _, err := stream.RequestedStart(pos, "from_offset", from) + return err +} + +// clampMaxMessages bounds a page the way the history side does, so a caller +// asking for more sees the same page size it would have been given anyway. +func clampMaxMessages(requested int32) int32 { + if requested <= 0 || requested > stream.DefaultMaxMessagesPerPoll { + return stream.DefaultMaxMessagesPerPoll + } + return requested +} + +func (h *FrontendHandler) CreateStream( + ctx context.Context, req *streamlib.CreateStreamRequest, +) (*streamlib.CreateStreamResponse, error) { + in := req.GetFrontendRequest() + id, err := h.namespaceID(in.GetNamespace()) + if err != nil { + return nil, err + } + if in.GetStreamId() == "" { + return nil, serviceerror.NewInvalidArgument("stream id is required") + } + if err := h.checkID("stream id", in.GetStreamId()); err != nil { + return nil, err + } + lifecycle, err := h.checkLifecycle(in.GetNamespace(), in.GetLifecycle()) + if err != nil { + return nil, err + } + in.Lifecycle = lifecycle + return h.client.CreateStream(ctx, &streamlib.CreateStreamRequest{ + NamespaceId: id, FrontendRequest: in, + }) +} + +func (h *FrontendHandler) AddMessages( + ctx context.Context, req *streamlib.AddMessagesRequest, +) (*streamlib.AddMessagesResponse, error) { + in := req.GetFrontendRequest() + id, err := h.namespaceID(in.GetNamespace()) + if err != nil { + return nil, err + } + if err := h.checkID("stream id", in.GetStreamId()); err != nil { + return nil, err + } + if err := h.checkID("producer id", in.GetProducerId()); err != nil { + return nil, err + } + return h.client.AddMessages(ctx, &streamlib.AddMessagesRequest{ + NamespaceId: id, FrontendRequest: in, + }) +} + +func (h *FrontendHandler) FinishWriting( + ctx context.Context, req *streamlib.FinishWritingRequest, +) (*streamlib.FinishWritingResponse, error) { + in := req.GetFrontendRequest() + id, err := h.namespaceID(in.GetNamespace()) + if err != nil { + return nil, err + } + if err := h.checkID("stream id", in.GetStreamId()); err != nil { + return nil, err + } + if err := h.checkID("producer id", in.GetProducerId()); err != nil { + return nil, err + } + return h.client.FinishWriting(ctx, &streamlib.FinishWritingRequest{ + NamespaceId: id, FrontendRequest: in, + }) +} + +func (h *FrontendHandler) SubscribeWorkflow( + ctx context.Context, req *streamlib.SubscribeWorkflowRequest, +) (*streamlib.SubscribeWorkflowResponse, error) { + in := req.GetFrontendRequest() + id, err := h.namespaceID(in.GetNamespace()) + if err != nil { + return nil, err + } + if err := h.checkID("stream id", in.GetStreamId()); err != nil { + return nil, err + } + if err := h.checkID("stream name", in.GetStreamName()); err != nil { + return nil, err + } + if err := h.checkID("workflow id", in.GetWorkflowId()); err != nil { + return nil, err + } + if _, err := stream.RequestedStart( + in.GetStartPosition(), "start_offset", in.GetStartOffset()); err != nil { + return nil, err + } + return h.client.SubscribeWorkflow(ctx, &streamlib.SubscribeWorkflowRequest{ + NamespaceId: id, FrontendRequest: in, + }) +} + +func (h *FrontendHandler) PollMessages( + ctx context.Context, req *streamlib.PollMessagesRequest, +) (*streamlib.PollMessagesResponse, error) { + in := req.GetFrontendRequest() + id, err := h.namespaceID(in.GetNamespace()) + if err != nil { + return nil, err + } + if err := h.checkID("stream id", in.GetStreamId()); err != nil { + return nil, err + } + if err := checkOffset("from offset", in.GetFromOffset()); err != nil { + return nil, err + } + if err := checkFirstPoll(in.GetStartPosition(), in.GetFromOffset()); err != nil { + return nil, err + } + in.MaxMessages = clampMaxMessages(in.GetMaxMessages()) + return h.client.PollMessages(ctx, &streamlib.PollMessagesRequest{ + NamespaceId: id, FrontendRequest: in, + }) +} + +func (h *FrontendHandler) PollWorkflowMessages( + ctx context.Context, req *streamlib.PollWorkflowMessagesRequest, +) (*streamlib.PollWorkflowMessagesResponse, error) { + in := req.GetFrontendRequest() + id, err := h.namespaceID(in.GetNamespace()) + if err != nil { + return nil, err + } + if err := h.checkID("stream name", in.GetStreamName()); err != nil { + return nil, err + } + if err := checkOffset("from offset", in.GetFromOffset()); err != nil { + return nil, err + } + if err := checkFirstPoll(in.GetStartPosition(), in.GetFromOffset()); err != nil { + return nil, err + } + owner, err := h.ownerOf(in.GetOwner(), in.GetWorkflowId(), in.GetOwnerRunId()) + if err != nil { + return nil, err + } + in.Owner, in.WorkflowId, in.OwnerRunId = owner, "", "" + in.MaxMessages = clampMaxMessages(in.GetMaxMessages()) + return h.client.PollWorkflowMessages(ctx, &streamlib.PollWorkflowMessagesRequest{ + NamespaceId: id, FrontendRequest: in, + }) +} + +func (h *FrontendHandler) DescribeWorkflowStream( + ctx context.Context, req *streamlib.DescribeWorkflowStreamRequest, +) (*streamlib.DescribeWorkflowStreamResponse, error) { + in := req.GetFrontendRequest() + id, err := h.namespaceID(in.GetNamespace()) + if err != nil { + return nil, err + } + if err := h.checkID("stream name", in.GetStreamName()); err != nil { + return nil, err + } + owner, err := h.ownerOf(in.GetOwner(), in.GetWorkflowId(), in.GetOwnerRunId()) + if err != nil { + return nil, err + } + in.Owner, in.WorkflowId, in.OwnerRunId = owner, "", "" + return h.client.DescribeWorkflowStream(ctx, &streamlib.DescribeWorkflowStreamRequest{ + NamespaceId: id, FrontendRequest: in, + }) +} + +func (h *FrontendHandler) AddWorkflowMessages( + ctx context.Context, req *streamlib.AddWorkflowMessagesRequest, +) (*streamlib.AddWorkflowMessagesResponse, error) { + in := req.GetFrontendRequest() + id, err := h.namespaceID(in.GetNamespace()) + if err != nil { + return nil, err + } + if err := h.checkID("stream name", in.GetStreamName()); err != nil { + return nil, err + } + owner, err := h.ownerOf(in.GetOwner(), in.GetWorkflowId(), in.GetOwnerRunId()) + if err != nil { + return nil, err + } + in.Owner, in.WorkflowId, in.OwnerRunId = owner, "", "" + if err := h.checkID("producer id", in.GetProducerId()); err != nil { + return nil, err + } + return h.client.AddWorkflowMessages(ctx, &streamlib.AddWorkflowMessagesRequest{ + NamespaceId: id, FrontendRequest: in, + }) +} + +func (h *FrontendHandler) DescribeStream( + ctx context.Context, req *streamlib.DescribeStreamRequest, +) (*streamlib.DescribeStreamResponse, error) { + in := req.GetFrontendRequest() + id, err := h.namespaceID(in.GetNamespace()) + if err != nil { + return nil, err + } + if err := h.checkID("stream id", in.GetStreamId()); err != nil { + return nil, err + } + return h.client.DescribeStream(ctx, &streamlib.DescribeStreamRequest{ + NamespaceId: id, FrontendRequest: in, + }) +} + +func (h *FrontendHandler) CloseStream( + ctx context.Context, req *streamlib.CloseStreamRequest, +) (*streamlib.CloseStreamResponse, error) { + in := req.GetFrontendRequest() + id, err := h.namespaceID(in.GetNamespace()) + if err != nil { + return nil, err + } + if err := h.checkID("stream id", in.GetStreamId()); err != nil { + return nil, err + } + return h.client.CloseStream(ctx, &streamlib.CloseStreamRequest{ + NamespaceId: id, FrontendRequest: in, + }) +} + +func (h *FrontendHandler) TruncateStream( + ctx context.Context, req *streamlib.TruncateStreamRequest, +) (*streamlib.TruncateStreamResponse, error) { + in := req.GetFrontendRequest() + id, err := h.namespaceID(in.GetNamespace()) + if err != nil { + return nil, err + } + if err := h.checkID("stream id", in.GetStreamId()); err != nil { + return nil, err + } + if err := checkOffset("new base offset", in.GetNewBaseOffset()); err != nil { + return nil, err + } + return h.client.TruncateStream(ctx, &streamlib.TruncateStreamRequest{ + NamespaceId: id, FrontendRequest: in, + }) +} + +func (h *FrontendHandler) DeleteStream( + ctx context.Context, req *streamlib.DeleteStreamRequest, +) (*streamlib.DeleteStreamResponse, error) { + in := req.GetFrontendRequest() + id, err := h.namespaceID(in.GetNamespace()) + if err != nil { + return nil, err + } + if err := h.checkID("stream id", in.GetStreamId()); err != nil { + return nil, err + } + return h.client.DeleteStream(ctx, &streamlib.DeleteStreamRequest{ + NamespaceId: id, FrontendRequest: in, + }) +} + +// ListStreams answers from visibility rather than from any one stream, so it +// does not route to a shard and is served here rather than on the history side. +func (h *FrontendHandler) ListStreams( + ctx context.Context, req *streamlib.ListStreamsRequest, +) (*streamlib.ListStreamsResponse, error) { + in := req.GetFrontendRequest() + if _, err := h.namespaceID(in.GetNamespace()); err != nil { + return nil, err + } + // The archetype is a predicate the caller's query can replace rather than + // one it is anded with, so a query naming the division would list any + // archetype in the namespace, workflows included, through a stream-scoped + // read-only RPC. + if strings.Contains( + strings.ToLower(in.GetQuery()), strings.ToLower(sadefs.TemporalNamespaceDivision)) { + return nil, serviceerror.NewInvalidArgumentf( + "a stream query cannot filter on %s", sadefs.TemporalNamespaceDivision) + } + + pageSize := int(in.GetPageSize()) + if pageSize <= 0 || pageSize > stream.MaxListPageSize { + pageSize = stream.MaxListPageSize + } + + resp, err := chasm.ListExecutions[*stream.Stream, *emptypb.Empty]( + ctx, + &chasm.ListExecutionsRequest{ + NamespaceName: in.GetNamespace(), + PageSize: pageSize, + NextPageToken: in.GetNextPageToken(), + Query: in.GetQuery(), + }, + ) + if err != nil { + return nil, err + } + + entries := make([]*streamlib.StreamListEntry, 0, len(resp.Executions)) + for _, e := range resp.Executions { + entries = append(entries, &streamlib.StreamListEntry{ + StreamId: e.BusinessID, + RunId: e.RunID, + }) + } + return &streamlib.ListStreamsResponse{ + FrontendResponse: &streamlib.ListStreamsOutput{ + Streams: entries, + NextPageToken: resp.NextPageToken, + }, + }, nil +} diff --git a/chasm/lib/stream/service/frontend_test.go b/chasm/lib/stream/service/frontend_test.go new file mode 100644 index 00000000000..5d702252385 --- /dev/null +++ b/chasm/lib/stream/service/frontend_test.go @@ -0,0 +1,211 @@ +package service + +import ( + "context" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/require" + "go.temporal.io/api/serviceerror" + persistencespb "go.temporal.io/server/api/persistence/v1" + "go.temporal.io/server/chasm/lib/stream" + streampb "go.temporal.io/server/chasm/lib/stream/gen/streampb/v1" + "go.temporal.io/server/common/dynamicconfig" + "go.temporal.io/server/common/log" + "go.temporal.io/server/common/namespace" + "go.uber.org/mock/gomock" + "google.golang.org/grpc" + "google.golang.org/protobuf/types/known/durationpb" +) + +// recordingClient captures what the frontend forwards, so a test can check +// what history would have been asked without a history service. +type recordingClient struct { + streampb.StreamServiceClient + polls []*streampb.PollMessagesRequest + truncates []*streampb.TruncateStreamRequest +} + +func (c *recordingClient) PollMessages( + _ context.Context, req *streampb.PollMessagesRequest, _ ...grpc.CallOption, +) (*streampb.PollMessagesResponse, error) { + c.polls = append(c.polls, req) + return &streampb.PollMessagesResponse{}, nil +} + +func (c *recordingClient) TruncateStream( + _ context.Context, req *streampb.TruncateStreamRequest, _ ...grpc.CallOption, +) (*streampb.TruncateStreamResponse, error) { + c.truncates = append(c.truncates, req) + return &streampb.TruncateStreamResponse{}, nil +} + +func newTestFrontend(t *testing.T, maxIDLength int) (*FrontendHandler, *recordingClient) { + t.Helper() + return newTestFrontendWith(t, maxIDLength, true) +} + +func newTestFrontendWith( + t *testing.T, maxIDLength int, enabled bool, +) (*FrontendHandler, *recordingClient) { + t.Helper() + registry := namespace.NewMockRegistry(gomock.NewController(t)) + registry.EXPECT().GetNamespaceID(namespace.Name("ns")).Return(namespace.ID("ns-id"), nil).AnyTimes() + registry.EXPECT().GetNamespace(namespace.Name("ns")).Return( + namespace.NewLocalNamespaceForTest( + &persistencespb.NamespaceInfo{Name: "ns"}, + &persistencespb.NamespaceConfig{Retention: durationpb.New(24 * time.Hour)}, + "cluster", + ), nil).AnyTimes() + client := &recordingClient{} + config := &stream.Config{ + Enabled: dynamicconfig.GetBoolPropertyFnFilteredByNamespace(enabled), + MaxIDLength: dynamicconfig.GetIntPropertyFn(maxIDLength), + } + return NewFrontendHandler(client, registry, log.NewNoopLogger(), config), client +} + +// Off by default, so merging this does not turn the surface on everywhere. The +// gate sits on the namespace resolution every RPC goes through. +func TestFrontendRefusesEveryCallWhenStreamsAreOff(t *testing.T) { + h, client := newTestFrontendWith(t, 1000, false) + ctx := context.Background() + var unimplemented *serviceerror.Unimplemented + + _, err := h.CreateStream(ctx, &streampb.CreateStreamRequest{ + FrontendRequest: &streampb.CreateStreamInput{Namespace: "ns", StreamId: "s"}, + }) + require.ErrorAs(t, err, &unimplemented) + + _, err = h.PollMessages(ctx, &streampb.PollMessagesRequest{ + FrontendRequest: &streampb.PollMessagesInput{Namespace: "ns", StreamId: "s"}, + }) + require.ErrorAs(t, err, &unimplemented) + + _, err = h.ListStreams(ctx, &streampb.ListStreamsRequest{ + FrontendRequest: &streampb.ListStreamsInput{Namespace: "ns"}, + }) + require.ErrorAs(t, err, &unimplemented) + require.Empty(t, client.polls, "a refused request is never routed") +} + +// A closed stream with no retention schedules no deletion, so it and its +// batches stay in the database for good. Left unset it takes the namespace's. +func TestFrontendSettlesTheStreamLifecycle(t *testing.T) { + h, _ := newTestFrontend(t, 1000) + + settled, err := h.checkLifecycle("ns", nil) + require.NoError(t, err) + require.Equal(t, 24*time.Hour, settled.GetRetention().AsDuration()) + + _, err = h.checkLifecycle("ns", &streampb.StreamLifecycle{Retention: durationpb.New(0)}) + var invalid *serviceerror.InvalidArgument + require.ErrorAs(t, err, &invalid, "an explicit zero is a mistake, not a request for forever") + + _, err = h.checkLifecycle("ns", &streampb.StreamLifecycle{ + Retention: durationpb.New(48 * time.Hour), + }) + require.ErrorAs(t, err, &invalid, "a stream cannot outlive its namespace's retention") + + _, err = h.checkLifecycle("ns", &streampb.StreamLifecycle{MaxItems: -1}) + require.ErrorAs(t, err, &invalid) +} + +// The checks the workflow handler makes for its own ids, made here for stream +// ids and names before a request is routed. +func TestFrontendRefusesWhatHistoryWouldOnlyDiscoverLater(t *testing.T) { + h, client := newTestFrontend(t, 16) + ctx := context.Background() + var invalid *serviceerror.InvalidArgument + + _, err := h.CreateStream(ctx, &streampb.CreateStreamRequest{ + FrontendRequest: &streampb.CreateStreamInput{Namespace: "ns", StreamId: strings.Repeat("x", 17)}, + }) + require.ErrorAs(t, err, &invalid, "a stream id becomes a business id and is bounded like one") + + _, err = h.PollWorkflowMessages(ctx, &streampb.PollWorkflowMessagesRequest{ + FrontendRequest: &streampb.PollWorkflowMessagesInput{ + Namespace: "ns", WorkflowId: "wf", StreamName: strings.Repeat("n", 17), + }, + }) + require.ErrorAs(t, err, &invalid, "a stream name becomes a state key and is bounded the same way") + + _, err = h.PollMessages(ctx, &streampb.PollMessagesRequest{ + FrontendRequest: &streampb.PollMessagesInput{Namespace: "ns", StreamId: "s", FromOffset: -1}, + }) + require.ErrorAs(t, err, &invalid) + + _, err = h.TruncateStream(ctx, &streampb.TruncateStreamRequest{ + FrontendRequest: &streampb.TruncateStreamInput{Namespace: "ns", StreamId: "s", NewBaseOffset: -5}, + }) + require.ErrorAs(t, err, &invalid) + require.Empty(t, client.polls) + require.Empty(t, client.truncates, "a refused request is never routed") +} + +// A page larger than the server serves is clamped before routing, so the +// caller sees the same page it would have been given anyway. +func TestFrontendClampsThePageSize(t *testing.T) { + h, client := newTestFrontend(t, 1000) + _, err := h.PollMessages(context.Background(), &streampb.PollMessagesRequest{ + FrontendRequest: &streampb.PollMessagesInput{ + Namespace: "ns", StreamId: "s", MaxMessages: stream.DefaultMaxMessagesPerPoll * 4, + }, + }) + require.NoError(t, err) + require.Len(t, client.polls, 1) + require.Equal(t, "ns-id", client.polls[0].GetNamespaceId()) + require.EqualValues(t, stream.DefaultMaxMessagesPerPoll, client.polls[0].GetFrontendRequest().GetMaxMessages()) +} + +// The archetype is a predicate the caller's query can replace rather than one +// it is anded with, so a query naming the division would reach executions this +// RPC has no business listing. +func TestFrontendRefusesAListQueryThatNamesTheArchetype(t *testing.T) { + h, _ := newTestFrontend(t, 1000) + var invalid *serviceerror.InvalidArgument + + _, err := h.ListStreams(context.Background(), &streampb.ListStreamsRequest{ + FrontendRequest: &streampb.ListStreamsInput{ + Namespace: "ns", + Query: `TemporalNamespaceDivision = "TemporalScheduler"`, + }, + }) + require.ErrorAs(t, err, &invalid) + + _, err = h.ListStreams(context.Background(), &streampb.ListStreamsRequest{ + FrontendRequest: &streampb.ListStreamsInput{ + Namespace: "ns", + Query: `temporalnamespacedivision = "x"`, + }, + }) + require.ErrorAs(t, err, &invalid, "the check does not depend on how the caller cased it") +} + +// A call that names a workflow by id alone means that workflow, so history +// routes and resolves on the owner only. Naming both ways is ambiguous. +func TestFrontendSettlesTheOwner(t *testing.T) { + h, _ := newTestFrontend(t, 100) + + owner, err := h.ownerOf(nil, "wf", "run") + require.NoError(t, err) + require.Equal(t, streampb.STREAM_OWNER_KIND_WORKFLOW, owner.GetKind()) + require.Equal(t, "wf", owner.GetId()) + require.Equal(t, "run", owner.GetRunId()) + + saa := &streampb.StreamOwner{Kind: streampb.STREAM_OWNER_KIND_ACTIVITY, Id: "act"} + owner, err = h.ownerOf(saa, "", "") + require.NoError(t, err) + require.Same(t, saa, owner) + + _, err = h.ownerOf(saa, "wf", "") + var invalid *serviceerror.InvalidArgument + require.ErrorAs(t, err, &invalid) + + _, err = h.ownerOf(&streampb.StreamOwner{ + Kind: streampb.STREAM_OWNER_KIND_WORKFLOW_ACTIVITY, Id: "wf", + ActivityId: strings.Repeat("a", 101), + }, "", "") + require.ErrorAs(t, err, &invalid, "the activity id is bounded like any other id") +} diff --git a/chasm/lib/stream/service/fx.go b/chasm/lib/stream/service/fx.go new file mode 100644 index 00000000000..030be46eeee --- /dev/null +++ b/chasm/lib/stream/service/fx.go @@ -0,0 +1,36 @@ +package service + +import ( + "go.temporal.io/server/chasm" + streampb "go.temporal.io/server/chasm/lib/stream/gen/streampb/v1" + "go.uber.org/fx" +) + +var HistoryModule = fx.Module( + "stream-history", + fx.Provide( + // Routes a call to the host owning a shard. History needs it too, not + // just the frontend: a step that spans two executions has to reach a + // shard this host may not own. The stream config comes from the + // workflow library module, which every service running this one has. + streampb.NewStreamServiceLayeredClient, + newHandler, + newRetentionTaskHandler, + newNotifyConsumersTaskHandler, + newAgeTaskHandler, + newLibrary, + ), + fx.Invoke(func(l *library, registry *chasm.Registry) error { + return registry.Register(l) + }), +) + +var FrontendModule = fx.Module( + "stream-frontend", + fx.Provide(streampb.NewStreamServiceLayeredClient), + fx.Provide(NewFrontendHandler), + fx.Provide(newComponentOnlyLibrary), + fx.Invoke(func(l *componentOnlyLibrary, registry *chasm.Registry) error { + return registry.Register(l) + }), +) diff --git a/chasm/lib/stream/service/handler.go b/chasm/lib/stream/service/handler.go new file mode 100644 index 00000000000..d09e67534f2 --- /dev/null +++ b/chasm/lib/stream/service/handler.go @@ -0,0 +1,1271 @@ +package service + +import ( + "context" + "errors" + "time" + + commonpb "go.temporal.io/api/common/v1" + "go.temporal.io/api/serviceerror" + streampb "go.temporal.io/api/stream/v1" + "go.temporal.io/server/chasm" + "go.temporal.io/server/chasm/lib/activity" + "go.temporal.io/server/chasm/lib/stream" + streamlib "go.temporal.io/server/chasm/lib/stream/gen/streampb/v1" + chasmworkflow "go.temporal.io/server/chasm/lib/workflow" + "go.temporal.io/server/common" + "go.temporal.io/server/common/clock" + "go.temporal.io/server/common/contextutil" + "go.temporal.io/server/common/headers" + "go.temporal.io/server/common/log" + "go.temporal.io/server/common/metrics" + "go.temporal.io/server/common/namespace" + "go.temporal.io/server/service/history/shard" + "google.golang.org/protobuf/proto" +) + +type handler struct { + streamlib.UnimplementedStreamServiceServer + + shardController shard.Controller + namespaceRegistry namespace.Registry + logger log.Logger + metricsHandler metrics.Handler + timeSource clock.TimeSource + config *stream.Config + limiters *namespaceLimiters + + // Routes a call to the host that owns a shard. A step spanning two + // executions cannot resolve both through the local controller, which + // refuses a shard this host does not own, so the far half goes back out + // through the service and lands wherever it belongs. + routed streamlib.StreamServiceClient +} + +func newHandler( + shardController shard.Controller, + namespaceRegistry namespace.Registry, + logger log.Logger, + metricsHandler metrics.Handler, + timeSource clock.TimeSource, + config *stream.Config, + routed streamlib.StreamServiceClient, +) *handler { + return &handler{ + shardController: shardController, + namespaceRegistry: namespaceRegistry, + logger: logger, + metricsHandler: metricsHandler, + timeSource: timeSource, + config: config, + limiters: newNamespaceLimiters(config), + routed: routed, + } +} + +// namespaceName resolves an id for the limits, the rate limiters and the +// metrics tag. An id the registry cannot name resolves to the empty string; +// the interceptors have already refused requests for namespaces that do not +// exist. +func (h *handler) namespaceName(namespaceID string) string { + name, err := h.namespaceRegistry.GetNamespaceName(namespace.ID(namespaceID)) + if err != nil { + return "" + } + return name.String() +} + +// limitsFor resolves the namespace's limits. An unnamed namespace falls back +// to the defaults. +func (h *handler) limitsFor(namespaceID string) stream.Limits { + name := h.namespaceName(namespaceID) + if name == "" { + return stream.DefaultLimits() + } + return h.config.LimitsFor(name) +} + +// admitAppend applies the namespace's append rate to a batch before the +// transition that would store it. +func (h *handler) admitAppend( + ns string, records []*streamlib.StreamRecord, limits stream.Limits, +) error { + return h.limiters.allowAppend( + ns, h.timeSource.Now(), len(records), recordsSize(records), limits) +} + +// admitPoll applies the namespace's poll rate and counts the poll once it is +// through. A refused poll cost nothing and is not counted. +func (h *handler) admitPoll(ns string) error { + if err := h.limiters.allowPoll(ns, h.timeSource.Now()); err != nil { + return err + } + metrics.StreamPolls.With(h.metricsHandler).Record(1, metrics.NamespaceTag(ns)) + return nil +} + +// meterAppend counts what an append stored. A deduplicated retry stored +// nothing, so the meter reads records kept rather than requests made. +func (h *handler) meterAppend( + ns string, records []*streamlib.StreamRecord, result stream.AddMessagesResult, +) { + if result.Deduplicated { + return + } + tag := metrics.NamespaceTag(ns) + metrics.StreamRecordsAppended.With(h.metricsHandler).Record(result.Count, tag) + metrics.StreamBytesAppended.With(h.metricsHandler).Record(int64(recordsSize(records)), tag) +} + +// meterDelivery counts what a poll handed back. +func (h *handler) meterDelivery(ns string, out *streamlib.PollMessagesOutput) { + records := out.GetRecords() + if len(records) == 0 { + return + } + tag := metrics.NamespaceTag(ns) + metrics.StreamRecordsDelivered.With(h.metricsHandler).Record(int64(len(records)), tag) + metrics.StreamBytesDelivered.With(h.metricsHandler).Record(int64(recordsSize(records)), tag) +} + +// recordsSize is the metered size of a batch: the records as the producer +// sent them, which is also what the component measures against its caps. +func recordsSize(records []*streamlib.StreamRecord) int { + total := 0 + for _, record := range records { + total += proto.Size(record) + } + return total +} + +// withCallerInfo tags the context so the stream's direct persistence calls are +// attributed to the namespace that caused them. Without it they carry no caller +// name, which means they escape namespace rate limiting and priority as well as +// going uncounted in per-namespace metrics. The RPC path sets this via +// interceptors; calls made outside a request handler have to set it themselves. +func (h *handler) withCallerInfo(ctx context.Context, namespaceID string) context.Context { + name, err := h.namespaceRegistry.GetNamespaceName(namespace.ID(namespaceID)) + if err != nil { + return ctx + } + return headers.SetCallerInfo(ctx, headers.NewCallerInfo( + name.String(), headers.CallerTypeAPI, "")) +} + +// refFor builds a reference to a stream. A supplied run ID lets the engine skip +// resolving the current run, which is otherwise a persistence lookup on every +// call and dominates the cost of an otherwise cheap read. +func refFor(namespaceID, streamID string) chasm.ComponentRef { + return refForRun(namespaceID, streamID, "") +} + +func refForRun(namespaceID, streamID, runID string) chasm.ComponentRef { + return chasm.NewComponentRef[*stream.Stream](chasm.ExecutionKey{ + NamespaceID: namespaceID, + BusinessID: streamID, + RunID: runID, + }) +} + +// workflowRef builds a reference to the execution that owns an attached +// stream. An attached stream is a subcomponent, so it has no id of its own and +// everything about it is reached through its owner. +// +// An empty runID means the current run. A caller that supplies one is pinning: +// an owned stream does not carry across continue-as-new, so the successor's +// stream of the same name is a different, empty one, and a caller that meant +// the predecessor would otherwise be redirected to it without being told. +func workflowRef(namespaceID, workflowID, runID string) chasm.ComponentRef { + return chasm.NewComponentRef[*chasmworkflow.Workflow](chasm.ExecutionKey{ + NamespaceID: namespaceID, + BusinessID: workflowID, + RunID: runID, + }) +} + +// activityRef builds a reference to a standalone activity, which is an +// execution of its own and so owns its streams the way a workflow does. +func activityRef(namespaceID, activityID, runID string) chasm.ComponentRef { + return chasm.NewComponentRef[*activity.Activity](chasm.ExecutionKey{ + NamespaceID: namespaceID, + BusinessID: activityID, + RunID: runID, + }) +} + +// ownedStreamName resolves a name the caller left empty the same way a publish +// command does, so a reader addresses the default stream by omission just as a +// writer creates it by omission. +func ownedStreamName(name string) string { + if name == "" { + return stream.DefaultStreamName + } + return name +} + +// ownedTarget is an owned stream resolved from the caller's reference: the +// execution that holds it and the key it is held under there. +type ownedTarget struct { + ref chasm.ComponentRef + key string +} + +// resolveOwned turns an owner and a stream name into where the stream lives. +// +// A workflow's activity is not an execution of its own, so its streams are held +// in the workflow's map under a reserved key and reached through the workflow. +// A workflow may not name that part of its map itself, or it could write into +// a stream its activity owns. +func resolveOwned( + namespaceID string, + owner *streamlib.StreamOwner, + name string, +) (ownedTarget, error) { + if err := checkOwner(owner); err != nil { + return ownedTarget{}, err + } + name = ownedStreamName(name) + switch owner.GetKind() { + case streamlib.STREAM_OWNER_KIND_WORKFLOW: + if err := chasmworkflow.CheckWorkflowStreamName(name); err != nil { + return ownedTarget{}, err + } + return ownedTarget{ + ref: workflowRef(namespaceID, owner.GetId(), owner.GetRunId()), + key: name, + }, nil + case streamlib.STREAM_OWNER_KIND_ACTIVITY: + if err := stream.CheckStreamName(name); err != nil { + return ownedTarget{}, err + } + return ownedTarget{ + ref: activityRef(namespaceID, owner.GetId(), owner.GetRunId()), + key: name, + }, nil + case streamlib.STREAM_OWNER_KIND_WORKFLOW_ACTIVITY: + if err := stream.CheckStreamName(name); err != nil { + return ownedTarget{}, err + } + return ownedTarget{ + ref: workflowRef(namespaceID, owner.GetId(), owner.GetRunId()), + key: chasmworkflow.ActivityStreamKey(owner.GetActivityId(), name), + }, nil + default: + return ownedTarget{}, serviceerror.NewInvalidArgument("owner kind is required") + } +} + +// checkOwner refuses an owner reference that names no execution, or names one +// in a way its kind does not allow. +func checkOwner(owner *streamlib.StreamOwner) error { + if owner.GetId() == "" { + return serviceerror.NewInvalidArgument("owner id is required") + } + switch owner.GetKind() { + case streamlib.STREAM_OWNER_KIND_WORKFLOW, streamlib.STREAM_OWNER_KIND_ACTIVITY: + if owner.GetActivityId() != "" { + return serviceerror.NewInvalidArgumentf( + "owner activity id is only for a workflow's activity, not for kind %v", + owner.GetKind()) + } + case streamlib.STREAM_OWNER_KIND_WORKFLOW_ACTIVITY: + if owner.GetActivityId() == "" { + return serviceerror.NewInvalidArgument( + "owner activity id is required for a workflow's activity") + } + default: + return serviceerror.NewInvalidArgument("owner kind is required") + } + return nil +} + +func (h *handler) CreateStream( + ctx context.Context, + req *streamlib.CreateStreamRequest, +) (*streamlib.CreateStreamResponse, error) { + in := req.GetFrontendRequest() + ctx = h.withCallerInfo(ctx, req.GetNamespaceId()) + if in.GetStreamId() == "" { + return nil, serviceerror.NewInvalidArgument("stream id is required") + } + + result, err := chasm.StartExecution( + ctx, + chasm.ExecutionKey{NamespaceID: req.GetNamespaceId(), BusinessID: in.GetStreamId()}, + func(mctx chasm.MutableContext, input *streamlib.CreateStreamInput) (*stream.Stream, error) { + return stream.NewStream(mctx, stream.NewStreamRequest{Lifecycle: input.GetLifecycle()}) + }, + in, + ) + if err != nil { + if started, ok := errors.AsType[*chasm.ExecutionAlreadyStartedError](err); ok { + return nil, h.existingStreamRefusal(ctx, req.GetNamespaceId(), in, started.CurrentRunID) + } + return nil, err + } + return &streamlib.CreateStreamResponse{ + FrontendResponse: &streamlib.CreateStreamOutput{RunId: result.ExecutionKey.RunID}, + }, nil +} + +// existingStreamRefusal answers a create that names a stream already there. +// The engine's own error is not a service error and reaches the caller as +// Unknown, so it is read here and answered in two ways a client can tell +// apart without a describe: a repeat of the existing lifecycle is an +// idempotent retry and says AlreadyExists; a different lifecycle is a change +// the caller has to make on purpose, under a new id, and is refused with its +// own reason. +func (h *handler) existingStreamRefusal( + ctx context.Context, + namespaceID string, + in *streamlib.CreateStreamInput, + runID string, +) error { + state, err := chasm.ReadComponent(ctx, + refForRun(namespaceID, in.GetStreamId(), runID), (*stream.Stream).Snapshot, struct{}{}) + if err != nil { + return err + } + existing := state.GetLifecycle() + if proto.Equal(existing, in.GetLifecycle()) { + return serviceerror.NewAlreadyExistsf( + "stream %q already exists with this lifecycle", in.GetStreamId()) + } + return stream.Refusal(stream.ReasonPolicyMismatch, + "stream %q already exists with a different lifecycle: retention %v, max items %d, "+ + "max bytes %d", + in.GetStreamId(), existing.GetRetention().AsDuration(), existing.GetMaxItems(), + existing.GetMaxBytes()) +} + +func (h *handler) AddMessages( + ctx context.Context, + req *streamlib.AddMessagesRequest, +) (*streamlib.AddMessagesResponse, error) { + in := req.GetFrontendRequest() + if len(in.GetRecords()) == 0 { + return nil, serviceerror.NewInvalidArgument("no records to append") + } + ctx = h.withCallerInfo(ctx, req.GetNamespaceId()) + ns := h.namespaceName(req.GetNamespaceId()) + limits := h.limitsFor(req.GetNamespaceId()) + if err := h.admitAppend(ns, in.GetRecords(), limits); err != nil { + return nil, err + } + + // The batch and the frontier commit in one transition, and the execution + // serializes transitions, so a producer that names no expected offset takes + // whatever offset it lands at. A retried sequence is answered by the + // producer table, not by a pin on the head. + addReq := stream.AddMessagesRequest{ + Records: in.GetRecords(), + ProducerID: in.GetProducerId(), + Sequence: in.GetSequence(), + Limits: limits, + } + if in.GetUseExpectedOffset() { + expected := in.GetExpectedOffset() + addReq.ExpectedOffset = &expected + } + + result, _, err := chasm.UpdateComponent(ctx, + refForRun(req.GetNamespaceId(), in.GetStreamId(), in.GetRunId()), + (*stream.Stream).AddMessages, addReq) + if err != nil { + return nil, err + } + h.meterAppend(ns, in.GetRecords(), result) + + return &streamlib.AddMessagesResponse{ + FrontendResponse: &streamlib.AddMessagesOutput{ + FirstOffset: result.FirstOffset, + NextOffset: result.NextOffset, + Count: result.Count, + Deduplicated: result.Deduplicated, + }, + }, nil +} + +// AddWorkflowMessages appends to a stream an execution owns, from outside that +// execution. The owner is a workflow, a standalone activity, or an activity a +// workflow scheduled. +// +// The workflow's own publishes ride its Workflow Task and cost no transition of +// their own. This producer is off-shard, so it pays one transition on the +// owning execution per batch, and batching is what keeps that cheap. It is the +// path a model activity streaming tokens takes, where the workflow is only +// bracketing what the activity produces. +// +// One transition does everything: the stream is created on first write, and +// the workflow's own publishes are serialized against this append by the +// execution, so neither producer needs to pin the head against the other. +func (h *handler) AddWorkflowMessages( + ctx context.Context, + req *streamlib.AddWorkflowMessagesRequest, +) (*streamlib.AddWorkflowMessagesResponse, error) { + in := req.GetFrontendRequest() + if len(in.GetRecords()) == 0 { + return nil, serviceerror.NewInvalidArgument("no records to append") + } + ctx = h.withCallerInfo(ctx, req.GetNamespaceId()) + + target, err := resolveOwned(req.GetNamespaceId(), in.GetOwner(), in.GetStreamName()) + if err != nil { + return nil, err + } + ns := h.namespaceName(req.GetNamespaceId()) + limits := h.limitsFor(req.GetNamespaceId()) + if err := h.admitAppend(ns, in.GetRecords(), limits); err != nil { + return nil, err + } + + // The records go in as sent, producer identity included. Who is writing is + // the caller's claim to make; the store only answers whether it fits. + addReq := stream.AddMessagesRequest{ + Records: in.GetRecords(), + ProducerID: in.GetProducerId(), + Sequence: in.GetSequence(), + Limits: limits, + } + result, _, err := chasm.UpdateComponent(ctx, target.ref, + func( + owner stream.Owner, mctx chasm.MutableContext, r stream.AddMessagesRequest, + ) (stream.AddMessagesResult, error) { + return owner.AppendToOwnedStream(mctx, target.key, r) + }, addReq) + if err != nil { + return nil, err + } + h.meterAppend(ns, in.GetRecords(), result) + + return &streamlib.AddWorkflowMessagesResponse{ + FrontendResponse: &streamlib.AddMessagesOutput{ + FirstOffset: result.FirstOffset, + NextOffset: result.NextOffset, + Count: result.Count, + Deduplicated: result.Deduplicated, + }, + }, nil +} + +func (h *handler) FinishWriting( + ctx context.Context, + req *streamlib.FinishWritingRequest, +) (*streamlib.FinishWritingResponse, error) { + in := req.GetFrontendRequest() + ctx = h.withCallerInfo(ctx, req.GetNamespaceId()) + _, _, err := chasm.UpdateComponent( + ctx, + refFor(req.GetNamespaceId(), in.GetStreamId()), + func(s *stream.Stream, mctx chasm.MutableContext, producerID string) (struct{}, error) { + return struct{}{}, s.FinishWriting(mctx, producerID) + }, + in.GetProducerId(), + ) + if err != nil { + return nil, err + } + return &streamlib.FinishWritingResponse{FrontendResponse: &streamlib.FinishWritingOutput{}}, nil +} + +// SubscribeWorkflow registers a workflow as a consumer of a stream it owns. +// +// The cursor is written into the workflow's own state, not the stream's, which +// is what lets every later advance commit with the event that records it. Only +// a stream the workflow owns can be subscribed here: reaching one in another +// execution needs that stream's frontier, and reading it from inside the +// consuming workflow's transaction is a separate problem. +func (h *handler) SubscribeWorkflow( + ctx context.Context, + req *streamlib.SubscribeWorkflowRequest, +) (*streamlib.SubscribeWorkflowResponse, error) { + in := req.GetFrontendRequest() + start, err := stream.RequestedStart(in.GetStartPosition(), "start_offset", in.GetStartOffset()) + if err != nil { + return nil, err + } + + if in.GetStreamId() != "" { + if in.GetStreamName() != "" { + return nil, serviceerror.NewInvalidArgument( + "set either stream id, for a standalone stream, or stream name, for one the " + + "workflow owns, not both") + } + return h.subscribeToExternalStream(ctx, req.GetNamespaceId(), in, start) + } + + limits := h.limitsFor(req.GetNamespaceId()) + startOffset, _, err := chasm.UpdateComponent( + ctx, + workflowRef(req.GetNamespaceId(), in.GetWorkflowId(), in.GetOwnerRunId()), + func( + wf *chasmworkflow.Workflow, mctx chasm.MutableContext, input *streamlib.SubscribeWorkflowInput, + ) (int64, error) { + return wf.SubscribeToOwnedStream(mctx, ownedStreamName(input.GetStreamName()), start, limits) + }, + in, + ) + if err != nil { + return nil, err + } + + return &streamlib.SubscribeWorkflowResponse{ + FrontendResponse: &streamlib.SubscribeWorkflowOutput{StartOffset: startOffset}, + }, nil +} + +// subscribeToExternalStream registers a cursor against a stream in another +// execution. +// +// The pin goes on the stream before the cursor goes on the workflow, and the +// order is the guarantee: interrupted after the first write there is a pin +// holding storage nothing reads, which costs space. Interrupted after the +// other order there would be a cursor with no pin, and truncation would be free +// to take a range that cursor still points at. +func (h *handler) subscribeToExternalStream( + ctx context.Context, + namespaceID string, + in *streamlib.SubscribeWorkflowInput, + start *streampb.StreamStartPosition, +) (*streamlib.SubscribeWorkflowResponse, error) { + // The pin is keyed by the consuming run, so the run is resolved first. It + // also pins the cursor write below to that run, so a run that ends between + // the two steps cannot leave the pin on one run and the cursor on another. + consumerRunID, err := chasm.ReadComponent(ctx, + workflowRef(namespaceID, in.GetWorkflowId(), in.GetOwnerRunId()), + func(_ *chasmworkflow.Workflow, cctx chasm.Context, _ struct{}) (string, error) { + return cctx.ExecutionKey().RunID, nil + }, struct{}{}) + if err != nil { + return nil, err + } + + // The stream half goes out and comes back on the shard that owns it. This + // handler was routed to the consuming workflow, so the stream may well be + // somewhere else, and resolving it here would fail on any cluster with more + // than one history host. + // + // The pin still lands before the cursor, which is the guarantee: interrupted + // between them there is a pin holding storage nothing reads, which costs + // space, where the other order would leave a cursor with no pin and let + // truncation take a range it still points at. + registered, err := h.routed.RegisterStreamConsumer(ctx, &streamlib.RegisterStreamConsumerRequest{ + NamespaceId: namespaceID, + FrontendRequest: &streamlib.RegisterStreamConsumerInput{ + Namespace: in.GetNamespace(), + StreamId: in.GetStreamId(), + ConsumerWorkflowId: in.GetWorkflowId(), + ConsumerRunId: consumerRunID, + StartPosition: start, + }, + }) + if err != nil { + return nil, err + } + pin := registered.GetFrontendResponse() + if pin.GetStreamAbsent() { + // The caller named a standalone stream id, so there is nothing to fall + // back to: an id that names nothing is the caller's mistake here. + return nil, serviceerror.NewNotFoundf( + "no stream with id %q in this namespace", in.GetStreamId()) + } + + startOffset, _, err := chasm.UpdateComponent( + ctx, + workflowRef(namespaceID, in.GetWorkflowId(), consumerRunID), + func(wf *chasmworkflow.Workflow, mctx chasm.MutableContext, offset int64) (int64, error) { + return wf.SubscribeToExternalStream(mctx, chasmworkflow.ExternalStreamSubscription{ + StreamID: in.GetStreamId(), + StartOffset: offset, + KnownHead: pin.GetKnownHead(), + }) + }, + pin.GetStartOffset(), + ) + if err != nil { + return nil, err + } + + return &streamlib.SubscribeWorkflowResponse{ + FrontendResponse: &streamlib.SubscribeWorkflowOutput{StartOffset: startOffset}, + }, nil +} + +// RegisterStreamConsumer takes the pin, on the shard that owns the stream. +// +// Internal. Called by SubscribeWorkflow and by the workflow task completion +// path, both of which run on the consumer's shard and so cannot reach the +// stream themselves. The start offset is resolved inside the transition that +// records it, against the same frontier it hands back, so the consumer records +// facts rather than readings. +func (h *handler) RegisterStreamConsumer( + ctx context.Context, + req *streamlib.RegisterStreamConsumerRequest, +) (*streamlib.RegisterStreamConsumerResponse, error) { + in := req.GetFrontendRequest() + ctx = h.withCallerInfo(ctx, req.GetNamespaceId()) + start, err := stream.RequestedStart(in.GetStartPosition(), "start_offset", in.GetStartOffset()) + if err != nil { + return nil, err + } + + limits := h.limitsFor(req.GetNamespaceId()) + pin, _, err := chasm.UpdateComponent( + ctx, + refFor(req.GetNamespaceId(), in.GetStreamId()), + func( + s *stream.Stream, mctx chasm.MutableContext, start *streampb.StreamStartPosition, + ) (*streamlib.RegisterStreamConsumerOutput, error) { + startOffset, err := s.RegisterConsumer(mctx, stream.ConsumerRegistration{ + ConsumerID: externalConsumerID(in.GetConsumerWorkflowId(), in.GetConsumerRunId()), + WorkflowID: in.GetConsumerWorkflowId(), + RunID: in.GetConsumerRunId(), + Start: start, + External: true, + MaxConsumers: limits.MaxConsumersPerStream, + }) + if err != nil { + return nil, err + } + return &streamlib.RegisterStreamConsumerOutput{ + StartOffset: startOffset, + KnownHead: s.State.GetHeadOffset(), + }, nil + }, + start, + ) + if err != nil { + // Only the absence of the execution itself is turned into an answer. + // The caller treats that as "the id names no standalone stream" and + // binds to a stream of its own instead, which would be the wrong thing + // to do about a registry miss or a shard that has moved. + if executionAbsent(err) { + return &streamlib.RegisterStreamConsumerResponse{ + FrontendResponse: &streamlib.RegisterStreamConsumerOutput{StreamAbsent: true}, + }, nil + } + return nil, err + } + return &streamlib.RegisterStreamConsumerResponse{FrontendResponse: pin}, nil +} + +// executionAbsent reports whether an error means no execution holds the stream. +// +// This runs on the shard that owns the stream, after routing, so a NotFound +// here is the engine's answer about the execution. A namespace the registry +// cannot name and a shard that has moved have error types of their own and do +// not reach it. +func executionAbsent(err error) bool { + var notFound *serviceerror.NotFound + return errors.As(err, ¬Found) +} + +// externalConsumerID names a workflow run's pin on a stream in another +// execution. Keyed by run, so a later run of the same workflow id registers +// fresh instead of inheriting a closed run's floor. +func externalConsumerID(workflowID, runID string) string { + return "workflow:" + workflowID + "/" + runID +} + +// consumerProbe is what one run says about a subscription: whether the run is +// still open, whether it holds a cursor for the stream, and where that cursor +// began, which is the floor a re-keyed pin has to hold. +type consumerProbe struct { + runID string + closed bool + consumes bool + startOffset int64 +} + +// probeConsumer reads a run without touching it. A run that is gone reads as +// closed, since the notify task only needs to know whether pushing at it can +// achieve anything. +func (h *handler) probeConsumer( + ctx context.Context, + namespaceID, workflowID, runID, streamID string, +) (consumerProbe, error) { + probe, err := chasm.ReadComponent(ctx, workflowRef(namespaceID, workflowID, runID), + func(wf *chasmworkflow.Workflow, cctx chasm.Context, _ struct{}) (consumerProbe, error) { + p := consumerProbe{ + runID: cctx.ExecutionKey().RunID, + closed: !cctx.ExecutionInfo().CloseTime.IsZero(), + } + // Only an external cursor counts. A cursor of the same key on a + // stream the workflow owns is a different stream that happens to + // share the name, and answering for it would keep a pin alive on + // a stream nothing reads. + if field, ok := wf.StreamCursors[streamID]; ok && field.Get(cctx).IsExternal() { + p.consumes = true + p.startOffset = field.Get(cctx).StartOffset() + } + return p, nil + }, struct{}{}) + if _, ok := errors.AsType[*serviceerror.NotFound](err); ok { + return consumerProbe{runID: runID, closed: true}, nil + } + return probe, err +} + +func (h *handler) pushHead( + ctx context.Context, + namespaceID, workflowID, runID, streamID string, + head int64, +) error { + _, _, err := chasm.UpdateComponent( + ctx, + workflowRef(namespaceID, workflowID, runID), + func(wf *chasmworkflow.Workflow, mctx chasm.MutableContext, at int64) (struct{}, error) { + return struct{}{}, wf.AdvanceKnownHead(mctx, streamID, at) + }, + head, + ) + return err +} + +// AdvanceConsumerHead tells one consumer that the frontier moved, on the shard +// that owns that consumer. +// +// Internal. Called by the notify task, which runs on the stream's shard and so +// cannot reach a consumer living anywhere else. The task pins a run, and a run +// ends: the answer then says whether a successor carries the subscription, so +// the stream re-keys its pin, or nothing does, so the stream releases it. +func (h *handler) AdvanceConsumerHead( + ctx context.Context, + req *streamlib.AdvanceConsumerHeadRequest, +) (*streamlib.AdvanceConsumerHeadResponse, error) { + in := req.GetFrontendRequest() + ctx = h.withCallerInfo(ctx, req.GetNamespaceId()) + namespaceID, workflowID, streamID := req.GetNamespaceId(), in.GetWorkflowId(), in.GetStreamId() + + pinned, err := h.probeConsumer(ctx, namespaceID, workflowID, in.GetOwnerRunId(), streamID) + if err != nil { + return nil, err + } + out := &streamlib.AdvanceConsumerHeadOutput{} + if !pinned.closed { + if !pinned.consumes { + out.ConsumerClosed = true + return &streamlib.AdvanceConsumerHeadResponse{FrontendResponse: out}, nil + } + err := h.pushHead(ctx, namespaceID, workflowID, pinned.runID, streamID, in.GetHeadOffset()) + if err != nil { + return nil, err + } + return &streamlib.AdvanceConsumerHeadResponse{FrontendResponse: out}, nil + } + + // The pinned run is over. A continue-as-new carries the subscription to the + // current run, and that is the only run worth pushing at. + current, err := h.probeConsumer(ctx, namespaceID, workflowID, "", streamID) + if err != nil { + return nil, err + } + if current.closed || current.runID == pinned.runID || !current.consumes { + out.ConsumerClosed = true + return &streamlib.AdvanceConsumerHeadResponse{FrontendResponse: out}, nil + } + err = h.pushHead(ctx, namespaceID, workflowID, current.runID, streamID, in.GetHeadOffset()) + if err != nil { + return nil, err + } + out.SuccessorRunId = current.runID + out.SuccessorStartOffset = current.startOffset + return &streamlib.AdvanceConsumerHeadResponse{FrontendResponse: out}, nil +} + +func (h *handler) PollMessages( + ctx context.Context, + req *streamlib.PollMessagesRequest, +) (*streamlib.PollMessagesResponse, error) { + in := req.GetFrontendRequest() + ctx = h.withCallerInfo(ctx, req.GetNamespaceId()) + ns := h.namespaceName(req.GetNamespaceId()) + if err := h.admitPoll(ns); err != nil { + return nil, err + } + + ref := refForRun(req.GetNamespaceId(), in.GetStreamId(), in.GetRunId()) + from, start := in.GetFromOffset(), in.GetStartPosition() + + // Only the blocking path needs the frontier before the read. On the + // ordinary path the window carries it, and this is the hottest call in the + // feature: the state clone walks the producer and consumer tables, which + // hold up to a thousand entries each. + if in.GetWaitNewMessages() { + // One budget for the whole call, however it is spent: on the stream + // coming into existence, and then on it moving. + pollCtx, cancel := pollBudget(ctx) + defer cancel() + + state, err := chasm.ReadComponent(ctx, ref, (*stream.Stream).Snapshot, struct{}{}) + if executionAbsent(err) && in.GetRunId() == "" { + state, err = h.waitForStream(pollCtx, ctx, ref, err) + } + if err != nil { + return nil, err + } + waitFrom, err := waitOffset(from, start, state) + if err != nil { + return nil, err + } + // Blocking is only worth it once the reader is genuinely caught up. + if waitFrom == state.GetHeadOffset() && !state.GetClosed() { + // The window is re-read below, so only the blocking matters here. + if _, err := h.waitForMessages(pollCtx, ctx, ref, waitFrom, state); err != nil { + return nil, err + } + from, start = waitFrom, nil + } + } + + wreq := stream.WindowRequest{ + From: from, Start: start, MaxMessages: in.GetMaxMessages(), Topics: in.GetTopics(), + } + w, err := chasm.ReadComponent(ctx, ref, (*stream.Stream).ReadWindow, wreq) + if err != nil { + return nil, err + } + out, err := formatWindow(w, wreq) + if err != nil { + return nil, err + } + h.meterDelivery(ns, out) + return &streamlib.PollMessagesResponse{FrontendResponse: out}, nil +} + +// PollWorkflowMessages reads a stream an execution owns. +// +// An attached stream is reached through its owner, so this call routes on the +// owner and both the frontier and the batches come out of the owner's +// component. +func (h *handler) PollWorkflowMessages( + ctx context.Context, + req *streamlib.PollWorkflowMessagesRequest, +) (*streamlib.PollWorkflowMessagesResponse, error) { + in := req.GetFrontendRequest() + ctx = h.withCallerInfo(ctx, req.GetNamespaceId()) + + target, err := resolveOwned(req.GetNamespaceId(), in.GetOwner(), in.GetStreamName()) + if err != nil { + return nil, err + } + ns := h.namespaceName(req.GetNamespaceId()) + if err := h.admitPoll(ns); err != nil { + return nil, err + } + from, start := in.GetFromOffset(), in.GetStartPosition() + + state, err := h.ownedStreamState(ctx, target) + if err != nil { + return nil, err + } + + if in.GetWaitNewMessages() { + waitFrom, err := waitOffset(from, start, state) + if err != nil { + return nil, err + } + if waitFrom == state.GetHeadOffset() && !state.GetClosed() { + pollCtx, cancel := pollBudget(ctx) + defer cancel() + if _, err := h.waitForOwnedMessages(pollCtx, ctx, target, waitFrom, state); err != nil { + return nil, err + } + from, start = waitFrom, nil + } + } + + wreq := stream.WindowRequest{ + From: from, Start: start, MaxMessages: in.GetMaxMessages(), Topics: in.GetTopics(), + } + w, err := chasm.ReadComponent(ctx, target.ref, readOwnedWindow, + ownedWindowRequest{Key: target.key, Window: wreq}) + if err != nil { + return nil, err + } + out, err := formatWindow(w, wreq) + if err != nil { + return nil, err + } + h.meterDelivery(ns, out) + return &streamlib.PollWorkflowMessagesResponse{FrontendResponse: out}, nil +} + +// formatWindow turns a component read into the wire response. The read happens +// in the component, so the frontier and the bytes it was served with cannot +// disagree. +// +// The reader is advanced only over offsets that were examined. CollectRecords +// steps past every message it filtered out, so a filtered page that matched +// nothing still moves the reader; a window whose batches stop short of its end +// must not be reported as read to the end. +func formatWindow(w stream.Window, req stream.WindowRequest) (*streamlib.PollMessagesOutput, error) { + out := &streamlib.PollMessagesOutput{ + NextOffset: w.From, + HeadOffset: w.State.GetHeadOffset(), + Closed: w.State.GetClosed(), + CloseReason: w.State.GetCloseReason(), + RunId: w.RunID, + } + if w.From == w.State.GetHeadOffset() { + return out, nil + } + + records, next, err := stream.CollectRecords( + w.Blobs, w.Starts, w.From, w.To, w.Limit, req.Topics) + if err != nil { + return nil, err + } + out.Records = records + out.NextOffset = next + return out, nil +} + +// ownedWindowRequest names which attached stream to read and what to read. +type ownedWindowRequest struct { + Key string + Window stream.WindowRequest +} + +// readOwnedWindow reads a stream attached to an owner. +// +// An owner that has ended can take no more records, so its stream is finished +// whether or not a producer said so. Without that a reader tailing a workflow +// or an activity that ended stays parked forever. +func readOwnedWindow( + owner stream.Owner, + cctx chasm.Context, + req ownedWindowRequest, +) (stream.Window, error) { + ended := owner.OwnedStreamEnded(cctx, req.Key) + s := owner.OwnedStream(cctx, req.Key) + if s == nil { + // Nothing published yet, which reads as an empty stream so a reader can + // attach before the first append. + state := &streamlib.StreamState{Closed: ended} + from := req.Window.From + if req.Window.Start != nil { + var err error + if from, err = stream.ResolveStart(req.Window.Start, state); err != nil { + return stream.Window{}, err + } + } + return stream.Window{State: state, From: from, To: from}, nil + } + w, err := s.ReadWindow(cctx, req.Window) + if err != nil { + return stream.Window{}, err + } + if ended { + w.State.Closed = true + } + return w, nil +} + +// ownedStreamState snapshots an attached stream through the component that +// owns it. A stream nothing has published to yet reads as an empty one, so a +// reader may attach before the first record. +func (h *handler) ownedStreamState( + ctx context.Context, + target ownedTarget, +) (*streamlib.StreamState, error) { + return chasm.ReadComponent(ctx, target.ref, readOwnedStream, target.key) +} + +// readOwnedStream snapshots an attached stream and reports whether anything +// can still be added to it, which is not the case once its owner has ended. +func readOwnedStream( + owner stream.Owner, + cctx chasm.Context, + key string, +) (*streamlib.StreamState, error) { + state := &streamlib.StreamState{} + if s := owner.OwnedStream(cctx, key); s != nil { + var err error + if state, err = s.Snapshot(cctx, struct{}{}); err != nil { + return nil, err + } + } + if owner.OwnedStreamEnded(cctx, key) { + state.Closed = true + } + return state, nil +} + +// pollBudget is how long one blocking poll may hold its caller, whatever it is +// waiting for. It ends before the caller's own deadline, so expiry can be +// answered with an empty response rather than an error. +func pollBudget(ctx context.Context) (context.Context, context.CancelFunc) { + return contextutil.WithDeadlineBuffer(ctx, stream.LongPollTimeout, stream.LongPollBuffer) +} + +// waitForStream parks a blocking poll on a standalone stream id that names +// nothing yet, asking again on an interval until the stream exists or the +// poll's budget runs out. A reader can then attach before the producer's first +// write, as it can on an owned stream, whose owner is there to wait on. On +// expiry the caller gets the refusal it would have got at once. +// +// Only an unpinned reference waits. A run id names one execution, and a stream +// created later is another run, so that wait could never be answered. +func (h *handler) waitForStream( + pollCtx, callerCtx context.Context, + ref chasm.ComponentRef, + absent error, +) (*streamlib.StreamState, error) { + ticker := time.NewTicker(h.config.CreateWaitRecheckInterval()) + defer ticker.Stop() + for { + select { + case <-pollCtx.Done(): + if callerCtx.Err() != nil { + return nil, callerCtx.Err() + } + return nil, absent + case <-ticker.C: + } + state, err := chasm.ReadComponent(pollCtx, ref, (*stream.Stream).Snapshot, struct{}{}) + switch { + case err == nil: + return state, nil + case executionAbsent(err), pollCtx.Err() != nil: + continue + default: + return nil, err + } + } +} + +// waitForMessages blocks until the head passes the reader's offset or the +// stream closes. On the server's long-poll timeout it returns the state it last +// saw, so the caller gets an empty response and polls again rather than an +// error it would have to distinguish from a real failure. +func (h *handler) waitForMessages( + pollCtx, callerCtx context.Context, + ref chasm.ComponentRef, + from int64, + current *streamlib.StreamState, +) (*streamlib.StreamState, error) { + state, _, err := chasm.PollComponent(pollCtx, ref, + func(s *stream.Stream, _ chasm.Context, offset int64) (*streamlib.StreamState, bool, error) { + // Monotonic, as PollComponent requires: the head only advances and + // closed never clears. + if !pollSatisfied(s.State, offset) { + return nil, false, nil + } + return common.CloneProto(s.State), true, nil + }, from) + return pollOutcome(pollCtx, callerCtx, state, err, current) +} + +// waitForOwnedMessages is waitForMessages against a stream reached through its +// owner. The predicate has to re-resolve the stream on every evaluation, +// because what the poll observes is the owning execution. +func (h *handler) waitForOwnedMessages( + pollCtx, callerCtx context.Context, + target ownedTarget, + from int64, + current *streamlib.StreamState, +) (*streamlib.StreamState, error) { + state, _, err := chasm.PollComponent(pollCtx, target.ref, + func( + owner stream.Owner, cctx chasm.Context, offset int64, + ) (*streamlib.StreamState, bool, error) { + owned, err := readOwnedStream(owner, cctx, target.key) + if err != nil { + return nil, false, err + } + if !pollSatisfied(owned, offset) { + return nil, false, nil + } + return owned, true, nil + }, from) + return pollOutcome(pollCtx, callerCtx, state, err, current) +} + +// waitOffset is the offset a blocking poll waits past. A first poll that names +// a start position resolves it against the frontier the wait begins from, and +// the read after the wait uses that offset: resolving the position again would +// put a tail at the new head and read nothing. +func waitOffset( + from int64, start *streampb.StreamStartPosition, state *streamlib.StreamState, +) (int64, error) { + if start == nil { + return from, nil + } + return stream.ResolveStart(start, state) +} + +// pollSatisfied is the monotonic condition PollComponent requires: the head +// only advances and closed never clears. +func pollSatisfied(state *streamlib.StreamState, from int64) bool { + return state.GetHeadOffset() > from || state.GetClosed() +} + +// pollOutcome turns a long-poll result into the state the reader should be +// served. +func pollOutcome( + pollCtx, callerCtx context.Context, + state *streamlib.StreamState, + err error, + current *streamlib.StreamState, +) (*streamlib.StreamState, error) { + if err != nil { + if pollCtx.Err() != nil && callerCtx.Err() == nil { + // Our long-poll budget expired, not the caller's. Hand back the + // state we already had so the reader gets an empty response and + // polls again, rather than an error it has to tell apart from a + // real failure. + return current, nil + } + return nil, err + } + if state == nil { + return current, nil + } + return state, nil +} + +func (h *handler) DescribeStream( + ctx context.Context, + req *streamlib.DescribeStreamRequest, +) (*streamlib.DescribeStreamResponse, error) { + in := req.GetFrontendRequest() + ctx = h.withCallerInfo(ctx, req.GetNamespaceId()) + state, err := chasm.ReadComponent(ctx, + refFor(req.GetNamespaceId(), in.GetStreamId()), (*stream.Stream).Snapshot, struct{}{}) + if err != nil { + return nil, err + } + return &streamlib.DescribeStreamResponse{ + FrontendResponse: &streamlib.DescribeStreamOutput{State: state}, + }, nil +} + +// DescribeWorkflowStream reports the frontier of a stream an execution owns. A +// reader needs it to start at the tail rather than at the beginning, which an +// attached stream offers no other way to find. +func (h *handler) DescribeWorkflowStream( + ctx context.Context, + req *streamlib.DescribeWorkflowStreamRequest, +) (*streamlib.DescribeWorkflowStreamResponse, error) { + in := req.GetFrontendRequest() + ctx = h.withCallerInfo(ctx, req.GetNamespaceId()) + + target, err := resolveOwned(req.GetNamespaceId(), in.GetOwner(), in.GetStreamName()) + if err != nil { + return nil, err + } + state, err := h.ownedStreamState(ctx, target) + if err != nil { + return nil, err + } + return &streamlib.DescribeWorkflowStreamResponse{ + FrontendResponse: &streamlib.DescribeStreamOutput{State: state}, + }, nil +} + +func (h *handler) CloseStream( + ctx context.Context, + req *streamlib.CloseStreamRequest, +) (*streamlib.CloseStreamResponse, error) { + in := req.GetFrontendRequest() + ctx = h.withCallerInfo(ctx, req.GetNamespaceId()) + _, _, err := chasm.UpdateComponent( + ctx, + refFor(req.GetNamespaceId(), in.GetStreamId()), + func(s *stream.Stream, mctx chasm.MutableContext, reason *commonpb.Payload) (struct{}, error) { + return struct{}{}, s.CloseAndSchedule(mctx, reason) + }, + in.GetReason(), + ) + if err != nil { + return nil, err + } + return &streamlib.CloseStreamResponse{FrontendResponse: &streamlib.CloseStreamOutput{}}, nil +} + +// TruncateStream advances the readable floor. +// +// A refusal means an active consumer's pin is below the requested base. Some +// of those pins belong to nobody: a subscription that took its pin and then +// failed to write its cursor leaves one behind, and the only thing that +// notices is the notify probe, which runs on append. A stream that has gone +// quiet never gets one. So a refusal probes the pins it was refused for and +// tries once more, and what survives that is a consumer that really is there. +func (h *handler) TruncateStream( + ctx context.Context, + req *streamlib.TruncateStreamRequest, +) (*streamlib.TruncateStreamResponse, error) { + in := req.GetFrontendRequest() + ctx = h.withCallerInfo(ctx, req.GetNamespaceId()) + ref := refFor(req.GetNamespaceId(), in.GetStreamId()) + + err := h.truncate(ctx, ref, in.GetNewBaseOffset()) + if _, ok := errors.AsType[*serviceerror.FailedPrecondition](err); ok { + state, readErr := chasm.ReadComponent(ctx, ref, (*stream.Stream).Snapshot, struct{}{}) + if readErr != nil { + return nil, err + } + notifier := consumerNotifier{logger: h.logger, routed: h.routed} + if probeErr := notifier.notify( + ctx, ref, in.GetNamespace(), state, true); probeErr != nil { + return nil, err + } + err = h.truncate(ctx, ref, in.GetNewBaseOffset()) + } + if err != nil { + return nil, err + } + return &streamlib.TruncateStreamResponse{FrontendResponse: &streamlib.TruncateStreamOutput{}}, nil +} + +func (h *handler) truncate(ctx context.Context, ref chasm.ComponentRef, newBase int64) error { + _, _, err := chasm.UpdateComponent( + ctx, + ref, + func(s *stream.Stream, mctx chasm.MutableContext, base int64) (struct{}, error) { + return struct{}{}, s.Truncate(mctx, base) + }, + newBase, + ) + return err +} + +// ListStreams is intentionally not implemented here. It queries visibility, so +// it has no business ID to route on and the frontend answers it directly. + +func (h *handler) DeleteStream( + ctx context.Context, + req *streamlib.DeleteStreamRequest, +) (*streamlib.DeleteStreamResponse, error) { + in := req.GetFrontendRequest() + ctx = h.withCallerInfo(ctx, req.GetNamespaceId()) + key := chasm.ExecutionKey{NamespaceID: req.GetNamespaceId(), BusinessID: in.GetStreamId()} + + // A workflow consuming the stream recorded ranges its replay will ask for. + // Deleting under it succeeds now and fails that workflow later, so the + // caller has to say it means it. Checked and then deleted rather than in + // one transition, which leaves a window for a subscription that lands in + // between; that consumer finds out at its next task, with a cause. + if !in.GetForce() { + state, err := chasm.ReadComponent(ctx, refFor(key.NamespaceID, key.BusinessID), + (*stream.Stream).Snapshot, struct{}{}) + if err != nil { + return nil, err + } + for id, consumer := range state.GetConsumers() { + if consumer.GetActive() { + return nil, serviceerror.NewFailedPreconditionf( + "stream %q is consumed by workflow %q (%s); set force to delete it anyway", + key.BusinessID, consumer.GetWorkflowId(), id) + } + } + } + + // The payload is component state, so deleting the execution takes it too. + err := chasm.DeleteExecution[*stream.Stream](ctx, key, chasm.DeleteExecutionRequest{}) + if err != nil { + return nil, err + } + return &streamlib.DeleteStreamResponse{FrontendResponse: &streamlib.DeleteStreamOutput{}}, nil +} diff --git a/chasm/lib/stream/service/handler_test.go b/chasm/lib/stream/service/handler_test.go new file mode 100644 index 00000000000..6a7a784bdde --- /dev/null +++ b/chasm/lib/stream/service/handler_test.go @@ -0,0 +1,108 @@ +package service + +import ( + "testing" + + "github.com/stretchr/testify/require" + commonpb "go.temporal.io/api/common/v1" + enumspb "go.temporal.io/api/enums/v1" + "go.temporal.io/api/serviceerror" + streampb "go.temporal.io/api/stream/v1" + "go.temporal.io/server/chasm/lib/stream" + streamlib "go.temporal.io/server/chasm/lib/stream/gen/streampb/v1" + chasmworkflow "go.temporal.io/server/chasm/lib/workflow" + "google.golang.org/protobuf/proto" +) + +func batchBlob(t *testing.T, topic string, bodies ...string) *commonpb.DataBlob { + t.Helper() + messages := make([]*streamlib.StreamRecord, len(bodies)) + for i, b := range bodies { + messages[i] = &streamlib.StreamRecord{ + Body: &commonpb.Payload{Data: []byte(b)}, + Topic: topic, + Kind: streampb.STREAM_RECORD_KIND_DATA, + } + } + data, err := proto.Marshal(&streamlib.StreamRecordBatch{Records: messages}) + require.NoError(t, err) + return &commonpb.DataBlob{EncodingType: enumspb.ENCODING_TYPE_PROTO3, Data: data} +} + +// A filtered page that matched nothing still advances the reader, but only +// over the offsets that were actually examined. A window whose batches stop +// short of its end must not report the end as the next offset, or the reader +// steps over messages it was never shown. +func TestFormatWindowAdvancesOnlyOverExaminedOffsets(t *testing.T) { + w := stream.Window{ + State: &streamlib.StreamState{HeadOffset: 10}, + Blobs: []*commonpb.DataBlob{batchBlob(t, "a", "m0", "m1", "m2")}, + Starts: []int64{0}, + To: 5, + Limit: 5, + } + out, err := formatWindow(w, stream.WindowRequest{From: 0, Topics: []string{"nothing"}}) + require.NoError(t, err) + require.Empty(t, out.GetRecords()) + require.Equal(t, int64(3), out.GetNextOffset(), + "offsets 3 and 4 were never read, so the reader must not be moved past them") + + // A page that filtered everything out but covered its whole window does + // advance to the end, so the reader does not loop on the same offsets. + w.Blobs = []*commonpb.DataBlob{batchBlob(t, "a", "m0", "m1", "m2", "m3", "m4")} + out, err = formatWindow(w, stream.WindowRequest{From: 0, Topics: []string{"nothing"}}) + require.NoError(t, err) + require.Empty(t, out.GetRecords()) + require.Equal(t, int64(5), out.GetNextOffset()) +} + +// Every owner kind resolves to the execution that holds the stream and the +// key it is held under there. A workflow's activity is routed on the workflow +// and held under a reserved key, which the workflow kind may not name. +func TestResolveOwnedStream(t *testing.T) { + wf := &streamlib.StreamOwner{ + Kind: streamlib.STREAM_OWNER_KIND_WORKFLOW, Id: "wf", RunId: "run", + } + target, err := resolveOwned("ns", wf, "") + require.NoError(t, err) + require.Equal(t, stream.DefaultStreamName, target.key) + require.Equal(t, "wf", target.ref.BusinessID) + require.Equal(t, "run", target.ref.RunID) + + saa := &streamlib.StreamOwner{Kind: streamlib.STREAM_OWNER_KIND_ACTIVITY, Id: "act"} + target, err = resolveOwned("ns", saa, "progress") + require.NoError(t, err) + require.Equal(t, "progress", target.key) + require.Equal(t, "act", target.ref.BusinessID) + + wfa := &streamlib.StreamOwner{ + Kind: streamlib.STREAM_OWNER_KIND_WORKFLOW_ACTIVITY, Id: "wf", ActivityId: "act", + } + target, err = resolveOwned("ns", wfa, "") + require.NoError(t, err) + require.Equal(t, "wf", target.ref.BusinessID, "a workflow's activity routes on the workflow") + require.Equal(t, chasmworkflow.ActivityStreamKey("act", stream.DefaultStreamName), target.key) + + _, err = resolveOwned("ns", wf, target.key) + require.ErrorContains(t, err, "reserved", "the workflow kind may not reach an activity's stream") +} + +func TestResolveOwnedRefusesAnIncompleteOwner(t *testing.T) { + for name, owner := range map[string]*streamlib.StreamOwner{ + "no owner": nil, + "no kind": {Id: "wf"}, + "no id": {Kind: streamlib.STREAM_OWNER_KIND_WORKFLOW}, + "activity of a standalone": { + Kind: streamlib.STREAM_OWNER_KIND_ACTIVITY, Id: "act", ActivityId: "other", + }, + "workflow activity without the activity": { + Kind: streamlib.STREAM_OWNER_KIND_WORKFLOW_ACTIVITY, Id: "wf", + }, + } { + t.Run(name, func(t *testing.T) { + _, err := resolveOwned("ns", owner, "") + var invalid *serviceerror.InvalidArgument + require.ErrorAs(t, err, &invalid) + }) + } +} diff --git a/chasm/lib/stream/service/library.go b/chasm/lib/stream/service/library.go new file mode 100644 index 00000000000..825c295e126 --- /dev/null +++ b/chasm/lib/stream/service/library.go @@ -0,0 +1,95 @@ +package service + +import ( + "go.temporal.io/server/chasm" + "go.temporal.io/server/chasm/lib/stream" + streampb "go.temporal.io/server/chasm/lib/stream/gen/streampb/v1" + "google.golang.org/grpc" +) + +const ( + libraryName = "stream" + componentName = "stream" +) + +var ( + Archetype = chasm.FullyQualifiedName(libraryName, componentName) + ArchetypeID = chasm.GenerateTypeID(Archetype) +) + +type library struct { + chasm.UnimplementedLibrary + handler *handler + retention *retentionTaskHandler + notifyConsumers *notifyConsumersTaskHandler + age *ageTaskHandler +} + +func newLibrary( + h *handler, + retention *retentionTaskHandler, + notifyConsumers *notifyConsumersTaskHandler, + age *ageTaskHandler, +) *library { + return &library{handler: h, retention: retention, notifyConsumers: notifyConsumers, age: age} +} + +// componentOnlyLibrary registers the component without the service, which is +// what the frontend needs in order to serialize component references. +type componentOnlyLibrary struct { + chasm.UnimplementedLibrary +} + +func newComponentOnlyLibrary() *componentOnlyLibrary { + return &componentOnlyLibrary{} +} + +func (l *componentOnlyLibrary) Name() string { + return libraryName +} + +func (l *componentOnlyLibrary) Components() []*chasm.RegistrableComponent { + return components() +} + +func components() []*chasm.RegistrableComponent { + return []*chasm.RegistrableComponent{ + chasm.NewRegistrableComponent[*stream.Stream]( + componentName, + chasm.WithBusinessIDAlias("StreamId"), + ), + // Registered here rather than with the workflow library because it is + // this package's type, even though it only ever hangs off a consuming + // workflow. + chasm.NewRegistrableComponent[*stream.Cursor]("streamCursor"), + } +} + +func (l *library) Name() string { + return libraryName +} + +func (l *library) Components() []*chasm.RegistrableComponent { + return components() +} + +func (l *library) Tasks() []*chasm.RegistrableTask { + return []*chasm.RegistrableTask{ + chasm.NewRegistrableSideEffectTask( + "streamRetention", + l.retention, + ), + chasm.NewRegistrableSideEffectTask( + "streamNotifyConsumers", + l.notifyConsumers, + ), + chasm.NewRegistrablePureTask( + "streamAge", + l.age, + ), + } +} + +func (l *library) RegisterServices(server *grpc.Server) { + streampb.RegisterStreamServiceServer(server, l.handler) +} diff --git a/chasm/lib/stream/service/ratelimit.go b/chasm/lib/stream/service/ratelimit.go new file mode 100644 index 00000000000..284d0c510cb --- /dev/null +++ b/chasm/lib/stream/service/ratelimit.go @@ -0,0 +1,103 @@ +package service + +import ( + "fmt" + "sync" + "time" + + enumspb "go.temporal.io/api/enums/v1" + "go.temporal.io/api/serviceerror" + "go.temporal.io/server/chasm/lib/stream" + "go.temporal.io/server/common/quotas" +) + +// limiterRefreshInterval is how long a changed rate takes to reach a namespace +// that already has limiters. +const limiterRefreshInterval = 10 * time.Second + +// namespaceLimiters enforces the per-namespace stream rates: records and bytes +// appended per second, and polls per second. One set per namespace, made on +// first use and refreshed from config on an interval, so a change lands +// without a restart. A rate of zero is not enforced at all. +type namespaceLimiters struct { + config *stream.Config + + mu sync.Mutex + byNamespace map[string]*limiterSet +} + +type limiterSet struct { + appendRecords quotas.RateLimiter + appendBytes quotas.RateLimiter + polls quotas.RateLimiter +} + +func newNamespaceLimiters(config *stream.Config) *namespaceLimiters { + return &namespaceLimiters{config: config, byNamespace: make(map[string]*limiterSet)} +} + +func (l *namespaceLimiters) forNamespace(ns string) *limiterSet { + l.mu.Lock() + defer l.mu.Unlock() + if set, ok := l.byNamespace[ns]; ok { + return set + } + set := &limiterSet{ + appendRecords: newLimiter( + func() int { return l.config.AppendRecordsPerSecond(ns) }, + func() int { return stream.MaxRecordsPerBatch }), + appendBytes: newLimiter( + func() int { return l.config.AppendBytesPerSecond(ns) }, + func() int { return l.config.LimitsFor(ns).MaxBatchBytes }), + polls: newLimiter( + func() int { return l.config.PollsPerSecond(ns) }, + func() int { return 1 }), + } + l.byNamespace[ns] = set + return set +} + +// newLimiter builds a limiter whose burst is one second of its rate and never +// less than minBurst. A burst below the batch cap would refuse a full batch +// forever, whatever the rate, so a single admissible batch always fits. +func newLimiter(rateFn func() int, minBurst func() int) quotas.RateLimiter { + return quotas.NewDynamicRateLimiter(quotas.NewRateBurst( + func() float64 { return float64(max(rateFn(), 0)) }, + func() int { return max(rateFn(), minBurst()) }, + ), limiterRefreshInterval) +} + +// allowAppend admits an append of the given size or refuses it. A batch over +// the namespace's own caps is left to the component, which refuses it with +// the cap it broke rather than with a rate it never had a chance against. +func (l *namespaceLimiters) allowAppend( + ns string, now time.Time, records int, size int, limits stream.Limits, +) error { + set := l.forNamespace(ns) + if rate := l.config.AppendRecordsPerSecond(ns); rate > 0 && + records <= stream.MaxRecordsPerBatch && !set.appendRecords.AllowN(now, records) { + return overRate(ns, rate, "records appended") + } + if rate := l.config.AppendBytesPerSecond(ns); rate > 0 && + size <= limits.MaxBatchBytes && !set.appendBytes.AllowN(now, size) { + return overRate(ns, rate, "bytes appended") + } + return nil +} + +// allowPoll admits one poll, blocking or not, or refuses it. +func (l *namespaceLimiters) allowPoll(ns string, now time.Time) error { + if rate := l.config.PollsPerSecond(ns); rate > 0 && !l.forNamespace(ns).polls.AllowN(now, 1) { + return overRate(ns, rate, "polls") + } + return nil +} + +func overRate(ns string, rate int, what string) error { + return &serviceerror.ResourceExhausted{ + Cause: enumspb.RESOURCE_EXHAUSTED_CAUSE_RPS_LIMIT, + Scope: enumspb.RESOURCE_EXHAUSTED_SCOPE_NAMESPACE, + Message: fmt.Sprintf( + "namespace %q is over its stream limit of %d %s per second", ns, rate, what), + } +} diff --git a/chasm/lib/stream/service/ratelimit_test.go b/chasm/lib/stream/service/ratelimit_test.go new file mode 100644 index 00000000000..0d95c5f6f46 --- /dev/null +++ b/chasm/lib/stream/service/ratelimit_test.go @@ -0,0 +1,97 @@ +package service + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + enumspb "go.temporal.io/api/enums/v1" + "go.temporal.io/api/serviceerror" + "go.temporal.io/server/chasm/lib/stream" + "go.temporal.io/server/common/dynamicconfig" +) + +// limitedConfig is the defaults with the three rates pinned, per namespace. +func limitedConfig(records, bytes, polls map[string]int) *stream.Config { + cfg := stream.NewConfig(dynamicconfig.NewNoopCollection()) + pick := func(rates map[string]int, def int) dynamicconfig.IntPropertyFnWithNamespaceFilter { + return func(ns string) int { + if rate, ok := rates[ns]; ok { + return rate + } + return def + } + } + cfg.AppendRecordsPerSecond = pick(records, stream.AppendRecordsPerSecond) + cfg.AppendBytesPerSecond = pick(bytes, stream.AppendBytesPerSecond) + cfg.PollsPerSecond = pick(polls, stream.PollsPerSecond) + return cfg +} + +func requireOverRate(t *testing.T, err error) { + t.Helper() + var exhausted *serviceerror.ResourceExhausted + require.ErrorAs(t, err, &exhausted) + require.Equal(t, enumspb.RESOURCE_EXHAUSTED_CAUSE_RPS_LIMIT, exhausted.Cause) + require.Equal(t, enumspb.RESOURCE_EXHAUSTED_SCOPE_NAMESPACE, exhausted.Scope) +} + +func TestAppendRecordsRateIsPerNamespace(t *testing.T) { + limiters := newNamespaceLimiters(limitedConfig(map[string]int{"slow": 3}, nil, nil)) + limits := stream.DefaultLimits() + now := time.Now() + + // The burst never drops below one full batch, so the first batch goes + // through whatever the rate; after that the bucket refills at the rate. + require.NoError(t, limiters.allowAppend("slow", now, stream.MaxRecordsPerBatch, 10, limits)) + requireOverRate(t, limiters.allowAppend("slow", now, 1, 10, limits)) + later := now.Add(time.Second) + require.NoError(t, limiters.allowAppend("slow", later, 3, 10, limits)) + requireOverRate(t, limiters.allowAppend("slow", later, 1, 10, limits)) + + // A batch over the cap is not the limiter's to refuse: the component names + // the cap it broke instead. + require.NoError(t, limiters.allowAppend("slow", later, stream.MaxRecordsPerBatch+1, 10, limits)) + + // Another namespace has a bucket of its own. + require.NoError(t, limiters.allowAppend("other", now, stream.MaxRecordsPerBatch, 10, limits)) +} + +func TestAppendBytesRateAdmitsAFullBatchWhateverTheRate(t *testing.T) { + limiters := newNamespaceLimiters(limitedConfig(nil, map[string]int{"slow": 1}, nil)) + limits := stream.DefaultLimits() + now := time.Now() + + // One byte per second, and a batch of the largest admissible size still + // goes through once: refusing it forever would make the rate a size cap. + require.NoError(t, limiters.allowAppend("slow", now, 1, limits.MaxBatchBytes, limits)) + requireOverRate(t, limiters.allowAppend("slow", now, 1, 1, limits)) + + // A batch over the cap is not the limiter's to refuse: the component names + // the cap it broke instead. + require.NoError(t, limiters.allowAppend("slow", now, 1, limits.MaxBatchBytes+1, limits)) +} + +func TestPollRateIsEnforcedAndZeroMeansUnlimited(t *testing.T) { + limiters := newNamespaceLimiters(limitedConfig(nil, nil, map[string]int{"slow": 1, "free": 0})) + now := time.Now() + + require.NoError(t, limiters.allowPoll("slow", now)) + requireOverRate(t, limiters.allowPoll("slow", now)) + require.NoError(t, limiters.allowPoll("slow", now.Add(time.Second))) + + for range 10_000 { + require.NoError(t, limiters.allowPoll("free", now)) + } +} + +func TestZeroAppendRatesAreNotEnforced(t *testing.T) { + limiters := newNamespaceLimiters(limitedConfig( + map[string]int{"free": 0}, map[string]int{"free": 0}, nil)) + limits := stream.DefaultLimits() + now := time.Now() + for range 100 { + require.NoError(t, limiters.allowAppend( + "free", now, stream.MaxRecordsPerBatch, limits.MaxBatchBytes, limits)) + } +} diff --git a/chasm/lib/stream/service/tasks.go b/chasm/lib/stream/service/tasks.go new file mode 100644 index 00000000000..ecc2beb415d --- /dev/null +++ b/chasm/lib/stream/service/tasks.go @@ -0,0 +1,349 @@ +package service + +import ( + "context" + "errors" + + "go.temporal.io/api/serviceerror" + "go.temporal.io/server/chasm" + "go.temporal.io/server/chasm/lib/stream" + streampb "go.temporal.io/server/chasm/lib/stream/gen/streampb/v1" + "go.temporal.io/server/common/headers" + "go.temporal.io/server/common/log" + "go.temporal.io/server/common/log/tag" + "go.temporal.io/server/common/namespace" + "go.temporal.io/server/service/history/shard" + "golang.org/x/sync/errgroup" +) + +// consumerNotifier pushes a stream's frontier at its workflow consumers and +// keeps the consumer table honest about which of them still exist. +// +// A consumer lives wherever its own execution does, so telling it goes back +// out through the service to be routed rather than resolved on this shard. The +// answer also says when the run that subscribed is over: the pin is then +// dropped, or moved to the run that continued it. +type consumerNotifier struct { + logger log.Logger + routed streampb.StreamServiceClient +} + +// notifyConcurrency bounds how many consumers are told at once. A stream may +// hold up to MaxConsumersPerStream of them, and telling them one at a time +// makes one task's runtime grow with the subscriber count, while telling them +// all at once puts that many routed calls on the wire from one task. +const notifyConcurrency = 16 + +// notify tells every active external consumer behind head that it moved. With +// probeAll it also tells the caught-up ones, which is how a closed stream +// learns whether anyone still holds it. +// +// The group carries no cancelling context on purpose: one unreachable consumer +// must not stop the others being told. The first error is what comes back, so +// the task retries and tells the whole set again. +func (n *consumerNotifier) notify( + ctx context.Context, + ref chasm.ComponentRef, + namespaceName string, + state *streampb.StreamState, + probeAll bool, +) error { + head := state.GetHeadOffset() + var group errgroup.Group + group.SetLimit(notifyConcurrency) + for consumerID, consumer := range state.GetConsumers() { + if !consumer.GetExternal() || !consumer.GetActive() { + continue + } + if !probeAll && consumer.GetOffset() >= head { + continue + } + group.Go(func() error { + return n.notifyOne(ctx, ref, namespaceName, consumerID, consumer, head) + }) + } + return group.Wait() +} + +// notifyOne tells one consumer, and acts on what it answers. +// +// The call carries its own deadline. The request's deadline belongs to the +// whole task, and a consumer on a slow host would otherwise take all of it and +// leave the rest untold. +func (n *consumerNotifier) notifyOne( + ctx context.Context, + ref chasm.ComponentRef, + namespaceName string, + consumerID string, + consumer *streampb.ConsumerCursor, + head int64, +) error { + callCtx, cancel := context.WithTimeout(ctx, stream.RoutedCallTimeout) + defer cancel() + + response, err := n.routed.AdvanceConsumerHead(callCtx, &streampb.AdvanceConsumerHeadRequest{ + NamespaceId: ref.NamespaceID, + FrontendRequest: &streampb.AdvanceConsumerHeadInput{ + // Set, because the routed request's GetNamespace() is what rate + // limiting, validation, authorization and redirection all resolve + // the call against. Left empty they resolve against no namespace. + Namespace: namespaceName, + WorkflowId: consumer.GetWorkflowId(), + OwnerRunId: consumer.GetRunId(), + StreamId: ref.BusinessID, + HeadOffset: head, + }, + }) + out := response.GetFrontendResponse() + switch { + case err != nil: + // A NotFound is not taken as proof the consumer is gone. The handler + // already reads a missing execution as closed and says so in its + // answer, so one arriving here is the transport's: a namespace id the + // far host cannot resolve, or a shard that has moved. Releasing a + // durability guarantee on that would be a guess. + n.logger.Error("failed to tell a stream consumer that the frontier moved", + tag.NewStringTag("stream-id", ref.BusinessID), + tag.NewStringTag("consumer-workflow-id", consumer.GetWorkflowId()), + tag.Error(err)) + return err + case out.GetConsumerClosed(): + // The run that subscribed is over and nothing carries the + // subscription on. Its replay floor is holding storage for a + // recovery that can no longer be asked for, and this probe is the + // one place that finds out. + return forgetConsumer(ctx, ref, consumerID) + case out.GetSuccessorRunId() != "": + // A continue-as-new moved the subscription to a new run. The pin + // follows it, with the floor the successor's cursor began at; the + // predecessor's ranges are in a closed history that never replays. + return rekeyConsumer(ctx, ref, consumer, out) + default: + // Told, and still there. Nothing to clean up. + return nil + } +} + +func forgetConsumer(ctx context.Context, ref chasm.ComponentRef, consumerID string) error { + _, _, err := chasm.UpdateComponent(ctx, ref, + func(s *stream.Stream, mctx chasm.MutableContext, id string) (struct{}, error) { + s.ForgetConsumer(mctx, id) + return struct{}{}, nil + }, consumerID) + return err +} + +func rekeyConsumer( + ctx context.Context, + ref chasm.ComponentRef, + consumer *streampb.ConsumerCursor, + out *streampb.AdvanceConsumerHeadOutput, +) error { + registration := stream.ConsumerRegistration{ + ConsumerID: externalConsumerID(consumer.GetWorkflowId(), out.GetSuccessorRunId()), + WorkflowID: consumer.GetWorkflowId(), + RunID: out.GetSuccessorRunId(), + Start: stream.AtOffset(out.GetSuccessorStartOffset()), + External: true, + } + _, _, err := chasm.UpdateComponent(ctx, ref, + func( + s *stream.Stream, mctx chasm.MutableContext, reg stream.ConsumerRegistration, + ) (struct{}, error) { + // Registering the successor drops the predecessor's entry, since + // they share a workflow id and differ in run. + _, err := s.RegisterConsumer(mctx, reg) + return struct{}{}, err + }, registration) + return err +} + +// backgroundCallerContext tags a task's context with the namespace, since the +// task runs outside any request and nothing else has done it. The name comes +// back too: a routed call the task makes has to carry it on the request, which +// is what the interceptors resolve the call against. +func backgroundCallerContext( + ctx context.Context, + registry namespace.Registry, + namespaceID string, +) (context.Context, string) { + name, err := registry.GetNamespaceName(namespace.ID(namespaceID)) + if err != nil { + return ctx, "" + } + return headers.SetCallerInfo( + ctx, headers.NewBackgroundLowCallerInfo(name.String())), name.String() +} + +// retentionTaskHandler deletes a stream once its retention has elapsed. Close +// only seals; deletion is deliberately later, so a consumer can still drain a +// finished stream without coordinating a shutdown with the producer. +type retentionTaskHandler struct { + chasm.SideEffectTaskHandlerBase[*streampb.StreamRetentionTask] + + shardController shard.Controller + namespaceRegistry namespace.Registry + logger log.Logger + config *stream.Config + notifier consumerNotifier +} + +func newRetentionTaskHandler( + shardController shard.Controller, + namespaceRegistry namespace.Registry, + logger log.Logger, + config *stream.Config, + routed streampb.StreamServiceClient, +) *retentionTaskHandler { + return &retentionTaskHandler{ + shardController: shardController, + namespaceRegistry: namespaceRegistry, + logger: logger, + config: config, + notifier: consumerNotifier{logger: logger, routed: routed}, + } +} + +func (h *retentionTaskHandler) Validate( + _ chasm.Context, + s *stream.Stream, + _ chasm.TaskInvocation, + _ *streampb.StreamRetentionTask, +) (bool, error) { + // A stream that was reopened, or never closed, has nothing to expire. The + // task is scheduled at close and only meaningful while that still holds. + return s.State.GetClosed(), nil +} + +// Execute deletes the stream unless a workflow still consumes it. +// +// A consumer's History depends on the ranges it consumed, and deleting them +// turns its next replay into a failure. So while any consumer is active the +// deletion waits and asks again later. A closed stream gets no appends, and +// appends are what otherwise make the stream discover that a consumer is +// gone, so the probe here is what lets a held stream ever be released. +func (h *retentionTaskHandler) Execute( + ctx context.Context, + ref chasm.ComponentRef, + _ chasm.TaskAttributes, + _ *streampb.StreamRetentionTask, +) error { + ctx, namespaceName := backgroundCallerContext(ctx, h.namespaceRegistry, ref.NamespaceID) + + state, err := chasm.ReadComponent(ctx, ref, (*stream.Stream).Snapshot, struct{}{}) + if err != nil { + return err + } + if err := h.notifier.notify(ctx, ref, namespaceName, state, true); err != nil { + return err + } + + held, _, err := chasm.UpdateComponent(ctx, ref, + func(s *stream.Stream, mctx chasm.MutableContext, _ struct{}) (bool, error) { + for _, consumer := range s.State.GetConsumers() { + if consumer.GetActive() { + mctx.AddTask(s, chasm.TaskAttributes{ + ScheduledTime: mctx.Now(s).Add(h.config.RetentionRecheckInterval()), + }, &streampb.StreamRetentionTask{}) + return true, nil + } + } + return false, nil + }, struct{}{}) + if err != nil || held { + return err + } + + // The payload is component state, so deleting the execution takes it too. + return chasm.DeleteExecution[*stream.Stream](ctx, ref.ExecutionKey, chasm.DeleteExecutionRequest{}) +} + +func (h *retentionTaskHandler) Discard( + _ context.Context, + _ chasm.ComponentRef, + _ chasm.TaskAttributes, + _ *streampb.StreamRetentionTask, +) error { + // Nothing to undo: the task carries no side effect until it executes. + return nil +} + +// notifyConsumersTaskHandler tells workflows in other executions that the +// stream moved. +// +// Appending never schedules a workflow task by itself, because a stream item is +// data an execution produced rather than a decision input to it. A workflow +// that subscribed is the exception, and it has no other way to find out: it +// cannot read another execution's frontier while closing its own transaction. +// So the frontier is pushed into its cursor, which dirties that execution and +// lets its own transaction close decide it owes a workflow task. +type notifyConsumersTaskHandler struct { + chasm.SideEffectTaskHandlerBase[*streampb.StreamNotifyConsumersTask] + + namespaceRegistry namespace.Registry + notifier consumerNotifier +} + +func newNotifyConsumersTaskHandler( + namespaceRegistry namespace.Registry, + logger log.Logger, + routed streampb.StreamServiceClient, +) *notifyConsumersTaskHandler { + return ¬ifyConsumersTaskHandler{ + namespaceRegistry: namespaceRegistry, + notifier: consumerNotifier{logger: logger, routed: routed}, + } +} + +// Validate accepts the task unconditionally. +// +// The scheduling append already decided there was someone to tell, and it +// raised a flag saying a task is outstanding so that later appends schedule +// none of their own. Turning the task down here would leave that flag up with +// nothing left to lower it, and no append would ever schedule another: a +// consumer that subscribed afterwards would wait forever. Execute lowers the +// flag first, so finding nothing to do there costs one state write and leaves +// the stream ready to schedule again. +func (h *notifyConsumersTaskHandler) Validate( + _ chasm.Context, + _ *stream.Stream, + _ chasm.TaskInvocation, + _ *streampb.StreamNotifyConsumersTask, +) (bool, error) { + return true, nil +} + +func (h *notifyConsumersTaskHandler) Execute( + ctx context.Context, + ref chasm.ComponentRef, + _ chasm.TaskAttributes, + _ *streampb.StreamNotifyConsumersTask, +) error { + ctx, namespaceName := backgroundCallerContext(ctx, h.namespaceRegistry, ref.NamespaceID) + + // A write rather than a read, because it also lowers the coalescing flag. + // Appends that commit after this transition schedule their own task. + state, _, err := chasm.UpdateComponent(ctx, ref, (*stream.Stream).TakeNotifySnapshot, struct{}{}) + if err != nil { + return err + } + return h.notifier.notify(ctx, ref, namespaceName, state, false) +} + +// Discard lowers the coalescing flag the scheduling append raised, for whatever +// reason the task is being dropped. Left up, the flag means no append ever +// schedules another notify task and a consumer subscribing later is never +// woken. A stream that is gone has no flag to lower. +func (h *notifyConsumersTaskHandler) Discard( + ctx context.Context, + ref chasm.ComponentRef, + _ chasm.TaskAttributes, + _ *streampb.StreamNotifyConsumersTask, +) error { + ctx, _ = backgroundCallerContext(ctx, h.namespaceRegistry, ref.NamespaceID) + _, _, err := chasm.UpdateComponent(ctx, ref, (*stream.Stream).TakeNotifySnapshot, struct{}{}) + if _, ok := errors.AsType[*serviceerror.NotFound](err); ok { + return nil + } + return err +} diff --git a/chasm/lib/stream/service/tasks_test.go b/chasm/lib/stream/service/tasks_test.go new file mode 100644 index 00000000000..0fb5085e5d2 --- /dev/null +++ b/chasm/lib/stream/service/tasks_test.go @@ -0,0 +1,34 @@ +package service + +import ( + "testing" + + "github.com/stretchr/testify/require" + "go.temporal.io/server/chasm" + "go.temporal.io/server/chasm/lib/stream" + streampb "go.temporal.io/server/chasm/lib/stream/gen/streampb/v1" +) + +// The append that scheduled this task also raised the coalescing flag, and +// nothing but the task's own run lowers it. Turning the task down for a stream +// whose consumers have gone quiet would leave the flag up for good, and no +// later append would schedule another task, so a consumer that subscribes +// afterwards would never be woken. +func TestNotifyTaskIsNotTurnedDownWhileTheCoalescingFlagIsUp(t *testing.T) { + h := ¬ifyConsumersTaskHandler{} + s := &stream.Stream{State: &streampb.StreamState{ + NotifyPending: true, + Consumers: map[string]*streampb.ConsumerCursor{}, + }} + + ok, err := h.Validate(nil, s, chasm.TaskInvocation{}, &streampb.StreamNotifyConsumersTask{}) + require.NoError(t, err) + require.True(t, ok, "the task has to run so that its own read lowers the flag") + + // An inactive entry is the same case: whoever the append meant to tell has + // gone, and the flag still has to come down. + s.State.Consumers["workflow:wf/run-1"] = &streampb.ConsumerCursor{External: true, Active: false} + ok, err = h.Validate(nil, s, chasm.TaskInvocation{}, &streampb.StreamNotifyConsumersTask{}) + require.NoError(t, err) + require.True(t, ok) +} diff --git a/chasm/lib/stream/start.go b/chasm/lib/stream/start.go new file mode 100644 index 00000000000..65bd54733b9 --- /dev/null +++ b/chasm/lib/stream/start.go @@ -0,0 +1,113 @@ +package stream + +import ( + "go.temporal.io/api/serviceerror" + streampb "go.temporal.io/api/stream/v1" + streamlib "go.temporal.io/server/chasm/lib/stream/gen/streampb/v1" +) + +// AtOffset starts at an absolute offset. +func AtOffset(offset int64) *streampb.StreamStartPosition { + return &streampb.StreamStartPosition{ + Position: &streampb.StreamStartPosition_Offset{Offset: offset}, + } +} + +// Earliest starts at the oldest record the stream still holds. +func Earliest() *streampb.StreamStartPosition { + return &streampb.StreamStartPosition{ + Position: &streampb.StreamStartPosition_Earliest{Earliest: true}, + } +} + +// Tail starts at the head, so only records appended afterwards are read. +func Tail() *streampb.StreamStartPosition { + return &streampb.StreamStartPosition{ + Position: &streampb.StreamStartPosition_Tail{Tail: true}, + } +} + +// LastN starts n records before the head, or at the floor when fewer are held. +func LastN(n int64) *streampb.StreamStartPosition { + return &streampb.StreamStartPosition{ + Position: &streampb.StreamStartPosition_LastN{LastN: n}, + } +} + +// RequestedStart reads where a caller asked to start from the two fields a +// request can say it in. Setting both is refused rather than ranked, because +// the two could disagree and neither is obviously the one the caller meant. +// +// A negative offset once meant the head. It is refused rather than +// reinterpreted, so a caller still sending it finds out instead of reading +// from somewhere it did not ask for. +func RequestedStart( + pos *streampb.StreamStartPosition, offsetField string, offset int64, +) (*streampb.StreamStartPosition, error) { + if pos != nil { + if offset != 0 { + return nil, serviceerror.NewInvalidArgumentf( + "set either start_position or %s, not both", offsetField) + } + return pos, CheckStart(pos) + } + if offset < 0 { + return nil, serviceerror.NewInvalidArgumentf( + "%s cannot be negative, got %d: ask for the head with start_position.tail", + offsetField, offset) + } + return AtOffset(offset), nil +} + +// CheckStart refuses a position that names nothing the server can resolve. +func CheckStart(pos *streampb.StreamStartPosition) error { + switch p := pos.GetPosition().(type) { + case *streampb.StreamStartPosition_Offset: + if p.Offset < 0 { + return serviceerror.NewInvalidArgumentf( + "start_position.offset cannot be negative, got %d", p.Offset) + } + case *streampb.StreamStartPosition_LastN: + if p.LastN <= 0 { + return serviceerror.NewInvalidArgumentf( + "start_position.last_n must be positive, got %d", p.LastN) + } + case *streampb.StreamStartPosition_Earliest: + if !p.Earliest { + return serviceerror.NewInvalidArgument("start_position.earliest must be true when set") + } + case *streampb.StreamStartPosition_Tail: + if !p.Tail { + return serviceerror.NewInvalidArgument("start_position.tail must be true when set") + } + default: + return serviceerror.NewInvalidArgument("start_position names no position") + } + return nil +} + +func (s *Stream) resolveStart(pos *streampb.StreamStartPosition) (int64, error) { + return ResolveStart(pos, s.State) +} + +// ResolveStart turns a position into the absolute offset it names against a +// stream's frontier. The caller checks the result against the floor and head +// in whatever terms its request uses. +func ResolveStart(pos *streampb.StreamStartPosition, state *streamlib.StreamState) (int64, error) { + if err := CheckStart(pos); err != nil { + return 0, err + } + base, head := state.GetBaseOffset(), state.GetHeadOffset() + switch p := pos.GetPosition().(type) { + case *streampb.StreamStartPosition_Offset: + return p.Offset, nil + case *streampb.StreamStartPosition_LastN: + // N counts records of every kind, because the server does not decode + // them to tell data from a producer's finish. + return max(base, head-p.LastN), nil + case *streampb.StreamStartPosition_Earliest: + return base, nil + default: + return head, nil + } +} diff --git a/chasm/lib/stream/start_test.go b/chasm/lib/stream/start_test.go new file mode 100644 index 00000000000..0eb888e4486 --- /dev/null +++ b/chasm/lib/stream/start_test.go @@ -0,0 +1,143 @@ +package stream + +import ( + "testing" + + "github.com/stretchr/testify/require" + commonpb "go.temporal.io/api/common/v1" + "go.temporal.io/api/serviceerror" + streampb "go.temporal.io/api/stream/v1" + "go.temporal.io/server/chasm" +) + +// A stream holding offsets 3 to 7: five records appended after three were +// truncated away. +func newTruncatedStream(t *testing.T) *Stream { + t.Helper() + s := newTestStream(t) + s.Batches = make(chasm.Map[int64, *commonpb.DataBlob]) + _, err := s.AddMessages(nil, AddMessagesRequest{Records: msgs("a", "b", "c")}) + require.NoError(t, err) + _, err = s.AddMessages(nil, AddMessagesRequest{Records: msgs("d", "e", "f", "g", "h")}) + require.NoError(t, err) + require.NoError(t, s.Truncate(nil, 3)) + return s +} + +func TestStartPositionsResolveAgainstTheFrontier(t *testing.T) { + s := newTruncatedStream(t) + for _, tc := range []struct { + name string + pos *streampb.StreamStartPosition + want int64 + }{ + {"offset", AtOffset(5), 5}, + {"earliest is the floor, not zero", Earliest(), 3}, + {"tail is the head", Tail(), 8}, + {"last n", LastN(2), 6}, + {"last n past the floor clamps to it", LastN(100), 3}, + } { + t.Run(tc.name, func(t *testing.T) { + got, err := s.resolveStart(tc.pos) + require.NoError(t, err) + require.Equal(t, tc.want, got) + }) + } +} + +func TestStartPositionsThatNameNothingAreRefused(t *testing.T) { + for _, tc := range []struct { + name string + pos *streampb.StreamStartPosition + }{ + {"no arm", &streampb.StreamStartPosition{}}, + {"negative offset", AtOffset(-1)}, + {"zero last n", LastN(0)}, + {"negative last n", LastN(-3)}, + {"earliest false", &streampb.StreamStartPosition{ + Position: &streampb.StreamStartPosition_Earliest{Earliest: false}, + }}, + {"tail false", &streampb.StreamStartPosition{ + Position: &streampb.StreamStartPosition_Tail{Tail: false}, + }}, + } { + t.Run(tc.name, func(t *testing.T) { + var invalid *serviceerror.InvalidArgument + require.ErrorAs(t, CheckStart(tc.pos), &invalid) + }) + } +} + +func TestRequestedStartReadsEitherField(t *testing.T) { + got, err := RequestedStart(nil, "start_offset", 4) + require.NoError(t, err) + require.Equal(t, int64(4), got.GetOffset()) + + got, err = RequestedStart(nil, "start_offset", 0) + require.NoError(t, err) + require.Equal(t, int64(0), got.GetOffset(), "an unset position and offset is offset zero") + + got, err = RequestedStart(Earliest(), "start_offset", 0) + require.NoError(t, err) + require.True(t, got.GetEarliest()) + + var invalid *serviceerror.InvalidArgument + _, err = RequestedStart(nil, "start_offset", -1) + require.ErrorAs(t, err, &invalid, "a negative offset is refused, not read as the head") + require.ErrorContains(t, err, "start_position.tail") + + _, err = RequestedStart(Earliest(), "start_offset", 2) + require.ErrorAs(t, err, &invalid, "both fields set could disagree") + require.ErrorContains(t, err, "start_offset") +} + +func TestRegisterConsumerRecordsTheResolvedStart(t *testing.T) { + s := newTruncatedStream(t) + + start, err := s.RegisterConsumer(nil, ConsumerRegistration{ + ConsumerID: "workflow:a", WorkflowID: "wf-1", RunID: "run-1", Start: Earliest(), + }) + require.NoError(t, err) + require.Equal(t, int64(3), start) + require.Equal(t, int64(3), s.State.Consumers["workflow:a"].GetReplayFloor()) + + start, err = s.RegisterConsumer(nil, ConsumerRegistration{ + ConsumerID: "workflow:b", WorkflowID: "wf-1", RunID: "run-1", Start: LastN(1), + }) + require.NoError(t, err) + require.Equal(t, int64(7), start) + + // A consumer already registered keeps its position whatever it asks for. + start, err = s.RegisterConsumer(nil, ConsumerRegistration{ + ConsumerID: "workflow:a", WorkflowID: "wf-1", RunID: "run-1", Start: Tail(), + }) + require.NoError(t, err) + require.Equal(t, int64(3), start) + + _, err = s.RegisterConsumer(nil, ConsumerRegistration{ + ConsumerID: "workflow:c", WorkflowID: "wf-1", RunID: "run-1", Start: AtOffset(0), + }) + var failed *serviceerror.FailedPrecondition + require.ErrorAs(t, err, &failed, "an absolute offset below the floor is still refused") +} + +func TestReadWindowResolvesAStartPositionInTheSameRead(t *testing.T) { + s := newTruncatedStream(t) + ctx := &chasm.MockContext{ + HandleExecutionKey: func() chasm.ExecutionKey { return chasm.ExecutionKey{RunID: "run-1"} }, + } + + w, err := s.ReadWindow(ctx, WindowRequest{Start: Earliest()}) + require.NoError(t, err) + require.Equal(t, int64(3), w.From, "earliest reads from the floor of a truncated stream") + require.Equal(t, int64(8), w.To) + + w, err = s.ReadWindow(ctx, WindowRequest{Start: LastN(2)}) + require.NoError(t, err) + require.Equal(t, int64(6), w.From) + + w, err = s.ReadWindow(ctx, WindowRequest{Start: Tail()}) + require.NoError(t, err) + require.Equal(t, int64(8), w.From) + require.Empty(t, w.Blobs) +} diff --git a/chasm/lib/stream/stream.go b/chasm/lib/stream/stream.go index 0f2eb579259..69d1858d767 100644 --- a/chasm/lib/stream/stream.go +++ b/chasm/lib/stream/stream.go @@ -75,6 +75,11 @@ type AddMessagesRequest struct { // The namespace's limits, resolved by the caller. A zero value means the // defaults. Limits Limits + + // What the other streams of the same owner already hold, so the per-owner + // aggregate can be checked here alongside this stream's own budget. Zero + // for a standalone stream, which has no siblings. + SiblingBytes int64 } type AddMessagesResult struct { @@ -196,7 +201,8 @@ func (s *Stream) AddMessages( if err := s.checkProducerRoom(req.ProducerID, limits.MaxProducersPerStream); err != nil { return AddMessagesResult{}, err } - if err := s.checkBudget(int64(len(req.Records)), int64(len(blob.Data))); err != nil { + if err := s.checkBudget( + limits, req.SiblingBytes, int64(len(req.Records)), int64(len(blob.Data))); err != nil { return AddMessagesResult{}, err } @@ -442,11 +448,22 @@ func (s *Stream) held() int64 { // mutable state of the execution that owns the stream, and the alternative to // refusing here is the execution size limit terminating that workflow later, // with nothing naming the stream as the cause. -func (s *Stream) checkBudget(count int64, size int64) error { +func (s *Stream) checkBudget(limits Limits, siblingBytes, count, size int64) error { budget := s.State.GetBudget() if budget == nil { return nil } + // The per-stream budget bounds one stream, and one execution can own many. + // Multiplied out they come to far more than the execution size limit, so + // the aggregate is what keeps that limit from terminating the workflow. + if limit := int64(limits.OwnedStreamsMaxBytesPerWorkflow); limit > 0 && + siblingBytes+s.State.AppendedBytes+size > limit { + return serviceerror.NewResourceExhaustedf( + enumspb.RESOURCE_EXHAUSTED_CAUSE_PERSISTENCE_STORAGE_LIMIT, + "the workflow's streams hold %d of their shared budget of %d bytes; the append "+ + "of %d does not fit", + siblingBytes+s.State.AppendedBytes, limit, size) + } if limit := budget.GetMaxItems(); limit > 0 && s.held()+count > limit { return serviceerror.NewResourceExhaustedf( enumspb.RESOURCE_EXHAUSTED_CAUSE_PERSISTENCE_STORAGE_LIMIT, @@ -641,7 +658,10 @@ func (s *Stream) batchStarts() []int64 { // WindowRequest asks for whatever a reader can be given from an offset. type WindowRequest struct { - From int64 + From int64 + // When set, where the read begins instead of From, resolved against the + // same view the read is served from so it cannot race with truncation. + Start *streampb.StreamStartPosition MaxMessages int32 Topics []string } @@ -652,8 +672,11 @@ type Window struct { State *streamlib.StreamState Blobs []*commonpb.DataBlob Starts []int64 - To int64 - Limit int + // Where the read began, which is the request's From unless it named a + // start position. + From int64 + To int64 + Limit int // The execution holding the stream, so a slice built from this window can // say which run it came from. RunID string @@ -663,6 +686,13 @@ type Window struct { // come from one view. Read separately they can disagree, because the frontier // moves while the bytes are being fetched. func (s *Stream) ReadWindow(ctx chasm.Context, req WindowRequest) (Window, error) { + if req.Start != nil { + from, err := s.resolveStart(req.Start) + if err != nil { + return Window{}, err + } + req.From = from + } if req.From < s.State.BaseOffset { return Window{}, Refusal(ReasonCursorBelowFloor, "offset %d has been truncated, the stream starts at %d", req.From, s.State.BaseOffset) @@ -680,6 +710,7 @@ func (s *Stream) ReadWindow(ctx chasm.Context, req WindowRequest) (Window, error } w := Window{ State: common.CloneProto(s.State), + From: req.From, To: req.From, Limit: limit, RunID: ctx.ExecutionKey().RunID, @@ -824,8 +855,9 @@ type ConsumerRegistration struct { ConsumerID string WorkflowID string RunID string - // Negative means the head as of the registering transition. - Offset int64 + // Where a new consumer starts, resolved against the registering + // transition's frontier. One already registered keeps its position. + Start *streampb.StreamStartPosition External bool // Zero means the default. MaxConsumers int @@ -836,9 +868,9 @@ type ConsumerRegistration struct { // consumer reads from: the resolved start for a new consumer, and the current // read position for one that is already registered. // -// A negative offset means the head as of this transition. Resolving it here, -// against the frontier the same transaction sees, is what makes the recorded -// start a fact rather than a reading taken a moment earlier. +// Resolving the start here, against the frontier the same transaction sees, is +// what makes the recorded start a fact rather than a reading taken a moment +// earlier. // // The floor it records is where the subscription started, not where it has read // to. The ranges this consumer already took are written into its History, and a @@ -852,9 +884,9 @@ func (s *Stream) RegisterConsumer(_ chasm.MutableContext, reg ConsumerRegistrati if maxConsumers <= 0 { maxConsumers = MaxConsumersPerStream } - offset := reg.Offset - if offset < 0 { - offset = s.State.HeadOffset + offset, err := s.resolveStart(reg.Start) + if err != nil { + return 0, err } if offset < s.State.BaseOffset { return 0, Refusal(ReasonCursorBelowFloor, diff --git a/chasm/lib/stream/stream_test.go b/chasm/lib/stream/stream_test.go index 316eceeadd4..9d9170563d8 100644 --- a/chasm/lib/stream/stream_test.go +++ b/chasm/lib/stream/stream_test.go @@ -327,7 +327,7 @@ func TestTruncateStopsAtAnActiveConsumersReplayFloor(t *testing.T) { require.NoError(t, err) _, err = s.RegisterConsumer(nil, ConsumerRegistration{ - ConsumerID: "wf-1", WorkflowID: "wf-1", RunID: "run-1", Offset: 0, + ConsumerID: "wf-1", WorkflowID: "wf-1", RunID: "run-1", Start: AtOffset(0), }) require.NoError(t, err) s.AdvanceConsumer(nil, "wf-1", 2) @@ -374,7 +374,7 @@ func TestCapRefusesAnAppendItCouldOnlyAbsorbByDroppingReadRecords(t *testing.T) s := newTestStream(t) s.State.Lifecycle = &streamlib.StreamLifecycle{MaxItems: 2} _, err := s.RegisterConsumer(nil, ConsumerRegistration{ - ConsumerID: "wf-1", WorkflowID: "wf-1", RunID: "run-1", Offset: 0, + ConsumerID: "wf-1", WorkflowID: "wf-1", RunID: "run-1", Start: AtOffset(0), }) require.NoError(t, err) @@ -416,7 +416,7 @@ func TestRegisterConsumerPinsFromWhereItSubscribed(t *testing.T) { // Subscribing at 2 says nothing about offsets 0 and 1, so those stay // droppable and everything from 2 up does not. _, err = s.RegisterConsumer(nil, ConsumerRegistration{ - ConsumerID: "workflow:output", WorkflowID: "wf-1", RunID: "run-1", Offset: 2, + ConsumerID: "workflow:output", WorkflowID: "wf-1", RunID: "run-1", Start: AtOffset(2), }) require.NoError(t, err) @@ -430,7 +430,7 @@ func TestAdvanceConsumerTracksWhereAConsumerHasReached(t *testing.T) { _, err := s.AddMessages(nil, AddMessagesRequest{Records: msgs("a", "b", "c", "d")}) require.NoError(t, err) _, err = s.RegisterConsumer(nil, ConsumerRegistration{ - ConsumerID: "workflow:output", WorkflowID: "wf-1", RunID: "run-1", Offset: 0, + ConsumerID: "workflow:output", WorkflowID: "wf-1", RunID: "run-1", Start: AtOffset(0), }) require.NoError(t, err) @@ -447,7 +447,7 @@ func TestAdvanceConsumerNeverRewinds(t *testing.T) { _, err := s.AddMessages(nil, AddMessagesRequest{Records: msgs("a", "b", "c", "d")}) require.NoError(t, err) _, err = s.RegisterConsumer(nil, ConsumerRegistration{ - ConsumerID: "workflow:output", WorkflowID: "wf-1", RunID: "run-1", Offset: 0, + ConsumerID: "workflow:output", WorkflowID: "wf-1", RunID: "run-1", Start: AtOffset(0), }) require.NoError(t, err) @@ -465,7 +465,7 @@ func TestRegisterConsumerRejectsAnOffsetBelowTheFloor(t *testing.T) { require.NoError(t, err) _, err = s.RegisterConsumer(nil, ConsumerRegistration{ - ConsumerID: "workflow:output", WorkflowID: "wf-1", RunID: "run-1", Offset: 1, + ConsumerID: "workflow:output", WorkflowID: "wf-1", RunID: "run-1", Start: AtOffset(1), }) require.ErrorContains(t, err, "below the stream's floor") require.Equal(t, ReasonCursorBelowFloor, ReasonOf(err.Error())) @@ -478,13 +478,13 @@ func TestRegisterConsumerTwiceKeepsThePin(t *testing.T) { _, err := s.AddMessages(nil, AddMessagesRequest{Records: msgs("a", "b", "c", "d")}) require.NoError(t, err) _, err = s.RegisterConsumer(nil, ConsumerRegistration{ - ConsumerID: "workflow:output", WorkflowID: "wf-1", RunID: "run-1", Offset: 0, + ConsumerID: "workflow:output", WorkflowID: "wf-1", RunID: "run-1", Start: AtOffset(0), }) require.NoError(t, err) s.AdvanceConsumer(nil, "workflow:output", 3) _, err = s.RegisterConsumer(nil, ConsumerRegistration{ - ConsumerID: "workflow:output", WorkflowID: "wf-1", RunID: "run-1", Offset: 0, + ConsumerID: "workflow:output", WorkflowID: "wf-1", RunID: "run-1", Start: AtOffset(0), }) require.NoError(t, err) @@ -496,7 +496,7 @@ func TestDeregisterConsumerReleasesThePin(t *testing.T) { _, err := s.AddMessages(nil, AddMessagesRequest{Records: msgs("a", "b", "c", "d")}) require.NoError(t, err) _, err = s.RegisterConsumer(nil, ConsumerRegistration{ - ConsumerID: "workflow:output", WorkflowID: "wf-1", RunID: "run-1", Offset: 1, + ConsumerID: "workflow:output", WorkflowID: "wf-1", RunID: "run-1", Start: AtOffset(1), }) require.NoError(t, err) @@ -531,7 +531,7 @@ func TestCapClampsToAConsumerThatRegisteredLate(t *testing.T) { _, err := s.AddMessages(nil, AddMessagesRequest{Records: msgs("a", "b")}) require.NoError(t, err) _, err = s.RegisterConsumer(nil, ConsumerRegistration{ - ConsumerID: "workflow:output", WorkflowID: "wf-1", RunID: "run-1", Offset: 0, + ConsumerID: "workflow:output", WorkflowID: "wf-1", RunID: "run-1", Start: AtOffset(0), }) require.NoError(t, err) s.State.Lifecycle = &streamlib.StreamLifecycle{MaxItems: 1} @@ -546,7 +546,7 @@ func TestAConsumerThatDeregisteredHoldsNothing(t *testing.T) { s := newTestStream(t) s.State.Lifecycle = &streamlib.StreamLifecycle{MaxItems: 2} _, err := s.RegisterConsumer(nil, ConsumerRegistration{ - ConsumerID: "workflow:output", WorkflowID: "wf-1", RunID: "run-1", Offset: 0, + ConsumerID: "workflow:output", WorkflowID: "wf-1", RunID: "run-1", Start: AtOffset(0), }) require.NoError(t, err) s.DeregisterConsumer(nil, "workflow:output") @@ -566,7 +566,7 @@ func TestReregisteringBelowTheFloorIsRefused(t *testing.T) { _, err := s.AddMessages(nil, AddMessagesRequest{Records: msgs("a", "b", "c", "d")}) require.NoError(t, err) _, err = s.RegisterConsumer(nil, ConsumerRegistration{ - ConsumerID: "workflow:output", WorkflowID: "wf-1", RunID: "run-1", Offset: 0, + ConsumerID: "workflow:output", WorkflowID: "wf-1", RunID: "run-1", Start: AtOffset(0), }) require.NoError(t, err) s.DeregisterConsumer(nil, "workflow:output") @@ -575,7 +575,7 @@ func TestReregisteringBelowTheFloorIsRefused(t *testing.T) { // Resubscribing further along does not repair the gap. What this consumer // already recorded starts at 0, and offsets 0 and 1 are gone. _, err = s.RegisterConsumer(nil, ConsumerRegistration{ - ConsumerID: "workflow:output", WorkflowID: "wf-1", RunID: "run-1", Offset: 3, + ConsumerID: "workflow:output", WorkflowID: "wf-1", RunID: "run-1", Start: AtOffset(3), }) require.ErrorContains(t, err, "the stream now starts at 2") } @@ -626,20 +626,21 @@ func TestStreamConsumerTableIsBounded(t *testing.T) { for i := range MaxConsumersPerStream { _, err := s.RegisterConsumer(nil, ConsumerRegistration{ - ConsumerID: fmt.Sprintf("c%d", i), WorkflowID: "wf", RunID: "run", External: true, + ConsumerID: fmt.Sprintf("c%d", i), WorkflowID: "wf", RunID: "run", Start: AtOffset(0), + External: true, }) require.NoError(t, err) } _, err := s.RegisterConsumer(nil, ConsumerRegistration{ - ConsumerID: "c-over", WorkflowID: "wf", RunID: "run", Offset: 0, External: true, + ConsumerID: "c-over", WorkflowID: "wf", RunID: "run", Start: AtOffset(0), External: true, }) var invalid *serviceerror.InvalidArgument require.ErrorAs(t, err, &invalid) // Re-registering an existing consumer is an update, not a new entry. _, err = s.RegisterConsumer(nil, ConsumerRegistration{ - ConsumerID: "c0", WorkflowID: "wf", RunID: "run", Offset: 0, External: true, + ConsumerID: "c0", WorkflowID: "wf", RunID: "run", Start: AtOffset(0), External: true, }) require.NoError(t, err) } @@ -713,12 +714,14 @@ func TestRegisterConsumerReplacesAnEntryFromAnotherRun(t *testing.T) { _, err := s.AddMessages(nil, AddMessagesRequest{Records: msgs("a", "b", "c", "d")}) require.NoError(t, err) _, err = s.RegisterConsumer(nil, ConsumerRegistration{ - ConsumerID: "workflow:wf/run-1", WorkflowID: "wf", RunID: "run-1", Offset: 0, External: true, + ConsumerID: "workflow:wf/run-1", WorkflowID: "wf", RunID: "run-1", Start: AtOffset(0), + External: true, }) require.NoError(t, err) start, err := s.RegisterConsumer(nil, ConsumerRegistration{ - ConsumerID: "workflow:wf/run-2", WorkflowID: "wf", RunID: "run-2", Offset: 3, External: true, + ConsumerID: "workflow:wf/run-2", WorkflowID: "wf", RunID: "run-2", Start: AtOffset(3), + External: true, }) require.NoError(t, err) require.Equal(t, int64(3), start) @@ -729,7 +732,7 @@ func TestRegisterConsumerReplacesAnEntryFromAnotherRun(t *testing.T) { // A different workflow id is not the same consumer and keeps its pin. _, err = s.RegisterConsumer(nil, ConsumerRegistration{ ConsumerID: "workflow:other/run-9", WorkflowID: "other", RunID: "run-9", - Offset: 3, External: true, + Start: AtOffset(3), External: true, }) require.NoError(t, err) require.Len(t, s.State.Consumers, 2) @@ -745,7 +748,8 @@ func TestNotifyCoalescesIntoOneOutstandingTask(t *testing.T) { s := newTestStream(t) s.Batches = make(chasm.Map[int64, *commonpb.DataBlob]) _, err := s.RegisterConsumer(mctx, ConsumerRegistration{ - ConsumerID: "workflow:wf/run-1", WorkflowID: "wf", RunID: "run-1", Offset: 0, External: true, + ConsumerID: "workflow:wf/run-1", WorkflowID: "wf", RunID: "run-1", Start: AtOffset(0), + External: true, }) require.NoError(t, err) diff --git a/chasm/lib/workflow/fx.go b/chasm/lib/workflow/fx.go index 52730794faa..676ee077e80 100644 --- a/chasm/lib/workflow/fx.go +++ b/chasm/lib/workflow/fx.go @@ -4,6 +4,7 @@ import ( "go.temporal.io/server/api/historyservice/v1" "go.temporal.io/server/chasm" "go.temporal.io/server/chasm/lib/nexusoperation" + "go.temporal.io/server/chasm/lib/stream" "go.uber.org/fx" ) @@ -12,6 +13,9 @@ var Module = fx.Module( fx.Provide(NewConfig), fx.Provide(NewRegistry), fx.Provide(newLibrary), + // Provided here rather than by the stream service module, because the + // command handlers need it in every service that runs this library. + fx.Provide(stream.NewConfig), fx.Invoke(func( chasmRegistry *chasm.Registry, library *library, diff --git a/chasm/lib/workflow/stream_admission_test.go b/chasm/lib/workflow/stream_admission_test.go new file mode 100644 index 00000000000..9a5c19209a1 --- /dev/null +++ b/chasm/lib/workflow/stream_admission_test.go @@ -0,0 +1,149 @@ +package workflow + +import ( + "testing" + + "github.com/stretchr/testify/require" + commonpb "go.temporal.io/api/common/v1" + "go.temporal.io/api/serviceerror" + streampb "go.temporal.io/api/stream/v1" + "go.temporal.io/server/chasm" + "go.temporal.io/server/chasm/lib/stream" + streamlib "go.temporal.io/server/chasm/lib/stream/gen/streampb/v1" +) + +func newStreamBudgetTestContext() chasm.MutableContext { + return &chasm.MockMutableContext{ + MockContext: chasm.MockContext{ + HandleExecutionKey: func() chasm.ExecutionKey { + return chasm.ExecutionKey{ + NamespaceID: "ns-1", + BusinessID: "wf-1", + RunID: "run-1", + } + }, + }, + } +} + +func budgetTestRecords(bytes int) []*streamlib.StreamRecord { + return []*streamlib.StreamRecord{{ + Body: &commonpb.Payload{Data: make([]byte, bytes)}, + Kind: streampb.STREAM_RECORD_KIND_DATA, + }} +} + +// The per-stream budget bounds one stream, and an outside writer can name as +// many as the stream count allows. Multiplied out they come to far more than +// the execution size limit, which terminates the workflow rather than refusing +// an append, so the sum is what the append is measured against. +func TestOwnedStreamsShareOneByteBudget(t *testing.T) { + ctx := newStreamBudgetTestContext() + w := &Workflow{} + limits := stream.Limits{ + MaxOwnedStreamsPerWorkflow: 10, + OwnedStreamMaxItems: 100, + OwnedStreamMaxBytes: 4096, + OwnedStreamsMaxBytesPerWorkflow: 4096, + } + + // Three streams of a thousand bytes each sit inside every per-stream + // budget and inside the shared one. + for _, name := range []string{"a", "b", "c"} { + _, err := w.AppendToOwnedStream(ctx, name, stream.AddMessagesRequest{ + Records: budgetTestRecords(1000), + Limits: limits, + }) + require.NoError(t, err, "stream %q", name) + } + + // The fourth still fits its own budget and no longer fits the shared one. + _, err := w.AppendToOwnedStream(ctx, "d", stream.AddMessagesRequest{ + Records: budgetTestRecords(1500), + Limits: limits, + }) + var exhausted *serviceerror.ResourceExhausted + require.ErrorAs(t, err, &exhausted) + + // The refusal is about the shared budget, so a small append still lands. + _, err = w.AppendToOwnedStream(ctx, "d", stream.AddMessagesRequest{ + Records: budgetTestRecords(100), + Limits: limits, + }) + require.NoError(t, err) +} + +// A stream the workflow owns is keyed by its name and one in another execution +// by its id, in one map. Where the two collide the subscription is refused: +// handing back the owned cursor reports a subscription that never delivers, +// and the pin taken on the standalone stream would be held by nothing. +func TestSubscriptionsOfBothOriginsCannotShareOneKey(t *testing.T) { + ctx := newStreamBudgetTestContext() + limits := stream.Limits{MaxOwnedStreamsPerWorkflow: 10, OwnedStreamMaxItems: 100} + + owned := &Workflow{} + _, err := owned.SubscribeToOwnedStream(ctx, "x", stream.AtOffset(0), limits) + require.NoError(t, err) + _, err = owned.SubscribeToExternalStream(ctx, ExternalStreamSubscription{ + StreamID: "x", StartOffset: 7, KnownHead: 9, + }) + var refused *serviceerror.FailedPrecondition + require.ErrorAs(t, err, &refused) + + external := &Workflow{} + _, err = external.SubscribeToExternalStream(ctx, ExternalStreamSubscription{ + StreamID: "x", StartOffset: 7, KnownHead: 9, + }) + require.NoError(t, err) + _, err = external.SubscribeToOwnedStream(ctx, "x", stream.AtOffset(0), limits) + require.ErrorAs(t, err, &refused) +} + +// A push carries the frontier of a stream in another execution. A cursor of +// the same key on a stream the workflow owns is a different stream that +// happens to share the name, and moving its frontier would answer for data it +// is not reading. +func TestKnownHeadIsOnlyPushedIntoAnExternalCursor(t *testing.T) { + ctx := newStreamBudgetTestContext() + w := &Workflow{} + _, err := w.SubscribeToOwnedStream( + ctx, "x", stream.AtOffset(0), + stream.Limits{MaxOwnedStreamsPerWorkflow: 10, OwnedStreamMaxItems: 100}) + require.NoError(t, err) + + err = w.AdvanceKnownHead(ctx, "x", 42) + var notFound *serviceerror.NotFound + require.ErrorAs(t, err, ¬Found) +} + +// 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) { + require.NotEqual(t, ActivityStreamKey("a/b", "c"), ActivityStreamKey("a", "b/c")) + require.True(t, IsActivityStreamKey(ActivityStreamKey("act", DefaultStreamName))) + require.False(t, IsActivityStreamKey(DefaultStreamName)) +} + +// The workflow may not name the reserved part of its own map, or it could +// write into, or subscribe to, a stream one of its activities owns. +func TestWorkflowCannotNameAnActivityStream(t *testing.T) { + ctx := newStreamBudgetTestContext() + w := &Workflow{} + limits := stream.Limits{MaxOwnedStreamsPerWorkflow: 10} + + key := ActivityStreamKey("act", DefaultStreamName) + _, err := w.streamNamed(ctx, key, limits) + var invalid *serviceerror.InvalidArgument + require.ErrorAs(t, err, &invalid) + + // The activity addressing reaches it, and it counts against the workflow's + // stream count and shared byte budget because it lives in the same state. + _, err = w.AppendToOwnedStream(ctx, key, stream.AddMessagesRequest{ + Records: budgetTestRecords(10), Limits: limits, + }) + require.NoError(t, err) + require.NotNil(t, w.OwnedStream(ctx, key)) + require.Positive(t, w.siblingStreamBytes(ctx, DefaultStreamName)+ + w.OwnedStream(ctx, key).State.GetAppendedBytes()) + require.Len(t, w.Streams, 1) +} diff --git a/chasm/lib/workflow/stream_commands.go b/chasm/lib/workflow/stream_commands.go new file mode 100644 index 00000000000..6d78a1b73cc --- /dev/null +++ b/chasm/lib/workflow/stream_commands.go @@ -0,0 +1,69 @@ +package workflow + +import ( + "net/url" + "strings" + + "go.temporal.io/api/serviceerror" + "go.temporal.io/server/chasm" + "go.temporal.io/server/chasm/lib/stream" +) + +// DefaultStreamName is the stream a command addresses when it names none. +const DefaultStreamName = stream.DefaultStreamName + +// activityStreamPrefix reserves the part of the workflow's stream map that +// holds streams owned by its activities. An activity scheduled by a workflow +// is not a component of its own, so its streams live in the workflow's map +// until it is, and nothing outside the server sees these keys. +const activityStreamPrefix = "activity/" + +// ActivityStreamKey is the key a stream owned by one of this workflow's +// activities is held under. +// +// The activity id is escaped because it may itself contain a slash, and +// without that ("a/b", "c") and ("a", "b/c") would share a key. +func ActivityStreamKey(activityID, name string) string { + return activityStreamPrefix + url.PathEscape(activityID) + "/" + name +} + +// IsActivityStreamKey reports whether a name falls in the reserved part of the +// map, which only the activity addressing may reach. +func IsActivityStreamKey(name string) bool { + return strings.HasPrefix(name, activityStreamPrefix) +} + +// CheckWorkflowStreamName refuses a name the workflow itself may not use, +// because it is too long or because it would reach an activity's stream. +func CheckWorkflowStreamName(name string) error { + if IsActivityStreamKey(name) { + return serviceerror.NewInvalidArgumentf( + "stream names starting with %q are reserved for streams activities own", + activityStreamPrefix) + } + return stream.CheckStreamName(name) +} + +func (w *Workflow) ownedStreams() stream.OwnedStreams { + return stream.OwnedStreams{Streams: &w.Streams, Kind: "workflow"} +} + +// 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. +func (w *Workflow) streamNamed( + ctx chasm.MutableContext, + name string, + limits stream.Limits, +) (*stream.Stream, error) { + if err := CheckWorkflowStreamName(name); err != nil { + return nil, err + } + return w.ownedStreams().Named(ctx, name, limits) +} + +// siblingStreamBytes is what every other stream this workflow owns holds, +// streams its activities own included, since they live in the same state. +func (w *Workflow) siblingStreamBytes(ctx chasm.Context, name string) int64 { + return w.ownedStreams().SiblingBytes(ctx, name) +} diff --git a/chasm/lib/workflow/workflow.go b/chasm/lib/workflow/workflow.go index a633f0c6017..747148698a0 100644 --- a/chasm/lib/workflow/workflow.go +++ b/chasm/lib/workflow/workflow.go @@ -7,10 +7,12 @@ import ( failurepb "go.temporal.io/api/failure/v1" historypb "go.temporal.io/api/history/v1" "go.temporal.io/api/serviceerror" + streampb "go.temporal.io/api/stream/v1" "go.temporal.io/server/chasm" "go.temporal.io/server/chasm/lib/callback" callbackspb "go.temporal.io/server/chasm/lib/callback/gen/callbackpb/v1" "go.temporal.io/server/chasm/lib/nexusoperation" + "go.temporal.io/server/chasm/lib/stream" chasmworkflowpb "go.temporal.io/server/chasm/lib/workflow/gen/workflowpb/v1" "go.temporal.io/server/service/history/historybuilder" "google.golang.org/protobuf/types/known/emptypb" @@ -39,6 +41,153 @@ type Workflow struct { // Updates indexed by update ID, used to store the update components. Updates chasm.Map[string, *WorkflowUpdate] + + // Streams the workflow owns, keyed by stream name. Co-located with the + // workflow so publishing rides its commit rather than crossing executions. + Streams chasm.Map[string, *stream.Stream] + + // Positions in streams the workflow consumes, keyed by stream name. Held + // here rather than on the stream so that folding in a delivered range + // commits with the event that records it. + // + // One keyspace holds both origins: a stream this workflow owns is keyed by + // its name, and one in another execution by its id. A subscription that + // would collide with the other origin under the same key is refused rather + // than silently handed the wrong cursor. + StreamCursors chasm.Map[string, *stream.Cursor] +} + +// streamConsumerID names this workflow's pin on a stream it owns. An attached +// stream has exactly one consumer, but the stream's map is keyed by consumer, +// so the entry still needs a stable name. +func streamConsumerID(streamName string) string { + return "workflow:" + streamName +} + +// SubscribeToOwnedStream registers this workflow as a consumer of a stream it +// owns, returning the offset the subscription actually starts from. +// +// The start position is resolved here and the offset stored, so the first +// recorded range begins at a fact rather than at a reading that would land +// somewhere else on replay. +func (w *Workflow) SubscribeToOwnedStream( + mctx chasm.MutableContext, + name string, + start *streampb.StreamStartPosition, + limits stream.Limits, +) (int64, error) { + // Created here as well as on first write, so a workflow can start reading a + // topic before anything has been published to it. An outside producer that + // arrives later appends to the same stream. + owned, err := w.streamNamed(mctx, name, limits) + if err != nil { + return 0, err + } + + if w.StreamCursors == nil { + w.StreamCursors = make(chasm.Map[string, *stream.Cursor]) + } + if existing, ok := w.StreamCursors[name]; ok { + cursor := existing.Get(mctx) + if cursor.IsExternal() { + return 0, serviceerror.NewFailedPreconditionf( + "this workflow already consumes a stream with id %q in another execution, so "+ + "it cannot also consume a stream of its own by that name", name) + } + // Resubscribing must not rewind a cursor: ranges below it are already + // recorded in History, and moving back would replay them as new. + return cursor.Offset(), nil + } + + // Pin the stream's floor in the same transaction as the cursor. Registered + // separately it could be lost while the cursor survived, and truncation + // would then be free to take a range the cursor still points at. + key := mctx.ExecutionKey() + startOffset, err := owned.RegisterConsumer(mctx, stream.ConsumerRegistration{ + ConsumerID: streamConsumerID(name), + WorkflowID: key.BusinessID, + RunID: key.RunID, + Start: start, + MaxConsumers: limits.MaxConsumersPerStream, + }) + if err != nil { + return 0, err + } + + cursor, err := stream.NewCursor(mctx, stream.NewCursorRequest{ + StreamID: name, + StartOffset: startOffset, + }) + if err != nil { + return 0, err + } + w.StreamCursors[name] = chasm.NewComponentField(mctx, cursor) + return startOffset, nil +} + +// ExternalStreamSubscription describes a stream in another execution, with the +// start offset already resolved and the frontier as it was when the consumer +// was registered there. +type ExternalStreamSubscription struct { + StreamID string + StartOffset int64 + KnownHead int64 +} + +// SubscribeToExternalStream registers this workflow as a consumer of a stream +// it does not own, returning the offset the subscription starts from. +func (w *Workflow) SubscribeToExternalStream( + mctx chasm.MutableContext, + req ExternalStreamSubscription, +) (int64, error) { + if w.StreamCursors == nil { + w.StreamCursors = make(chasm.Map[string, *stream.Cursor]) + } + if existing, ok := w.StreamCursors[req.StreamID]; ok { + cursor := existing.Get(mctx) + // Refused rather than answered with the owned cursor. Handing that one + // back reports a subscription that never delivers, and the pin taken on + // the standalone stream is then held by nothing. + if !cursor.IsExternal() { + return 0, serviceerror.NewFailedPreconditionf( + "this workflow already owns a stream named %q, so it cannot also consume a "+ + "stream with that id in another execution", req.StreamID) + } + // Resubscribing must not rewind: ranges below the cursor are already + // recorded in History, and moving back would replay them as new. + return cursor.Offset(), nil + } + + cursor, err := stream.NewCursor(mctx, stream.NewCursorRequest{ + StreamID: req.StreamID, + External: true, + StartOffset: req.StartOffset, + }) + if err != nil { + return 0, err + } + cursor.AdvanceKnownHead(mctx, req.KnownHead) + w.StreamCursors[req.StreamID] = chasm.NewComponentField(mctx, cursor) + return req.StartOffset, nil +} + +// AdvanceKnownHead records how far a stream in another execution has moved. +// +// A workflow cannot read that frontier itself while closing its own +// transaction, so the stream pushes it here. Writing it dirties this execution, +// and the transaction close then sees the cursor is behind and schedules a +// workflow task, which is the same path an owned stream takes. +func (w *Workflow) AdvanceKnownHead(mctx chasm.MutableContext, streamID string, head int64) error { + field, ok := w.StreamCursors[streamID] + // Only an external cursor takes a push. A cursor of the same key on a + // stream this workflow owns belongs to a different stream that happens to + // share the name, and moving its frontier would answer for data it is not + // reading. + if !ok || !field.Get(mctx).IsExternal() { + return serviceerror.NewNotFoundf("workflow does not consume stream %q", streamID) + } + field.Get(mctx).AdvanceKnownHead(mctx, head) + return nil } func NewWorkflow( @@ -309,3 +458,40 @@ func (w *Workflow) HasAnyBufferedEvent(filter historybuilder.BufferedEventFilter func (w *Workflow) WorkflowTypeName() string { return w.GetWorkflowTypeName() } + +var _ stream.Owner = (*Workflow)(nil) + +// OwnedStream returns the attached stream under key, or nil when nothing has +// created it yet. Reads that need the payload and not just the frontier go +// through here, so both come from one view of the component. +func (w *Workflow) OwnedStream( + ctx chasm.Context, + key string, +) *stream.Stream { + return w.ownedStreams().Get(ctx, key) +} + +// OwnedStreamEnded reports that the workflow is closed. A closed execution can +// take no more publishes, from its own Workflow Task or from anywhere else, so +// every stream it holds is finished whether or not a producer said so. +func (w *Workflow) OwnedStreamEnded(ctx chasm.Context, _ string) bool { + return !ctx.ExecutionInfo().CloseTime.IsZero() +} + +// AppendToOwnedStream appends under key on behalf of a writer outside the +// execution. +// +// The workflow's own publishes go through the command handler, which advances +// the frontier inside the Workflow Task's commit. This is the other producer: +// it advances the same frontier in a transition of its own, so the two are +// serialized by the execution rather than by anything the stream does. +// +// The key is already resolved, and checked, by the caller: it is either a +// name the workflow may use or the reserved key of one of its activities. +func (w *Workflow) AppendToOwnedStream( + mctx chasm.MutableContext, + key string, + req stream.AddMessagesRequest, +) (stream.AddMessagesResult, error) { + return w.ownedStreams().Append(mctx, key, req) +} diff --git a/cmd/tools/protogen/main.go b/cmd/tools/protogen/main.go index cf2c00b2e6e..251f2a26adb 100644 --- a/cmd/tools/protogen/main.go +++ b/cmd/tools/protogen/main.go @@ -165,16 +165,65 @@ func newGenerator() (*generator, error) { return &gen, nil } +// removeExistingGenDirs clears out the previous run's output so a proto that +// was deleted does not leave its Go behind. +// +// Only generated files go. A gen package sometimes needs a hand-written file +// beside the generated ones, because a method has to be declared in the same +// package as the type it is on, and wiping the directory deleted those without +// saying so. Empty directories are then pruned, which is what removing a proto +// used to rely on. func (g *generator) removeExistingGenDirs() error { for _, dir := range g.chasmLibDirs { genDir := filepath.Join(dir, "gen") - if err := os.RemoveAll(genDir); err != nil { - return fmt.Errorf("error removing directory %s: %w", genDir, err) + if !exists(genDir) { + continue + } + if err := filepath.Walk(genDir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() || !strings.HasSuffix(info.Name(), ".pb.go") { + return nil + } + return os.Remove(path) + }); err != nil { + return fmt.Errorf("error removing generated files under %s: %w", genDir, err) + } + if err := pruneEmptyDirs(genDir); err != nil { + return fmt.Errorf("error pruning empty directories under %s: %w", genDir, err) } } return nil } +// pruneEmptyDirs removes dir and any descendant left with nothing in it, +// deepest first. +func pruneEmptyDirs(dir string) error { + entries, err := os.ReadDir(dir) + if err != nil { + if os.IsNotExist(err) { + return nil + } + return err + } + for _, entry := range entries { + if entry.IsDir() { + if err := pruneEmptyDirs(filepath.Join(dir, entry.Name())); err != nil { + return err + } + } + } + remaining, err := os.ReadDir(dir) + if err != nil { + return err + } + if len(remaining) == 0 { + return os.Remove(dir) + } + return nil +} + func (g *generator) backupProtos() error { if exists(g.protoOut) { if err := os.Rename(g.protoOut, g.protoBackup); err != nil { diff --git a/common/api/metadata.go b/common/api/metadata.go index 08c52b72869..5fa0c4d5c71 100644 --- a/common/api/metadata.go +++ b/common/api/metadata.go @@ -64,6 +64,9 @@ const ( MatchingServicePrefix = "/temporal.server.api.matchingservice.v1.MatchingService/" // Technically not a gRPC service, but still using this format for metadata. NexusServicePrefix = "/temporal.api.nexusservice.v1.NexusService/" + // The stream service is served on the frontend next to the workflow + // service, so its methods need the same authorization metadata. + StreamServicePrefix = "/temporal.server.chasm.lib.stream.proto.v1.StreamService/" ) var ( @@ -218,10 +221,50 @@ var ( "CompleteNexusOperation": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, "CompleteNexusOperationChasm": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, } + // Every stream request carries its namespace inside `frontend_request`, + // exposed through a hand-written `GetNamespace()`, which is what namespace + // scope needs. + // + // Truncating and deleting are admin, not write. Both destroy records that + // another workflow's History refers to, and the comparable operation on a + // workflow, DeleteWorkflowExecution, needs an operator for the same reason. + // Closing stays a write: it seals the stream and the records stay readable. + // The two internal calls History makes on itself are admin so that no + // namespace-level role can reach them through a frontend. + streamServiceMetadata = map[string]MethodMetadata{ + "CreateStream": namespaceWrite, + "AddMessages": namespaceWrite, + "FinishWriting": namespaceWrite, + "SubscribeWorkflow": namespaceWrite, + "PollMessages": namespaceReadPoll, + "DescribeStream": namespaceRead, + "PollWorkflowMessages": namespaceReadPoll, + "DescribeWorkflowStream": namespaceRead, + "AddWorkflowMessages": namespaceWrite, + "RegisterStreamConsumer": namespaceAdmin, + "AdvanceConsumerHead": namespaceAdmin, + "CloseStream": namespaceWrite, + "TruncateStream": namespaceAdmin, + "ListStreams": namespaceRead, + "DeleteStream": namespaceAdmin, + } + + namespaceRead = MethodMetadata{ + Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingNone, + } + namespaceReadPoll = MethodMetadata{ + Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingCapable, + } + namespaceWrite = MethodMetadata{ + Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone, + } + namespaceAdmin = MethodMetadata{ + Scope: ScopeNamespace, Access: AccessAdmin, Polling: PollingNone, + } ) // GetMethodMetadata gets metadata for a given API method in one of the services exported by -// frontend (WorkflowService, OperatorService, AdminService). +// frontend (WorkflowService, OperatorService, AdminService, StreamService). func GetMethodMetadata(fullApiName string) MethodMetadata { switch { case strings.HasPrefix(fullApiName, WorkflowServicePrefix): @@ -230,6 +273,8 @@ func GetMethodMetadata(fullApiName string) MethodMetadata { return operatorServiceMetadata[MethodName(fullApiName)] case strings.HasPrefix(fullApiName, NexusServicePrefix): return nexusServiceMetadata[MethodName(fullApiName)] + case strings.HasPrefix(fullApiName, StreamServicePrefix): + return streamServiceMetadata[MethodName(fullApiName)] case strings.HasPrefix(fullApiName, AdminServicePrefix): return MethodMetadata{Scope: ScopeCluster, Access: AccessAdmin} default: diff --git a/common/api/metadata_stream_test.go b/common/api/metadata_stream_test.go new file mode 100644 index 00000000000..be19207a131 --- /dev/null +++ b/common/api/metadata_stream_test.go @@ -0,0 +1,72 @@ +package api_test + +// An external test package, because the generated stream package depends on +// this one and an internal test importing it would be a cycle. + +import ( + "reflect" + "testing" + + "github.com/stretchr/testify/require" + streampb "go.temporal.io/server/chasm/lib/stream/gen/streampb/v1" + "go.temporal.io/server/common/api" + "go.temporal.io/server/common/testing/temporalapi" +) + +// The access each stream method declares. Kept as a table so a new method +// fails here until someone decides what it needs, and so a change of mind +// shows up in the diff. +var expectedStreamAccess = map[string]api.Access{ + "CreateStream": api.AccessWrite, + "AddMessages": api.AccessWrite, + "FinishWriting": api.AccessWrite, + "SubscribeWorkflow": api.AccessWrite, + "PollMessages": api.AccessReadOnly, + "DescribeStream": api.AccessReadOnly, + "PollWorkflowMessages": api.AccessReadOnly, + "DescribeWorkflowStream": api.AccessReadOnly, + "AddWorkflowMessages": api.AccessWrite, + "RegisterStreamConsumer": api.AccessAdmin, + "AdvanceConsumerHead": api.AccessAdmin, + "CloseStream": api.AccessWrite, + "TruncateStream": api.AccessAdmin, + "ListStreams": api.AccessReadOnly, + "DeleteStream": api.AccessAdmin, +} + +func TestStreamServiceMetadata(t *testing.T) { + var server streampb.StreamServiceServer + seen := make(map[string]struct{}) + temporalapi.WalkExportedMethods(&server, func(method reflect.Method) { + seen[method.Name] = struct{}{} + md := api.GetMethodMetadata(api.StreamServicePrefix + method.Name) + + expected, ok := expectedStreamAccess[method.Name] + require.Truef(t, ok, + "%s has no entry in expectedStreamAccess: decide its access and add it", method.Name) + require.Equalf(t, expected, md.Access, "%s access", method.Name) + require.Equalf(t, api.ScopeNamespace, md.Scope, "%s scope", method.Name) + + // Namespace scope is only enforceable if the interceptors can find the + // namespace on the request, and these requests carry it through a method + // rather than a top-level field. + requestType := method.Type.In(1) + getter, ok := requestType.MethodByName("GetNamespace") + require.Truef(t, ok, "%s request has no GetNamespace()", method.Name) + require.Equal(t, reflect.TypeFor[string](), getter.Type.Out(0)) + }) + + for name := range expectedStreamAccess { + _, ok := seen[name] + require.Truef(t, ok, "%s is in expectedStreamAccess but not on the service", name) + } +} + +func TestStreamPollsAreMarkedAsPolling(t *testing.T) { + for _, name := range []string{"PollMessages", "PollWorkflowMessages"} { + md := api.GetMethodMetadata(api.StreamServicePrefix + name) + require.Equalf(t, api.PollingCapable, md.Polling, "%s blocks only when asked to", name) + } + md := api.GetMethodMetadata(api.StreamServicePrefix + "DescribeStream") + require.Equal(t, api.PollingNone, md.Polling) +} diff --git a/common/api/metadata_test.go b/common/api/metadata_test.go index 6b7791a6cd5..7921c7d92e9 100644 --- a/common/api/metadata_test.go +++ b/common/api/metadata_test.go @@ -84,6 +84,15 @@ func TestGetMethodMetadata(t *testing.T) { assert.Equal(t, ScopeCluster, md.Scope) assert.Equal(t, AccessAdmin, md.Access) + md = GetMethodMetadata(StreamServicePrefix + "AddWorkflowMessages") + require.Equal(t, ScopeNamespace, md.Scope) + require.Equal(t, AccessWrite, md.Access) + + md = GetMethodMetadata(StreamServicePrefix + "PollWorkflowMessages") + require.Equal(t, ScopeNamespace, md.Scope) + require.Equal(t, AccessReadOnly, md.Access) + require.Equal(t, PollingCapable, md.Polling) + md = GetMethodMetadata("/OtherService/Method1") assert.Equal(t, ScopeUnknown, md.Scope) assert.Equal(t, AccessUnknown, md.Access) diff --git a/common/metrics/metric_defs.go b/common/metrics/metric_defs.go index b42c67b2bd2..fce10f82415 100644 --- a/common/metrics/metric_defs.go +++ b/common/metrics/metric_defs.go @@ -761,6 +761,28 @@ var ( "http_service_requests", WithDescription("The number of HTTP requests received by the service."), ) + // Stream service meters, per namespace. Appends count what was stored, so a + // deduplicated retry adds nothing; deliveries count what a poll returned. + StreamRecordsAppended = NewCounterDef( + "stream_records_appended", + WithDescription("The number of stream records appended through the stream service."), + ) + StreamBytesAppended = NewCounterDef( + "stream_bytes_appended", + WithDescription("The number of stream record bytes appended through the stream service."), + ) + StreamRecordsDelivered = NewCounterDef( + "stream_records_delivered", + WithDescription("The number of stream records returned by stream polls."), + ) + StreamBytesDelivered = NewCounterDef( + "stream_bytes_delivered", + WithDescription("The number of stream record bytes returned by stream polls."), + ) + StreamPolls = NewCounterDef( + "stream_polls", + WithDescription("The number of stream polls admitted by the stream service."), + ) NexusRequests = NewCounterDef( "nexus_requests", WithDescription("The number of Nexus requests received by the service."), diff --git a/common/rpc/interceptor/redirection_test.go b/common/rpc/interceptor/redirection_test.go index c74ec389340..b336d2e2471 100644 --- a/common/rpc/interceptor/redirection_test.go +++ b/common/rpc/interceptor/redirection_test.go @@ -11,6 +11,7 @@ import ( "go.temporal.io/api/serviceerror" "go.temporal.io/api/workflowservice/v1" persistencespb "go.temporal.io/server/api/persistence/v1" + streampb "go.temporal.io/server/chasm/lib/stream/gen/streampb/v1" "go.temporal.io/server/client" "go.temporal.io/server/common/clock" "go.temporal.io/server/common/cluster" @@ -703,3 +704,82 @@ func (s *redirectionInterceptorSuite) TestHandleGlobalAPIInvocation_RemoteRoutin s.Len(failureMetrics, 1) s.Equal(int64(1), failureMetrics[0].Value) } + +// streamNamespaceActiveElsewhere registers a global namespace whose active +// cluster is the alternative one, so a call for it has to be forwarded. +func (s *redirectionInterceptorSuite) streamNamespaceActiveElsewhere() namespace.Name { + namespaceName := namespace.Name("stream-namespace-active-elsewhere") + namespaceEntry := namespace.NewGlobalNamespaceForTest( + &persistencespb.NamespaceInfo{Id: uuid.NewString(), Name: namespaceName.String()}, + &persistencespb.NamespaceConfig{Retention: timestamp.DurationFromDays(1)}, + &persistencespb.NamespaceReplicationConfig{ + ActiveClusterName: cluster.TestAlternativeClusterName, + Clusters: []string{ + cluster.TestCurrentClusterName, + cluster.TestAlternativeClusterName, + }, + }, + 1, + ) + s.namespaceCache.EXPECT().GetNamespace(namespaceName).Return(namespaceEntry, nil).AnyTimes() + return namespaceName +} + +// A stream call registered as redirectable is forwarded to the namespace's +// active cluster, resolved from the namespace inside `frontend_request`. +func (s *redirectionInterceptorSuite) TestStreamAPI_ForwardedWhenRegistered() { + namespaceName := s.streamNamespaceActiveElsewhere() + info := &grpc.UnaryServerInfo{ + FullMethod: streampb.StreamService_AddWorkflowMessages_FullMethodName, + } + req := &streampb.AddWorkflowMessagesRequest{ + FrontendRequest: &streampb.AddWorkflowMessagesInput{ + Namespace: namespaceName.String(), + WorkflowId: "wf", + }, + } + redirector := s.redirector.WithRedirectResponses(map[string]func() any{ + info.FullMethod: func() any { return &streampb.AddWorkflowMessagesResponse{} }, + }) + + grpcConn := &mockClientConnInterface{ + Suite: &s.Suite, + targetMethod: info.FullMethod, + targetResponse: &streampb.AddWorkflowMessagesResponse{}, + } + s.clientBean.EXPECT().GetRemoteFrontendClient(cluster.TestAlternativeClusterName). + Return(grpcConn, nil, nil).Times(1) + + resp, err := redirector.Intercept(context.Background(), req, info, + func(context.Context, any) (any, error) { + s.Fail("a call for a namespace active elsewhere must not be served locally") + return nil, nil + }) + s.NoError(err) + s.IsType(&streampb.AddWorkflowMessagesResponse{}, resp) +} + +// Without registration a stream call is served wherever it lands, which is +// what the bare interceptor does for any service other than WorkflowService. +func (s *redirectionInterceptorSuite) TestStreamAPI_ServedLocallyWhenNotRegistered() { + namespaceName := s.streamNamespaceActiveElsewhere() + info := &grpc.UnaryServerInfo{ + FullMethod: streampb.StreamService_AddWorkflowMessages_FullMethodName, + } + req := &streampb.AddWorkflowMessagesRequest{ + FrontendRequest: &streampb.AddWorkflowMessagesInput{ + Namespace: namespaceName.String(), + WorkflowId: "wf", + }, + } + + served := false + resp, err := s.redirector.Intercept(context.Background(), req, info, + func(context.Context, any) (any, error) { + served = true + return &streampb.AddWorkflowMessagesResponse{}, nil + }) + s.NoError(err) + s.True(served) + s.IsType(&streampb.AddWorkflowMessagesResponse{}, resp) +} diff --git a/service/frontend/configs/quotas.go b/service/frontend/configs/quotas.go index 0d59b57446b..14d16372d47 100644 --- a/service/frontend/configs/quotas.go +++ b/service/frontend/configs/quotas.go @@ -23,6 +23,10 @@ const ( PollWorkflowHistoryAPIName = "/temporal.api.workflowservice.v1.WorkflowService/PollWorkflowExecutionHistory" // PollActivityExecutionAPIName is used instead of DescribeActivityExecution if LongPollToken is set in request. PollActivityExecutionAPIName = "/temporal.api.workflowservice.v1.WorkflowService/PollActivityExecutionDescription" + + // The stream service is served on the same frontend server as the workflow + // service and shares its rate limiters. + streamServicePrefix = "/temporal.server.chasm.lib.stream.proto.v1.StreamService/" ) var ( @@ -63,6 +67,10 @@ var ( // Dispatching a Nexus task is a potentially long running RPC, it's classified in the same bucket as QueryWorkflow. DispatchNexusTaskByNamespaceAndTaskQueueAPIName: 1, DispatchNexusTaskByEndpointAPIName: 1, + + // Stream reads block only when asked to wait for new records. + streamServicePrefix + "PollMessages": 1, + streamServicePrefix + "PollWorkflowMessages": 1, } // PollTaskAPISet is the set of API methods for which NamespaceRateLimitInterceptor will @@ -215,6 +223,26 @@ var ( // Informational API that aren't required for the temporal service to function OpenAPIV3APIName: 5, OpenAPIV2APIName: 5, + + // Stream service. Appends and subscriptions are external events like a + // signal; closing, truncating and deleting change state; describes are + // status reads; the two polls sit with the other polls. + streamServicePrefix + "CreateStream": 1, + streamServicePrefix + "AddMessages": 1, + streamServicePrefix + "FinishWriting": 1, + streamServicePrefix + "SubscribeWorkflow": 1, + streamServicePrefix + "AddWorkflowMessages": 1, + streamServicePrefix + "CloseStream": 2, + streamServicePrefix + "TruncateStream": 2, + streamServicePrefix + "DeleteStream": 2, + streamServicePrefix + "DescribeStream": 3, + streamServicePrefix + "DescribeWorkflowStream": 3, + streamServicePrefix + "PollMessages": 4, + streamServicePrefix + "PollWorkflowMessages": 4, + // History calls these on itself; the frontend answers them with + // Unimplemented, so they only ever cost a refusal. + streamServicePrefix + "RegisterStreamConsumer": 5, + streamServicePrefix + "AdvanceConsumerHead": 5, } ExecutionAPIPrioritiesOrdered = []int{0, 1, 2, 3, 4, 5} @@ -240,6 +268,9 @@ var ( "/temporal.api.workflowservice.v1.WorkflowService/ListDeployments": 1, "/temporal.api.workflowservice.v1.WorkflowService/GetDeploymentReachability": 1, "/temporal.api.workflowservice.v1.WorkflowService/ListWorkerDeployments": 1, + + // Answered from visibility on the frontend, like the other lists. + streamServicePrefix + "ListStreams": 1, } VisibilityAPIPrioritiesOrdered = []int{0, 1} diff --git a/service/frontend/configs/quotas_test.go b/service/frontend/configs/quotas_test.go index e96d8202578..46499d20508 100644 --- a/service/frontend/configs/quotas_test.go +++ b/service/frontend/configs/quotas_test.go @@ -9,6 +9,8 @@ import ( "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" "go.temporal.io/api/workflowservice/v1" + streampb "go.temporal.io/server/chasm/lib/stream/gen/streampb/v1" + "go.temporal.io/server/common/api" "go.temporal.io/server/common/headers" "go.temporal.io/server/common/log" "go.temporal.io/server/common/quotas" @@ -136,6 +138,9 @@ func (s *quotasSuite) TestVisibilityAPIs() { apiToPriority[apiName] = VisibilityAPIToPriority[apiName] } } + // The one stream method answered from visibility. + listStreams := streamServicePrefix + "ListStreams" + apiToPriority[listStreams] = VisibilityAPIToPriority[listStreams] s.Equal(apiToPriority, VisibilityAPIToPriority) } @@ -186,6 +191,27 @@ func (s *quotasSuite) TestAllAPIs() { s.Truef(ok, "missing priority for API: %q", CompleteNexusOperation) } +// Every stream method has a priority, the two polls are counted as +// long-running, and the one list is a visibility read. +func (s *quotasSuite) TestStreamServiceAPIs() { + var service streampb.StreamServiceServer + temporalapi.WalkExportedMethods(&service, func(m reflect.Method) { + apiName := streamServicePrefix + m.Name + _, inExecution := APIToPriority[apiName] + _, inVisibility := VisibilityAPIToPriority[apiName] + s.Truef(inExecution || inVisibility, "missing priority for API: %v", m.Name) + s.Falsef(inExecution && inVisibility, "API in two limiters: %v", m.Name) + }) + + for _, poll := range []string{"PollMessages", "PollWorkflowMessages"} { + _, ok := ExecutionAPICountLimitOverride[streamServicePrefix+poll] + s.Truef(ok, "%s can block, so it counts against concurrent long polls", poll) + } + s.Equal(1, VisibilityAPIToPriority[streamServicePrefix+"ListStreams"]) + s.Equal(streamServicePrefix, api.StreamServicePrefix, + "the quota table and the authorization table have to agree on the service name") +} + func (s *quotasSuite) TestOperatorPriority_Execution() { limiter := NewExecutionPriorityRateLimiter(testRateBurstFn, testOperatorRPSRatioFn) s.testOperatorPrioritized(limiter, "DescribeWorkflowExecution") diff --git a/service/frontend/fx.go b/service/frontend/fx.go index 772a37e82e4..6dbf6b651c5 100644 --- a/service/frontend/fx.go +++ b/service/frontend/fx.go @@ -14,6 +14,7 @@ import ( nexusoperationpb "go.temporal.io/server/chasm/lib/nexusoperation/gen/nexusoperationpb/v1" chasmscheduler "go.temporal.io/server/chasm/lib/scheduler" "go.temporal.io/server/chasm/lib/scheduler/gen/schedulerpb/v1" + chasmstream "go.temporal.io/server/chasm/lib/stream/service" chasmtests "go.temporal.io/server/chasm/lib/tests" chasmworkflow "go.temporal.io/server/chasm/lib/workflow" "go.temporal.io/server/client" @@ -157,6 +158,7 @@ var Module = fx.Options( chasmworkflow.Module, chasmcallback.Module, activity.FrontendModule, + chasmstream.FrontendModule, fx.Provide(visibility.ChasmVisibilityManagerProvider), fx.Provide(chasm.ChasmVisibilityInterceptorProvider), ) @@ -169,6 +171,7 @@ func NewServiceProvider( handler Handler, adminHandler *AdminHandler, operatorHandler *OperatorHandlerImpl, + streamHandler *chasmstream.FrontendHandler, versionChecker *VersionChecker, visibilityMgr manager.VisibilityManager, logger log.SnTaggedLogger, @@ -184,6 +187,7 @@ func NewServiceProvider( handler, adminHandler, operatorHandler, + streamHandler, versionChecker, visibilityMgr, logger, @@ -428,7 +432,7 @@ func RedirectionInterceptorProvider( metricsHandler, timeSource, clusterMetadata, - ) + ).WithRedirectResponses(chasmstream.RedirectableMethods()) } func BusinessIDInterceptorProvider( diff --git a/service/frontend/service.go b/service/frontend/service.go index 0717e4b660b..5c33b287b7a 100644 --- a/service/frontend/service.go +++ b/service/frontend/service.go @@ -13,6 +13,8 @@ import ( "go.temporal.io/server/chasm/lib/activity" chasmcallback "go.temporal.io/server/chasm/lib/callback" chasmnexus "go.temporal.io/server/chasm/lib/nexusoperation" + streampb "go.temporal.io/server/chasm/lib/stream/gen/streampb/v1" + chasmstream "go.temporal.io/server/chasm/lib/stream/service" chasmworkflow "go.temporal.io/server/chasm/lib/workflow" "go.temporal.io/server/common/callbacks" "go.temporal.io/server/common/dynamicconfig" @@ -473,6 +475,7 @@ type Service struct { handler Handler adminHandler *AdminHandler operatorHandler *OperatorHandlerImpl + streamHandler *chasmstream.FrontendHandler versionChecker *VersionChecker visibilityManager manager.VisibilityManager server *grpc.Server @@ -492,6 +495,7 @@ func NewService( handler Handler, adminHandler *AdminHandler, operatorHandler *OperatorHandlerImpl, + streamHandler *chasmstream.FrontendHandler, versionChecker *VersionChecker, visibilityMgr manager.VisibilityManager, logger log.Logger, @@ -507,6 +511,7 @@ func NewService( handler: handler, adminHandler: adminHandler, operatorHandler: operatorHandler, + streamHandler: streamHandler, versionChecker: versionChecker, visibilityManager: visibilityMgr, logger: logger, @@ -524,6 +529,7 @@ func (s *Service) Start() { workflowservice.RegisterWorkflowServiceServer(s.server, s.handler) adminservice.RegisterAdminServiceServer(s.server, s.adminHandler) operatorservice.RegisterOperatorServiceServer(s.server, s.operatorHandler) + streampb.RegisterStreamServiceServer(s.server, s.streamHandler) reflection.Register(s.server) diff --git a/service/history/fx.go b/service/history/fx.go index 69f02f79295..595bce956c2 100644 --- a/service/history/fx.go +++ b/service/history/fx.go @@ -12,6 +12,7 @@ import ( "go.temporal.io/server/chasm/lib/callback" chasmnexus "go.temporal.io/server/chasm/lib/nexusoperation" "go.temporal.io/server/chasm/lib/scheduler" + chasmstream "go.temporal.io/server/chasm/lib/stream/service" chasmtests "go.temporal.io/server/chasm/lib/tests" chasmworkflow "go.temporal.io/server/chasm/lib/workflow" "go.temporal.io/server/common" @@ -127,6 +128,7 @@ var Module = fx.Options( hsmnexusoperations.Module, fx.Invoke(hsmnexusworkflow.RegisterCommandHandlers), activity.HistoryModule, + chasmstream.HistoryModule, scheduler.Module, callback.Module, chasmnexus.Module, diff --git a/tests/stream_age_test.go b/tests/stream_age_test.go new file mode 100644 index 00000000000..7aa6c3a101c --- /dev/null +++ b/tests/stream_age_test.go @@ -0,0 +1,118 @@ +package tests + +import ( + "testing" + "time" + + "github.com/google/uuid" + "github.com/stretchr/testify/require" + commandpb "go.temporal.io/api/command/v1" + commonpb "go.temporal.io/api/common/v1" + enumspb "go.temporal.io/api/enums/v1" + taskqueuepb "go.temporal.io/api/taskqueue/v1" + "go.temporal.io/api/workflowservice/v1" + chasmstream "go.temporal.io/server/chasm/lib/stream" + streamlib "go.temporal.io/server/chasm/lib/stream/gen/streampb/v1" + "go.temporal.io/server/common/testing/await" + "go.temporal.io/server/tests/testcore" + "google.golang.org/grpc/codes" + "google.golang.org/protobuf/types/known/durationpb" +) + +// Retention is an age on an open stream: a record older than it is reclaimed +// behind the floor on the recheck interval, and never past an active +// consumer's floor. +func TestStreamAgesRecordsOutOnAnOpenStream(t *testing.T) { + // Dedicated, because the recheck interval is a global setting. + env := testcore.NewEnv(t, testcore.WithDedicatedCluster()) + env.OverrideDynamicConfig(chasmstream.RetentionRecheckIntervalSetting, time.Second) + s := newStreamTestEnvFrom(t, env) + ctx := streamCtx(t) + + const retention = 2 * time.Second + const aging, pinned = "stream-aging", "stream-aging-pinned" + for _, id := range []string{aging, pinned} { + _, err := s.client.CreateStream(ctx, &streamlib.CreateStreamRequest{ + FrontendRequest: &streamlib.CreateStreamInput{ + Namespace: s.ns, StreamId: id, + Lifecycle: &streamlib.StreamLifecycle{Retention: durationpb.New(retention)}, + }, + }) + require.NoError(t, err) + } + describe := func(id string) *streamlib.StreamState { + t.Helper() + resp, err := s.client.DescribeStream(ctx, &streamlib.DescribeStreamRequest{ + FrontendRequest: &streamlib.DescribeStreamInput{Namespace: s.ns, StreamId: id}, + }) + require.NoError(t, err) + return resp.GetFrontendResponse().GetState() + } + + // A workflow pins the second stream at offset 0 before anything is written. + workflowID := "stream-age-consumer-" + uuid.NewString() + tq := &taskqueuepb.TaskQueue{Name: workflowID + "-tq", Kind: enumspb.TASK_QUEUE_KIND_NORMAL} + _, err := env.FrontendClient().StartWorkflowExecution(ctx, + &workflowservice.StartWorkflowExecutionRequest{ + RequestId: uuid.NewString(), + Namespace: s.ns, + WorkflowId: workflowID, + WorkflowType: &commonpb.WorkflowType{Name: "stream-consumer"}, + TaskQueue: tq, + WorkflowRunTimeout: durationpb.New(100 * time.Second), + WorkflowTaskTimeout: durationpb.New(10 * time.Second), + Identity: "tester", + }) + require.NoError(t, err) + //nolint:staticcheck // SA1019: consistent with the other stream tests. + poller := &testcore.TaskPoller{ + Client: env.FrontendClient(), + Namespace: s.ns, + TaskQueue: tq, + Identity: "tester", + WorkflowTaskHandler: func( + *workflowservice.PollWorkflowTaskQueueResponse, + ) ([]*commandpb.Command, error) { + return nil, nil + }, + Logger: env.Logger, + T: t, + } + _, err = poller.PollAndProcessWorkflowTask() + require.NoError(t, err) + _, err = s.client.SubscribeWorkflow(ctx, &streamlib.SubscribeWorkflowRequest{ + FrontendRequest: &streamlib.SubscribeWorkflowInput{ + Namespace: s.ns, WorkflowId: workflowID, StreamId: pinned, StartOffset: 0, + }, + }) + require.NoError(t, err) + + for _, id := range []string{aging, pinned} { + _, err := s.add(ctx, t, id, &streamlib.AddMessagesInput{Records: streamMsgs("t", "old")}) + require.NoError(t, err) + } + require.Equal(t, []string{"old"}, bodies(s.poll(ctx, t, aging, 0).GetRecords())) + + // A retention and a recheck or two later the record is gone from the open + // stream, and the floor says so. + await.RequireTrue(t, func() bool { + return describe(aging).GetBaseOffset() == 1 + }, 30*time.Second, 200*time.Millisecond) + require.Equal(t, int64(0), describe(aging).GetHeldBytes()) + _, err = s.client.PollMessages(ctx, &streamlib.PollMessagesRequest{ + FrontendRequest: &streamlib.PollMessagesInput{Namespace: s.ns, StreamId: aging, FromOffset: 0}, + }) + requireReason(t, err, codes.FailedPrecondition, chasmstream.ReasonCursorBelowFloor) + + // The stream stays open: a new record lands at the next offset and reads + // back from the floor. + _, err = s.add(ctx, t, aging, &streamlib.AddMessagesInput{Records: streamMsgs("t", "new")}) + require.NoError(t, err) + require.Equal(t, []string{"new"}, bodies(s.poll(ctx, t, aging, 1).GetRecords())) + + // The pinned stream's record is just as old, and stays: a workflow's + // History depends on it. + held := describe(pinned) + require.Equal(t, int64(0), held.GetBaseOffset(), "the age never crosses an active consumer's floor") + require.Equal(t, []string{"old"}, bodies(s.poll(ctx, t, pinned, 0).GetRecords())) +} diff --git a/tests/stream_lifecycle_test.go b/tests/stream_lifecycle_test.go new file mode 100644 index 00000000000..f92c839ae17 --- /dev/null +++ b/tests/stream_lifecycle_test.go @@ -0,0 +1,79 @@ +package tests + +import ( + "testing" + + "github.com/stretchr/testify/require" + streamlib "go.temporal.io/server/chasm/lib/stream/gen/streampb/v1" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +// The creator's byte cap is a ceiling on what the stream holds: an append that +// would cross it is refused, describe reports the cap and the held bytes, and +// truncating behind the floor gives the room back. +func TestStreamByteCapRefusesAndDescribeReportsIt(t *testing.T) { + s := newStreamTestEnv(t) + ctx := streamCtx(t) + const id = "stream-byte-cap" + const maxBytes = 120 + + _, err := s.client.CreateStream(ctx, &streamlib.CreateStreamRequest{ + FrontendRequest: &streamlib.CreateStreamInput{ + Namespace: s.ns, StreamId: id, + Lifecycle: &streamlib.StreamLifecycle{MaxBytes: maxBytes}, + }, + }) + require.NoError(t, err) + + describe := func() *streamlib.StreamState { + t.Helper() + resp, err := s.client.DescribeStream(ctx, &streamlib.DescribeStreamRequest{ + FrontendRequest: &streamlib.DescribeStreamInput{Namespace: s.ns, StreamId: id}, + }) + require.NoError(t, err) + return resp.GetFrontendResponse().GetState() + } + require.Equal(t, int64(maxBytes), describe().GetLifecycle().GetMaxBytes()) + + appended := 0 + for appended < 100 { + _, err := s.add(ctx, t, id, &streamlib.AddMessagesInput{Records: streamMsgs("t", "0123456789")}) + if err != nil { + require.Equal(t, codes.ResourceExhausted, status.Code(err), "%v", err) + break + } + appended++ + } + require.Positive(t, appended, "a cap of a few batches admits at least one") + require.Less(t, appended, 100, "and refuses eventually") + + state := describe() + require.Positive(t, state.GetHeldBytes()) + require.LessOrEqual(t, state.GetHeldBytes(), int64(maxBytes)) + require.Equal(t, int64(appended), state.GetHeadOffset(), "the refused append wrote nothing") + require.Equal(t, int64(0), state.GetBaseOffset(), "the byte cap reclaims nothing on its own") + + // Truncating behind the floor frees the room. + _, err = s.client.TruncateStream(ctx, &streamlib.TruncateStreamRequest{ + FrontendRequest: &streamlib.TruncateStreamInput{ + Namespace: s.ns, StreamId: id, NewBaseOffset: state.GetHeadOffset(), + }, + }) + require.NoError(t, err) + require.Equal(t, int64(0), describe().GetHeldBytes()) + _, err = s.add(ctx, t, id, &streamlib.AddMessagesInput{Records: streamMsgs("t", "0123456789")}) + require.NoError(t, err) +} + +func TestStreamLifecycleRefusesANegativeByteCap(t *testing.T) { + s := newStreamTestEnv(t) + ctx := streamCtx(t) + _, err := s.client.CreateStream(ctx, &streamlib.CreateStreamRequest{ + FrontendRequest: &streamlib.CreateStreamInput{ + Namespace: s.ns, StreamId: "stream-negative-bytes", + Lifecycle: &streamlib.StreamLifecycle{MaxBytes: -1}, + }, + }) + require.Equal(t, codes.InvalidArgument, status.Code(err), "%v", err) +} diff --git a/tests/stream_metering_test.go b/tests/stream_metering_test.go new file mode 100644 index 00000000000..620872dac6a --- /dev/null +++ b/tests/stream_metering_test.go @@ -0,0 +1,109 @@ +package tests + +import ( + "testing" + + "github.com/stretchr/testify/require" + enumspb "go.temporal.io/api/enums/v1" + "go.temporal.io/api/serviceerror" + chasmstream "go.temporal.io/server/chasm/lib/stream" + streamlib "go.temporal.io/server/chasm/lib/stream/gen/streampb/v1" + "go.temporal.io/server/tests/testcore" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +// The stream service counts what a namespace appends and reads, tagged with +// the namespace, so metering has a unit to read. +func TestStreamServiceMetersAppendsAndDeliveriesPerNamespace(t *testing.T) { + env := testcore.NewEnv(t) + capture := env.StartNamespaceMetricCapture() + s := newStreamTestEnvFrom(t, env) + ctx := streamCtx(t) + const id = "stream-metered" + s.create(ctx, t, id) + + in := &streamlib.AddMessagesInput{ + Records: streamMsgs("t", "a", "bb", "ccc"), ProducerId: "p1", Sequence: 1, + } + _, err := s.add(ctx, t, id, in) + require.NoError(t, err) + // A deduplicated retry stored nothing and adds nothing to the meter. + retry, err := s.add(ctx, t, id, &streamlib.AddMessagesInput{ + Records: streamMsgs("t", "a", "bb", "ccc"), ProducerId: "p1", Sequence: 1, + }) + require.NoError(t, err) + require.True(t, retry.GetDeduplicated()) + + got := s.poll(ctx, t, id, 0) + require.Len(t, got.GetRecords(), 3) + empty := s.poll(ctx, t, id, 3) + require.Empty(t, empty.GetRecords()) + + sum := func(name string) int64 { + var total int64 + for _, rec := range capture.Metric(name) { + require.Equal(t, s.ns, rec.Tags["namespace"]) + total += rec.Value.(int64) + } + return total + } + require.Equal(t, int64(3), sum("stream_records_appended")) + require.Equal(t, int64(3), sum("stream_records_delivered")) + require.Equal(t, int64(2), sum("stream_polls"), "every admitted poll counts, empty or not") + require.Positive(t, sum("stream_bytes_appended")) + require.Positive(t, sum("stream_bytes_delivered")) +} + +// A namespace over its poll rate is refused with the standard cause and the +// namespace scope, the same shape as the frontend's own rate limits, so a +// client's existing handling applies. +func TestStreamPollsAreRateLimitedPerNamespace(t *testing.T) { + env := testcore.NewEnv(t) + env.OverrideDynamicConfig(chasmstream.PollsPerSecondSetting, 1) + s := newStreamTestEnvFrom(t, env) + ctx := streamCtx(t) + const id = "stream-poll-limited" + s.create(ctx, t, id) + _, err := s.add(ctx, t, id, &streamlib.AddMessagesInput{Records: streamMsgs("t", "a")}) + require.NoError(t, err) + + // One per second with a burst of one: the first poll is admitted and the + // next, within the same second, is not. + got := s.poll(ctx, t, id, 0) + require.Equal(t, []string{"a"}, bodies(got.GetRecords())) + + _, err = s.client.PollMessages(ctx, &streamlib.PollMessagesRequest{ + FrontendRequest: &streamlib.PollMessagesInput{Namespace: s.ns, StreamId: id, FromOffset: 0}, + }) + require.Equal(t, codes.ResourceExhausted, status.Code(err), "%v", err) + var exhausted *serviceerror.ResourceExhausted + require.ErrorAs(t, serviceerror.FromStatus(status.Convert(err)), &exhausted) + require.Equal(t, enumspb.RESOURCE_EXHAUSTED_CAUSE_RPS_LIMIT, exhausted.Cause) + require.Equal(t, enumspb.RESOURCE_EXHAUSTED_SCOPE_NAMESPACE, exhausted.Scope) +} + +func TestStreamAppendsAreRateLimitedPerNamespace(t *testing.T) { + env := testcore.NewEnv(t) + env.OverrideDynamicConfig(chasmstream.AppendRecordsPerSecondSetting, 2) + s := newStreamTestEnvFrom(t, env) + ctx := streamCtx(t) + const id = "stream-append-limited" + s.create(ctx, t, id) + + // The burst never drops below one full batch, so the first full batch is + // admitted whatever the rate and the next record is over it. + full := make([]string, streamMaxBatch) + for i := range full { + full[i] = "x" + } + _, err := s.add(ctx, t, id, &streamlib.AddMessagesInput{Records: streamMsgs("t", full...)}) + require.NoError(t, err) + _, err = s.add(ctx, t, id, &streamlib.AddMessagesInput{Records: streamMsgs("t", "over")}) + require.Equal(t, codes.ResourceExhausted, status.Code(err), "%v", err) + + // Nothing over the rate was stored. + got := s.poll(ctx, t, id, 0) + require.Len(t, got.GetRecords(), streamMaxBatch) + require.Equal(t, int64(streamMaxBatch), got.GetHeadOffset()) +} diff --git a/tests/stream_owner_test.go b/tests/stream_owner_test.go new file mode 100644 index 00000000000..574b49f9cc6 --- /dev/null +++ b/tests/stream_owner_test.go @@ -0,0 +1,401 @@ +package tests + +import ( + "testing" + "time" + + "github.com/google/uuid" + "github.com/stretchr/testify/require" + commandpb "go.temporal.io/api/command/v1" + commonpb "go.temporal.io/api/common/v1" + enumspb "go.temporal.io/api/enums/v1" + failurepb "go.temporal.io/api/failure/v1" + streampb "go.temporal.io/api/stream/v1" + taskqueuepb "go.temporal.io/api/taskqueue/v1" + "go.temporal.io/api/workflowservice/v1" + "go.temporal.io/server/chasm/lib/activity" + chasmstream "go.temporal.io/server/chasm/lib/stream" + streamlib "go.temporal.io/server/chasm/lib/stream/gen/streampb/v1" + "go.temporal.io/server/common/dynamicconfig" + "go.temporal.io/server/tests/testcore" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/durationpb" +) + +// Streams owned by activities. A standalone activity is an execution of its +// own and owns a map of named streams the way a workflow does; an activity a +// workflow scheduled keeps its streams in the workflow's map and is addressed +// through the workflow. Both are written and read from outside over the +// owned-stream calls, with an owner reference naming which execution. + +func newStreamActivityEnv(t *testing.T) (*testcore.TestEnv, *streamTestEnv) { + env := testcore.NewEnv(t) + on := []dynamicconfig.ConstrainedValue{{ + Constraints: dynamicconfig.Constraints{Namespace: env.Namespace().String()}, + Value: true, + }} + cluster := env.GetTestCluster() + cluster.OverrideDynamicConfig(t, dynamicconfig.EnableChasm, on) + cluster.OverrideDynamicConfig(t, activity.Enabled, on) + return env, newStreamTestEnvFrom(t, env) +} + +func (s *streamTestEnv) addOwned( + t *testing.T, owner *streamlib.StreamOwner, name string, in *streamlib.AddWorkflowMessagesInput, +) (*streamlib.AddMessagesOutput, error) { + t.Helper() + in.Namespace, in.Owner, in.StreamName = s.ns, owner, name + resp, err := s.client.AddWorkflowMessages(s.ctx(), &streamlib.AddWorkflowMessagesRequest{ + FrontendRequest: in, + }) + return resp.GetFrontendResponse(), err +} + +func (s *streamTestEnv) pollOwned( + t *testing.T, owner *streamlib.StreamOwner, name string, from int64, wait bool, +) *streamlib.PollMessagesOutput { + t.Helper() + resp, err := s.client.PollWorkflowMessages(s.ctx(), &streamlib.PollWorkflowMessagesRequest{ + FrontendRequest: &streamlib.PollWorkflowMessagesInput{ + Namespace: s.ns, Owner: owner, StreamName: name, FromOffset: from, + WaitNewMessages: wait, + }, + }) + require.NoError(t, err) + return resp.GetFrontendResponse() +} + +func (s *streamTestEnv) describeOwned( + t *testing.T, owner *streamlib.StreamOwner, name string, +) *streamlib.StreamState { + t.Helper() + resp, err := s.client.DescribeWorkflowStream(s.ctx(), &streamlib.DescribeWorkflowStreamRequest{ + FrontendRequest: &streamlib.DescribeWorkflowStreamInput{ + Namespace: s.ns, Owner: owner, StreamName: name, + }, + }) + require.NoError(t, err) + return resp.GetFrontendResponse().GetState() +} + +// attemptRecords is what a producer writing for one activity attempt sends: +// every record says which attempt wrote it, which is what lets a reader see +// where a retry took over. +func attemptRecords(attempt int64, bodies ...string) []*streamlib.StreamRecord { + out := streamMsgs("", bodies...) + for _, r := range out { + r.Attempt = attempt + } + return out +} + +func pollActivityTask( + t *testing.T, env *testcore.TestEnv, s *streamTestEnv, taskQueue string, +) *workflowservice.PollActivityTaskQueueResponse { + t.Helper() + task, err := env.FrontendClient().PollActivityTaskQueue(s.ctx(), + &workflowservice.PollActivityTaskQueueRequest{ + Namespace: s.ns, + TaskQueue: &taskqueuepb.TaskQueue{Name: taskQueue, Kind: enumspb.TASK_QUEUE_KIND_NORMAL}, + Identity: "tester", + }) + require.NoError(t, err) + require.NotEmpty(t, task.GetTaskToken(), "no activity task arrived") + return task +} + +func failActivityAttempt( + t *testing.T, env *testcore.TestEnv, s *streamTestEnv, + task *workflowservice.PollActivityTaskQueueResponse, +) { + t.Helper() + _, err := env.FrontendClient().RespondActivityTaskFailed(s.ctx(), + &workflowservice.RespondActivityTaskFailedRequest{ + Namespace: s.ns, + TaskToken: task.GetTaskToken(), + Failure: &failurepb.Failure{ + Message: "attempt failed", + FailureInfo: &failurepb.Failure_ApplicationFailureInfo{ + ApplicationFailureInfo: &failurepb.ApplicationFailureInfo{Type: "Retryable"}, + }, + }, + Identity: "tester", + }) + require.NoError(t, err) +} + +func completeActivityAttempt( + t *testing.T, env *testcore.TestEnv, s *streamTestEnv, + task *workflowservice.PollActivityTaskQueueResponse, +) { + t.Helper() + _, err := env.FrontendClient().RespondActivityTaskCompleted(s.ctx(), + &workflowservice.RespondActivityTaskCompletedRequest{ + Namespace: s.ns, TaskToken: task.GetTaskToken(), Identity: "tester", + }) + require.NoError(t, err) +} + +var streamActivityRetryPolicy = &commonpb.RetryPolicy{ + InitialInterval: durationpb.New(time.Second), + BackoffCoefficient: 1, + MaximumAttempts: 3, +} + +// A standalone activity owns a default stream and named ones, written and read +// from outside by activity id. A retry inherits the stream, so a reader sees +// one log with the attempt on every record, and the stream ends when the +// activity reaches a terminal status rather than when an attempt fails. +func TestStreamStandaloneActivityOwnsStreams(t *testing.T) { + env, s := newStreamActivityEnv(t) + + activityID := "stream-saa-" + uuid.NewString() + taskQueue := activityID + "-tq" + started, err := env.FrontendClient().StartActivityExecution(s.ctx(), + &workflowservice.StartActivityExecutionRequest{ + Namespace: s.ns, + ActivityId: activityID, + ActivityType: &commonpb.ActivityType{Name: "streaming-activity"}, + TaskQueue: &taskqueuepb.TaskQueue{Name: taskQueue}, + StartToCloseTimeout: durationpb.New(time.Minute), + RetryPolicy: streamActivityRetryPolicy, + RequestId: uuid.NewString(), + }) + require.NoError(t, err) + + owner := &streamlib.StreamOwner{ + Kind: streamlib.STREAM_OWNER_KIND_ACTIVITY, Id: activityID, + } + pinned := &streamlib.StreamOwner{ + Kind: streamlib.STREAM_OWNER_KIND_ACTIVITY, Id: activityID, RunId: started.GetRunId(), + } + + // A reader may attach before anything is written, the same as on a + // workflow's stream. + empty := s.pollOwned(t, owner, "", 0, false) + require.Empty(t, empty.GetRecords()) + require.False(t, empty.GetClosed()) + + first := pollActivityTask(t, env, s, taskQueue) + require.EqualValues(t, 1, first.GetAttempt()) + + out, err := s.addOwned(t, owner, "", &streamlib.AddWorkflowMessagesInput{ + Records: attemptRecords(1, "token one", "token two"), ProducerId: "worker#1", Sequence: 1, + }) + require.NoError(t, err) + require.Equal(t, int64(0), out.GetFirstOffset()) + _, err = s.addOwned(t, pinned, "progress", &streamlib.AddWorkflowMessagesInput{ + Records: attemptRecords(1, "step 1"), + }) + require.NoError(t, err, "a named stream, addressed with the run pinned") + + // The default stream and the named one are separate logs. + require.Equal(t, []string{"token one", "token two"}, + bodies(s.pollOwned(t, owner, chasmstream.DefaultStreamName, 0, false).GetRecords()), + "an empty name and the default name reach the same stream") + require.Equal(t, []string{"step 1"}, + bodies(s.pollOwned(t, pinned, "progress", 0, false).GetRecords())) + + // A failed attempt with a retry to come is not the end of the activity. + failActivityAttempt(t, env, s, first) + require.False(t, s.describeOwned(t, owner, "").GetClosed(), + "the stream outlives an attempt that will be retried") + + second := pollActivityTask(t, env, s, taskQueue) + require.EqualValues(t, 2, second.GetAttempt()) + out, err = s.addOwned(t, owner, "", &streamlib.AddWorkflowMessagesInput{ + Records: attemptRecords(2, "token one again"), ProducerId: "worker#2", Sequence: 1, + }) + require.NoError(t, err) + require.Equal(t, int64(2), out.GetFirstOffset(), "the retry continues the same log") + + read := s.pollOwned(t, owner, "", 0, false) + require.Equal(t, []string{"token one", "token two", "token one again"}, bodies(read.GetRecords())) + var attempts []int64 + for _, r := range read.GetRecords() { + attempts = append(attempts, r.GetAttempt()) + } + require.Equal(t, []int64{1, 1, 2}, attempts, + "each record names its attempt, which is where a reader sees the retry take over") + + // A reader tailing the stream is released when the activity ends. + type result struct { + out *streamlib.PollMessagesOutput + err error + } + done := make(chan result, 1) + go func() { + resp, err := s.client.PollWorkflowMessages(s.ctx(), &streamlib.PollWorkflowMessagesRequest{ + FrontendRequest: &streamlib.PollWorkflowMessagesInput{ + Namespace: s.ns, Owner: owner, FromOffset: read.GetNextOffset(), WaitNewMessages: true, + }, + }) + done <- result{resp.GetFrontendResponse(), err} + }() + // Give the reader time to park, so the wake is what releases it. Nothing + // observable marks a parked reader, so a wait is the only option. + time.Sleep(500 * time.Millisecond) //nolint:forbidigo + completeActivityAttempt(t, env, s, second) + + select { + case r := <-done: + require.NoError(t, r.err) + require.True(t, r.out.GetClosed(), "the parked reader learns the activity ended") + require.Empty(t, r.out.GetRecords()) + case <-time.After(25 * time.Second): + t.Fatal("the parked reader was not released when the activity completed") + } + + // Ended, not gone: what was written stays readable, and nothing more can be + // added. + require.True(t, s.describeOwned(t, owner, "progress").GetClosed()) + require.Len(t, s.pollOwned(t, owner, "", 0, false).GetRecords(), 3) + _, err = s.addOwned(t, owner, "", &streamlib.AddWorkflowMessagesInput{ + Records: attemptRecords(2, "too late"), + }) + require.Equal(t, codes.FailedPrecondition, status.Code(err), "%v", err) + + // A run id that is not this activity's names nothing. + _, err = s.client.PollWorkflowMessages(s.ctx(), &streamlib.PollWorkflowMessagesRequest{ + FrontendRequest: &streamlib.PollWorkflowMessagesInput{ + Namespace: s.ns, + Owner: &streamlib.StreamOwner{ + Kind: streamlib.STREAM_OWNER_KIND_ACTIVITY, Id: activityID, RunId: uuid.NewString(), + }, + }, + }) + require.Equal(t, codes.NotFound, status.Code(err), "%v", err) +} + +// scheduleStreamingActivity starts a workflow whose first task schedules one +// activity, and returns the workflow's run id. +func scheduleStreamingActivity( + t *testing.T, env *testcore.TestEnv, s *streamTestEnv, workflowID, activityID string, + retry *commonpb.RetryPolicy, +) string { + t.Helper() + tq := &taskqueuepb.TaskQueue{Name: workflowID + "-tq", Kind: enumspb.TASK_QUEUE_KIND_NORMAL} + we, err := env.FrontendClient().StartWorkflowExecution(s.ctx(), + &workflowservice.StartWorkflowExecutionRequest{ + RequestId: uuid.NewString(), + Namespace: s.ns, + WorkflowId: workflowID, + WorkflowType: &commonpb.WorkflowType{Name: "activity-streams"}, + TaskQueue: tq, + WorkflowRunTimeout: durationpb.New(100 * time.Second), + WorkflowTaskTimeout: durationpb.New(10 * time.Second), + Identity: "tester", + }) + require.NoError(t, err) + + //nolint:staticcheck // SA1019: the raw poller is enough to schedule one activity. + poller := &testcore.TaskPoller{ + Client: env.FrontendClient(), + Namespace: s.ns, + TaskQueue: tq, + Identity: "tester", + WorkflowTaskHandler: func( + *workflowservice.PollWorkflowTaskQueueResponse, + ) ([]*commandpb.Command, error) { + return []*commandpb.Command{{ + CommandType: enumspb.COMMAND_TYPE_SCHEDULE_ACTIVITY_TASK, + Attributes: &commandpb.Command_ScheduleActivityTaskCommandAttributes{ + ScheduleActivityTaskCommandAttributes: &commandpb.ScheduleActivityTaskCommandAttributes{ + ActivityId: activityID, + ActivityType: &commonpb.ActivityType{Name: "streaming-activity"}, + TaskQueue: tq, + StartToCloseTimeout: durationpb.New(time.Minute), + RetryPolicy: retry, + }, + }, + }}, nil + }, + Logger: env.Logger, + T: t, + } + _, err = poller.PollAndProcessWorkflowTask() + require.NoError(t, err) + return we.GetRunId() +} + +// An activity a workflow scheduled owns streams of its own, apart from the +// workflow's. They are addressed as the workflow, the activity and a name, so +// an outside producer and reader never see where the server keeps them, and +// the workflow cannot reach them under its own names. +func TestStreamWorkflowActivityOwnsStreams(t *testing.T) { + env, s := newStreamActivityEnv(t) + + workflowID := "stream-wfa-" + uuid.NewString() + runID := scheduleStreamingActivity(t, env, s, workflowID, "model-call", nil) + pollActivityTask(t, env, s, workflowID+"-tq") + + owner := &streamlib.StreamOwner{ + Kind: streamlib.STREAM_OWNER_KIND_WORKFLOW_ACTIVITY, + Id: workflowID, + RunId: runID, + ActivityId: "model-call", + } + _, err := s.addOwned(t, owner, "", &streamlib.AddWorkflowMessagesInput{ + Records: attemptRecords(1, "token"), ProducerId: "worker#1", Sequence: 1, + }) + require.NoError(t, err) + _, err = s.addOwned(t, owner, "reasoning", &streamlib.AddWorkflowMessagesInput{ + Records: attemptRecords(1, "thinking"), + }) + require.NoError(t, err) + + require.Equal(t, []string{"token"}, bodies(s.pollOwned(t, owner, "", 0, false).GetRecords())) + require.Equal(t, []string{"thinking"}, + bodies(s.pollOwned(t, owner, "reasoning", 0, false).GetRecords())) + require.False(t, s.describeOwned(t, owner, "").GetClosed()) + + // The workflow's own default stream is a different stream. + workflowOwner := &streamlib.StreamOwner{ + Kind: streamlib.STREAM_OWNER_KIND_WORKFLOW, Id: workflowID, + } + require.Empty(t, s.pollOwned(t, workflowOwner, "", 0, false).GetRecords(), + "an activity's stream is not the workflow's") + + // Another activity of the same workflow has streams of its own too. + other := &streamlib.StreamOwner{ + Kind: streamlib.STREAM_OWNER_KIND_WORKFLOW_ACTIVITY, Id: workflowID, ActivityId: "other", + } + require.Empty(t, s.pollOwned(t, other, "", 0, false).GetRecords()) + + // The workflow kind may not name the part of the map its activities use. + _, err = s.addOwned(t, workflowOwner, "activity/model-call/output", + &streamlib.AddWorkflowMessagesInput{Records: streamMsgs("", "forged")}) + require.Equal(t, codes.InvalidArgument, status.Code(err), "%v", err) + + // The workflow fields and an owner together are ambiguous. + _, err = s.client.AddWorkflowMessages(s.ctx(), &streamlib.AddWorkflowMessagesRequest{ + FrontendRequest: &streamlib.AddWorkflowMessagesInput{ + Namespace: s.ns, WorkflowId: workflowID, Owner: owner, + Records: streamMsgs("", "which one"), + }, + }) + require.Equal(t, codes.InvalidArgument, status.Code(err), "%v", err) +} + +// The owned-stream calls still accept a workflow named by its id alone, which +// is the WORKFLOW owner spelled without one. +func TestStreamOwnerDefaultsToTheNamedWorkflow(t *testing.T) { + env, s := newStreamActivityEnv(t) + + workflowID := "stream-owner-default-" + uuid.NewString() + scheduleStreamingActivity(t, env, s, workflowID, "unused", nil) + + _, err := s.client.AddWorkflowMessages(s.ctx(), &streamlib.AddWorkflowMessagesRequest{ + FrontendRequest: &streamlib.AddWorkflowMessagesInput{ + Namespace: s.ns, WorkflowId: workflowID, + Records: []*streamlib.StreamRecord{{ + Body: &commonpb.Payload{Data: []byte("by id")}, + Kind: streampb.STREAM_RECORD_KIND_DATA, + }}, + }, + }) + require.NoError(t, err) + owner := &streamlib.StreamOwner{Kind: streamlib.STREAM_OWNER_KIND_WORKFLOW, Id: workflowID} + require.Equal(t, []string{"by id"}, bodies(s.pollOwned(t, owner, "", 0, false).GetRecords())) +} diff --git a/tests/stream_policy_test.go b/tests/stream_policy_test.go new file mode 100644 index 00000000000..a3d9318336d --- /dev/null +++ b/tests/stream_policy_test.go @@ -0,0 +1,65 @@ +package tests + +import ( + "testing" + + "github.com/stretchr/testify/require" + chasmstream "go.temporal.io/server/chasm/lib/stream" + streamlib "go.temporal.io/server/chasm/lib/stream/gen/streampb/v1" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/durationpb" +) + +// A create naming a stream that already exists answers in two ways a client +// tells apart without a describe: the same lifecycle is an idempotent retry, +// a different one is a policy change it has to make under a new id. +func TestStreamCreateOnAnExistingIdTellsARetryFromAPolicyChange(t *testing.T) { + s := newStreamTestEnv(t) + ctx := streamCtx(t) + const id = "stream-create-twice" + create := func(lifecycle *streamlib.StreamLifecycle) error { + _, err := s.client.CreateStream(ctx, &streamlib.CreateStreamRequest{ + FrontendRequest: &streamlib.CreateStreamInput{ + Namespace: s.ns, StreamId: id, Lifecycle: lifecycle, + }, + }) + return err + } + policy := &streamlib.StreamLifecycle{MaxItems: 4, MaxBytes: 1 << 20} + require.NoError(t, create(policy)) + + err := create(policy) + require.Equal(t, codes.AlreadyExists, status.Code(err), "%v", err) + + // The frontend settles an unset retention to the namespace's, so a repeat + // that spells the settled value out is still the same lifecycle. + settled, err := s.client.DescribeStream(ctx, &streamlib.DescribeStreamRequest{ + FrontendRequest: &streamlib.DescribeStreamInput{Namespace: s.ns, StreamId: id}, + }) + require.NoError(t, err) + spelledOut := &streamlib.StreamLifecycle{ + MaxItems: 4, MaxBytes: 1 << 20, + Retention: settled.GetFrontendResponse().GetState().GetLifecycle().GetRetention(), + } + err = create(spelledOut) + require.Equal(t, codes.AlreadyExists, status.Code(err), "%v", err) + + for name, changed := range map[string]*streamlib.StreamLifecycle{ + "more items": {MaxItems: 5, MaxBytes: 1 << 20}, + "no byte cap": {MaxItems: 4}, + "other retention": {MaxItems: 4, MaxBytes: 1 << 20, Retention: durationpb.New(1)}, + "no policy at all": nil, + } { + t.Run(name, func(t *testing.T) { + requireReason(t, create(changed), codes.FailedPrecondition, chasmstream.ReasonPolicyMismatch) + }) + } + + // The stream is untouched by any of it. + described, err := s.client.DescribeStream(ctx, &streamlib.DescribeStreamRequest{ + FrontendRequest: &streamlib.DescribeStreamInput{Namespace: s.ns, StreamId: id}, + }) + require.NoError(t, err) + require.Equal(t, int64(4), described.GetFrontendResponse().GetState().GetLifecycle().GetMaxItems()) +} diff --git a/tests/stream_start_test.go b/tests/stream_start_test.go new file mode 100644 index 00000000000..1eef8a41e4a --- /dev/null +++ b/tests/stream_start_test.go @@ -0,0 +1,280 @@ +package tests + +import ( + "testing" + "time" + + "github.com/google/uuid" + "github.com/stretchr/testify/require" + commonpb "go.temporal.io/api/common/v1" + streampb "go.temporal.io/api/stream/v1" + taskqueuepb "go.temporal.io/api/taskqueue/v1" + "go.temporal.io/api/workflowservice/v1" + chasmstream "go.temporal.io/server/chasm/lib/stream" + streamlib "go.temporal.io/server/chasm/lib/stream/gen/streampb/v1" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/durationpb" +) + +// A reader with no offset yet names where it starts, and the server resolves +// that in the read it serves, so the first poll cannot race with truncation. + +func (s *streamTestEnv) pollFrom( + t *testing.T, streamID string, start *streampb.StreamStartPosition, +) (*streamlib.PollMessagesOutput, error) { + t.Helper() + resp, err := s.client.PollMessages(s.ctx(), &streamlib.PollMessagesRequest{ + FrontendRequest: &streamlib.PollMessagesInput{ + Namespace: s.ns, StreamId: streamID, StartPosition: start, + }, + }) + return resp.GetFrontendResponse(), err +} + +func (s *streamTestEnv) pollOwnedFrom( + t *testing.T, owner *streamlib.StreamOwner, name string, start *streampb.StreamStartPosition, +) *streamlib.PollMessagesOutput { + t.Helper() + resp, err := s.client.PollWorkflowMessages(s.ctx(), &streamlib.PollWorkflowMessagesRequest{ + FrontendRequest: &streamlib.PollWorkflowMessagesInput{ + Namespace: s.ns, Owner: owner, StreamName: name, StartPosition: start, + }, + }) + require.NoError(t, err) + return resp.GetFrontendResponse() +} + +func TestStreamFirstPollStartPositions(t *testing.T) { + s := newStreamTestEnv(t) + ctx := streamCtx(t) + const id = "stream-start-positions" + s.create(ctx, t, id) + _, err := s.add(ctx, t, id, &streamlib.AddMessagesInput{ + Records: streamMsgs("", "a", "b", "c", "d", "e"), + }) + require.NoError(t, err) + _, err = s.client.TruncateStream(ctx, &streamlib.TruncateStreamRequest{ + FrontendRequest: &streamlib.TruncateStreamInput{ + Namespace: s.ns, StreamId: id, NewBaseOffset: 2, + }, + }) + require.NoError(t, err) + + for _, tc := range []struct { + name string + start *streampb.StreamStartPosition + want []string + }{ + {"earliest reads from the floor", chasmstream.Earliest(), []string{"c", "d", "e"}}, + {"last n", chasmstream.LastN(2), []string{"d", "e"}}, + {"last n past the floor", chasmstream.LastN(100), []string{"c", "d", "e"}}, + {"offset", chasmstream.AtOffset(3), []string{"d", "e"}}, + {"tail reads nothing yet", chasmstream.Tail(), []string{}}, + } { + t.Run(tc.name, func(t *testing.T) { + got, err := s.pollFrom(t, id, tc.start) + require.NoError(t, err) + require.Equal(t, tc.want, bodies(got.GetRecords())) + require.Equal(t, int64(5), got.GetNextOffset(), + "next_offset carries the resolved start for the polls after it") + }) + } + + _, err = s.pollFrom(t, id, chasmstream.AtOffset(0)) + require.ErrorContains(t, err, "truncated", "an absolute offset below the floor is refused") +} + +func TestStreamFirstPollRefusesAStartItCannotResolve(t *testing.T) { + s := newStreamTestEnv(t) + ctx := streamCtx(t) + const id = "stream-start-refused" + s.create(ctx, t, id) + + for _, start := range []*streampb.StreamStartPosition{ + {}, + chasmstream.LastN(0), + {Position: &streampb.StreamStartPosition_Earliest{Earliest: false}}, + } { + _, err := s.pollFrom(t, id, start) + require.Equal(t, codes.InvalidArgument, status.Code(err), "%v", err) + } + + // A reader that already has an offset resumes from it. + _, err := s.client.PollMessages(ctx, &streamlib.PollMessagesRequest{ + FrontendRequest: &streamlib.PollMessagesInput{ + Namespace: s.ns, StreamId: id, FromOffset: 1, StartPosition: chasmstream.Earliest(), + }, + }) + require.Equal(t, codes.InvalidArgument, status.Code(err), "%v", err) +} + +// A tail read that blocks resolves the head once, before it waits. Resolving +// it again after the wake would land on the new head and return nothing. +func TestStreamTailPollWaitsForTheNextAppend(t *testing.T) { + s := newStreamTestEnv(t) + ctx := streamCtx(t) + const id = "stream-start-tail-wait" + s.create(ctx, t, id) + _, err := s.add(ctx, t, id, &streamlib.AddMessagesInput{Records: streamMsgs("", "old")}) + require.NoError(t, err) + + type result struct { + out *streamlib.PollMessagesOutput + err error + } + done := make(chan result, 1) + go func() { + resp, err := s.client.PollMessages(ctx, &streamlib.PollMessagesRequest{ + FrontendRequest: &streamlib.PollMessagesInput{ + Namespace: s.ns, StreamId: id, StartPosition: chasmstream.Tail(), + WaitNewMessages: true, + }, + }) + done <- result{resp.GetFrontendResponse(), err} + }() + + // Appended until the parked poll takes one, since there is no signal for + // when it has parked. + deadline := time.After(25 * time.Second) + for { + _, err := s.add(ctx, t, id, &streamlib.AddMessagesInput{Records: streamMsgs("", "new")}) + require.NoError(t, err) + select { + case r := <-done: + require.NoError(t, r.err) + require.NotEmpty(t, r.out.GetRecords()) + for _, b := range bodies(r.out.GetRecords()) { + require.Equal(t, "new", b, "a tail read never returns what was there before it") + } + return + case <-time.After(500 * time.Millisecond): + case <-deadline: + t.Fatal("tail poll did not wake on append") + } + } +} + +func TestStreamActivityStreamStartPositions(t *testing.T) { + env, s := newStreamActivityEnv(t) + + activityID := "stream-start-saa-" + uuid.NewString() + taskQueue := activityID + "-tq" + _, err := env.FrontendClient().StartActivityExecution(s.ctx(), + &workflowservice.StartActivityExecutionRequest{ + Namespace: s.ns, + ActivityId: activityID, + ActivityType: &commonpb.ActivityType{Name: "streaming-activity"}, + TaskQueue: &taskqueuepb.TaskQueue{Name: taskQueue}, + StartToCloseTimeout: durationpb.New(time.Minute), + RequestId: uuid.NewString(), + }) + require.NoError(t, err) + owner := &streamlib.StreamOwner{Kind: streamlib.STREAM_OWNER_KIND_ACTIVITY, Id: activityID} + + // Nothing published yet reads as an empty stream at offset zero, whatever + // the position. + for _, start := range []*streampb.StreamStartPosition{ + chasmstream.Earliest(), chasmstream.Tail(), chasmstream.LastN(3), + } { + empty := s.pollOwnedFrom(t, owner, "", start) + require.Empty(t, empty.GetRecords()) + require.Equal(t, int64(0), empty.GetNextOffset()) + } + + pollActivityTask(t, env, s, taskQueue) + _, err = s.addOwned(t, owner, "", &streamlib.AddWorkflowMessagesInput{ + Records: attemptRecords(1, "one", "two", "three"), + }) + require.NoError(t, err) + + require.Equal(t, []string{"one", "two", "three"}, + bodies(s.pollOwnedFrom(t, owner, "", chasmstream.Earliest()).GetRecords())) + require.Equal(t, []string{"three"}, + bodies(s.pollOwnedFrom(t, owner, "", chasmstream.LastN(1)).GetRecords())) + tail := s.pollOwnedFrom(t, owner, "", chasmstream.Tail()) + require.Empty(t, tail.GetRecords()) + require.Equal(t, int64(3), tail.GetNextOffset()) +} + +func TestStreamWorkflowActivityStreamStartPositions(t *testing.T) { + env, s := newStreamActivityEnv(t) + + workflowID := "stream-start-wfa-" + uuid.NewString() + runID := scheduleStreamingActivity(t, env, s, workflowID, "model-call", nil) + pollActivityTask(t, env, s, workflowID+"-tq") + owner := &streamlib.StreamOwner{ + Kind: streamlib.STREAM_OWNER_KIND_WORKFLOW_ACTIVITY, + Id: workflowID, + RunId: runID, + ActivityId: "model-call", + } + _, err := s.addOwned(t, owner, "", &streamlib.AddWorkflowMessagesInput{ + Records: attemptRecords(1, "a", "b", "c"), + }) + require.NoError(t, err) + + require.Equal(t, []string{"b", "c"}, + bodies(s.pollOwnedFrom(t, owner, "", chasmstream.LastN(2)).GetRecords())) + require.Equal(t, []string{"a", "b", "c"}, + bodies(s.pollOwnedFrom(t, owner, "", chasmstream.Earliest()).GetRecords())) +} + +// A subscription records the offset its position resolved to, so the first +// range a workflow task carries starts from a fact. +func TestStreamSubscribeWorkflowResolvesItsStartPosition(t *testing.T) { + env, s := newStreamActivityEnv(t) + ctx := streamCtx(t) + workflowID := "stream-start-sub-" + uuid.NewString() + scheduleStreamingActivity(t, env, s, workflowID, "unused", nil) + + workflowOwner := &streamlib.StreamOwner{Kind: streamlib.STREAM_OWNER_KIND_WORKFLOW, Id: workflowID} + _, err := s.addOwned(t, workflowOwner, "inputs", &streamlib.AddWorkflowMessagesInput{ + Records: streamMsgs("", "a", "b", "c", "d"), + }) + require.NoError(t, err) + + subscribe := func(in *streamlib.SubscribeWorkflowInput) (int64, error) { + in.Namespace, in.WorkflowId = s.ns, workflowID + resp, err := s.client.SubscribeWorkflow(ctx, &streamlib.SubscribeWorkflowRequest{ + FrontendRequest: in, + }) + return resp.GetFrontendResponse().GetStartOffset(), err + } + + start, err := subscribe(&streamlib.SubscribeWorkflowInput{ + StreamName: "inputs", StartPosition: chasmstream.LastN(1), + }) + require.NoError(t, err) + require.Equal(t, int64(3), start) + + // A standalone stream truncated below its head: earliest is its floor, + // where offset zero is refused. + const id = "stream-start-sub-standalone" + s.create(ctx, t, id) + _, err = s.add(ctx, t, id, &streamlib.AddMessagesInput{Records: streamMsgs("", "x", "y", "z")}) + require.NoError(t, err) + _, err = s.client.TruncateStream(ctx, &streamlib.TruncateStreamRequest{ + FrontendRequest: &streamlib.TruncateStreamInput{ + Namespace: s.ns, StreamId: id, NewBaseOffset: 2, + }, + }) + require.NoError(t, err) + _, err = subscribe(&streamlib.SubscribeWorkflowInput{StreamId: id}) + require.ErrorContains(t, err, "below the stream's floor") + start, err = subscribe(&streamlib.SubscribeWorkflowInput{ + StreamId: id, StartPosition: chasmstream.Earliest(), + }) + require.NoError(t, err) + require.Equal(t, int64(2), start) + + _, err = subscribe(&streamlib.SubscribeWorkflowInput{ + StreamName: "other", StartOffset: 1, StartPosition: chasmstream.Tail(), + }) + require.Equal(t, codes.InvalidArgument, status.Code(err), "%v", err) + + // The negative offset that used to mean the head names the field to use. + _, err = subscribe(&streamlib.SubscribeWorkflowInput{StreamName: "other", StartOffset: -1}) + require.Equal(t, codes.InvalidArgument, status.Code(err), "%v", err) + require.ErrorContains(t, err, "start_position.tail") +} diff --git a/tests/stream_test.go b/tests/stream_test.go new file mode 100644 index 00000000000..1469137694c --- /dev/null +++ b/tests/stream_test.go @@ -0,0 +1,747 @@ +package tests + +import ( + "context" + "fmt" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/require" + commonpb "go.temporal.io/api/common/v1" + streampb "go.temporal.io/api/stream/v1" + chasmstream "go.temporal.io/server/chasm/lib/stream" + streamlib "go.temporal.io/server/chasm/lib/stream/gen/streampb/v1" + "go.temporal.io/server/common/testing/await" + "go.temporal.io/server/tests/testcore" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/status" +) + +// requireReason checks a refusal the way an SDK reads it off a raw gRPC +// client: by status code and by the reason token in the message prefix. +func requireReason(t *testing.T, err error, code codes.Code, reason string) { + t.Helper() + require.Equal(t, code, status.Code(err), "%v", err) + require.Equal(t, reason, chasmstream.ReasonOf(status.Convert(err).Message()), "%v", err) +} + +// End-to-end coverage of the native stream path: append through the frontend, +// read back by offset, and the lifecycle transitions around it. This is the +// path the benchmark will eventually compare against the Signal-and-Update +// baseline in streaming_baseline_test.go. + +const streamMaxBatch = chasmstream.MaxRecordsPerBatch + +type streamTestEnv struct { + env *testcore.TestEnv + client streamlib.StreamServiceClient + ns string + + // Guards cleanup: a test that races outside producers against the + // workflow asks for contexts from several goroutines at once. + mu sync.Mutex + cleanup []func() +} + +func newStreamTestEnv(t *testing.T) *streamTestEnv { + return newStreamTestEnvFrom(t, testcore.NewEnv(t)) +} + +// newStreamTestEnvFrom lets a test that needs its own env, for example one +// driving the raw task poller, still reach the stream API. +func newStreamTestEnvFrom(t *testing.T, env *testcore.TestEnv) *streamTestEnv { + + conn, err := grpc.NewClient(env.FrontendGRPCAddress(), + grpc.WithTransportCredentials(insecure.NewCredentials())) + require.NoError(t, err) + t.Cleanup(func() { _ = conn.Close() }) + + env2 := &streamTestEnv{ + env: env, client: streamlib.NewStreamServiceClient(conn), ns: env.Namespace().String(), + } + t.Cleanup(func() { + env2.mu.Lock() + defer env2.mu.Unlock() + for _, c := range env2.cleanup { + c() + } + }) + + return env2 +} + +func (s *streamTestEnv) ctx() context.Context { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + s.mu.Lock() + s.cleanup = append(s.cleanup, cancel) + s.mu.Unlock() + return ctx +} + +func (s *streamTestEnv) create(ctx context.Context, t *testing.T, streamID string) { + t.Helper() + _, err := s.client.CreateStream(ctx, &streamlib.CreateStreamRequest{ + FrontendRequest: &streamlib.CreateStreamInput{Namespace: s.ns, StreamId: streamID}, + }) + require.NoError(t, err) +} + +func (s *streamTestEnv) add( + ctx context.Context, t *testing.T, streamID string, in *streamlib.AddMessagesInput, +) (*streamlib.AddMessagesOutput, error) { + t.Helper() + in.Namespace = s.ns + in.StreamId = streamID + resp, err := s.client.AddMessages(ctx, &streamlib.AddMessagesRequest{FrontendRequest: in}) + if err != nil { + return nil, err + } + return resp.GetFrontendResponse(), nil +} + +func (s *streamTestEnv) poll( + ctx context.Context, t *testing.T, streamID string, from int64, topics ...string, +) *streamlib.PollMessagesOutput { + t.Helper() + resp, err := s.client.PollMessages(ctx, &streamlib.PollMessagesRequest{ + FrontendRequest: &streamlib.PollMessagesInput{ + Namespace: s.ns, StreamId: streamID, FromOffset: from, Topics: topics, + }, + }) + require.NoError(t, err) + return resp.GetFrontendResponse() +} + +func streamMsgs(topic string, bodies ...string) []*streamlib.StreamRecord { + out := make([]*streamlib.StreamRecord, len(bodies)) + for i, b := range bodies { + out[i] = &streamlib.StreamRecord{ + Body: &commonpb.Payload{Data: []byte(b)}, + Topic: topic, + Kind: streampb.STREAM_RECORD_KIND_DATA, + } + } + return out +} + +func bodies(msgs []*streamlib.StreamRecord) []string { + out := make([]string, len(msgs)) + for i, m := range msgs { + out[i] = string(m.GetBody().GetData()) + } + return out +} + +func streamCtx(t *testing.T) context.Context { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + t.Cleanup(cancel) + return ctx +} + +func TestStreamAppendAndRead(t *testing.T) { + s := newStreamTestEnv(t) + ctx := streamCtx(t) + const id = "stream-append-read" + s.create(ctx, t, id) + + first, err := s.add(ctx, t, id, + &streamlib.AddMessagesInput{Records: streamMsgs("", "a", "b", "c")}) + require.NoError(t, err) + require.Equal(t, int64(0), first.GetFirstOffset()) + require.Equal(t, int64(3), first.GetNextOffset()) + + second, err := s.add(ctx, t, id, &streamlib.AddMessagesInput{Records: streamMsgs("", "d")}) + require.NoError(t, err) + require.Equal(t, int64(3), second.GetFirstOffset()) + + all := s.poll(ctx, t, id, 0) + require.Equal(t, []string{"a", "b", "c", "d"}, bodies(all.GetRecords())) + require.Equal(t, int64(4), all.GetNextOffset()) + require.Equal(t, int64(4), all.GetHeadOffset()) + require.False(t, all.GetClosed()) + + // A reader owns its cursor, so resuming mid-stream is just another read. + tail := s.poll(ctx, t, id, 2) + require.Equal(t, []string{"c", "d"}, bodies(tail.GetRecords())) + + // Caught up returns empty rather than erroring. + caughtUp := s.poll(ctx, t, id, 4) + require.Empty(t, caughtUp.GetRecords()) + require.Equal(t, int64(4), caughtUp.GetNextOffset()) +} + +func TestStreamManyReadersAreIndependent(t *testing.T) { + s := newStreamTestEnv(t) + ctx := streamCtx(t) + const id = "stream-many-readers" + s.create(ctx, t, id) + + _, err := s.add(ctx, t, id, &streamlib.AddMessagesInput{Records: streamMsgs("", "a", "b")}) + require.NoError(t, err) + + // No durable per-subscriber state, so reader count is not a state-machine + // concern and there is no equivalent of the 10-subscriber ceiling the + // Signal-and-Update pattern hits. + for range 25 { + got := s.poll(ctx, t, id, 0) + require.Equal(t, []string{"a", "b"}, bodies(got.GetRecords())) + } +} + +func TestStreamProducerDedup(t *testing.T) { + s := newStreamTestEnv(t) + ctx := streamCtx(t) + const id = "stream-dedup" + s.create(ctx, t, id) + + in := &streamlib.AddMessagesInput{ + Records: streamMsgs("", "a", "b"), ProducerId: "p1", Sequence: 1, + } + first, err := s.add(ctx, t, id, in) + require.NoError(t, err) + require.False(t, first.GetDeduplicated()) + + retry, err := s.add(ctx, t, id, &streamlib.AddMessagesInput{ + Records: streamMsgs("", "a", "b"), ProducerId: "p1", Sequence: 1, + }) + require.NoError(t, err) + require.True(t, retry.GetDeduplicated()) + require.Equal(t, first.GetFirstOffset(), retry.GetFirstOffset()) + + // The retry must not have appended a second copy. + got := s.poll(ctx, t, id, 0) + require.Equal(t, []string{"a", "b"}, bodies(got.GetRecords())) + + // Same sequence with different content is a client bug. Returning the + // recorded offsets would report success while dropping the data. + _, err = s.add(ctx, t, id, &streamlib.AddMessagesInput{ + Records: streamMsgs("", "different"), ProducerId: "p1", Sequence: 1, + }) + require.ErrorContains(t, err, "different content") + requireReason(t, err, codes.FailedPrecondition, chasmstream.ReasonProducerConflict) + + // The table keeps the producer's most recent sequence only, so once a later + // one lands an earlier one cannot be answered and is refused as stale. + _, err = s.add(ctx, t, id, &streamlib.AddMessagesInput{ + Records: streamMsgs("", "c"), ProducerId: "p1", Sequence: 2, + }) + require.NoError(t, err) + _, err = s.add(ctx, t, id, &streamlib.AddMessagesInput{ + Records: streamMsgs("", "a", "b"), ProducerId: "p1", Sequence: 1, + }) + requireReason(t, err, codes.FailedPrecondition, chasmstream.ReasonProducerStaleSequence) + require.Equal(t, []string{"a", "b", "c"}, bodies(s.poll(ctx, t, id, 0).GetRecords())) +} + +func TestStreamTopicFilter(t *testing.T) { + s := newStreamTestEnv(t) + ctx := streamCtx(t) + const id = "stream-topics" + s.create(ctx, t, id) + + _, err := s.add(ctx, t, id, &streamlib.AddMessagesInput{Records: streamMsgs("tokens", "t1")}) + require.NoError(t, err) + _, err = s.add(ctx, t, id, &streamlib.AddMessagesInput{Records: streamMsgs("tools", "x1")}) + require.NoError(t, err) + _, err = s.add(ctx, t, id, &streamlib.AddMessagesInput{Records: streamMsgs("tokens", "t2")}) + require.NoError(t, err) + + got := s.poll(ctx, t, id, 0, "tokens") + require.Equal(t, []string{"t1", "t2"}, bodies(got.GetRecords())) + // Offsets are global, so a filtered read still advances past what it skipped. + require.Equal(t, int64(3), got.GetNextOffset()) +} + +func TestStreamFinishWritingIsPerProducer(t *testing.T) { + s := newStreamTestEnv(t) + ctx := streamCtx(t) + const id = "stream-finish" + s.create(ctx, t, id) + + _, err := s.client.FinishWriting(ctx, &streamlib.FinishWritingRequest{ + FrontendRequest: &streamlib.FinishWritingInput{ + Namespace: s.ns, StreamId: id, ProducerId: "p1", + }, + }) + require.NoError(t, err) + + _, err = s.add(ctx, t, id, &streamlib.AddMessagesInput{ + Records: streamMsgs("", "a"), ProducerId: "p1", Sequence: 1, + }) + require.Error(t, err) + + // Finishing is per-producer, not a close, so others carry on. + _, err = s.add(ctx, t, id, &streamlib.AddMessagesInput{ + Records: streamMsgs("", "b"), ProducerId: "p2", Sequence: 1, + }) + require.NoError(t, err) +} + +func TestStreamCloseAndTruncate(t *testing.T) { + s := newStreamTestEnv(t) + ctx := streamCtx(t) + const id = "stream-lifecycle" + s.create(ctx, t, id) + + _, err := s.add(ctx, t, id, &streamlib.AddMessagesInput{Records: streamMsgs("", "a", "b", "c")}) + require.NoError(t, err) + + _, err = s.client.TruncateStream(ctx, &streamlib.TruncateStreamRequest{ + FrontendRequest: &streamlib.TruncateStreamInput{ + Namespace: s.ns, StreamId: id, NewBaseOffset: 1, + }, + }) + require.NoError(t, err) + + // A reader below the floor gets a distinguishable error carrying the floor, + // so it can jump forward rather than fail. + _, err = s.client.PollMessages(ctx, &streamlib.PollMessagesRequest{ + FrontendRequest: &streamlib.PollMessagesInput{Namespace: s.ns, StreamId: id, FromOffset: 0}, + }) + require.ErrorContains(t, err, "truncated") + requireReason(t, err, codes.FailedPrecondition, chasmstream.ReasonCursorBelowFloor) + + _, err = s.client.CloseStream(ctx, &streamlib.CloseStreamRequest{ + FrontendRequest: &streamlib.CloseStreamInput{Namespace: s.ns, StreamId: id}, + }) + require.NoError(t, err) + + _, err = s.add(ctx, t, id, &streamlib.AddMessagesInput{Records: streamMsgs("", "d")}) + requireReason(t, err, codes.FailedPrecondition, chasmstream.ReasonStreamClosed) + + // Closed is a state a reader observes, not an error, and the data stays + // readable rather than requiring a shutdown handshake with the producer. + got := s.poll(ctx, t, id, 1) + require.True(t, got.GetClosed()) + require.Equal(t, []string{"b", "c"}, bodies(got.GetRecords())) +} + +func TestStreamReadStartingInsideABatch(t *testing.T) { + s := newStreamTestEnv(t) + ctx := streamCtx(t) + const id = "stream-mid-batch" + s.create(ctx, t, id) + + // One batch covering 0..2, a second covering 3. A node is addressed by the + // first offset of its batch, so reading from 2 has to find the node that + // contains it rather than the node whose ID equals it. Getting that wrong + // silently drops the messages before the boundary. + _, err := s.add(ctx, t, id, &streamlib.AddMessagesInput{Records: streamMsgs("", "a", "b", "c")}) + require.NoError(t, err) + _, err = s.add(ctx, t, id, &streamlib.AddMessagesInput{Records: streamMsgs("", "d")}) + require.NoError(t, err) + + for from, want := range map[int64][]string{ + 0: {"a", "b", "c", "d"}, + 1: {"b", "c", "d"}, + 2: {"c", "d"}, + 3: {"d"}, + } { + got := s.poll(ctx, t, id, from) + require.Equal(t, want, bodies(got.GetRecords()), "reading from offset %d", from) + require.Equal(t, int64(4), got.GetNextOffset()) + } +} + +func TestStreamBatchSizeIsBounded(t *testing.T) { + s := newStreamTestEnv(t) + ctx := streamCtx(t) + const id = "stream-batch-bound" + s.create(ctx, t, id) + + // The bound is not only admission control: it bounds how far a read has to + // step back to find the node containing an arbitrary offset. + tooMany := make([]string, streamMaxBatch+1) + for i := range tooMany { + tooMany[i] = "x" + } + _, err := s.add(ctx, t, id, &streamlib.AddMessagesInput{Records: streamMsgs("", tooMany...)}) + require.ErrorContains(t, err, "exceeds the limit") +} + +func (s *streamTestEnv) pollWait( + ctx context.Context, streamID string, from int64, +) (*streamlib.PollMessagesOutput, error) { + resp, err := s.client.PollMessages(ctx, &streamlib.PollMessagesRequest{ + FrontendRequest: &streamlib.PollMessagesInput{ + Namespace: s.ns, StreamId: streamID, FromOffset: from, WaitNewMessages: true, + }, + }) + if err != nil { + return nil, err + } + return resp.GetFrontendResponse(), nil +} + +func TestStreamLongPollWakesOnAppend(t *testing.T) { + s := newStreamTestEnv(t) + ctx := streamCtx(t) + const id = "stream-longpoll-append" + s.create(ctx, t, id) + + type result struct { + out *streamlib.PollMessagesOutput + err error + } + done := make(chan result, 1) + go func() { + out, err := s.pollWait(ctx, id, 0) + done <- result{out, err} + }() + + // The poll is parked on an empty stream; the append is what releases it. + _, err := s.add(ctx, t, id, &streamlib.AddMessagesInput{Records: streamMsgs("", "a")}) + require.NoError(t, err) + + select { + case r := <-done: + require.NoError(t, r.err) + require.Equal(t, []string{"a"}, bodies(r.out.GetRecords())) + require.Equal(t, int64(1), r.out.GetNextOffset()) + case <-time.After(25 * time.Second): + t.Fatal("long poll did not wake on append") + } +} + +func TestStreamLongPollWakesOnClose(t *testing.T) { + s := newStreamTestEnv(t) + ctx := streamCtx(t) + const id = "stream-longpoll-close" + s.create(ctx, t, id) + + done := make(chan *streamlib.PollMessagesOutput, 1) + go func() { + out, err := s.pollWait(ctx, id, 0) + if err == nil { + done <- out + } + }() + + _, err := s.client.CloseStream(ctx, &streamlib.CloseStreamRequest{ + FrontendRequest: &streamlib.CloseStreamInput{Namespace: s.ns, StreamId: id}, + }) + require.NoError(t, err) + + // Closing has to release a parked reader, otherwise a consumer of a + // finished stream waits out the full timeout for no reason. + select { + case out := <-done: + require.True(t, out.GetClosed()) + require.Empty(t, out.GetRecords()) + case <-time.After(25 * time.Second): + t.Fatal("long poll did not wake on close") + } +} + +func TestStreamLongPollReturnsEmptyOnTimeout(t *testing.T) { + s := newStreamTestEnv(t) + ctx := streamCtx(t) + const id = "stream-longpoll-timeout" + s.create(ctx, t, id) + + // A timeout is an empty response, not an error: the caller polls again + // rather than distinguishing a quiet stream from a failure. + start := time.Now() + out, err := s.pollWait(ctx, id, 0) + require.NoError(t, err) + require.Empty(t, out.GetRecords()) + require.Equal(t, int64(0), out.GetNextOffset()) + require.False(t, out.GetClosed()) + require.Greater(t, time.Since(start), 5*time.Second, + "the poll should have parked, not returned immediately") +} + +func TestStreamLongPollReturnsImmediatelyWhenBehind(t *testing.T) { + s := newStreamTestEnv(t) + ctx := streamCtx(t) + const id = "stream-longpoll-behind" + s.create(ctx, t, id) + + _, err := s.add(ctx, t, id, &streamlib.AddMessagesInput{Records: streamMsgs("", "a", "b")}) + require.NoError(t, err) + + // Waiting is only for a reader that is caught up. One that is behind must + // not be parked behind data that already exists. + start := time.Now() + out, err := s.pollWait(ctx, id, 0) + require.NoError(t, err) + require.Equal(t, []string{"a", "b"}, bodies(out.GetRecords())) + require.Less(t, time.Since(start), 5*time.Second) +} + +// A blocking poll on a standalone stream id that names nothing yet parks until +// the stream exists and then reads it, so a reader can attach before the +// producer's first write without racing the create. +func TestStreamLongPollWaitsForTheStreamToBeCreated(t *testing.T) { + s := newStreamTestEnv(t) + ctx := streamCtx(t) + const id = "stream-longpoll-create" + + type outcome struct { + out *streamlib.PollMessagesOutput + err error + took time.Duration + } + done := make(chan outcome, 1) + started := time.Now() + go func() { + out, err := s.pollWait(ctx, id, 0) + done <- outcome{out: out, err: err, took: time.Since(started)} + }() + + // Long enough for the poll to have found nothing at least once. There is + // nothing to await: a parked poll gives no signal that it has parked. + const createAfter = 600 * time.Millisecond + time.Sleep(createAfter) //nolint:forbidigo + s.create(ctx, t, id) + _, err := s.add(ctx, t, id, &streamlib.AddMessagesInput{Records: streamMsgs("", "first")}) + require.NoError(t, err) + + got := <-done + require.NoError(t, got.err) + require.Equal(t, []string{"first"}, bodies(got.out.GetRecords())) + require.GreaterOrEqual(t, got.took, createAfter, "the poll parked rather than failing") +} + +func TestStreamPollOnAMissingStreamIsNotFoundUnlessItWaits(t *testing.T) { + s := newStreamTestEnv(t) + ctx := streamCtx(t) + const id = "stream-missing" + + // Not asked to wait: the answer comes at once. + _, err := s.client.PollMessages(ctx, &streamlib.PollMessagesRequest{ + FrontendRequest: &streamlib.PollMessagesInput{Namespace: s.ns, StreamId: id, FromOffset: 0}, + }) + require.Equal(t, codes.NotFound, status.Code(err), "%v", err) + + // Asked to wait: the poll parks for the caller's budget, then gives the + // same answer. The budget is the caller's deadline less the long-poll + // buffer, so a short deadline keeps the test short. + short, cancel := context.WithTimeout(ctx, chasmstream.LongPollBuffer+time.Second) + defer cancel() + started := time.Now() + _, err = s.pollWait(short, id, 0) + require.Equal(t, codes.NotFound, status.Code(err), "%v", err) + require.GreaterOrEqual(t, time.Since(started), 500*time.Millisecond) + + // Pinned to a run there is nothing to wait for: a stream created later + // would be another run. + _, err = s.client.PollMessages(ctx, &streamlib.PollMessagesRequest{ + FrontendRequest: &streamlib.PollMessagesInput{ + Namespace: s.ns, StreamId: id, RunId: "00000000-0000-0000-0000-000000000000", + WaitNewMessages: true, + }, + }) + require.Equal(t, codes.NotFound, status.Code(err), "%v", err) +} + +func TestStreamCapTruncatesAndReclaims(t *testing.T) { + s := newStreamTestEnv(t) + ctx := streamCtx(t) + const id = "stream-cap" + + _, err := s.client.CreateStream(ctx, &streamlib.CreateStreamRequest{ + FrontendRequest: &streamlib.CreateStreamInput{ + Namespace: s.ns, StreamId: id, + Lifecycle: &streamlib.StreamLifecycle{MaxItems: 4}, + }, + }) + require.NoError(t, err) + + for _, batch := range [][]string{{"a", "b"}, {"c", "d"}, {"e", "f"}} { + _, err := s.add(ctx, t, id, &streamlib.AddMessagesInput{Records: streamMsgs("", batch...)}) + require.NoError(t, err) + } + + // Six appended against a cap of four, so the floor advanced without anyone + // asking. Reading from the floor still works and returns exactly what the + // cap retained. + got := s.poll(ctx, t, id, 2) + require.Equal(t, []string{"c", "d", "e", "f"}, bodies(got.GetRecords())) + + // Below the floor is a distinguishable error, not silence. + _, err = s.client.PollMessages(ctx, &streamlib.PollMessagesRequest{ + FrontendRequest: &streamlib.PollMessagesInput{Namespace: s.ns, StreamId: id, FromOffset: 0}, + }) + require.ErrorContains(t, err, "truncated") + requireReason(t, err, codes.FailedPrecondition, chasmstream.ReasonCursorBelowFloor) +} + +func TestStreamClosedStaysReadable(t *testing.T) { + s := newStreamTestEnv(t) + ctx := streamCtx(t) + const id = "stream-closed-readable" + s.create(ctx, t, id) + + _, err := s.add(ctx, t, id, &streamlib.AddMessagesInput{Records: streamMsgs("", "a", "b")}) + require.NoError(t, err) + _, err = s.client.CloseStream(ctx, &streamlib.CloseStreamRequest{ + FrontendRequest: &streamlib.CloseStreamInput{Namespace: s.ns, StreamId: id}, + }) + require.NoError(t, err) + + // Close seals, it does not delete. A consumer can finish draining without + // coordinating a shutdown with the producer, which is the handshake the + // signal-based implementation forces today. + got := s.poll(ctx, t, id, 0) + require.Equal(t, []string{"a", "b"}, bodies(got.GetRecords())) + require.True(t, got.GetClosed()) + + desc, err := s.client.DescribeStream(ctx, &streamlib.DescribeStreamRequest{ + FrontendRequest: &streamlib.DescribeStreamInput{Namespace: s.ns, StreamId: id}, + }) + require.NoError(t, err) + require.NotNil(t, desc.GetFrontendResponse().GetState().GetCloseTime()) +} + +func TestStreamListStreams(t *testing.T) { + s := newStreamTestEnv(t) + ctx := streamCtx(t) + + created := []string{"list-a", "list-b", "list-c"} + for _, id := range created { + s.create(ctx, t, id) + } + + // Visibility is written by a task after the create commits, so this is + // eventually consistent by design rather than by accident. + await.RequireTrue(t, func() bool { + resp, err := s.client.ListStreams(ctx, &streamlib.ListStreamsRequest{ + FrontendRequest: &streamlib.ListStreamsInput{Namespace: s.ns}, + }) + if err != nil { + return false + } + found := make(map[string]bool) + for _, e := range resp.GetFrontendResponse().GetStreams() { + found[e.GetStreamId()] = true + } + for _, id := range created { + if !found[id] { + return false + } + } + return true + }, 20*time.Second, 250*time.Millisecond) +} + +// A capped poll must not read the whole stream to answer. The read is clipped +// to the offsets the caller can be given, so a reader asking for one message +// off a long stream does not pull the rest into the history host on its way. +func TestStreamPollReadsOnlyWhatItReturns(t *testing.T) { + s := newStreamTestEnv(t) + ctx := streamCtx(t) + const id = "stream-poll-cap" + s.create(ctx, t, id) + + for i := range 40 { + _, err := s.add(ctx, t, id, &streamlib.AddMessagesInput{ + Records: streamMsgs("", fmt.Sprintf("m%d", i)), + }) + require.NoError(t, err) + } + + got := s.pollMax(ctx, t, id, 0, 1) + require.Equal(t, []string{"m0"}, bodies(got.GetRecords())) + require.Equal(t, int64(1), got.GetNextOffset(), "a capped read advances only over what it gave") + require.Equal(t, int64(40), got.GetHeadOffset(), "the frontier is still reported in full") + + // Paging from there covers the rest, so the cap trims the read and not the stream. + got = s.pollMax(ctx, t, id, got.GetNextOffset(), 10) + require.Equal(t, []string{"m1", "m2", "m3", "m4", "m5", "m6", "m7", "m8", "m9", "m10"}, + bodies(got.GetRecords())) + require.Equal(t, int64(11), got.GetNextOffset()) + + // A filter finding nothing in the page still has to leave the reader able to + // go on, and it must stop at the page rather than scanning to the head. + got = s.pollMaxTopics(ctx, t, id, 0, 5, "nothing-matches-this") + require.Empty(t, got.GetRecords()) + require.Equal(t, int64(5), got.GetNextOffset(), + "a filtered page advanced past its own bound, so the read ran to the head") +} + +// A stream id can be reused. The bytes belong to the execution, not to the +// name, so a reader of the new stream must never be served the old one's. +func TestStreamPollAfterIdIsReusedServesTheNewStream(t *testing.T) { + s := newStreamTestEnv(t) + ctx := streamCtx(t) + const id = "stream-reused-id" + + s.create(ctx, t, id) + _, err := s.add(ctx, t, id, &streamlib.AddMessagesInput{Records: streamMsgs("", "old")}) + require.NoError(t, err) + // Read it back so the bytes are certain to be cached before the id is reused. + require.Equal(t, []string{"old"}, bodies(s.poll(ctx, t, id, 0).GetRecords())) + + _, err = s.client.DeleteStream(ctx, &streamlib.DeleteStreamRequest{ + FrontendRequest: &streamlib.DeleteStreamInput{Namespace: s.ns, StreamId: id}, + }) + require.NoError(t, err) + + s.create(ctx, t, id) + _, err = s.add(ctx, t, id, &streamlib.AddMessagesInput{Records: streamMsgs("", "new")}) + require.NoError(t, err) + + got := s.poll(ctx, t, id, 0) + require.Equal(t, []string{"new"}, bodies(got.GetRecords()), + "a reused id served bytes from the deleted stream") +} + +// A filtered read hands back messages whose offsets are not contiguous, which +// is why the offset rides on the message. A reader cannot count them. +func TestStreamFilteredReadReportsRealOffsets(t *testing.T) { + s := newStreamTestEnv(t) + ctx := streamCtx(t) + const id = "stream-filtered-offsets" + s.create(ctx, t, id) + + for i, topic := range []string{"a", "b", "a", "b", "a"} { + _, err := s.add(ctx, t, id, &streamlib.AddMessagesInput{ + Records: streamMsgs(topic, fmt.Sprintf("m%d", i)), + }) + require.NoError(t, err) + } + + got := s.pollMaxTopics(ctx, t, id, 0, 0, "a") + require.Equal(t, []string{"m0", "m2", "m4"}, bodies(got.GetRecords())) + require.Equal(t, []int64{0, 2, 4}, offsets(got.GetRecords())) +} + +func (s *streamTestEnv) pollMaxTopics( + ctx context.Context, t *testing.T, streamID string, from int64, maxMessages int32, + topics ...string, +) *streamlib.PollMessagesOutput { + t.Helper() + resp, err := s.client.PollMessages(ctx, &streamlib.PollMessagesRequest{ + FrontendRequest: &streamlib.PollMessagesInput{ + Namespace: s.ns, StreamId: streamID, FromOffset: from, + MaxMessages: maxMessages, Topics: topics, + }, + }) + require.NoError(t, err) + return resp.GetFrontendResponse() +} + +func (s *streamTestEnv) pollMax( + ctx context.Context, t *testing.T, streamID string, from int64, maxMessages int32, +) *streamlib.PollMessagesOutput { + t.Helper() + resp, err := s.client.PollMessages(ctx, &streamlib.PollMessagesRequest{ + FrontendRequest: &streamlib.PollMessagesInput{ + Namespace: s.ns, StreamId: streamID, FromOffset: from, MaxMessages: maxMessages, + }, + }) + require.NoError(t, err) + return resp.GetFrontendResponse() +} + +func offsets(msgs []*streamlib.StreamRecord) []int64 { + out := make([]int64, len(msgs)) + for i, m := range msgs { + out[i] = m.GetOffset() + } + return out +} diff --git a/tests/testcore/dynamic_config_overrides.go b/tests/testcore/dynamic_config_overrides.go index 6b7728aa46c..2af671b2f06 100644 --- a/tests/testcore/dynamic_config_overrides.go +++ b/tests/testcore/dynamic_config_overrides.go @@ -3,6 +3,7 @@ package testcore import ( "time" + "go.temporal.io/server/chasm/lib/stream" "go.temporal.io/server/common/dynamicconfig" "go.temporal.io/server/common/persistence/visibility" "go.temporal.io/server/service/history/hsm/nexusoperations" @@ -86,5 +87,9 @@ var ( // exercise the percent gate can override per-test. dynamicconfig.CHASMSchedulerCreationRolloutPercent.Key(): 100, dynamicconfig.CHASMSchedulerMigrationRolloutPercent.Key(): 100, + + // Streams are off by default in a deployment. The functional suites + // exercise them, so they are on here. + stream.EnabledSetting.Key(): true, } )