From e5feee86a9d2eb39a5875442d7e1de76c3305991 Mon Sep 17 00:00:00 2001 From: Moe Dashti Date: Fri, 25 Sep 2026 08:11:39 -0700 Subject: [PATCH 01/17] Added the stream service RPCs and the frontend wiring. The stream is reachable over gRPC next to the workflow service: ids, names, offsets and page sizes are bounded on the frontend, every method declares its authorization scope and rate-limit priority, and a call for a namespace active elsewhere is forwarded like a workflow RPC. --- chasm/lib/stream/gen/streampb/v1/namespace.go | 75 + .../stream/gen/streampb/v1/namespace_test.go | 38 + .../v1/request_response.go-helpers.pb.go | 2152 ++++++++++ .../gen/streampb/v1/request_response.pb.go | 3658 +++++++++++++++++ .../lib/stream/gen/streampb/v1/service.pb.go | 140 + .../gen/streampb/v1/service_client.pb.go | 713 ++++ .../stream/gen/streampb/v1/service_grpc.pb.go | 640 +++ .../stream/proto/v1/request_response.proto | 384 ++ chasm/lib/stream/proto/v1/service.proto | 92 + chasm/lib/stream/service/frontend.go | 389 ++ chasm/lib/stream/service/frontend_test.go | 95 + chasm/lib/stream/service/fx.go | 35 + chasm/lib/stream/service/handler.go | 900 ++++ chasm/lib/stream/service/handler_test.go | 55 + chasm/lib/stream/service/library.go | 89 + chasm/lib/stream/service/tasks.go | 292 ++ chasm/lib/workflow/fx.go | 4 + chasm/lib/workflow/stream_commands.go | 56 + chasm/lib/workflow/workflow.go | 180 + cmd/tools/protogen/main.go | 53 +- common/api/metadata.go | 43 +- common/api/metadata_stream_test.go | 72 + common/api/metadata_test.go | 9 + common/rpc/interceptor/redirection_test.go | 80 + service/frontend/configs/quotas.go | 31 + service/frontend/configs/quotas_test.go | 26 + service/frontend/fx.go | 6 +- service/frontend/service.go | 6 + service/history/fx.go | 2 + tests/stream_test.go | 656 +++ 30 files changed, 10967 insertions(+), 4 deletions(-) create mode 100644 chasm/lib/stream/gen/streampb/v1/namespace.go create mode 100644 chasm/lib/stream/gen/streampb/v1/namespace_test.go create mode 100644 chasm/lib/stream/gen/streampb/v1/request_response.go-helpers.pb.go create mode 100644 chasm/lib/stream/gen/streampb/v1/request_response.pb.go create mode 100644 chasm/lib/stream/gen/streampb/v1/service.pb.go create mode 100644 chasm/lib/stream/gen/streampb/v1/service_client.pb.go create mode 100644 chasm/lib/stream/gen/streampb/v1/service_grpc.pb.go create mode 100644 chasm/lib/stream/proto/v1/request_response.proto create mode 100644 chasm/lib/stream/proto/v1/service.proto create mode 100644 chasm/lib/stream/service/frontend.go create mode 100644 chasm/lib/stream/service/frontend_test.go create mode 100644 chasm/lib/stream/service/fx.go create mode 100644 chasm/lib/stream/service/handler.go create mode 100644 chasm/lib/stream/service/handler_test.go create mode 100644 chasm/lib/stream/service/library.go create mode 100644 chasm/lib/stream/service/tasks.go create mode 100644 chasm/lib/workflow/stream_commands.go create mode 100644 common/api/metadata_stream_test.go create mode 100644 tests/stream_test.go 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..7c11d9434cd --- /dev/null +++ b/chasm/lib/stream/gen/streampb/v1/request_response.go-helpers.pb.go @@ -0,0 +1,2152 @@ +// Code generated by protoc-gen-go-helpers. DO NOT EDIT. +package streampb + +import ( + "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 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) +} 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..ab1d3863ef8 --- /dev/null +++ b/chasm/lib/stream/gen/streampb/v1/request_response.pb.go @@ -0,0 +1,3658 @@ +// 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" + sync "sync" + unsafe "unsafe" + + v1 "go.temporal.io/api/common/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) +) + +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. Resolved here rather than at delivery, so the first + // recorded range starts from a fact instead of a reading. + StartOffset int64 `protobuf:"varint,4,opt,name=start_offset,json=startOffset,proto3" json:"start_offset,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 +} + +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"` + 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) 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 *v1.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() *v1.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 "" +} + +// A stream a workflow owns lives inside that workflow's execution, 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"` + 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,8,opt,name=owner_run_id,json=ownerRunId,proto3" json:"owner_run_id,omitempty"` + // Empty means the workflow'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"` + 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[11] + 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[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 PollWorkflowMessagesInput.ProtoReflect.Descriptor instead. +func (*PollWorkflowMessagesInput) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{11} +} + +func (x *PollWorkflowMessagesInput) GetNamespace() string { + if x != nil { + return x.Namespace + } + return "" +} + +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) 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"` + 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,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[12] + 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[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 DescribeWorkflowStreamInput.ProtoReflect.Descriptor instead. +func (*DescribeWorkflowStreamInput) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{12} +} + +func (x *DescribeWorkflowStreamInput) GetNamespace() string { + if x != nil { + return x.Namespace + } + return "" +} + +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 a stream a workflow owns, from outside that workflow. The +// workflow's own publishes ride its Workflow Task instead. +type AddWorkflowMessagesInput 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,7,opt,name=owner_run_id,json=ownerRunId,proto3" json:"owner_run_id,omitempty"` + // Empty means the workflow'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[13] + 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[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 AddWorkflowMessagesInput.ProtoReflect.Descriptor instead. +func (*AddWorkflowMessagesInput) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{13} +} + +func (x *AddWorkflowMessagesInput) GetNamespace() string { + if x != nil { + return x.Namespace + } + return "" +} + +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[14] + 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[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 DescribeStreamOutput.ProtoReflect.Descriptor instead. +func (*DescribeStreamOutput) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{14} +} + +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 *v1.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[15] + 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[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 CloseStreamInput.ProtoReflect.Descriptor instead. +func (*CloseStreamInput) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{15} +} + +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() *v1.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[16] + 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[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 CloseStreamOutput.ProtoReflect.Descriptor instead. +func (*CloseStreamOutput) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{16} +} + +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[17] + 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[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 TruncateStreamInput.ProtoReflect.Descriptor instead. +func (*TruncateStreamInput) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{17} +} + +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[18] + 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[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 TruncateStreamOutput.ProtoReflect.Descriptor instead. +func (*TruncateStreamOutput) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{18} +} + +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[19] + 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[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 DeleteStreamInput.ProtoReflect.Descriptor instead. +func (*DeleteStreamInput) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{19} +} + +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[20] + 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[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 DeleteStreamOutput.ProtoReflect.Descriptor instead. +func (*DeleteStreamOutput) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{20} +} + +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[21] + 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[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 CreateStreamRequest.ProtoReflect.Descriptor instead. +func (*CreateStreamRequest) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{21} +} + +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[22] + 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[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 CreateStreamResponse.ProtoReflect.Descriptor instead. +func (*CreateStreamResponse) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{22} +} + +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[23] + 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[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 AddMessagesRequest.ProtoReflect.Descriptor instead. +func (*AddMessagesRequest) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{23} +} + +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[24] + 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[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 AddMessagesResponse.ProtoReflect.Descriptor instead. +func (*AddMessagesResponse) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{24} +} + +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[25] + 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[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 FinishWritingRequest.ProtoReflect.Descriptor instead. +func (*FinishWritingRequest) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{25} +} + +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[26] + 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[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 FinishWritingResponse.ProtoReflect.Descriptor instead. +func (*FinishWritingResponse) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{26} +} + +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[27] + 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[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 SubscribeWorkflowRequest.ProtoReflect.Descriptor instead. +func (*SubscribeWorkflowRequest) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{27} +} + +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[28] + 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[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 SubscribeWorkflowResponse.ProtoReflect.Descriptor instead. +func (*SubscribeWorkflowResponse) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{28} +} + +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[29] + 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[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 PollMessagesRequest.ProtoReflect.Descriptor instead. +func (*PollMessagesRequest) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{29} +} + +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[30] + 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[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 PollMessagesResponse.ProtoReflect.Descriptor instead. +func (*PollMessagesResponse) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{30} +} + +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[31] + 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[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 DescribeStreamRequest.ProtoReflect.Descriptor instead. +func (*DescribeStreamRequest) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{31} +} + +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[32] + 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[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 DescribeStreamResponse.ProtoReflect.Descriptor instead. +func (*DescribeStreamResponse) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{32} +} + +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[33] + 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[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 PollWorkflowMessagesRequest.ProtoReflect.Descriptor instead. +func (*PollWorkflowMessagesRequest) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{33} +} + +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[34] + 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[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 PollWorkflowMessagesResponse.ProtoReflect.Descriptor instead. +func (*PollWorkflowMessagesResponse) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{34} +} + +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[35] + 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[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 DescribeWorkflowStreamRequest.ProtoReflect.Descriptor instead. +func (*DescribeWorkflowStreamRequest) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{35} +} + +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[36] + 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[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 DescribeWorkflowStreamResponse.ProtoReflect.Descriptor instead. +func (*DescribeWorkflowStreamResponse) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{36} +} + +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"` + // Negative means from wherever the stream is when the pin is taken. Resolved + // here, where the frontier is, and returned so the cursor records a fact. + StartOffset int64 `protobuf:"varint,4,opt,name=start_offset,json=startOffset,proto3" json:"start_offset,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[37] + 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[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 RegisterStreamConsumerInput.ProtoReflect.Descriptor instead. +func (*RegisterStreamConsumerInput) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{37} +} + +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 +} + +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"` + 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[38] + 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[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 RegisterStreamConsumerOutput.ProtoReflect.Descriptor instead. +func (*RegisterStreamConsumerOutput) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{38} +} + +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 +} + +// 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[39] + 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[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 AdvanceConsumerHeadInput.ProtoReflect.Descriptor instead. +func (*AdvanceConsumerHeadInput) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{39} +} + +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[40] + 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[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 AdvanceConsumerHeadOutput.ProtoReflect.Descriptor instead. +func (*AdvanceConsumerHeadOutput) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{40} +} + +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[41] + 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[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 AddWorkflowMessagesRequest.ProtoReflect.Descriptor instead. +func (*AddWorkflowMessagesRequest) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{41} +} + +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[42] + 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[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 AddWorkflowMessagesResponse.ProtoReflect.Descriptor instead. +func (*AddWorkflowMessagesResponse) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{42} +} + +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[43] + 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[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 RegisterStreamConsumerRequest.ProtoReflect.Descriptor instead. +func (*RegisterStreamConsumerRequest) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{43} +} + +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[44] + 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[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 RegisterStreamConsumerResponse.ProtoReflect.Descriptor instead. +func (*RegisterStreamConsumerResponse) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{44} +} + +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[45] + 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[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 AdvanceConsumerHeadRequest.ProtoReflect.Descriptor instead. +func (*AdvanceConsumerHeadRequest) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{45} +} + +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[46] + 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[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 AdvanceConsumerHeadResponse.ProtoReflect.Descriptor instead. +func (*AdvanceConsumerHeadResponse) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{46} +} + +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[47] + 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[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 CloseStreamRequest.ProtoReflect.Descriptor instead. +func (*CloseStreamRequest) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{47} +} + +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[48] + 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[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 CloseStreamResponse.ProtoReflect.Descriptor instead. +func (*CloseStreamResponse) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{48} +} + +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[49] + 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[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 TruncateStreamRequest.ProtoReflect.Descriptor instead. +func (*TruncateStreamRequest) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{49} +} + +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[50] + 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[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 TruncateStreamResponse.ProtoReflect.Descriptor instead. +func (*TruncateStreamResponse) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{50} +} + +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[51] + 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[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 ListStreamsInput.ProtoReflect.Descriptor instead. +func (*ListStreamsInput) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{51} +} + +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[52] + 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[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 StreamListEntry.ProtoReflect.Descriptor instead. +func (*StreamListEntry) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{52} +} + +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[53] + 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[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 ListStreamsOutput.ProtoReflect.Descriptor instead. +func (*ListStreamsOutput) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{53} +} + +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[54] + 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[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 ListStreamsRequest.ProtoReflect.Descriptor instead. +func (*ListStreamsRequest) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{54} +} + +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[55] + 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[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 ListStreamsResponse.ProtoReflect.Descriptor instead. +func (*ListStreamsResponse) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{55} +} + +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[56] + 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[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 DeleteStreamRequest.ProtoReflect.Descriptor instead. +func (*DeleteStreamRequest) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{56} +} + +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[57] + 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[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 DeleteStreamResponse.ProtoReflect.Descriptor instead. +func (*DeleteStreamResponse) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{57} +} + +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\"\xd5\x01\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\"l\n" + + "\x1cRegisterStreamConsumerOutput\x12!\n" + + "\fstart_offset\x18\x01 \x01(\x03R\vstartOffset\x12\x1d\n" + + "\n" + + "known_head\x18\x04 \x01(\x03R\tknownHeadJ\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\x10frontendResponseB>Z temporal.server.chasm.lib.stream.proto.v1.StreamLifecycle + 59, // 1: temporal.server.chasm.lib.stream.proto.v1.AddMessagesInput.records:type_name -> temporal.server.chasm.lib.stream.proto.v1.StreamRecord + 59, // 2: temporal.server.chasm.lib.stream.proto.v1.PollMessagesOutput.records:type_name -> temporal.server.chasm.lib.stream.proto.v1.StreamRecord + 60, // 3: temporal.server.chasm.lib.stream.proto.v1.PollMessagesOutput.close_reason:type_name -> temporal.api.common.v1.Payload + 59, // 4: temporal.server.chasm.lib.stream.proto.v1.AddWorkflowMessagesInput.records:type_name -> temporal.server.chasm.lib.stream.proto.v1.StreamRecord + 61, // 5: temporal.server.chasm.lib.stream.proto.v1.DescribeStreamOutput.state:type_name -> temporal.server.chasm.lib.stream.proto.v1.StreamState + 60, // 6: temporal.server.chasm.lib.stream.proto.v1.CloseStreamInput.reason:type_name -> temporal.api.common.v1.Payload + 0, // 7: temporal.server.chasm.lib.stream.proto.v1.CreateStreamRequest.frontend_request:type_name -> temporal.server.chasm.lib.stream.proto.v1.CreateStreamInput + 1, // 8: temporal.server.chasm.lib.stream.proto.v1.CreateStreamResponse.frontend_response:type_name -> temporal.server.chasm.lib.stream.proto.v1.CreateStreamOutput + 2, // 9: temporal.server.chasm.lib.stream.proto.v1.AddMessagesRequest.frontend_request:type_name -> temporal.server.chasm.lib.stream.proto.v1.AddMessagesInput + 3, // 10: temporal.server.chasm.lib.stream.proto.v1.AddMessagesResponse.frontend_response:type_name -> temporal.server.chasm.lib.stream.proto.v1.AddMessagesOutput + 4, // 11: temporal.server.chasm.lib.stream.proto.v1.FinishWritingRequest.frontend_request:type_name -> temporal.server.chasm.lib.stream.proto.v1.FinishWritingInput + 5, // 12: temporal.server.chasm.lib.stream.proto.v1.FinishWritingResponse.frontend_response:type_name -> temporal.server.chasm.lib.stream.proto.v1.FinishWritingOutput + 6, // 13: temporal.server.chasm.lib.stream.proto.v1.SubscribeWorkflowRequest.frontend_request:type_name -> temporal.server.chasm.lib.stream.proto.v1.SubscribeWorkflowInput + 7, // 14: temporal.server.chasm.lib.stream.proto.v1.SubscribeWorkflowResponse.frontend_response:type_name -> temporal.server.chasm.lib.stream.proto.v1.SubscribeWorkflowOutput + 8, // 15: temporal.server.chasm.lib.stream.proto.v1.PollMessagesRequest.frontend_request:type_name -> temporal.server.chasm.lib.stream.proto.v1.PollMessagesInput + 9, // 16: temporal.server.chasm.lib.stream.proto.v1.PollMessagesResponse.frontend_response:type_name -> temporal.server.chasm.lib.stream.proto.v1.PollMessagesOutput + 10, // 17: temporal.server.chasm.lib.stream.proto.v1.DescribeStreamRequest.frontend_request:type_name -> temporal.server.chasm.lib.stream.proto.v1.DescribeStreamInput + 14, // 18: temporal.server.chasm.lib.stream.proto.v1.DescribeStreamResponse.frontend_response:type_name -> temporal.server.chasm.lib.stream.proto.v1.DescribeStreamOutput + 11, // 19: temporal.server.chasm.lib.stream.proto.v1.PollWorkflowMessagesRequest.frontend_request:type_name -> temporal.server.chasm.lib.stream.proto.v1.PollWorkflowMessagesInput + 9, // 20: temporal.server.chasm.lib.stream.proto.v1.PollWorkflowMessagesResponse.frontend_response:type_name -> temporal.server.chasm.lib.stream.proto.v1.PollMessagesOutput + 12, // 21: temporal.server.chasm.lib.stream.proto.v1.DescribeWorkflowStreamRequest.frontend_request:type_name -> temporal.server.chasm.lib.stream.proto.v1.DescribeWorkflowStreamInput + 14, // 22: temporal.server.chasm.lib.stream.proto.v1.DescribeWorkflowStreamResponse.frontend_response:type_name -> temporal.server.chasm.lib.stream.proto.v1.DescribeStreamOutput + 13, // 23: temporal.server.chasm.lib.stream.proto.v1.AddWorkflowMessagesRequest.frontend_request:type_name -> temporal.server.chasm.lib.stream.proto.v1.AddWorkflowMessagesInput + 3, // 24: temporal.server.chasm.lib.stream.proto.v1.AddWorkflowMessagesResponse.frontend_response:type_name -> temporal.server.chasm.lib.stream.proto.v1.AddMessagesOutput + 37, // 25: temporal.server.chasm.lib.stream.proto.v1.RegisterStreamConsumerRequest.frontend_request:type_name -> temporal.server.chasm.lib.stream.proto.v1.RegisterStreamConsumerInput + 38, // 26: temporal.server.chasm.lib.stream.proto.v1.RegisterStreamConsumerResponse.frontend_response:type_name -> temporal.server.chasm.lib.stream.proto.v1.RegisterStreamConsumerOutput + 39, // 27: temporal.server.chasm.lib.stream.proto.v1.AdvanceConsumerHeadRequest.frontend_request:type_name -> temporal.server.chasm.lib.stream.proto.v1.AdvanceConsumerHeadInput + 40, // 28: temporal.server.chasm.lib.stream.proto.v1.AdvanceConsumerHeadResponse.frontend_response:type_name -> temporal.server.chasm.lib.stream.proto.v1.AdvanceConsumerHeadOutput + 15, // 29: temporal.server.chasm.lib.stream.proto.v1.CloseStreamRequest.frontend_request:type_name -> temporal.server.chasm.lib.stream.proto.v1.CloseStreamInput + 16, // 30: temporal.server.chasm.lib.stream.proto.v1.CloseStreamResponse.frontend_response:type_name -> temporal.server.chasm.lib.stream.proto.v1.CloseStreamOutput + 17, // 31: temporal.server.chasm.lib.stream.proto.v1.TruncateStreamRequest.frontend_request:type_name -> temporal.server.chasm.lib.stream.proto.v1.TruncateStreamInput + 18, // 32: temporal.server.chasm.lib.stream.proto.v1.TruncateStreamResponse.frontend_response:type_name -> temporal.server.chasm.lib.stream.proto.v1.TruncateStreamOutput + 52, // 33: temporal.server.chasm.lib.stream.proto.v1.ListStreamsOutput.streams:type_name -> temporal.server.chasm.lib.stream.proto.v1.StreamListEntry + 51, // 34: temporal.server.chasm.lib.stream.proto.v1.ListStreamsRequest.frontend_request:type_name -> temporal.server.chasm.lib.stream.proto.v1.ListStreamsInput + 53, // 35: temporal.server.chasm.lib.stream.proto.v1.ListStreamsResponse.frontend_response:type_name -> temporal.server.chasm.lib.stream.proto.v1.ListStreamsOutput + 19, // 36: temporal.server.chasm.lib.stream.proto.v1.DeleteStreamRequest.frontend_request:type_name -> temporal.server.chasm.lib.stream.proto.v1.DeleteStreamInput + 20, // 37: temporal.server.chasm.lib.stream.proto.v1.DeleteStreamResponse.frontend_response:type_name -> temporal.server.chasm.lib.stream.proto.v1.DeleteStreamOutput + 38, // [38:38] is the sub-list for method output_type + 38, // [38:38] is the sub-list for method input_type + 38, // [38:38] is the sub-list for extension type_name + 38, // [38:38] is the sub-list for extension extendee + 0, // [0:38] 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: 0, + NumMessages: 58, + 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, + 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..e0bb7981366 --- /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\xf8\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\xd1\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\x1e\x1a\x1cfrontend_request.workflow_id\x12\xd7\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\x1e\x1a\x1cfrontend_request.workflow_id\x12\xce\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\x1e\x1a\x1cfrontend_request.workflow_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..946c94e2831 --- /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().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.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().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.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().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.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..0a9d2f93a28 --- /dev/null +++ b/chasm/lib/stream/gen/streampb/v1/service_grpc.pb.go @@ -0,0 +1,640 @@ +// 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) + // Routed on the owner, because the stream it reads has no id of its own. + 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) + // Routed on the owner, because the stream it reads has no id of its own. + 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/proto/v1/request_response.proto b/chasm/lib/stream/proto/v1/request_response.proto new file mode 100644 index 00000000000..66e5dadb63f --- /dev/null +++ b/chasm/lib/stream/proto/v1/request_response.proto @@ -0,0 +1,384 @@ +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"; + +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. Resolved here rather than at delivery, so the first + // recorded range starts from a fact instead of a reading. + int64 start_offset = 4; +} + +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; + 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; +} + +// A stream a workflow owns lives inside that workflow's execution, 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; + 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 = 8; + + // Empty means the workflow's default output stream. + string stream_name = 3; + int64 from_offset = 4; + int32 max_messages = 5; + // Filters as on PollMessagesInput. + repeated string topics = 6; + bool wait_new_messages = 7; +} + +message DescribeWorkflowStreamInput { + 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 = 4; + + string stream_name = 3; +} + +// Appending to a stream a workflow owns, from outside that workflow. The +// workflow's own publishes ride its Workflow Task instead. +message AddWorkflowMessagesInput { + 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 = 7; + + // Empty means the workflow'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; + // Negative means from wherever the stream is when the pin is taken. Resolved + // here, where the frontier is, and returned so the cursor records a fact. + int64 start_offset = 4; +} + +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; +} + +// 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..bdd0a918551 --- /dev/null +++ b/chasm/lib/stream/proto/v1/service.proto @@ -0,0 +1,92 @@ +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; + } + + // Routed on the owner, because the stream it reads has no id of its own. + rpc PollWorkflowMessages(PollWorkflowMessagesRequest) returns (PollWorkflowMessagesResponse) { + option (temporal.server.api.routing.v1.routing).business_id = "frontend_request.workflow_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.workflow_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.workflow_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/service/frontend.go b/chasm/lib/stream/service/frontend.go new file mode 100644 index 00000000000..431ae37ec06 --- /dev/null +++ b/chasm/lib/stream/service/frontend.go @@ -0,0 +1,389 @@ +package service + +import ( + "context" + + "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/log" + "go.temporal.io/server/common/namespace" + "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 { + streampb.UnimplementedStreamServiceServer + + client streampb.StreamServiceClient + namespaceRegistry namespace.Registry + logger log.Logger + config *stream.Config +} + +func NewFrontendHandler( + client streampb.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{ + streampb.StreamService_CreateStream_FullMethodName: func() any { + return &streampb.CreateStreamResponse{} + }, + streampb.StreamService_AddMessages_FullMethodName: func() any { + return &streampb.AddMessagesResponse{} + }, + streampb.StreamService_FinishWriting_FullMethodName: func() any { + return &streampb.FinishWritingResponse{} + }, + streampb.StreamService_SubscribeWorkflow_FullMethodName: func() any { + return &streampb.SubscribeWorkflowResponse{} + }, + streampb.StreamService_PollMessages_FullMethodName: func() any { + return &streampb.PollMessagesResponse{} + }, + streampb.StreamService_DescribeStream_FullMethodName: func() any { + return &streampb.DescribeStreamResponse{} + }, + streampb.StreamService_PollWorkflowMessages_FullMethodName: func() any { + return &streampb.PollWorkflowMessagesResponse{} + }, + streampb.StreamService_DescribeWorkflowStream_FullMethodName: func() any { + return &streampb.DescribeWorkflowStreamResponse{} + }, + streampb.StreamService_AddWorkflowMessages_FullMethodName: func() any { + return &streampb.AddWorkflowMessagesResponse{} + }, + streampb.StreamService_CloseStream_FullMethodName: func() any { + return &streampb.CloseStreamResponse{} + }, + streampb.StreamService_TruncateStream_FullMethodName: func() any { + return &streampb.TruncateStreamResponse{} + }, + streampb.StreamService_ListStreams_FullMethodName: func() any { + return &streampb.ListStreamsResponse{} + }, + streampb.StreamService_DeleteStream_FullMethodName: func() any { + return &streampb.DeleteStreamResponse{} + }, + } +} + +func (h *FrontendHandler) namespaceID(name string) (string, error) { + if name == "" { + return "", serviceerror.NewInvalidArgument("namespace is required") + } + id, err := h.namespaceRegistry.GetNamespaceID(namespace.Name(name)) + if err != nil { + return "", err + } + return id.String(), 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 +} + +func checkOffset(field string, value int64) error { + if value < 0 { + return serviceerror.NewInvalidArgumentf("%s cannot be negative, got %d", field, value) + } + return nil +} + +// 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 *streampb.CreateStreamRequest, +) (*streampb.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 + } + return h.client.CreateStream(ctx, &streampb.CreateStreamRequest{ + NamespaceId: id, FrontendRequest: in, + }) +} + +func (h *FrontendHandler) AddMessages( + ctx context.Context, req *streampb.AddMessagesRequest, +) (*streampb.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 + } + return h.client.AddMessages(ctx, &streampb.AddMessagesRequest{ + NamespaceId: id, FrontendRequest: in, + }) +} + +func (h *FrontendHandler) FinishWriting( + ctx context.Context, req *streampb.FinishWritingRequest, +) (*streampb.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 + } + return h.client.FinishWriting(ctx, &streampb.FinishWritingRequest{ + NamespaceId: id, FrontendRequest: in, + }) +} + +func (h *FrontendHandler) SubscribeWorkflow( + ctx context.Context, req *streampb.SubscribeWorkflowRequest, +) (*streampb.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 + } + return h.client.SubscribeWorkflow(ctx, &streampb.SubscribeWorkflowRequest{ + NamespaceId: id, FrontendRequest: in, + }) +} + +func (h *FrontendHandler) PollMessages( + ctx context.Context, req *streampb.PollMessagesRequest, +) (*streampb.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 + } + in.MaxMessages = clampMaxMessages(in.GetMaxMessages()) + return h.client.PollMessages(ctx, &streampb.PollMessagesRequest{ + NamespaceId: id, FrontendRequest: in, + }) +} + +func (h *FrontendHandler) PollWorkflowMessages( + ctx context.Context, req *streampb.PollWorkflowMessagesRequest, +) (*streampb.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 + } + in.MaxMessages = clampMaxMessages(in.GetMaxMessages()) + return h.client.PollWorkflowMessages(ctx, &streampb.PollWorkflowMessagesRequest{ + NamespaceId: id, FrontendRequest: in, + }) +} + +func (h *FrontendHandler) DescribeWorkflowStream( + ctx context.Context, req *streampb.DescribeWorkflowStreamRequest, +) (*streampb.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 + } + return h.client.DescribeWorkflowStream(ctx, &streampb.DescribeWorkflowStreamRequest{ + NamespaceId: id, FrontendRequest: in, + }) +} + +func (h *FrontendHandler) AddWorkflowMessages( + ctx context.Context, req *streampb.AddWorkflowMessagesRequest, +) (*streampb.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 + } + return h.client.AddWorkflowMessages(ctx, &streampb.AddWorkflowMessagesRequest{ + NamespaceId: id, FrontendRequest: in, + }) +} + +func (h *FrontendHandler) DescribeStream( + ctx context.Context, req *streampb.DescribeStreamRequest, +) (*streampb.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, &streampb.DescribeStreamRequest{ + NamespaceId: id, FrontendRequest: in, + }) +} + +func (h *FrontendHandler) CloseStream( + ctx context.Context, req *streampb.CloseStreamRequest, +) (*streampb.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, &streampb.CloseStreamRequest{ + NamespaceId: id, FrontendRequest: in, + }) +} + +func (h *FrontendHandler) TruncateStream( + ctx context.Context, req *streampb.TruncateStreamRequest, +) (*streampb.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, &streampb.TruncateStreamRequest{ + NamespaceId: id, FrontendRequest: in, + }) +} + +func (h *FrontendHandler) DeleteStream( + ctx context.Context, req *streampb.DeleteStreamRequest, +) (*streampb.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, &streampb.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 *streampb.ListStreamsRequest, +) (*streampb.ListStreamsResponse, error) { + in := req.GetFrontendRequest() + if in.GetNamespace() == "" { + return nil, serviceerror.NewInvalidArgument("namespace is required") + } + + 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([]*streampb.StreamListEntry, 0, len(resp.Executions)) + for _, e := range resp.Executions { + entries = append(entries, &streampb.StreamListEntry{ + StreamId: e.BusinessID, + RunId: e.RunID, + }) + } + return &streampb.ListStreamsResponse{ + FrontendResponse: &streampb.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..b861176fc80 --- /dev/null +++ b/chasm/lib/stream/service/frontend_test.go @@ -0,0 +1,95 @@ +package service + +import ( + "context" + "strings" + "testing" + + "github.com/stretchr/testify/require" + "go.temporal.io/api/serviceerror" + "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" +) + +// 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() + registry := namespace.NewMockRegistry(gomock.NewController(t)) + registry.EXPECT().GetNamespaceID(namespace.Name("ns")).Return(namespace.ID("ns-id"), nil).AnyTimes() + client := &recordingClient{} + config := &stream.Config{MaxIDLength: dynamicconfig.GetIntPropertyFn(maxIDLength)} + return NewFrontendHandler(client, registry, log.NewNoopLogger(), config), client +} + +// 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()) +} diff --git a/chasm/lib/stream/service/fx.go b/chasm/lib/stream/service/fx.go new file mode 100644 index 00000000000..bbbd4778537 --- /dev/null +++ b/chasm/lib/stream/service/fx.go @@ -0,0 +1,35 @@ +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, + 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..2d716ce671c --- /dev/null +++ b/chasm/lib/stream/service/handler.go @@ -0,0 +1,900 @@ +package service + +import ( + "context" + "errors" + + commonpb "go.temporal.io/api/common/v1" + "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" + chasmworkflow "go.temporal.io/server/chasm/lib/workflow" + "go.temporal.io/server/common" + "go.temporal.io/server/common/contextutil" + "go.temporal.io/server/common/headers" + "go.temporal.io/server/common/log" + "go.temporal.io/server/common/namespace" + "go.temporal.io/server/service/history/shard" +) + +type handler struct { + streampb.UnimplementedStreamServiceServer + + shardController shard.Controller + namespaceRegistry namespace.Registry + logger log.Logger + config *stream.Config + + // 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 streampb.StreamServiceClient +} + +func newHandler( + shardController shard.Controller, + namespaceRegistry namespace.Registry, + logger log.Logger, + config *stream.Config, + routed streampb.StreamServiceClient, +) *handler { + return &handler{ + shardController: shardController, + namespaceRegistry: namespaceRegistry, + logger: logger, + config: config, + routed: routed, + } +} + +// limitsFor resolves the namespace's limits. An id the registry cannot name +// falls back to the defaults; the interceptors have already refused requests +// for namespaces that do not exist. +func (h *handler) limitsFor(namespaceID string) stream.Limits { + name, err := h.namespaceRegistry.GetNamespaceName(namespace.ID(namespaceID)) + if err != nil { + return stream.DefaultLimits() + } + return h.config.LimitsFor(name.String()) +} + +// 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, + }) +} + +// 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 chasmworkflow.DefaultStreamName + } + return name +} + +func (h *handler) CreateStream( + ctx context.Context, + req *streampb.CreateStreamRequest, +) (*streampb.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 *streampb.CreateStreamInput) (*stream.Stream, error) { + return stream.NewStream(mctx, stream.NewStreamRequest{Lifecycle: input.GetLifecycle()}) + }, + in, + ) + if err != nil { + return nil, err + } + return &streampb.CreateStreamResponse{ + FrontendResponse: &streampb.CreateStreamOutput{RunId: result.ExecutionKey.RunID}, + }, nil +} + +func (h *handler) AddMessages( + ctx context.Context, + req *streampb.AddMessagesRequest, +) (*streampb.AddMessagesResponse, error) { + in := req.GetFrontendRequest() + if len(in.GetRecords()) == 0 { + return nil, serviceerror.NewInvalidArgument("no records to append") + } + ctx = h.withCallerInfo(ctx, req.GetNamespaceId()) + + // 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: h.limitsFor(req.GetNamespaceId()), + } + 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 + } + + return &streampb.AddMessagesResponse{ + FrontendResponse: &streampb.AddMessagesOutput{ + FirstOffset: result.FirstOffset, + NextOffset: result.NextOffset, + Count: result.Count, + Deduplicated: result.Deduplicated, + }, + }, nil +} + +// AddWorkflowMessages appends to a stream a workflow owns, from outside that +// workflow. +// +// 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 *streampb.AddWorkflowMessagesRequest, +) (*streampb.AddWorkflowMessagesResponse, error) { + in := req.GetFrontendRequest() + if len(in.GetRecords()) == 0 { + return nil, serviceerror.NewInvalidArgument("no records to append") + } + ctx = h.withCallerInfo(ctx, req.GetNamespaceId()) + + // 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. + name := ownedStreamName(in.GetStreamName()) + addReq := stream.AddMessagesRequest{ + Records: in.GetRecords(), + ProducerID: in.GetProducerId(), + Sequence: in.GetSequence(), + Limits: h.limitsFor(req.GetNamespaceId()), + } + result, _, err := chasm.UpdateComponent(ctx, + workflowRef(req.GetNamespaceId(), in.GetWorkflowId(), in.GetOwnerRunId()), + func( + wf *chasmworkflow.Workflow, mctx chasm.MutableContext, r stream.AddMessagesRequest, + ) (stream.AddMessagesResult, error) { + return wf.AppendToOwnedStream(mctx, name, r) + }, addReq) + if err != nil { + return nil, err + } + + return &streampb.AddWorkflowMessagesResponse{ + FrontendResponse: &streampb.AddMessagesOutput{ + FirstOffset: result.FirstOffset, + NextOffset: result.NextOffset, + Count: result.Count, + Deduplicated: result.Deduplicated, + }, + }, nil +} + +func (h *handler) FinishWriting( + ctx context.Context, + req *streampb.FinishWritingRequest, +) (*streampb.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 &streampb.FinishWritingResponse{FrontendResponse: &streampb.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 *streampb.SubscribeWorkflowRequest, +) (*streampb.SubscribeWorkflowResponse, error) { + in := req.GetFrontendRequest() + + if in.GetStreamId() != "" { + return h.subscribeToExternalStream(ctx, req.GetNamespaceId(), in) + } + + 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 *streampb.SubscribeWorkflowInput, + ) (int64, error) { + return wf.SubscribeToOwnedStream( + mctx, ownedStreamName(input.GetStreamName()), input.GetStartOffset(), limits) + }, + in, + ) + if err != nil { + return nil, err + } + + return &streampb.SubscribeWorkflowResponse{ + FrontendResponse: &streampb.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 *streampb.SubscribeWorkflowInput, +) (*streampb.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, &streampb.RegisterStreamConsumerRequest{ + NamespaceId: namespaceID, + FrontendRequest: &streampb.RegisterStreamConsumerInput{ + Namespace: in.GetNamespace(), + StreamId: in.GetStreamId(), + ConsumerWorkflowId: in.GetWorkflowId(), + ConsumerRunId: consumerRunID, + StartOffset: in.GetStartOffset(), + }, + }) + if err != nil { + return nil, err + } + pin := registered.GetFrontendResponse() + + 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 &streampb.SubscribeWorkflowResponse{ + FrontendResponse: &streampb.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 *streampb.RegisterStreamConsumerRequest, +) (*streampb.RegisterStreamConsumerResponse, error) { + in := req.GetFrontendRequest() + ctx = h.withCallerInfo(ctx, req.GetNamespaceId()) + + limits := h.limitsFor(req.GetNamespaceId()) + pin, _, err := chasm.UpdateComponent( + ctx, + refFor(req.GetNamespaceId(), in.GetStreamId()), + func( + s *stream.Stream, mctx chasm.MutableContext, offset int64, + ) (*streampb.RegisterStreamConsumerOutput, error) { + startOffset, err := s.RegisterConsumer(mctx, stream.ConsumerRegistration{ + ConsumerID: externalConsumerID(in.GetConsumerWorkflowId(), in.GetConsumerRunId()), + WorkflowID: in.GetConsumerWorkflowId(), + RunID: in.GetConsumerRunId(), + Offset: offset, + External: true, + MaxConsumers: limits.MaxConsumersPerStream, + }) + if err != nil { + return nil, err + } + return &streampb.RegisterStreamConsumerOutput{ + StartOffset: startOffset, + KnownHead: s.State.GetHeadOffset(), + }, nil + }, + in.GetStartOffset(), + ) + if err != nil { + return nil, err + } + return &streampb.RegisterStreamConsumerResponse{FrontendResponse: pin}, nil +} + +// 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(), + } + if field, ok := wf.StreamCursors[streamID]; ok { + p.consumes = true + p.startOffset = field.Get(cctx).StartOffset() + } + return p, nil + }, struct{}{}) + var notFound *serviceerror.NotFound + if errors.As(err, ¬Found) { + 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 *streampb.AdvanceConsumerHeadRequest, +) (*streampb.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 := &streampb.AdvanceConsumerHeadOutput{} + if !pinned.closed { + if !pinned.consumes { + out.ConsumerClosed = true + return &streampb.AdvanceConsumerHeadResponse{FrontendResponse: out}, nil + } + err := h.pushHead(ctx, namespaceID, workflowID, pinned.runID, streamID, in.GetHeadOffset()) + if err != nil { + return nil, err + } + return &streampb.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 &streampb.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 &streampb.AdvanceConsumerHeadResponse{FrontendResponse: out}, nil +} + +func (h *handler) PollMessages( + ctx context.Context, + req *streampb.PollMessagesRequest, +) (*streampb.PollMessagesResponse, error) { + in := req.GetFrontendRequest() + ctx = h.withCallerInfo(ctx, req.GetNamespaceId()) + + ref := refForRun(req.GetNamespaceId(), in.GetStreamId(), in.GetRunId()) + from := in.GetFromOffset() + + state, err := chasm.ReadComponent(ctx, ref, (*stream.Stream).Snapshot, struct{}{}) + if err != nil { + return nil, err + } + + // Blocking is only worth it once the reader is genuinely caught up. + if in.GetWaitNewMessages() && from == state.GetHeadOffset() && !state.GetClosed() { + // The window is re-read below, so only the blocking matters here. + if _, err := h.waitForMessages(ctx, ref, from, state); err != nil { + return nil, err + } + } + + wreq := stream.WindowRequest{From: from, 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 + } + return &streampb.PollMessagesResponse{FrontendResponse: out}, nil +} + +// PollWorkflowMessages reads a stream a workflow owns. +// +// An attached stream is reached through its owner, so this call routes on the +// workflow id and both the frontier and the batches come out of the owner's +// component. +func (h *handler) PollWorkflowMessages( + ctx context.Context, + req *streampb.PollWorkflowMessagesRequest, +) (*streampb.PollWorkflowMessagesResponse, error) { + in := req.GetFrontendRequest() + ctx = h.withCallerInfo(ctx, req.GetNamespaceId()) + + ref := workflowRef(req.GetNamespaceId(), in.GetWorkflowId(), in.GetOwnerRunId()) + name := ownedStreamName(in.GetStreamName()) + from := in.GetFromOffset() + + state, err := h.ownedStreamState(ctx, ref, name) + if err != nil { + return nil, err + } + + if in.GetWaitNewMessages() && from == state.GetHeadOffset() && !state.GetClosed() { + if _, err := h.waitForOwnedMessages(ctx, ref, name, from, state); err != nil { + return nil, err + } + } + + wreq := stream.WindowRequest{From: from, MaxMessages: in.GetMaxMessages(), Topics: in.GetTopics()} + w, err := chasm.ReadComponent(ctx, ref, readOwnedWindow, + ownedWindowRequest{Name: name, Window: wreq}) + if err != nil { + return nil, err + } + out, err := formatWindow(w, wreq) + if err != nil { + return nil, err + } + return &streampb.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) (*streampb.PollMessagesOutput, error) { + out := &streampb.PollMessagesOutput{ + NextOffset: req.From, + HeadOffset: w.State.GetHeadOffset(), + Closed: w.State.GetClosed(), + CloseReason: w.State.GetCloseReason(), + RunId: w.RunID, + } + if req.From == w.State.GetHeadOffset() { + return out, nil + } + + records, next, err := stream.CollectRecords( + w.Blobs, w.Starts, req.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 { + Name string + Window stream.WindowRequest +} + +// readOwnedWindow reads a stream attached to a workflow. +// +// A closed execution can take no more publishes, from its own Workflow Task or +// from anywhere else, so its stream is finished whether or not a producer said +// so. Without that a reader tailing a workflow that ended stays parked forever. +func readOwnedWindow( + wf *chasmworkflow.Workflow, + cctx chasm.Context, + req ownedWindowRequest, +) (stream.Window, error) { + s := wf.OwnedStream(cctx, req.Name) + if s == nil { + // Nothing published yet, which reads as an empty stream so a reader can + // attach before the first append. + return stream.Window{ + State: &streampb.StreamState{Closed: !cctx.ExecutionInfo().CloseTime.IsZero()}, + To: req.Window.From, + }, nil + } + w, err := s.ReadWindow(cctx, req.Window) + if err != nil { + return stream.Window{}, err + } + if !cctx.ExecutionInfo().CloseTime.IsZero() { + w.State.Closed = true + } + return w, nil +} + +// ownedStreamState snapshots an attached stream through the component that +// owns it. A stream the workflow has not published to yet reads as an empty +// one, so a reader may attach before the first event. +func (h *handler) ownedStreamState( + ctx context.Context, + ref chasm.ComponentRef, + name string, +) (*streampb.StreamState, error) { + state, err := chasm.ReadComponent(ctx, ref, readOwnedStream, name) + if err != nil { + return nil, err + } + return state, nil +} + +// readOwnedStream snapshots an attached stream and reports whether anything +// can still be added to it. +// +// A closed execution can take no more publishes, from its own Workflow Task or +// from anywhere else, so its stream is finished whether or not a producer said +// so. Without this a reader tailing a workflow that ended stays parked forever. +func readOwnedStream( + wf *chasmworkflow.Workflow, + cctx chasm.Context, + name string, +) (*streampb.StreamState, error) { + state, err := wf.OwnedStreamState(cctx, name) + if err != nil { + return nil, err + } + if state == nil { + state = &streampb.StreamState{} + } + if !cctx.ExecutionInfo().CloseTime.IsZero() { + state.Closed = true + } + return state, nil +} + +// 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( + ctx context.Context, + ref chasm.ComponentRef, + from int64, + current *streampb.StreamState, +) (*streampb.StreamState, error) { + pollCtx, cancel := contextutil.WithDeadlineBuffer( + ctx, stream.LongPollTimeout, stream.LongPollBuffer) + defer cancel() + + state, _, err := chasm.PollComponent(pollCtx, ref, + func(s *stream.Stream, _ chasm.Context, offset int64) (*streampb.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, ctx, 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( + ctx context.Context, + ref chasm.ComponentRef, + name string, + from int64, + current *streampb.StreamState, +) (*streampb.StreamState, error) { + pollCtx, cancel := contextutil.WithDeadlineBuffer( + ctx, stream.LongPollTimeout, stream.LongPollBuffer) + defer cancel() + + state, _, err := chasm.PollComponent(pollCtx, ref, + func( + wf *chasmworkflow.Workflow, cctx chasm.Context, offset int64, + ) (*streampb.StreamState, bool, error) { + owned, err := readOwnedStream(wf, cctx, name) + if err != nil { + return nil, false, err + } + if !pollSatisfied(owned, offset) { + return nil, false, nil + } + return owned, true, nil + }, from) + return pollOutcome(pollCtx, ctx, state, err, current) +} + +// pollSatisfied is the monotonic condition PollComponent requires: the head +// only advances and closed never clears. +func pollSatisfied(state *streampb.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 *streampb.StreamState, + err error, + current *streampb.StreamState, +) (*streampb.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 *streampb.DescribeStreamRequest, +) (*streampb.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 &streampb.DescribeStreamResponse{ + FrontendResponse: &streampb.DescribeStreamOutput{State: state}, + }, nil +} + +// DescribeWorkflowStream reports the frontier of a stream a workflow 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 *streampb.DescribeWorkflowStreamRequest, +) (*streampb.DescribeWorkflowStreamResponse, error) { + in := req.GetFrontendRequest() + ctx = h.withCallerInfo(ctx, req.GetNamespaceId()) + + state, err := h.ownedStreamState(ctx, + workflowRef(req.GetNamespaceId(), in.GetWorkflowId(), in.GetOwnerRunId()), + ownedStreamName(in.GetStreamName())) + if err != nil { + return nil, err + } + return &streampb.DescribeWorkflowStreamResponse{ + FrontendResponse: &streampb.DescribeStreamOutput{State: state}, + }, nil +} + +func (h *handler) CloseStream( + ctx context.Context, + req *streampb.CloseStreamRequest, +) (*streampb.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 &streampb.CloseStreamResponse{FrontendResponse: &streampb.CloseStreamOutput{}}, nil +} + +func (h *handler) TruncateStream( + ctx context.Context, + req *streampb.TruncateStreamRequest, +) (*streampb.TruncateStreamResponse, error) { + in := req.GetFrontendRequest() + ctx = h.withCallerInfo(ctx, req.GetNamespaceId()) + + if _, _, err := chasm.UpdateComponent( + ctx, + refFor(req.GetNamespaceId(), in.GetStreamId()), + func(s *stream.Stream, mctx chasm.MutableContext, newBase int64) (struct{}, error) { + return struct{}{}, s.Truncate(mctx, newBase) + }, + in.GetNewBaseOffset(), + ); err != nil { + return nil, err + } + return &streampb.TruncateStreamResponse{FrontendResponse: &streampb.TruncateStreamOutput{}}, nil +} + +// 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 *streampb.DeleteStreamRequest, +) (*streampb.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 &streampb.DeleteStreamResponse{FrontendResponse: &streampb.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..68d4259859c --- /dev/null +++ b/chasm/lib/stream/service/handler_test.go @@ -0,0 +1,55 @@ +package service + +import ( + "testing" + + "github.com/stretchr/testify/require" + commonpb "go.temporal.io/api/common/v1" + enumspb "go.temporal.io/api/enums/v1" + 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" + "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()) +} diff --git a/chasm/lib/stream/service/library.go b/chasm/lib/stream/service/library.go new file mode 100644 index 00000000000..8abf549cbbe --- /dev/null +++ b/chasm/lib/stream/service/library.go @@ -0,0 +1,89 @@ +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 +} + +func newLibrary( + h *handler, + retention *retentionTaskHandler, + notifyConsumers *notifyConsumersTaskHandler, +) *library { + return &library{handler: h, retention: retention, notifyConsumers: notifyConsumers} +} + +// 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, + ), + } +} + +func (l *library) RegisterServices(server *grpc.Server) { + streampb.RegisterStreamServiceServer(server, l.handler) +} diff --git a/chasm/lib/stream/service/tasks.go b/chasm/lib/stream/service/tasks.go new file mode 100644 index 00000000000..3471da034b2 --- /dev/null +++ b/chasm/lib/stream/service/tasks.go @@ -0,0 +1,292 @@ +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" +) + +// 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 +} + +// 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. Errors are collected rather than +// returned at the first, so one unreachable consumer does not stop the others +// being told, and returned at the end so the task retries. +func (n *consumerNotifier) notify( + ctx context.Context, + ref chasm.ComponentRef, + state *streampb.StreamState, + probeAll bool, +) error { + head := state.GetHeadOffset() + var notifyErrs []error + for consumerID, consumer := range state.GetConsumers() { + if !consumer.GetExternal() || !consumer.GetActive() { + continue + } + if !probeAll && consumer.GetOffset() >= head { + continue + } + + response, err := n.routed.AdvanceConsumerHead(ctx, &streampb.AdvanceConsumerHeadRequest{ + NamespaceId: ref.NamespaceID, + FrontendRequest: &streampb.AdvanceConsumerHeadInput{ + WorkflowId: consumer.GetWorkflowId(), + OwnerRunId: consumer.GetRunId(), + StreamId: ref.BusinessID, + HeadOffset: head, + }, + }) + var gone *serviceerror.NotFound + out := response.GetFrontendResponse() + switch { + case errors.As(err, &gone), err == nil && 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. + notifyErrs = append(notifyErrs, forgetConsumer(ctx, ref, consumerID)) + case err != nil: + 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)) + notifyErrs = append(notifyErrs, err) + 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. + notifyErrs = append(notifyErrs, rekeyConsumer(ctx, ref, consumer, out)) + default: + // Told, and still there. Nothing to clean up. + } + } + return errors.Join(notifyErrs...) +} + +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(), + Offset: 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. +func backgroundCallerContext( + ctx context.Context, + registry namespace.Registry, + namespaceID string, +) context.Context { + if name, err := registry.GetNamespaceName(namespace.ID(namespaceID)); err == nil { + return headers.SetCallerInfo(ctx, headers.NewBackgroundLowCallerInfo(name.String())) + } + return ctx +} + +// 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 = 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, 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}, + } +} + +func (h *notifyConsumersTaskHandler) Validate( + _ chasm.Context, + s *stream.Stream, + _ chasm.TaskInvocation, + _ *streampb.StreamNotifyConsumersTask, +) (bool, error) { + for _, consumer := range s.State.GetConsumers() { + if consumer.GetExternal() && consumer.GetActive() { + return true, nil + } + } + return false, nil +} + +func (h *notifyConsumersTaskHandler) Execute( + ctx context.Context, + ref chasm.ComponentRef, + _ chasm.TaskAttributes, + _ *streampb.StreamNotifyConsumersTask, +) error { + ctx = 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, state, false) +} + +func (h *notifyConsumersTaskHandler) Discard( + _ context.Context, + _ chasm.ComponentRef, + _ chasm.TaskAttributes, + _ *streampb.StreamNotifyConsumersTask, +) error { + return nil +} 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_commands.go b/chasm/lib/workflow/stream_commands.go new file mode 100644 index 00000000000..56bf32f2c9c --- /dev/null +++ b/chasm/lib/workflow/stream_commands.go @@ -0,0 +1,56 @@ +package workflow + +import ( + "go.temporal.io/api/serviceerror" + "go.temporal.io/server/chasm" + "go.temporal.io/server/chasm/lib/stream" + streamlib "go.temporal.io/server/chasm/lib/stream/gen/streampb/v1" +) + +// DefaultStreamName is the stream a command addresses when it names none. +const DefaultStreamName = "output" + +// 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 w.Streams == nil { + w.Streams = make(chasm.Map[string, *stream.Stream]) + } + if field, ok := w.Streams[name]; ok { + return field.Get(ctx), nil + } + + // Checked only on the create path, so an existing stream is never refused + // for room. The name 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 terminates the + // workflow. + if len(name) > stream.MaxStreamNameLength { + return nil, serviceerror.NewInvalidArgumentf( + "stream name is %d characters, over the %d limit", len(name), stream.MaxStreamNameLength) + } + if len(w.Streams) >= limits.MaxOwnedStreamsPerWorkflow { + return nil, serviceerror.NewFailedPreconditionf( + "workflow already owns %d streams, the limit", limits.MaxOwnedStreamsPerWorkflow) + } + + // Budgeted, because the batches live in this execution's mutable state and + // the size limit on that terminates the workflow instead of refusing. + created, err := stream.NewStream(ctx, stream.NewStreamRequest{ + Attached: true, + Budget: &streamlib.StreamBudget{ + MaxItems: int64(limits.OwnedStreamMaxItems), + MaxBytes: int64(limits.OwnedStreamMaxBytes), + }, + }) + if err != nil { + return nil, err + } + w.Streams[name] = chasm.NewComponentField(ctx, created) + return created, nil +} diff --git a/chasm/lib/workflow/workflow.go b/chasm/lib/workflow/workflow.go index 2b5409a4147..1f6713d4faf 100644 --- a/chasm/lib/workflow/workflow.go +++ b/chasm/lib/workflow/workflow.go @@ -11,6 +11,8 @@ import ( "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" + streamlib "go.temporal.io/server/chasm/lib/stream/gen/streampb/v1" 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,129 @@ 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. + 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. +// +// A negative start offset means "from wherever the stream is now". That is +// resolved here and 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, + startOffset int64, + 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 { + // Resubscribing must not rewind a cursor: ranges below it are already + // recorded in History, and moving back would replay them as new. + return existing.Get(mctx).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, + Offset: startOffset, + 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 { + // Resubscribing must not rewind: ranges below the cursor are already + // recorded in History, and moving back would replay them as new. + return existing.Get(mctx).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] + if !ok { + return serviceerror.NewNotFoundf("workflow does not consume stream %q", streamID) + } + field.Get(mctx).AdvanceKnownHead(mctx, head) + return nil } func NewWorkflow( @@ -317,3 +442,58 @@ func (w *Workflow) HasAnyBufferedEvent(filter historybuilder.BufferedEventFilter func (w *Workflow) WorkflowTypeName() string { return w.GetWorkflowTypeName() } + +// OwnedStreamState returns the state of a stream this workflow owns, or nil if +// it owns none by that name. +// +// An attached stream has no id of its own, so this is the only way to see its +// frontier from outside the execution. Reading it needs the owner's component, +// which is why it lives here rather than on the stream. +// +// Absent is not an error. An owned stream is created by the first publish to +// it, so a reader that arrives before the workflow has published anything is +// the ordinary case rather than a mistake, and from outside the execution +// "not created yet" and "never will be" are the same observation. +func (w *Workflow) OwnedStreamState( + ctx chasm.Context, + name string, +) (*streamlib.StreamState, error) { + field, ok := w.Streams[name] + if !ok { + return nil, nil + } + return field.Get(ctx).Snapshot(ctx, struct{}{}) +} + +// OwnedStream returns the attached stream itself, or nil when the workflow has +// not 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, + name string, +) *stream.Stream { + field, ok := w.Streams[name] + if !ok { + return nil + } + return field.Get(ctx) +} + +// AppendToOwnedStream appends to a stream this workflow owns 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. +func (w *Workflow) AppendToOwnedStream( + mctx chasm.MutableContext, + name string, + req stream.AddMessagesRequest, +) (stream.AddMessagesResult, error) { + s, err := w.streamNamed(mctx, name, req.Limits) + if err != nil { + return stream.AddMessagesResult{}, err + } + return s.AddMessages(mctx, 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 41843262284..d7d4dbef5fe 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 ( @@ -213,10 +216,46 @@ 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. Closing, truncating and deleting a stream are ordinary + // writes here; whether they should need an operator is a policy call this + // table does not make. 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": namespaceWrite, + "ListStreams": namespaceRead, + "DeleteStream": namespaceWrite, + } + + 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): @@ -225,6 +264,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..a914bdea5c4 --- /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.AccessWrite, + "ListStreams": api.AccessReadOnly, + "DeleteStream": api.AccessWrite, +} + +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/rpc/interceptor/redirection_test.go b/common/rpc/interceptor/redirection_test.go index 11e6df3a681..66bf8f1c548 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" @@ -698,3 +699,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 beaf12baa2a..b93c0fa9731 100644 --- a/service/frontend/configs/quotas.go +++ b/service/frontend/configs/quotas.go @@ -22,6 +22,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 ( @@ -61,6 +65,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 @@ -208,6 +216,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} @@ -233,6 +261,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 efb5c49291d..5e3d028df76 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" @@ -121,6 +123,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) } @@ -171,6 +176,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 651551de210..470506c4ce3 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" @@ -146,6 +147,7 @@ var Module = fx.Options( chasmworkflow.Module, chasmcallback.Module, activity.FrontendModule, + chasmstream.FrontendModule, fx.Provide(visibility.ChasmVisibilityManagerProvider), fx.Provide(chasm.ChasmVisibilityInterceptorProvider), ) @@ -158,6 +160,7 @@ func NewServiceProvider( handler Handler, adminHandler *AdminHandler, operatorHandler *OperatorHandlerImpl, + streamHandler *chasmstream.FrontendHandler, versionChecker *VersionChecker, visibilityMgr manager.VisibilityManager, logger log.SnTaggedLogger, @@ -173,6 +176,7 @@ func NewServiceProvider( handler, adminHandler, operatorHandler, + streamHandler, versionChecker, visibilityMgr, logger, @@ -412,7 +416,7 @@ func RedirectionInterceptorProvider( metricsHandler, timeSource, clusterMetadata, - ) + ).WithRedirectResponses(chasmstream.RedirectableMethods()) } func BusinessIDInterceptorProvider( diff --git a/service/frontend/service.go b/service/frontend/service.go index f48d86c33bc..62da8849873 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" "go.temporal.io/server/common/callbacks" "go.temporal.io/server/common/dynamicconfig" "go.temporal.io/server/common/log" @@ -458,6 +460,7 @@ type Service struct { handler Handler adminHandler *AdminHandler operatorHandler *OperatorHandlerImpl + streamHandler *chasmstream.FrontendHandler versionChecker *VersionChecker visibilityManager manager.VisibilityManager server *grpc.Server @@ -477,6 +480,7 @@ func NewService( handler Handler, adminHandler *AdminHandler, operatorHandler *OperatorHandlerImpl, + streamHandler *chasmstream.FrontendHandler, versionChecker *VersionChecker, visibilityMgr manager.VisibilityManager, logger log.Logger, @@ -492,6 +496,7 @@ func NewService( handler: handler, adminHandler: adminHandler, operatorHandler: operatorHandler, + streamHandler: streamHandler, versionChecker: versionChecker, visibilityManager: visibilityMgr, logger: logger, @@ -509,6 +514,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 dcf233185fe..0b004c3ab3e 100644 --- a/service/history/fx.go +++ b/service/history/fx.go @@ -11,6 +11,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" @@ -123,6 +124,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_test.go b/tests/stream_test.go new file mode 100644 index 00000000000..9eae35308c3 --- /dev/null +++ b/tests/stream_test.go @@ -0,0 +1,656 @@ +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/credentials/insecure" +) + +// 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") +} + +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") + + _, 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")}) + require.Error(t, err) + + // 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) +} + +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") +} + +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 +} From 7abbfa9758167f8d9ea9a59c5927781991f59fb6 Mon Sep 17 00:00:00 2001 From: Moe Dashti Date: Fri, 25 Sep 2026 13:13:12 -0700 Subject: [PATCH 02/17] Tightened stream service admission and consumer notification. Registering the service turned the whole surface on everywhere, so it is off by default now and the calls that destroy other workflows' data need an operator. A workflow's owned streams share one byte budget, because their per-stream budgets multiplied out well past the mutable state limit. The notify task no longer strands its coalescing flag or drops a consumer's pin on a transport error. --- chasm/lib/stream/config.go | 54 +++++- .../gen/streampb/v1/request_response.pb.go | 21 ++- .../stream/proto/v1/request_response.proto | 8 + chasm/lib/stream/service/frontend.go | 93 +++++++++- chasm/lib/stream/service/frontend_test.go | 91 +++++++++- chasm/lib/stream/service/handler.go | 104 +++++++++-- chasm/lib/stream/service/tasks.go | 170 ++++++++++++------ chasm/lib/stream/service/tasks_test.go | 34 ++++ chasm/lib/stream/stream.go | 21 ++- chasm/lib/workflow/stream_admission_test.go | 116 ++++++++++++ chasm/lib/workflow/stream_commands.go | 22 +++ chasm/lib/workflow/workflow.go | 31 +++- common/api/metadata.go | 16 +- common/api/metadata_stream_test.go | 4 +- tests/testcore/dynamic_config_overrides.go | 5 + 15 files changed, 696 insertions(+), 94 deletions(-) create mode 100644 chasm/lib/stream/service/tasks_test.go create mode 100644 chasm/lib/workflow/stream_admission_test.go diff --git a/chasm/lib/stream/config.go b/chasm/lib/stream/config.go index f4d10083133..24601649651 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,6 +163,13 @@ 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, @@ -154,6 +180,7 @@ consumers holding it are still running.`, // 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 @@ -167,10 +194,14 @@ 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 } func NewConfig(dc *dynamicconfig.Collection) *Config { return &Config{ + Enabled: EnabledSetting.Get(dc), MaxIDLength: dynamicconfig.MaxIDLengthLimit.Get(dc), RetentionRecheckInterval: RetentionRecheckIntervalSetting.Get(dc), MaxConsumeItemsPerTask: MaxConsumeItemsPerTaskSetting.Get(dc), @@ -182,6 +213,8 @@ func NewConfig(dc *dynamicconfig.Collection) *Config { MaxOwnedStreamsPerWorkflow: MaxOwnedStreamsPerWorkflowSetting.Get(dc), OwnedStreamMaxBytes: OwnedStreamMaxBytesSetting.Get(dc), OwnedStreamMaxItems: OwnedStreamMaxItemsSetting.Get(dc), + + OwnedStreamsMaxBytesPerWorkflow: OwnedStreamsMaxBytesPerWorkflowSetting.Get(dc), } } @@ -197,6 +230,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 +250,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 +283,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 +295,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/gen/streampb/v1/request_response.pb.go b/chasm/lib/stream/gen/streampb/v1/request_response.pb.go index ab1d3863ef8..fc5105a86f3 100644 --- a/chasm/lib/stream/gen/streampb/v1/request_response.pb.go +++ b/chasm/lib/stream/gen/streampb/v1/request_response.pb.go @@ -2244,7 +2244,14 @@ type RegisterStreamConsumerOutput struct { 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"` + 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 } @@ -2293,6 +2300,13 @@ func (x *RegisterStreamConsumerOutput) GetKnownHead() int64 { 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 { @@ -3443,11 +3457,12 @@ const file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawD "\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\"l\n" + + "\fstart_offset\x18\x04 \x01(\x03R\vstartOffset\"\x91\x01\n" + "\x1cRegisterStreamConsumerOutput\x12!\n" + "\fstart_offset\x18\x01 \x01(\x03R\vstartOffset\x12\x1d\n" + "\n" + - "known_head\x18\x04 \x01(\x03R\tknownHeadJ\x04\b\x02\x10\x03J\x04\b\x03\x10\x04\"\xb9\x01\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" + diff --git a/chasm/lib/stream/proto/v1/request_response.proto b/chasm/lib/stream/proto/v1/request_response.proto index 66e5dadb63f..bca04fc1276 100644 --- a/chasm/lib/stream/proto/v1/request_response.proto +++ b/chasm/lib/stream/proto/v1/request_response.proto @@ -285,6 +285,14 @@ message RegisterStreamConsumerOutput { // 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 diff --git a/chasm/lib/stream/service/frontend.go b/chasm/lib/stream/service/frontend.go index 431ae37ec06..17c9b99e6c3 100644 --- a/chasm/lib/stream/service/frontend.go +++ b/chasm/lib/stream/service/frontend.go @@ -2,13 +2,17 @@ package service import ( "context" + "strings" "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" "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" ) @@ -98,10 +102,20 @@ func RedirectableMethods() map[string]func() any { } } +// 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 @@ -109,6 +123,46 @@ func (h *FrontendHandler) namespaceID(name string) (string, error) { 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 *streampb.StreamLifecycle, +) (*streampb.StreamLifecycle, error) { + ns, err := h.namespaceRegistry.GetNamespace(namespace.Name(namespaceName)) + if err != nil { + return nil, err + } + if lifecycle == nil { + lifecycle = &streampb.StreamLifecycle{} + } + lifecycle = common.CloneProto(lifecycle) + + if lifecycle.GetMaxItems() < 0 { + return nil, serviceerror.NewInvalidArgumentf( + "max items cannot be negative, got %d", lifecycle.GetMaxItems()) + } + + 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()) + } + 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. @@ -150,6 +204,11 @@ func (h *FrontendHandler) CreateStream( 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, &streampb.CreateStreamRequest{ NamespaceId: id, FrontendRequest: in, }) @@ -166,6 +225,9 @@ func (h *FrontendHandler) AddMessages( 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, &streampb.AddMessagesRequest{ NamespaceId: id, FrontendRequest: in, }) @@ -182,6 +244,9 @@ func (h *FrontendHandler) FinishWriting( 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, &streampb.FinishWritingRequest{ NamespaceId: id, FrontendRequest: in, }) @@ -201,6 +266,9 @@ func (h *FrontendHandler) SubscribeWorkflow( 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 + } return h.client.SubscribeWorkflow(ctx, &streampb.SubscribeWorkflowRequest{ NamespaceId: id, FrontendRequest: in, }) @@ -240,6 +308,9 @@ func (h *FrontendHandler) PollWorkflowMessages( if err := checkOffset("from offset", in.GetFromOffset()); err != nil { return nil, err } + if err := h.checkID("workflow id", in.GetWorkflowId()); err != nil { + return nil, err + } in.MaxMessages = clampMaxMessages(in.GetMaxMessages()) return h.client.PollWorkflowMessages(ctx, &streampb.PollWorkflowMessagesRequest{ NamespaceId: id, FrontendRequest: in, @@ -257,6 +328,9 @@ func (h *FrontendHandler) DescribeWorkflowStream( 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 + } return h.client.DescribeWorkflowStream(ctx, &streampb.DescribeWorkflowStreamRequest{ NamespaceId: id, FrontendRequest: in, }) @@ -273,6 +347,12 @@ func (h *FrontendHandler) AddWorkflowMessages( 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 := h.checkID("producer id", in.GetProducerId()); err != nil { + return nil, err + } return h.client.AddWorkflowMessages(ctx, &streampb.AddWorkflowMessagesRequest{ NamespaceId: id, FrontendRequest: in, }) @@ -351,8 +431,17 @@ func (h *FrontendHandler) ListStreams( ctx context.Context, req *streampb.ListStreamsRequest, ) (*streampb.ListStreamsResponse, error) { in := req.GetFrontendRequest() - if in.GetNamespace() == "" { - return nil, serviceerror.NewInvalidArgument("namespace is required") + 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()) diff --git a/chasm/lib/stream/service/frontend_test.go b/chasm/lib/stream/service/frontend_test.go index b861176fc80..ea1326c1c71 100644 --- a/chasm/lib/stream/service/frontend_test.go +++ b/chasm/lib/stream/service/frontend_test.go @@ -4,9 +4,11 @@ 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" @@ -14,6 +16,7 @@ import ( "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 @@ -39,14 +42,76 @@ func (c *recordingClient) TruncateStream( } 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{MaxIDLength: dynamicconfig.GetIntPropertyFn(maxIDLength)} + 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) { @@ -93,3 +158,27 @@ func TestFrontendClampsThePageSize(t *testing.T) { 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") +} diff --git a/chasm/lib/stream/service/handler.go b/chasm/lib/stream/service/handler.go index 2d716ce671c..2c356c1fbb8 100644 --- a/chasm/lib/stream/service/handler.go +++ b/chasm/lib/stream/service/handler.go @@ -269,6 +269,11 @@ func (h *handler) SubscribeWorkflow( in := req.GetFrontendRequest() 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) } @@ -341,6 +346,12 @@ func (h *handler) subscribeToExternalStream( 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, @@ -403,11 +414,31 @@ func (h *handler) RegisterStreamConsumer( in.GetStartOffset(), ) 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 &streampb.RegisterStreamConsumerResponse{ + FrontendResponse: &streampb.RegisterStreamConsumerOutput{StreamAbsent: true}, + }, nil + } return nil, err } return &streampb.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. @@ -438,7 +469,11 @@ func (h *handler) probeConsumer( runID: cctx.ExecutionKey().RunID, closed: !cctx.ExecutionInfo().CloseTime.IsZero(), } - if field, ok := wf.StreamCursors[streamID]; ok { + // 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() } @@ -528,17 +563,22 @@ func (h *handler) PollMessages( ref := refForRun(req.GetNamespaceId(), in.GetStreamId(), in.GetRunId()) from := in.GetFromOffset() - state, err := chasm.ReadComponent(ctx, ref, (*stream.Stream).Snapshot, struct{}{}) - if err != nil { - return nil, err - } - - // Blocking is only worth it once the reader is genuinely caught up. - if in.GetWaitNewMessages() && from == state.GetHeadOffset() && !state.GetClosed() { - // The window is re-read below, so only the blocking matters here. - if _, err := h.waitForMessages(ctx, ref, from, state); err != nil { + // 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() { + state, err := chasm.ReadComponent(ctx, ref, (*stream.Stream).Snapshot, struct{}{}) + if err != nil { return nil, err } + // Blocking is only worth it once the reader is genuinely caught up. + if from == state.GetHeadOffset() && !state.GetClosed() { + // The window is re-read below, so only the blocking matters here. + if _, err := h.waitForMessages(ctx, ref, from, state); err != nil { + return nil, err + } + } } wreq := stream.WindowRequest{From: from, MaxMessages: in.GetMaxMessages(), Topics: in.GetTopics()} @@ -840,26 +880,54 @@ func (h *handler) CloseStream( return &streampb.CloseStreamResponse{FrontendResponse: &streampb.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 *streampb.TruncateStreamRequest, ) (*streampb.TruncateStreamResponse, error) { in := req.GetFrontendRequest() ctx = h.withCallerInfo(ctx, req.GetNamespaceId()) + ref := refFor(req.GetNamespaceId(), in.GetStreamId()) - if _, _, err := chasm.UpdateComponent( - ctx, - refFor(req.GetNamespaceId(), in.GetStreamId()), - func(s *stream.Stream, mctx chasm.MutableContext, newBase int64) (struct{}, error) { - return struct{}{}, s.Truncate(mctx, newBase) - }, - in.GetNewBaseOffset(), - ); err != nil { + err := h.truncate(ctx, ref, in.GetNewBaseOffset()) + var pinned *serviceerror.FailedPrecondition + if errors.As(err, &pinned) { + 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 &streampb.TruncateStreamResponse{FrontendResponse: &streampb.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. diff --git a/chasm/lib/stream/service/tasks.go b/chasm/lib/stream/service/tasks.go index 3471da034b2..4f7f78778b0 100644 --- a/chasm/lib/stream/service/tasks.go +++ b/chasm/lib/stream/service/tasks.go @@ -13,6 +13,7 @@ import ( "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 @@ -27,19 +28,29 @@ type consumerNotifier struct { 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. Errors are collected rather than -// returned at the first, so one unreachable consumer does not stop the others -// being told, and returned at the end so the task retries. +// 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 notifyErrs []error + var group errgroup.Group + group.SetLimit(notifyConcurrency) for consumerID, consumer := range state.GetConsumers() { if !consumer.GetExternal() || !consumer.GetActive() { continue @@ -47,41 +58,70 @@ func (n *consumerNotifier) notify( if !probeAll && consumer.GetOffset() >= head { continue } - - response, err := n.routed.AdvanceConsumerHead(ctx, &streampb.AdvanceConsumerHeadRequest{ - NamespaceId: ref.NamespaceID, - FrontendRequest: &streampb.AdvanceConsumerHeadInput{ - WorkflowId: consumer.GetWorkflowId(), - OwnerRunId: consumer.GetRunId(), - StreamId: ref.BusinessID, - HeadOffset: head, - }, + group.Go(func() error { + return n.notifyOne(ctx, ref, namespaceName, consumerID, consumer, head) }) - var gone *serviceerror.NotFound - out := response.GetFrontendResponse() - switch { - case errors.As(err, &gone), err == nil && 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. - notifyErrs = append(notifyErrs, forgetConsumer(ctx, ref, consumerID)) - case err != nil: - 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)) - notifyErrs = append(notifyErrs, err) - 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. - notifyErrs = append(notifyErrs, rekeyConsumer(ctx, ref, consumer, out)) - default: - // Told, and still there. Nothing to clean up. - } } - return errors.Join(notifyErrs...) + 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 { @@ -119,16 +159,20 @@ func rekeyConsumer( } // backgroundCallerContext tags a task's context with the namespace, since the -// task runs outside any request and nothing else has done it. +// 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 { - if name, err := registry.GetNamespaceName(namespace.ID(namespaceID)); err == nil { - return headers.SetCallerInfo(ctx, headers.NewBackgroundLowCallerInfo(name.String())) +) (context.Context, string) { + name, err := registry.GetNamespaceName(namespace.ID(namespaceID)) + if err != nil { + return ctx, "" } - return ctx + return headers.SetCallerInfo( + ctx, headers.NewBackgroundLowCallerInfo(name.String())), name.String() } // retentionTaskHandler deletes a stream once its retention has elapsed. Close @@ -184,13 +228,13 @@ func (h *retentionTaskHandler) Execute( _ chasm.TaskAttributes, _ *streampb.StreamRetentionTask, ) error { - ctx = backgroundCallerContext(ctx, h.namespaceRegistry, ref.NamespaceID) + 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, state, true); err != nil { + if err := h.notifier.notify(ctx, ref, namespaceName, state, true); err != nil { return err } @@ -251,18 +295,22 @@ func newNotifyConsumersTaskHandler( } } +// 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, - s *stream.Stream, + _ *stream.Stream, _ chasm.TaskInvocation, _ *streampb.StreamNotifyConsumersTask, ) (bool, error) { - for _, consumer := range s.State.GetConsumers() { - if consumer.GetExternal() && consumer.GetActive() { - return true, nil - } - } - return false, nil + return true, nil } func (h *notifyConsumersTaskHandler) Execute( @@ -271,7 +319,7 @@ func (h *notifyConsumersTaskHandler) Execute( _ chasm.TaskAttributes, _ *streampb.StreamNotifyConsumersTask, ) error { - ctx = backgroundCallerContext(ctx, h.namespaceRegistry, ref.NamespaceID) + 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. @@ -279,14 +327,24 @@ func (h *notifyConsumersTaskHandler) Execute( if err != nil { return err } - return h.notifier.notify(ctx, ref, state, false) + 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( - _ context.Context, - _ chasm.ComponentRef, + ctx context.Context, + ref chasm.ComponentRef, _ chasm.TaskAttributes, _ *streampb.StreamNotifyConsumersTask, ) error { - return nil + ctx, _ = backgroundCallerContext(ctx, h.namespaceRegistry, ref.NamespaceID) + _, _, err := chasm.UpdateComponent(ctx, ref, (*stream.Stream).TakeNotifySnapshot, struct{}{}) + var notFound *serviceerror.NotFound + if errors.As(err, ¬Found) { + 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/stream.go b/chasm/lib/stream/stream.go index dd3bd6b1905..03ff5fd04db 100644 --- a/chasm/lib/stream/stream.go +++ b/chasm/lib/stream/stream.go @@ -76,6 +76,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 { @@ -180,7 +185,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 } @@ -309,11 +315,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, diff --git a/chasm/lib/workflow/stream_admission_test.go b/chasm/lib/workflow/stream_admission_test.go new file mode 100644 index 00000000000..f0e85bb3037 --- /dev/null +++ b/chasm/lib/workflow/stream_admission_test.go @@ -0,0 +1,116 @@ +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", 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", 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", 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) +} diff --git a/chasm/lib/workflow/stream_commands.go b/chasm/lib/workflow/stream_commands.go index 56bf32f2c9c..8bfdc131469 100644 --- a/chasm/lib/workflow/stream_commands.go +++ b/chasm/lib/workflow/stream_commands.go @@ -54,3 +54,25 @@ func (w *Workflow) streamNamed( w.Streams[name] = chasm.NewComponentField(ctx, created) return created, nil } + +// siblingStreamBytes is what every other stream this workflow owns 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 the 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 (w *Workflow) siblingStreamBytes(ctx chasm.Context, name string) int64 { + if len(w.Streams) < 2 { + return 0 + } + var total int64 + for other, field := range w.Streams { + if other == name { + continue + } + total += field.Get(ctx).State.GetAppendedBytes() + } + return total +} diff --git a/chasm/lib/workflow/workflow.go b/chasm/lib/workflow/workflow.go index 1f6713d4faf..17a3697f92e 100644 --- a/chasm/lib/workflow/workflow.go +++ b/chasm/lib/workflow/workflow.go @@ -49,6 +49,11 @@ type Workflow struct { // 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] } @@ -83,9 +88,15 @@ func (w *Workflow) SubscribeToOwnedStream( 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 existing.Get(mctx).Offset(), nil + return cursor.Offset(), nil } // Pin the stream's floor in the same transaction as the cursor. Registered @@ -133,9 +144,18 @@ func (w *Workflow) SubscribeToExternalStream( 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 existing.Get(mctx).Offset(), nil + return cursor.Offset(), nil } cursor, err := stream.NewCursor(mctx, stream.NewCursorRequest{ @@ -159,7 +179,11 @@ func (w *Workflow) SubscribeToExternalStream( // 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] - if !ok { + // 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) @@ -495,5 +519,6 @@ func (w *Workflow) AppendToOwnedStream( if err != nil { return stream.AddMessagesResult{}, err } + req.SiblingBytes = w.siblingStreamBytes(mctx, name) return s.AddMessages(mctx, req) } diff --git a/common/api/metadata.go b/common/api/metadata.go index d7d4dbef5fe..17780b7da2e 100644 --- a/common/api/metadata.go +++ b/common/api/metadata.go @@ -218,10 +218,14 @@ var ( } // Every stream request carries its namespace inside `frontend_request`, // exposed through a hand-written `GetNamespace()`, which is what namespace - // scope needs. Closing, truncating and deleting a stream are ordinary - // writes here; whether they should need an operator is a policy call this - // table does not make. The two internal calls History makes on itself are - // admin so that no namespace-level role can reach them through a frontend. + // 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, @@ -235,9 +239,9 @@ var ( "RegisterStreamConsumer": namespaceAdmin, "AdvanceConsumerHead": namespaceAdmin, "CloseStream": namespaceWrite, - "TruncateStream": namespaceWrite, + "TruncateStream": namespaceAdmin, "ListStreams": namespaceRead, - "DeleteStream": namespaceWrite, + "DeleteStream": namespaceAdmin, } namespaceRead = MethodMetadata{ diff --git a/common/api/metadata_stream_test.go b/common/api/metadata_stream_test.go index a914bdea5c4..be19207a131 100644 --- a/common/api/metadata_stream_test.go +++ b/common/api/metadata_stream_test.go @@ -29,9 +29,9 @@ var expectedStreamAccess = map[string]api.Access{ "RegisterStreamConsumer": api.AccessAdmin, "AdvanceConsumerHead": api.AccessAdmin, "CloseStream": api.AccessWrite, - "TruncateStream": api.AccessWrite, + "TruncateStream": api.AccessAdmin, "ListStreams": api.AccessReadOnly, - "DeleteStream": api.AccessWrite, + "DeleteStream": api.AccessAdmin, } func TestStreamServiceMetadata(t *testing.T) { diff --git a/tests/testcore/dynamic_config_overrides.go b/tests/testcore/dynamic_config_overrides.go index c7e4ac8f74c..3f22c092423 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/components/nexusoperations" @@ -87,5 +88,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, } ) From acffaab92e2f897b723fe30a55d04e321ce12c79 Mon Sep 17 00:00:00 2001 From: Moe Dashti Date: Fri, 25 Sep 2026 16:59:20 -0700 Subject: [PATCH 03/17] Took the stream handler to errors.AsType. Main now builds under Go 1.27, where go fix rewrites this shape, so make fmt was left dirty on these two files. --- chasm/lib/stream/service/handler.go | 6 ++---- chasm/lib/stream/service/tasks.go | 3 +-- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/chasm/lib/stream/service/handler.go b/chasm/lib/stream/service/handler.go index 2c356c1fbb8..b2cd403d4a8 100644 --- a/chasm/lib/stream/service/handler.go +++ b/chasm/lib/stream/service/handler.go @@ -479,8 +479,7 @@ func (h *handler) probeConsumer( } return p, nil }, struct{}{}) - var notFound *serviceerror.NotFound - if errors.As(err, ¬Found) { + if _, ok := errors.AsType[*serviceerror.NotFound](err); ok { return consumerProbe{runID: runID, closed: true}, nil } return probe, err @@ -897,8 +896,7 @@ func (h *handler) TruncateStream( ref := refFor(req.GetNamespaceId(), in.GetStreamId()) err := h.truncate(ctx, ref, in.GetNewBaseOffset()) - var pinned *serviceerror.FailedPrecondition - if errors.As(err, &pinned) { + if _, ok := errors.AsType[*serviceerror.FailedPrecondition](err); ok { state, readErr := chasm.ReadComponent(ctx, ref, (*stream.Stream).Snapshot, struct{}{}) if readErr != nil { return nil, err diff --git a/chasm/lib/stream/service/tasks.go b/chasm/lib/stream/service/tasks.go index 4f7f78778b0..ceb1e313b81 100644 --- a/chasm/lib/stream/service/tasks.go +++ b/chasm/lib/stream/service/tasks.go @@ -342,8 +342,7 @@ func (h *notifyConsumersTaskHandler) Discard( ) error { ctx, _ = backgroundCallerContext(ctx, h.namespaceRegistry, ref.NamespaceID) _, _, err := chasm.UpdateComponent(ctx, ref, (*stream.Stream).TakeNotifySnapshot, struct{}{}) - var notFound *serviceerror.NotFound - if errors.As(err, ¬Found) { + if _, ok := errors.AsType[*serviceerror.NotFound](err); ok { return nil } return err From 3fcbf9fb6c979baea5f3a29cbbe24597c5ea8030 Mon Sep 17 00:00:00 2001 From: Moe Dashti Date: Mon, 28 Sep 2026 15:40:17 -0700 Subject: [PATCH 04/17] Let activities own streams, addressed by an owner reference. A standalone activity gets its own stream map, and an activity a workflow scheduled keeps its streams in the workflow's map under reserved keys until it is a component of its own. One owner reference on the owned-stream calls reaches all three owners, so no second set of calls per owner kind is needed. --- chasm/lib/activity/activity.go | 6 + chasm/lib/activity/streams.go | 45 ++ chasm/lib/activity/streams_test.go | 102 +++ .../v1/request_response.go-helpers.pb.go | 59 ++ .../gen/streampb/v1/request_response.pb.go | 747 +++++++++++------- .../lib/stream/gen/streampb/v1/service.pb.go | 10 +- .../gen/streampb/v1/service_client.pb.go | 6 +- .../stream/gen/streampb/v1/service_grpc.pb.go | 6 +- chasm/lib/stream/owned.go | 137 ++++ .../stream/proto/v1/request_response.proto | 55 +- chasm/lib/stream/proto/v1/service.proto | 9 +- chasm/lib/stream/service/frontend.go | 44 +- chasm/lib/stream/service/frontend_test.go | 27 + chasm/lib/stream/service/handler.go | 199 +++-- chasm/lib/stream/service/handler_test.go | 53 ++ chasm/lib/workflow/stream_admission_test.go | 32 + chasm/lib/workflow/stream_commands.go | 99 ++- chasm/lib/workflow/workflow.go | 58 +- 18 files changed, 1230 insertions(+), 464 deletions(-) create mode 100644 chasm/lib/activity/streams.go create mode 100644 chasm/lib/activity/streams_test.go create mode 100644 chasm/lib/stream/owned.go 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..f0c32a25975 --- /dev/null +++ b/chasm/lib/activity/streams.go @@ -0,0 +1,45 @@ +package activity + +import ( + "go.temporal.io/api/serviceerror" + "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{}, serviceerror.NewFailedPreconditionf( + "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..fe294c03128 --- /dev/null +++ b/chasm/lib/activity/streams_test.go @@ -0,0 +1,102 @@ +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.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/gen/streampb/v1/request_response.go-helpers.pb.go b/chasm/lib/stream/gen/streampb/v1/request_response.go-helpers.pb.go index 7c11d9434cd..ae21fe1d337 100644 --- 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 @@ -2,6 +2,8 @@ package streampb import ( + "fmt" + "google.golang.org/protobuf/proto" ) @@ -412,6 +414,43 @@ func (this *DescribeStreamInput) Equal(that interface{}) bool { 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) @@ -2150,3 +2189,23 @@ func (this *DeleteStreamResponse) Equal(that interface{}) bool { 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 index fc5105a86f3..b10fee610da 100644 --- a/chasm/lib/stream/gen/streampb/v1/request_response.pb.go +++ b/chasm/lib/stream/gen/streampb/v1/request_response.pb.go @@ -8,6 +8,7 @@ package streampb import ( reflect "reflect" + "strconv" sync "sync" unsafe "unsafe" @@ -23,6 +24,74 @@ const ( _ = 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"` @@ -773,18 +842,93 @@ func (x *DescribeStreamInput) GetStreamId() string { return "" } -// A stream a workflow owns lives inside that workflow's execution, 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"` - 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 +// 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 workflow's default output stream. + // 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"` MaxMessages int32 `protobuf:"varint,5,opt,name=max_messages,json=maxMessages,proto3" json:"max_messages,omitempty"` @@ -797,7 +941,7 @@ type PollWorkflowMessagesInput struct { func (x *PollWorkflowMessagesInput) Reset() { *x = PollWorkflowMessagesInput{} - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[11] + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -809,7 +953,7 @@ func (x *PollWorkflowMessagesInput) String() string { func (*PollWorkflowMessagesInput) ProtoMessage() {} func (x *PollWorkflowMessagesInput) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[11] + 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 { @@ -822,7 +966,7 @@ func (x *PollWorkflowMessagesInput) ProtoReflect() protoreflect.Message { // 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{11} + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{12} } func (x *PollWorkflowMessagesInput) GetNamespace() string { @@ -832,6 +976,13 @@ func (x *PollWorkflowMessagesInput) GetNamespace() string { 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 @@ -882,12 +1033,11 @@ func (x *PollWorkflowMessagesInput) GetWaitNewMessages() bool { } type DescribeWorkflowStreamInput 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. + 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 @@ -896,7 +1046,7 @@ type DescribeWorkflowStreamInput struct { func (x *DescribeWorkflowStreamInput) Reset() { *x = DescribeWorkflowStreamInput{} - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[12] + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -908,7 +1058,7 @@ func (x *DescribeWorkflowStreamInput) String() string { func (*DescribeWorkflowStreamInput) ProtoMessage() {} func (x *DescribeWorkflowStreamInput) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[12] + 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 { @@ -921,7 +1071,7 @@ func (x *DescribeWorkflowStreamInput) ProtoReflect() protoreflect.Message { // 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{12} + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{13} } func (x *DescribeWorkflowStreamInput) GetNamespace() string { @@ -931,6 +1081,13 @@ func (x *DescribeWorkflowStreamInput) GetNamespace() string { 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 @@ -952,17 +1109,16 @@ func (x *DescribeWorkflowStreamInput) GetStreamName() string { return "" } -// Appending to a stream a workflow owns, from outside that workflow. The -// workflow's own publishes ride its Workflow Task instead. +// 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"` - 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. + 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 workflow's default output stream. + // 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. @@ -974,7 +1130,7 @@ type AddWorkflowMessagesInput struct { func (x *AddWorkflowMessagesInput) Reset() { *x = AddWorkflowMessagesInput{} - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[13] + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -986,7 +1142,7 @@ func (x *AddWorkflowMessagesInput) String() string { func (*AddWorkflowMessagesInput) ProtoMessage() {} func (x *AddWorkflowMessagesInput) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[13] + 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 { @@ -999,7 +1155,7 @@ func (x *AddWorkflowMessagesInput) ProtoReflect() protoreflect.Message { // 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{13} + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{14} } func (x *AddWorkflowMessagesInput) GetNamespace() string { @@ -1009,6 +1165,13 @@ func (x *AddWorkflowMessagesInput) GetNamespace() string { 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 @@ -1060,7 +1223,7 @@ type DescribeStreamOutput struct { func (x *DescribeStreamOutput) Reset() { *x = DescribeStreamOutput{} - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[14] + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1072,7 +1235,7 @@ func (x *DescribeStreamOutput) String() string { func (*DescribeStreamOutput) ProtoMessage() {} func (x *DescribeStreamOutput) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[14] + 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 { @@ -1085,7 +1248,7 @@ func (x *DescribeStreamOutput) ProtoReflect() protoreflect.Message { // 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{14} + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{15} } func (x *DescribeStreamOutput) GetState() *StreamState { @@ -1106,7 +1269,7 @@ type CloseStreamInput struct { func (x *CloseStreamInput) Reset() { *x = CloseStreamInput{} - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[15] + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1118,7 +1281,7 @@ func (x *CloseStreamInput) String() string { func (*CloseStreamInput) ProtoMessage() {} func (x *CloseStreamInput) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[15] + 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 { @@ -1131,7 +1294,7 @@ func (x *CloseStreamInput) ProtoReflect() protoreflect.Message { // 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{15} + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{16} } func (x *CloseStreamInput) GetNamespace() string { @@ -1163,7 +1326,7 @@ type CloseStreamOutput struct { func (x *CloseStreamOutput) Reset() { *x = CloseStreamOutput{} - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[16] + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1175,7 +1338,7 @@ func (x *CloseStreamOutput) String() string { func (*CloseStreamOutput) ProtoMessage() {} func (x *CloseStreamOutput) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[16] + 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 { @@ -1188,7 +1351,7 @@ func (x *CloseStreamOutput) ProtoReflect() protoreflect.Message { // 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{16} + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{17} } type TruncateStreamInput struct { @@ -1202,7 +1365,7 @@ type TruncateStreamInput struct { func (x *TruncateStreamInput) Reset() { *x = TruncateStreamInput{} - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[17] + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1214,7 +1377,7 @@ func (x *TruncateStreamInput) String() string { func (*TruncateStreamInput) ProtoMessage() {} func (x *TruncateStreamInput) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[17] + 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 { @@ -1227,7 +1390,7 @@ func (x *TruncateStreamInput) ProtoReflect() protoreflect.Message { // 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{17} + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{18} } func (x *TruncateStreamInput) GetNamespace() string { @@ -1259,7 +1422,7 @@ type TruncateStreamOutput struct { func (x *TruncateStreamOutput) Reset() { *x = TruncateStreamOutput{} - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[18] + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1271,7 +1434,7 @@ func (x *TruncateStreamOutput) String() string { func (*TruncateStreamOutput) ProtoMessage() {} func (x *TruncateStreamOutput) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[18] + 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 { @@ -1284,7 +1447,7 @@ func (x *TruncateStreamOutput) ProtoReflect() protoreflect.Message { // 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{18} + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{19} } type DeleteStreamInput struct { @@ -1301,7 +1464,7 @@ type DeleteStreamInput struct { func (x *DeleteStreamInput) Reset() { *x = DeleteStreamInput{} - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[19] + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1313,7 +1476,7 @@ func (x *DeleteStreamInput) String() string { func (*DeleteStreamInput) ProtoMessage() {} func (x *DeleteStreamInput) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[19] + 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 { @@ -1326,7 +1489,7 @@ func (x *DeleteStreamInput) ProtoReflect() protoreflect.Message { // 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{19} + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{20} } func (x *DeleteStreamInput) GetNamespace() string { @@ -1358,7 +1521,7 @@ type DeleteStreamOutput struct { func (x *DeleteStreamOutput) Reset() { *x = DeleteStreamOutput{} - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[20] + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1370,7 +1533,7 @@ func (x *DeleteStreamOutput) String() string { func (*DeleteStreamOutput) ProtoMessage() {} func (x *DeleteStreamOutput) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[20] + 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 { @@ -1383,7 +1546,7 @@ func (x *DeleteStreamOutput) ProtoReflect() protoreflect.Message { // 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{20} + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{21} } type CreateStreamRequest struct { @@ -1396,7 +1559,7 @@ type CreateStreamRequest struct { func (x *CreateStreamRequest) Reset() { *x = CreateStreamRequest{} - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[21] + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1408,7 +1571,7 @@ func (x *CreateStreamRequest) String() string { func (*CreateStreamRequest) ProtoMessage() {} func (x *CreateStreamRequest) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[21] + 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 { @@ -1421,7 +1584,7 @@ func (x *CreateStreamRequest) ProtoReflect() protoreflect.Message { // 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{21} + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{22} } func (x *CreateStreamRequest) GetNamespaceId() string { @@ -1447,7 +1610,7 @@ type CreateStreamResponse struct { func (x *CreateStreamResponse) Reset() { *x = CreateStreamResponse{} - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[22] + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1459,7 +1622,7 @@ func (x *CreateStreamResponse) String() string { func (*CreateStreamResponse) ProtoMessage() {} func (x *CreateStreamResponse) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[22] + 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 { @@ -1472,7 +1635,7 @@ func (x *CreateStreamResponse) ProtoReflect() protoreflect.Message { // 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{22} + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{23} } func (x *CreateStreamResponse) GetFrontendResponse() *CreateStreamOutput { @@ -1492,7 +1655,7 @@ type AddMessagesRequest struct { func (x *AddMessagesRequest) Reset() { *x = AddMessagesRequest{} - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[23] + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1504,7 +1667,7 @@ func (x *AddMessagesRequest) String() string { func (*AddMessagesRequest) ProtoMessage() {} func (x *AddMessagesRequest) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[23] + 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 { @@ -1517,7 +1680,7 @@ func (x *AddMessagesRequest) ProtoReflect() protoreflect.Message { // 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{23} + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{24} } func (x *AddMessagesRequest) GetNamespaceId() string { @@ -1543,7 +1706,7 @@ type AddMessagesResponse struct { func (x *AddMessagesResponse) Reset() { *x = AddMessagesResponse{} - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[24] + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1555,7 +1718,7 @@ func (x *AddMessagesResponse) String() string { func (*AddMessagesResponse) ProtoMessage() {} func (x *AddMessagesResponse) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[24] + 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 { @@ -1568,7 +1731,7 @@ func (x *AddMessagesResponse) ProtoReflect() protoreflect.Message { // 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{24} + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{25} } func (x *AddMessagesResponse) GetFrontendResponse() *AddMessagesOutput { @@ -1588,7 +1751,7 @@ type FinishWritingRequest struct { func (x *FinishWritingRequest) Reset() { *x = FinishWritingRequest{} - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[25] + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1600,7 +1763,7 @@ func (x *FinishWritingRequest) String() string { func (*FinishWritingRequest) ProtoMessage() {} func (x *FinishWritingRequest) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[25] + 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 { @@ -1613,7 +1776,7 @@ func (x *FinishWritingRequest) ProtoReflect() protoreflect.Message { // 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{25} + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{26} } func (x *FinishWritingRequest) GetNamespaceId() string { @@ -1639,7 +1802,7 @@ type FinishWritingResponse struct { func (x *FinishWritingResponse) Reset() { *x = FinishWritingResponse{} - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[26] + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1651,7 +1814,7 @@ func (x *FinishWritingResponse) String() string { func (*FinishWritingResponse) ProtoMessage() {} func (x *FinishWritingResponse) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[26] + 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 { @@ -1664,7 +1827,7 @@ func (x *FinishWritingResponse) ProtoReflect() protoreflect.Message { // 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{26} + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{27} } func (x *FinishWritingResponse) GetFrontendResponse() *FinishWritingOutput { @@ -1684,7 +1847,7 @@ type SubscribeWorkflowRequest struct { func (x *SubscribeWorkflowRequest) Reset() { *x = SubscribeWorkflowRequest{} - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[27] + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1696,7 +1859,7 @@ func (x *SubscribeWorkflowRequest) String() string { func (*SubscribeWorkflowRequest) ProtoMessage() {} func (x *SubscribeWorkflowRequest) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[27] + 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 { @@ -1709,7 +1872,7 @@ func (x *SubscribeWorkflowRequest) ProtoReflect() protoreflect.Message { // 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{27} + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{28} } func (x *SubscribeWorkflowRequest) GetNamespaceId() string { @@ -1735,7 +1898,7 @@ type SubscribeWorkflowResponse struct { func (x *SubscribeWorkflowResponse) Reset() { *x = SubscribeWorkflowResponse{} - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[28] + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1747,7 +1910,7 @@ func (x *SubscribeWorkflowResponse) String() string { func (*SubscribeWorkflowResponse) ProtoMessage() {} func (x *SubscribeWorkflowResponse) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[28] + 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 { @@ -1760,7 +1923,7 @@ func (x *SubscribeWorkflowResponse) ProtoReflect() protoreflect.Message { // 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{28} + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{29} } func (x *SubscribeWorkflowResponse) GetFrontendResponse() *SubscribeWorkflowOutput { @@ -1780,7 +1943,7 @@ type PollMessagesRequest struct { func (x *PollMessagesRequest) Reset() { *x = PollMessagesRequest{} - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[29] + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1792,7 +1955,7 @@ func (x *PollMessagesRequest) String() string { func (*PollMessagesRequest) ProtoMessage() {} func (x *PollMessagesRequest) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[29] + 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 { @@ -1805,7 +1968,7 @@ func (x *PollMessagesRequest) ProtoReflect() protoreflect.Message { // 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{29} + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{30} } func (x *PollMessagesRequest) GetNamespaceId() string { @@ -1831,7 +1994,7 @@ type PollMessagesResponse struct { func (x *PollMessagesResponse) Reset() { *x = PollMessagesResponse{} - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[30] + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[31] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1843,7 +2006,7 @@ func (x *PollMessagesResponse) String() string { func (*PollMessagesResponse) ProtoMessage() {} func (x *PollMessagesResponse) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[30] + 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 { @@ -1856,7 +2019,7 @@ func (x *PollMessagesResponse) ProtoReflect() protoreflect.Message { // 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{30} + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{31} } func (x *PollMessagesResponse) GetFrontendResponse() *PollMessagesOutput { @@ -1876,7 +2039,7 @@ type DescribeStreamRequest struct { func (x *DescribeStreamRequest) Reset() { *x = DescribeStreamRequest{} - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[31] + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[32] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1888,7 +2051,7 @@ func (x *DescribeStreamRequest) String() string { func (*DescribeStreamRequest) ProtoMessage() {} func (x *DescribeStreamRequest) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[31] + 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 { @@ -1901,7 +2064,7 @@ func (x *DescribeStreamRequest) ProtoReflect() protoreflect.Message { // 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{31} + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{32} } func (x *DescribeStreamRequest) GetNamespaceId() string { @@ -1927,7 +2090,7 @@ type DescribeStreamResponse struct { func (x *DescribeStreamResponse) Reset() { *x = DescribeStreamResponse{} - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[32] + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[33] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1939,7 +2102,7 @@ func (x *DescribeStreamResponse) String() string { func (*DescribeStreamResponse) ProtoMessage() {} func (x *DescribeStreamResponse) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[32] + 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 { @@ -1952,7 +2115,7 @@ func (x *DescribeStreamResponse) ProtoReflect() protoreflect.Message { // 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{32} + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{33} } func (x *DescribeStreamResponse) GetFrontendResponse() *DescribeStreamOutput { @@ -1972,7 +2135,7 @@ type PollWorkflowMessagesRequest struct { func (x *PollWorkflowMessagesRequest) Reset() { *x = PollWorkflowMessagesRequest{} - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[33] + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[34] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1984,7 +2147,7 @@ func (x *PollWorkflowMessagesRequest) String() string { func (*PollWorkflowMessagesRequest) ProtoMessage() {} func (x *PollWorkflowMessagesRequest) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[33] + 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 { @@ -1997,7 +2160,7 @@ func (x *PollWorkflowMessagesRequest) ProtoReflect() protoreflect.Message { // 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{33} + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{34} } func (x *PollWorkflowMessagesRequest) GetNamespaceId() string { @@ -2023,7 +2186,7 @@ type PollWorkflowMessagesResponse struct { func (x *PollWorkflowMessagesResponse) Reset() { *x = PollWorkflowMessagesResponse{} - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[34] + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[35] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2035,7 +2198,7 @@ func (x *PollWorkflowMessagesResponse) String() string { func (*PollWorkflowMessagesResponse) ProtoMessage() {} func (x *PollWorkflowMessagesResponse) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[34] + 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 { @@ -2048,7 +2211,7 @@ func (x *PollWorkflowMessagesResponse) ProtoReflect() protoreflect.Message { // 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{34} + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{35} } func (x *PollWorkflowMessagesResponse) GetFrontendResponse() *PollMessagesOutput { @@ -2068,7 +2231,7 @@ type DescribeWorkflowStreamRequest struct { func (x *DescribeWorkflowStreamRequest) Reset() { *x = DescribeWorkflowStreamRequest{} - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[35] + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[36] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2080,7 +2243,7 @@ func (x *DescribeWorkflowStreamRequest) String() string { func (*DescribeWorkflowStreamRequest) ProtoMessage() {} func (x *DescribeWorkflowStreamRequest) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[35] + 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 { @@ -2093,7 +2256,7 @@ func (x *DescribeWorkflowStreamRequest) ProtoReflect() protoreflect.Message { // 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{35} + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{36} } func (x *DescribeWorkflowStreamRequest) GetNamespaceId() string { @@ -2119,7 +2282,7 @@ type DescribeWorkflowStreamResponse struct { func (x *DescribeWorkflowStreamResponse) Reset() { *x = DescribeWorkflowStreamResponse{} - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[36] + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[37] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2131,7 +2294,7 @@ func (x *DescribeWorkflowStreamResponse) String() string { func (*DescribeWorkflowStreamResponse) ProtoMessage() {} func (x *DescribeWorkflowStreamResponse) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[36] + 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 { @@ -2144,7 +2307,7 @@ func (x *DescribeWorkflowStreamResponse) ProtoReflect() protoreflect.Message { // 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{36} + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{37} } func (x *DescribeWorkflowStreamResponse) GetFrontendResponse() *DescribeStreamOutput { @@ -2176,7 +2339,7 @@ type RegisterStreamConsumerInput struct { func (x *RegisterStreamConsumerInput) Reset() { *x = RegisterStreamConsumerInput{} - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[37] + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[38] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2188,7 +2351,7 @@ func (x *RegisterStreamConsumerInput) String() string { func (*RegisterStreamConsumerInput) ProtoMessage() {} func (x *RegisterStreamConsumerInput) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[37] + 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 { @@ -2201,7 +2364,7 @@ func (x *RegisterStreamConsumerInput) ProtoReflect() protoreflect.Message { // 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{37} + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{38} } func (x *RegisterStreamConsumerInput) GetNamespace() string { @@ -2258,7 +2421,7 @@ type RegisterStreamConsumerOutput struct { func (x *RegisterStreamConsumerOutput) Reset() { *x = RegisterStreamConsumerOutput{} - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[38] + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[39] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2270,7 +2433,7 @@ func (x *RegisterStreamConsumerOutput) String() string { func (*RegisterStreamConsumerOutput) ProtoMessage() {} func (x *RegisterStreamConsumerOutput) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[38] + 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 { @@ -2283,7 +2446,7 @@ func (x *RegisterStreamConsumerOutput) ProtoReflect() protoreflect.Message { // 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{38} + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{39} } func (x *RegisterStreamConsumerOutput) GetStartOffset() int64 { @@ -2324,7 +2487,7 @@ type AdvanceConsumerHeadInput struct { func (x *AdvanceConsumerHeadInput) Reset() { *x = AdvanceConsumerHeadInput{} - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[39] + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[40] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2336,7 +2499,7 @@ func (x *AdvanceConsumerHeadInput) String() string { func (*AdvanceConsumerHeadInput) ProtoMessage() {} func (x *AdvanceConsumerHeadInput) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[39] + 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 { @@ -2349,7 +2512,7 @@ func (x *AdvanceConsumerHeadInput) ProtoReflect() protoreflect.Message { // 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{39} + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{40} } func (x *AdvanceConsumerHeadInput) GetNamespace() string { @@ -2403,7 +2566,7 @@ type AdvanceConsumerHeadOutput struct { func (x *AdvanceConsumerHeadOutput) Reset() { *x = AdvanceConsumerHeadOutput{} - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[40] + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[41] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2415,7 +2578,7 @@ func (x *AdvanceConsumerHeadOutput) String() string { func (*AdvanceConsumerHeadOutput) ProtoMessage() {} func (x *AdvanceConsumerHeadOutput) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[40] + 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 { @@ -2428,7 +2591,7 @@ func (x *AdvanceConsumerHeadOutput) ProtoReflect() protoreflect.Message { // 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{40} + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{41} } func (x *AdvanceConsumerHeadOutput) GetConsumerClosed() bool { @@ -2462,7 +2625,7 @@ type AddWorkflowMessagesRequest struct { func (x *AddWorkflowMessagesRequest) Reset() { *x = AddWorkflowMessagesRequest{} - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[41] + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[42] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2474,7 +2637,7 @@ func (x *AddWorkflowMessagesRequest) String() string { func (*AddWorkflowMessagesRequest) ProtoMessage() {} func (x *AddWorkflowMessagesRequest) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[41] + 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 { @@ -2487,7 +2650,7 @@ func (x *AddWorkflowMessagesRequest) ProtoReflect() protoreflect.Message { // 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{41} + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{42} } func (x *AddWorkflowMessagesRequest) GetNamespaceId() string { @@ -2513,7 +2676,7 @@ type AddWorkflowMessagesResponse struct { func (x *AddWorkflowMessagesResponse) Reset() { *x = AddWorkflowMessagesResponse{} - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[42] + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[43] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2525,7 +2688,7 @@ func (x *AddWorkflowMessagesResponse) String() string { func (*AddWorkflowMessagesResponse) ProtoMessage() {} func (x *AddWorkflowMessagesResponse) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[42] + 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 { @@ -2538,7 +2701,7 @@ func (x *AddWorkflowMessagesResponse) ProtoReflect() protoreflect.Message { // 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{42} + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{43} } func (x *AddWorkflowMessagesResponse) GetFrontendResponse() *AddMessagesOutput { @@ -2558,7 +2721,7 @@ type RegisterStreamConsumerRequest struct { func (x *RegisterStreamConsumerRequest) Reset() { *x = RegisterStreamConsumerRequest{} - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[43] + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[44] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2570,7 +2733,7 @@ func (x *RegisterStreamConsumerRequest) String() string { func (*RegisterStreamConsumerRequest) ProtoMessage() {} func (x *RegisterStreamConsumerRequest) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[43] + 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 { @@ -2583,7 +2746,7 @@ func (x *RegisterStreamConsumerRequest) ProtoReflect() protoreflect.Message { // 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{43} + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{44} } func (x *RegisterStreamConsumerRequest) GetNamespaceId() string { @@ -2609,7 +2772,7 @@ type RegisterStreamConsumerResponse struct { func (x *RegisterStreamConsumerResponse) Reset() { *x = RegisterStreamConsumerResponse{} - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[44] + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[45] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2621,7 +2784,7 @@ func (x *RegisterStreamConsumerResponse) String() string { func (*RegisterStreamConsumerResponse) ProtoMessage() {} func (x *RegisterStreamConsumerResponse) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[44] + 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 { @@ -2634,7 +2797,7 @@ func (x *RegisterStreamConsumerResponse) ProtoReflect() protoreflect.Message { // 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{44} + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{45} } func (x *RegisterStreamConsumerResponse) GetFrontendResponse() *RegisterStreamConsumerOutput { @@ -2654,7 +2817,7 @@ type AdvanceConsumerHeadRequest struct { func (x *AdvanceConsumerHeadRequest) Reset() { *x = AdvanceConsumerHeadRequest{} - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[45] + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[46] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2666,7 +2829,7 @@ func (x *AdvanceConsumerHeadRequest) String() string { func (*AdvanceConsumerHeadRequest) ProtoMessage() {} func (x *AdvanceConsumerHeadRequest) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[45] + 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 { @@ -2679,7 +2842,7 @@ func (x *AdvanceConsumerHeadRequest) ProtoReflect() protoreflect.Message { // 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{45} + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{46} } func (x *AdvanceConsumerHeadRequest) GetNamespaceId() string { @@ -2705,7 +2868,7 @@ type AdvanceConsumerHeadResponse struct { func (x *AdvanceConsumerHeadResponse) Reset() { *x = AdvanceConsumerHeadResponse{} - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[46] + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[47] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2717,7 +2880,7 @@ func (x *AdvanceConsumerHeadResponse) String() string { func (*AdvanceConsumerHeadResponse) ProtoMessage() {} func (x *AdvanceConsumerHeadResponse) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[46] + 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 { @@ -2730,7 +2893,7 @@ func (x *AdvanceConsumerHeadResponse) ProtoReflect() protoreflect.Message { // 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{46} + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{47} } func (x *AdvanceConsumerHeadResponse) GetFrontendResponse() *AdvanceConsumerHeadOutput { @@ -2750,7 +2913,7 @@ type CloseStreamRequest struct { func (x *CloseStreamRequest) Reset() { *x = CloseStreamRequest{} - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[47] + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[48] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2762,7 +2925,7 @@ func (x *CloseStreamRequest) String() string { func (*CloseStreamRequest) ProtoMessage() {} func (x *CloseStreamRequest) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[47] + 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 { @@ -2775,7 +2938,7 @@ func (x *CloseStreamRequest) ProtoReflect() protoreflect.Message { // 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{47} + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{48} } func (x *CloseStreamRequest) GetNamespaceId() string { @@ -2801,7 +2964,7 @@ type CloseStreamResponse struct { func (x *CloseStreamResponse) Reset() { *x = CloseStreamResponse{} - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[48] + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[49] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2813,7 +2976,7 @@ func (x *CloseStreamResponse) String() string { func (*CloseStreamResponse) ProtoMessage() {} func (x *CloseStreamResponse) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[48] + 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 { @@ -2826,7 +2989,7 @@ func (x *CloseStreamResponse) ProtoReflect() protoreflect.Message { // 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{48} + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{49} } func (x *CloseStreamResponse) GetFrontendResponse() *CloseStreamOutput { @@ -2846,7 +3009,7 @@ type TruncateStreamRequest struct { func (x *TruncateStreamRequest) Reset() { *x = TruncateStreamRequest{} - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[49] + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[50] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2858,7 +3021,7 @@ func (x *TruncateStreamRequest) String() string { func (*TruncateStreamRequest) ProtoMessage() {} func (x *TruncateStreamRequest) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[49] + 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 { @@ -2871,7 +3034,7 @@ func (x *TruncateStreamRequest) ProtoReflect() protoreflect.Message { // 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{49} + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{50} } func (x *TruncateStreamRequest) GetNamespaceId() string { @@ -2897,7 +3060,7 @@ type TruncateStreamResponse struct { func (x *TruncateStreamResponse) Reset() { *x = TruncateStreamResponse{} - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[50] + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[51] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2909,7 +3072,7 @@ func (x *TruncateStreamResponse) String() string { func (*TruncateStreamResponse) ProtoMessage() {} func (x *TruncateStreamResponse) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[50] + 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 { @@ -2922,7 +3085,7 @@ func (x *TruncateStreamResponse) ProtoReflect() protoreflect.Message { // 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{50} + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{51} } func (x *TruncateStreamResponse) GetFrontendResponse() *TruncateStreamOutput { @@ -2944,7 +3107,7 @@ type ListStreamsInput struct { func (x *ListStreamsInput) Reset() { *x = ListStreamsInput{} - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[51] + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[52] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2956,7 +3119,7 @@ func (x *ListStreamsInput) String() string { func (*ListStreamsInput) ProtoMessage() {} func (x *ListStreamsInput) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[51] + 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 { @@ -2969,7 +3132,7 @@ func (x *ListStreamsInput) ProtoReflect() protoreflect.Message { // 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{51} + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{52} } func (x *ListStreamsInput) GetNamespace() string { @@ -3010,7 +3173,7 @@ type StreamListEntry struct { func (x *StreamListEntry) Reset() { *x = StreamListEntry{} - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[52] + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[53] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3022,7 +3185,7 @@ func (x *StreamListEntry) String() string { func (*StreamListEntry) ProtoMessage() {} func (x *StreamListEntry) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[52] + 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 { @@ -3035,7 +3198,7 @@ func (x *StreamListEntry) ProtoReflect() protoreflect.Message { // 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{52} + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{53} } func (x *StreamListEntry) GetStreamId() string { @@ -3062,7 +3225,7 @@ type ListStreamsOutput struct { func (x *ListStreamsOutput) Reset() { *x = ListStreamsOutput{} - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[53] + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[54] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3074,7 +3237,7 @@ func (x *ListStreamsOutput) String() string { func (*ListStreamsOutput) ProtoMessage() {} func (x *ListStreamsOutput) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[53] + 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 { @@ -3087,7 +3250,7 @@ func (x *ListStreamsOutput) ProtoReflect() protoreflect.Message { // 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{53} + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{54} } func (x *ListStreamsOutput) GetStreams() []*StreamListEntry { @@ -3114,7 +3277,7 @@ type ListStreamsRequest struct { func (x *ListStreamsRequest) Reset() { *x = ListStreamsRequest{} - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[54] + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[55] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3126,7 +3289,7 @@ func (x *ListStreamsRequest) String() string { func (*ListStreamsRequest) ProtoMessage() {} func (x *ListStreamsRequest) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[54] + 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 { @@ -3139,7 +3302,7 @@ func (x *ListStreamsRequest) ProtoReflect() protoreflect.Message { // 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{54} + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{55} } func (x *ListStreamsRequest) GetNamespaceId() string { @@ -3165,7 +3328,7 @@ type ListStreamsResponse struct { func (x *ListStreamsResponse) Reset() { *x = ListStreamsResponse{} - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[55] + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[56] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3177,7 +3340,7 @@ func (x *ListStreamsResponse) String() string { func (*ListStreamsResponse) ProtoMessage() {} func (x *ListStreamsResponse) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[55] + 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 { @@ -3190,7 +3353,7 @@ func (x *ListStreamsResponse) ProtoReflect() protoreflect.Message { // 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{55} + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{56} } func (x *ListStreamsResponse) GetFrontendResponse() *ListStreamsOutput { @@ -3210,7 +3373,7 @@ type DeleteStreamRequest struct { func (x *DeleteStreamRequest) Reset() { *x = DeleteStreamRequest{} - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[56] + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[57] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3222,7 +3385,7 @@ func (x *DeleteStreamRequest) String() string { func (*DeleteStreamRequest) ProtoMessage() {} func (x *DeleteStreamRequest) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[56] + 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 { @@ -3235,7 +3398,7 @@ func (x *DeleteStreamRequest) ProtoReflect() protoreflect.Message { // 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{56} + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{57} } func (x *DeleteStreamRequest) GetNamespaceId() string { @@ -3261,7 +3424,7 @@ type DeleteStreamResponse struct { func (x *DeleteStreamResponse) Reset() { *x = DeleteStreamResponse{} - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[57] + mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[58] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3273,7 +3436,7 @@ func (x *DeleteStreamResponse) String() string { func (*DeleteStreamResponse) ProtoMessage() {} func (x *DeleteStreamResponse) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_msgTypes[57] + 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 { @@ -3286,7 +3449,7 @@ func (x *DeleteStreamResponse) ProtoReflect() protoreflect.Message { // 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{57} + return file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawDescGZIP(), []int{58} } func (x *DeleteStreamResponse) GetFrontendResponse() *DeleteStreamOutput { @@ -3361,9 +3524,16 @@ const file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawD "\x06run_id\x18\x06 \x01(\tR\x05runId\"P\n" + "\x13DescribeStreamInput\x12\x1c\n" + "\tnamespace\x18\x01 \x01(\tR\tnamespace\x12\x1b\n" + - "\tstream_id\x18\x02 \x01(\tR\bstreamId\"\xa5\x02\n" + + "\tstream_id\x18\x02 \x01(\tR\bstreamId\"\xa5\x01\n" + + "\vStreamOwner\x12N\n" + + "\x04kind\x18\x01 \x01(\x0e2:.temporal.server.chasm.lib.stream.proto.v1.StreamOwnerKindR\x04kind\x12\x0e\n" + + "\x02id\x18\x02 \x01(\tR\x02id\x12\x15\n" + + "\x06run_id\x18\x03 \x01(\tR\x05runId\x12\x1f\n" + + "\vactivity_id\x18\x04 \x01(\tR\n" + + "activityId\"\xf3\x02\n" + "\x19PollWorkflowMessagesInput\x12\x1c\n" + - "\tnamespace\x18\x01 \x01(\tR\tnamespace\x12\x1f\n" + + "\tnamespace\x18\x01 \x01(\tR\tnamespace\x12L\n" + + "\x05owner\x18\t \x01(\v26.temporal.server.chasm.lib.stream.proto.v1.StreamOwnerR\x05owner\x12\x1f\n" + "\vworkflow_id\x18\x02 \x01(\tR\n" + "workflowId\x12 \n" + "\fowner_run_id\x18\b \x01(\tR\n" + @@ -3374,17 +3544,19 @@ const file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawD "fromOffset\x12!\n" + "\fmax_messages\x18\x05 \x01(\x05R\vmaxMessages\x12\x16\n" + "\x06topics\x18\x06 \x03(\tR\x06topics\x12*\n" + - "\x11wait_new_messages\x18\a \x01(\bR\x0fwaitNewMessages\"\x9f\x01\n" + + "\x11wait_new_messages\x18\a \x01(\bR\x0fwaitNewMessages\"\xed\x01\n" + "\x1bDescribeWorkflowStreamInput\x12\x1c\n" + - "\tnamespace\x18\x01 \x01(\tR\tnamespace\x12\x1f\n" + + "\tnamespace\x18\x01 \x01(\tR\tnamespace\x12L\n" + + "\x05owner\x18\x05 \x01(\v26.temporal.server.chasm.lib.stream.proto.v1.StreamOwnerR\x05owner\x12\x1f\n" + "\vworkflow_id\x18\x02 \x01(\tR\n" + "workflowId\x12 \n" + "\fowner_run_id\x18\x04 \x01(\tR\n" + "ownerRunId\x12\x1f\n" + "\vstream_name\x18\x03 \x01(\tR\n" + - "streamName\"\xac\x02\n" + + "streamName\"\xfa\x02\n" + "\x18AddWorkflowMessagesInput\x12\x1c\n" + - "\tnamespace\x18\x01 \x01(\tR\tnamespace\x12\x1f\n" + + "\tnamespace\x18\x01 \x01(\tR\tnamespace\x12L\n" + + "\x05owner\x18\b \x01(\v26.temporal.server.chasm.lib.stream.proto.v1.StreamOwnerR\x05owner\x12\x1f\n" + "\vworkflow_id\x18\x02 \x01(\tR\n" + "workflowId\x12 \n" + "\fowner_run_id\x18\a \x01(\tR\n" + @@ -3521,7 +3693,12 @@ const file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_rawD "\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\x10frontendResponseB>ZZ temporal.server.chasm.lib.stream.proto.v1.StreamLifecycle - 59, // 1: temporal.server.chasm.lib.stream.proto.v1.AddMessagesInput.records:type_name -> temporal.server.chasm.lib.stream.proto.v1.StreamRecord - 59, // 2: temporal.server.chasm.lib.stream.proto.v1.PollMessagesOutput.records:type_name -> temporal.server.chasm.lib.stream.proto.v1.StreamRecord - 60, // 3: temporal.server.chasm.lib.stream.proto.v1.PollMessagesOutput.close_reason:type_name -> temporal.api.common.v1.Payload - 59, // 4: temporal.server.chasm.lib.stream.proto.v1.AddWorkflowMessagesInput.records:type_name -> temporal.server.chasm.lib.stream.proto.v1.StreamRecord - 61, // 5: temporal.server.chasm.lib.stream.proto.v1.DescribeStreamOutput.state:type_name -> temporal.server.chasm.lib.stream.proto.v1.StreamState - 60, // 6: temporal.server.chasm.lib.stream.proto.v1.CloseStreamInput.reason:type_name -> temporal.api.common.v1.Payload - 0, // 7: temporal.server.chasm.lib.stream.proto.v1.CreateStreamRequest.frontend_request:type_name -> temporal.server.chasm.lib.stream.proto.v1.CreateStreamInput - 1, // 8: temporal.server.chasm.lib.stream.proto.v1.CreateStreamResponse.frontend_response:type_name -> temporal.server.chasm.lib.stream.proto.v1.CreateStreamOutput - 2, // 9: temporal.server.chasm.lib.stream.proto.v1.AddMessagesRequest.frontend_request:type_name -> temporal.server.chasm.lib.stream.proto.v1.AddMessagesInput - 3, // 10: temporal.server.chasm.lib.stream.proto.v1.AddMessagesResponse.frontend_response:type_name -> temporal.server.chasm.lib.stream.proto.v1.AddMessagesOutput - 4, // 11: temporal.server.chasm.lib.stream.proto.v1.FinishWritingRequest.frontend_request:type_name -> temporal.server.chasm.lib.stream.proto.v1.FinishWritingInput - 5, // 12: temporal.server.chasm.lib.stream.proto.v1.FinishWritingResponse.frontend_response:type_name -> temporal.server.chasm.lib.stream.proto.v1.FinishWritingOutput - 6, // 13: temporal.server.chasm.lib.stream.proto.v1.SubscribeWorkflowRequest.frontend_request:type_name -> temporal.server.chasm.lib.stream.proto.v1.SubscribeWorkflowInput - 7, // 14: temporal.server.chasm.lib.stream.proto.v1.SubscribeWorkflowResponse.frontend_response:type_name -> temporal.server.chasm.lib.stream.proto.v1.SubscribeWorkflowOutput - 8, // 15: temporal.server.chasm.lib.stream.proto.v1.PollMessagesRequest.frontend_request:type_name -> temporal.server.chasm.lib.stream.proto.v1.PollMessagesInput - 9, // 16: temporal.server.chasm.lib.stream.proto.v1.PollMessagesResponse.frontend_response:type_name -> temporal.server.chasm.lib.stream.proto.v1.PollMessagesOutput - 10, // 17: temporal.server.chasm.lib.stream.proto.v1.DescribeStreamRequest.frontend_request:type_name -> temporal.server.chasm.lib.stream.proto.v1.DescribeStreamInput - 14, // 18: temporal.server.chasm.lib.stream.proto.v1.DescribeStreamResponse.frontend_response:type_name -> temporal.server.chasm.lib.stream.proto.v1.DescribeStreamOutput - 11, // 19: temporal.server.chasm.lib.stream.proto.v1.PollWorkflowMessagesRequest.frontend_request:type_name -> temporal.server.chasm.lib.stream.proto.v1.PollWorkflowMessagesInput - 9, // 20: temporal.server.chasm.lib.stream.proto.v1.PollWorkflowMessagesResponse.frontend_response:type_name -> temporal.server.chasm.lib.stream.proto.v1.PollMessagesOutput - 12, // 21: temporal.server.chasm.lib.stream.proto.v1.DescribeWorkflowStreamRequest.frontend_request:type_name -> temporal.server.chasm.lib.stream.proto.v1.DescribeWorkflowStreamInput - 14, // 22: temporal.server.chasm.lib.stream.proto.v1.DescribeWorkflowStreamResponse.frontend_response:type_name -> temporal.server.chasm.lib.stream.proto.v1.DescribeStreamOutput - 13, // 23: temporal.server.chasm.lib.stream.proto.v1.AddWorkflowMessagesRequest.frontend_request:type_name -> temporal.server.chasm.lib.stream.proto.v1.AddWorkflowMessagesInput - 3, // 24: temporal.server.chasm.lib.stream.proto.v1.AddWorkflowMessagesResponse.frontend_response:type_name -> temporal.server.chasm.lib.stream.proto.v1.AddMessagesOutput - 37, // 25: temporal.server.chasm.lib.stream.proto.v1.RegisterStreamConsumerRequest.frontend_request:type_name -> temporal.server.chasm.lib.stream.proto.v1.RegisterStreamConsumerInput - 38, // 26: temporal.server.chasm.lib.stream.proto.v1.RegisterStreamConsumerResponse.frontend_response:type_name -> temporal.server.chasm.lib.stream.proto.v1.RegisterStreamConsumerOutput - 39, // 27: temporal.server.chasm.lib.stream.proto.v1.AdvanceConsumerHeadRequest.frontend_request:type_name -> temporal.server.chasm.lib.stream.proto.v1.AdvanceConsumerHeadInput - 40, // 28: temporal.server.chasm.lib.stream.proto.v1.AdvanceConsumerHeadResponse.frontend_response:type_name -> temporal.server.chasm.lib.stream.proto.v1.AdvanceConsumerHeadOutput - 15, // 29: temporal.server.chasm.lib.stream.proto.v1.CloseStreamRequest.frontend_request:type_name -> temporal.server.chasm.lib.stream.proto.v1.CloseStreamInput - 16, // 30: temporal.server.chasm.lib.stream.proto.v1.CloseStreamResponse.frontend_response:type_name -> temporal.server.chasm.lib.stream.proto.v1.CloseStreamOutput - 17, // 31: temporal.server.chasm.lib.stream.proto.v1.TruncateStreamRequest.frontend_request:type_name -> temporal.server.chasm.lib.stream.proto.v1.TruncateStreamInput - 18, // 32: temporal.server.chasm.lib.stream.proto.v1.TruncateStreamResponse.frontend_response:type_name -> temporal.server.chasm.lib.stream.proto.v1.TruncateStreamOutput - 52, // 33: temporal.server.chasm.lib.stream.proto.v1.ListStreamsOutput.streams:type_name -> temporal.server.chasm.lib.stream.proto.v1.StreamListEntry - 51, // 34: temporal.server.chasm.lib.stream.proto.v1.ListStreamsRequest.frontend_request:type_name -> temporal.server.chasm.lib.stream.proto.v1.ListStreamsInput - 53, // 35: temporal.server.chasm.lib.stream.proto.v1.ListStreamsResponse.frontend_response:type_name -> temporal.server.chasm.lib.stream.proto.v1.ListStreamsOutput - 19, // 36: temporal.server.chasm.lib.stream.proto.v1.DeleteStreamRequest.frontend_request:type_name -> temporal.server.chasm.lib.stream.proto.v1.DeleteStreamInput - 20, // 37: temporal.server.chasm.lib.stream.proto.v1.DeleteStreamResponse.frontend_response:type_name -> temporal.server.chasm.lib.stream.proto.v1.DeleteStreamOutput - 38, // [38:38] is the sub-list for method output_type - 38, // [38:38] is the sub-list for method input_type - 38, // [38:38] is the sub-list for extension type_name - 38, // [38:38] is the sub-list for extension extendee - 0, // [0:38] is the sub-list for field type_name + 60, // 0: temporal.server.chasm.lib.stream.proto.v1.CreateStreamInput.lifecycle:type_name -> 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 + 61, // 2: temporal.server.chasm.lib.stream.proto.v1.PollMessagesOutput.records:type_name -> temporal.server.chasm.lib.stream.proto.v1.StreamRecord + 62, // 3: temporal.server.chasm.lib.stream.proto.v1.PollMessagesOutput.close_reason:type_name -> temporal.api.common.v1.Payload + 0, // 4: temporal.server.chasm.lib.stream.proto.v1.StreamOwner.kind:type_name -> temporal.server.chasm.lib.stream.proto.v1.StreamOwnerKind + 12, // 5: temporal.server.chasm.lib.stream.proto.v1.PollWorkflowMessagesInput.owner:type_name -> temporal.server.chasm.lib.stream.proto.v1.StreamOwner + 12, // 6: temporal.server.chasm.lib.stream.proto.v1.DescribeWorkflowStreamInput.owner:type_name -> temporal.server.chasm.lib.stream.proto.v1.StreamOwner + 12, // 7: temporal.server.chasm.lib.stream.proto.v1.AddWorkflowMessagesInput.owner:type_name -> temporal.server.chasm.lib.stream.proto.v1.StreamOwner + 61, // 8: temporal.server.chasm.lib.stream.proto.v1.AddWorkflowMessagesInput.records:type_name -> temporal.server.chasm.lib.stream.proto.v1.StreamRecord + 63, // 9: temporal.server.chasm.lib.stream.proto.v1.DescribeStreamOutput.state:type_name -> temporal.server.chasm.lib.stream.proto.v1.StreamState + 62, // 10: temporal.server.chasm.lib.stream.proto.v1.CloseStreamInput.reason:type_name -> temporal.api.common.v1.Payload + 1, // 11: temporal.server.chasm.lib.stream.proto.v1.CreateStreamRequest.frontend_request:type_name -> temporal.server.chasm.lib.stream.proto.v1.CreateStreamInput + 2, // 12: temporal.server.chasm.lib.stream.proto.v1.CreateStreamResponse.frontend_response:type_name -> temporal.server.chasm.lib.stream.proto.v1.CreateStreamOutput + 3, // 13: temporal.server.chasm.lib.stream.proto.v1.AddMessagesRequest.frontend_request:type_name -> temporal.server.chasm.lib.stream.proto.v1.AddMessagesInput + 4, // 14: temporal.server.chasm.lib.stream.proto.v1.AddMessagesResponse.frontend_response:type_name -> temporal.server.chasm.lib.stream.proto.v1.AddMessagesOutput + 5, // 15: temporal.server.chasm.lib.stream.proto.v1.FinishWritingRequest.frontend_request:type_name -> temporal.server.chasm.lib.stream.proto.v1.FinishWritingInput + 6, // 16: temporal.server.chasm.lib.stream.proto.v1.FinishWritingResponse.frontend_response:type_name -> temporal.server.chasm.lib.stream.proto.v1.FinishWritingOutput + 7, // 17: temporal.server.chasm.lib.stream.proto.v1.SubscribeWorkflowRequest.frontend_request:type_name -> temporal.server.chasm.lib.stream.proto.v1.SubscribeWorkflowInput + 8, // 18: temporal.server.chasm.lib.stream.proto.v1.SubscribeWorkflowResponse.frontend_response:type_name -> temporal.server.chasm.lib.stream.proto.v1.SubscribeWorkflowOutput + 9, // 19: temporal.server.chasm.lib.stream.proto.v1.PollMessagesRequest.frontend_request:type_name -> temporal.server.chasm.lib.stream.proto.v1.PollMessagesInput + 10, // 20: temporal.server.chasm.lib.stream.proto.v1.PollMessagesResponse.frontend_response:type_name -> temporal.server.chasm.lib.stream.proto.v1.PollMessagesOutput + 11, // 21: temporal.server.chasm.lib.stream.proto.v1.DescribeStreamRequest.frontend_request:type_name -> temporal.server.chasm.lib.stream.proto.v1.DescribeStreamInput + 16, // 22: temporal.server.chasm.lib.stream.proto.v1.DescribeStreamResponse.frontend_response:type_name -> temporal.server.chasm.lib.stream.proto.v1.DescribeStreamOutput + 13, // 23: temporal.server.chasm.lib.stream.proto.v1.PollWorkflowMessagesRequest.frontend_request:type_name -> temporal.server.chasm.lib.stream.proto.v1.PollWorkflowMessagesInput + 10, // 24: temporal.server.chasm.lib.stream.proto.v1.PollWorkflowMessagesResponse.frontend_response:type_name -> temporal.server.chasm.lib.stream.proto.v1.PollMessagesOutput + 14, // 25: temporal.server.chasm.lib.stream.proto.v1.DescribeWorkflowStreamRequest.frontend_request:type_name -> temporal.server.chasm.lib.stream.proto.v1.DescribeWorkflowStreamInput + 16, // 26: temporal.server.chasm.lib.stream.proto.v1.DescribeWorkflowStreamResponse.frontend_response:type_name -> temporal.server.chasm.lib.stream.proto.v1.DescribeStreamOutput + 15, // 27: temporal.server.chasm.lib.stream.proto.v1.AddWorkflowMessagesRequest.frontend_request:type_name -> temporal.server.chasm.lib.stream.proto.v1.AddWorkflowMessagesInput + 4, // 28: temporal.server.chasm.lib.stream.proto.v1.AddWorkflowMessagesResponse.frontend_response:type_name -> temporal.server.chasm.lib.stream.proto.v1.AddMessagesOutput + 39, // 29: temporal.server.chasm.lib.stream.proto.v1.RegisterStreamConsumerRequest.frontend_request:type_name -> temporal.server.chasm.lib.stream.proto.v1.RegisterStreamConsumerInput + 40, // 30: temporal.server.chasm.lib.stream.proto.v1.RegisterStreamConsumerResponse.frontend_response:type_name -> temporal.server.chasm.lib.stream.proto.v1.RegisterStreamConsumerOutput + 41, // 31: temporal.server.chasm.lib.stream.proto.v1.AdvanceConsumerHeadRequest.frontend_request:type_name -> temporal.server.chasm.lib.stream.proto.v1.AdvanceConsumerHeadInput + 42, // 32: temporal.server.chasm.lib.stream.proto.v1.AdvanceConsumerHeadResponse.frontend_response:type_name -> temporal.server.chasm.lib.stream.proto.v1.AdvanceConsumerHeadOutput + 17, // 33: temporal.server.chasm.lib.stream.proto.v1.CloseStreamRequest.frontend_request:type_name -> temporal.server.chasm.lib.stream.proto.v1.CloseStreamInput + 18, // 34: temporal.server.chasm.lib.stream.proto.v1.CloseStreamResponse.frontend_response:type_name -> temporal.server.chasm.lib.stream.proto.v1.CloseStreamOutput + 19, // 35: temporal.server.chasm.lib.stream.proto.v1.TruncateStreamRequest.frontend_request:type_name -> temporal.server.chasm.lib.stream.proto.v1.TruncateStreamInput + 20, // 36: temporal.server.chasm.lib.stream.proto.v1.TruncateStreamResponse.frontend_response:type_name -> temporal.server.chasm.lib.stream.proto.v1.TruncateStreamOutput + 54, // 37: temporal.server.chasm.lib.stream.proto.v1.ListStreamsOutput.streams:type_name -> temporal.server.chasm.lib.stream.proto.v1.StreamListEntry + 53, // 38: temporal.server.chasm.lib.stream.proto.v1.ListStreamsRequest.frontend_request:type_name -> temporal.server.chasm.lib.stream.proto.v1.ListStreamsInput + 55, // 39: temporal.server.chasm.lib.stream.proto.v1.ListStreamsResponse.frontend_response:type_name -> temporal.server.chasm.lib.stream.proto.v1.ListStreamsOutput + 21, // 40: temporal.server.chasm.lib.stream.proto.v1.DeleteStreamRequest.frontend_request:type_name -> temporal.server.chasm.lib.stream.proto.v1.DeleteStreamInput + 22, // 41: temporal.server.chasm.lib.stream.proto.v1.DeleteStreamResponse.frontend_response:type_name -> temporal.server.chasm.lib.stream.proto.v1.DeleteStreamOutput + 42, // [42:42] is the sub-list for method output_type + 42, // [42:42] is the sub-list for method input_type + 42, // [42:42] is the sub-list for extension type_name + 42, // [42:42] is the sub-list for extension extendee + 0, // [0:42] is the sub-list for field type_name } func init() { file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_init() } @@ -3658,13 +3842,14 @@ func file_temporal_server_chasm_lib_stream_proto_v1_request_response_proto_init( 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: 0, - NumMessages: 58, + 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 diff --git a/chasm/lib/stream/gen/streampb/v1/service.pb.go b/chasm/lib/stream/gen/streampb/v1/service.pb.go index e0bb7981366..908ee46128a 100644 --- a/chasm/lib/stream/gen/streampb/v1/service.pb.go +++ b/chasm/lib/stream/gen/streampb/v1/service.pb.go @@ -27,17 +27,17 @@ var File_temporal_server_chasm_lib_stream_proto_v1_service_proto protoreflect.Fi 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\xf8\x16\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\xd1\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\x1e\x1a\x1cfrontend_request.workflow_id\x12\xd7\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\x1e\x1a\x1cfrontend_request.workflow_id\x12\xce\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\x1e\x1a\x1cfrontend_request.workflow_id\x12\xd5\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" + diff --git a/chasm/lib/stream/gen/streampb/v1/service_client.pb.go b/chasm/lib/stream/gen/streampb/v1/service_client.pb.go index 946c94e2831..213969200b4 100644 --- a/chasm/lib/stream/gen/streampb/v1/service_client.pb.go +++ b/chasm/lib/stream/gen/streampb/v1/service_client.pb.go @@ -346,7 +346,7 @@ func (c *StreamServiceLayeredClient) callPollWorkflowMessagesNoRetry( } metrics.ClientLatency.With(metricsHandler).Record(time.Since(startTime)) }() - shardID := common.WorkflowIDToHistoryShard(request.GetNamespaceId(), request.GetFrontendRequest().GetWorkflowId(), c.numShards) + 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) @@ -389,7 +389,7 @@ func (c *StreamServiceLayeredClient) callDescribeWorkflowStreamNoRetry( } metrics.ClientLatency.With(metricsHandler).Record(time.Since(startTime)) }() - shardID := common.WorkflowIDToHistoryShard(request.GetNamespaceId(), request.GetFrontendRequest().GetWorkflowId(), c.numShards) + 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) @@ -432,7 +432,7 @@ func (c *StreamServiceLayeredClient) callAddWorkflowMessagesNoRetry( } metrics.ClientLatency.With(metricsHandler).Record(time.Since(startTime)) }() - shardID := common.WorkflowIDToHistoryShard(request.GetNamespaceId(), request.GetFrontendRequest().GetWorkflowId(), c.numShards) + 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) diff --git a/chasm/lib/stream/gen/streampb/v1/service_grpc.pb.go b/chasm/lib/stream/gen/streampb/v1/service_grpc.pb.go index 0a9d2f93a28..f5fa6d3bcb1 100644 --- a/chasm/lib/stream/gen/streampb/v1/service_grpc.pb.go +++ b/chasm/lib/stream/gen/streampb/v1/service_grpc.pb.go @@ -47,7 +47,8 @@ type StreamServiceClient interface { 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) - // Routed on the owner, because the stream it reads has no id of its own. + // 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) @@ -217,7 +218,8 @@ type StreamServiceServer interface { SubscribeWorkflow(context.Context, *SubscribeWorkflowRequest) (*SubscribeWorkflowResponse, error) PollMessages(context.Context, *PollMessagesRequest) (*PollMessagesResponse, error) DescribeStream(context.Context, *DescribeStreamRequest) (*DescribeStreamResponse, error) - // Routed on the owner, because the stream it reads has no id of its own. + // 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) 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 index bca04fc1276..31d2f9c46b0 100644 --- a/chasm/lib/stream/proto/v1/request_response.proto +++ b/chasm/lib/stream/proto/v1/request_response.proto @@ -117,18 +117,43 @@ message DescribeStreamInput { string stream_id = 2; } -// A stream a workflow owns lives inside that workflow's execution, 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. +// 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; - // 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 = 8; - // Empty means the workflow's default output stream. + // Empty means the owner's default output stream. string stream_name = 3; int64 from_offset = 4; int32 max_messages = 5; @@ -139,26 +164,24 @@ message PollWorkflowMessagesInput { message DescribeWorkflowStreamInput { string namespace = 1; + StreamOwner owner = 5; + // Shorthand for an owner of kind WORKFLOW, and refused alongside owner. 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 = 4; string stream_name = 3; } -// Appending to a stream a workflow owns, from outside that workflow. The -// workflow's own publishes ride its Workflow Task instead. +// 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; - // 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 = 7; - // Empty means the workflow's default output stream. + // Empty means the owner's default output stream. string stream_name = 3; repeated StreamRecord records = 4; // Optional idempotency, as on AddMessagesInput. diff --git a/chasm/lib/stream/proto/v1/service.proto b/chasm/lib/stream/proto/v1/service.proto index bdd0a918551..fa2850d1abd 100644 --- a/chasm/lib/stream/proto/v1/service.proto +++ b/chasm/lib/stream/proto/v1/service.proto @@ -39,19 +39,20 @@ service StreamService { option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; } - // Routed on the owner, because the stream it reads has no id of its own. + // 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.workflow_id"; + 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.workflow_id"; + 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.workflow_id"; + 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; } diff --git a/chasm/lib/stream/service/frontend.go b/chasm/lib/stream/service/frontend.go index 17c9b99e6c3..ecf3f5d3bc4 100644 --- a/chasm/lib/stream/service/frontend.go +++ b/chasm/lib/stream/service/frontend.go @@ -174,6 +174,38 @@ func (h *FrontendHandler) checkID(field, value string) error { 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 *streampb.StreamOwner, + workflowID, runID string, +) (*streampb.StreamOwner, error) { + if owner == nil { + owner = &streampb.StreamOwner{ + Kind: streampb.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) @@ -308,9 +340,11 @@ func (h *FrontendHandler) PollWorkflowMessages( if err := checkOffset("from offset", in.GetFromOffset()); err != nil { return nil, err } - if err := h.checkID("workflow id", in.GetWorkflowId()); err != nil { + 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, &streampb.PollWorkflowMessagesRequest{ NamespaceId: id, FrontendRequest: in, @@ -328,9 +362,11 @@ func (h *FrontendHandler) DescribeWorkflowStream( if err := h.checkID("stream name", in.GetStreamName()); err != nil { return nil, err } - if err := h.checkID("workflow id", in.GetWorkflowId()); err != nil { + 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, &streampb.DescribeWorkflowStreamRequest{ NamespaceId: id, FrontendRequest: in, }) @@ -347,9 +383,11 @@ func (h *FrontendHandler) AddWorkflowMessages( if err := h.checkID("stream name", in.GetStreamName()); err != nil { return nil, err } - if err := h.checkID("workflow id", in.GetWorkflowId()); err != nil { + 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 } diff --git a/chasm/lib/stream/service/frontend_test.go b/chasm/lib/stream/service/frontend_test.go index ea1326c1c71..5d702252385 100644 --- a/chasm/lib/stream/service/frontend_test.go +++ b/chasm/lib/stream/service/frontend_test.go @@ -182,3 +182,30 @@ func TestFrontendRefusesAListQueryThatNamesTheArchetype(t *testing.T) { }) 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/handler.go b/chasm/lib/stream/service/handler.go index b2cd403d4a8..75dd345350b 100644 --- a/chasm/lib/stream/service/handler.go +++ b/chasm/lib/stream/service/handler.go @@ -7,6 +7,7 @@ import ( commonpb "go.temporal.io/api/common/v1" "go.temporal.io/api/serviceerror" "go.temporal.io/server/chasm" + "go.temporal.io/server/chasm/lib/activity" "go.temporal.io/server/chasm/lib/stream" streampb "go.temporal.io/server/chasm/lib/stream/gen/streampb/v1" chasmworkflow "go.temporal.io/server/chasm/lib/workflow" @@ -105,16 +106,101 @@ func workflowRef(namespaceID, workflowID, runID string) chasm.ComponentRef { }) } +// 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 chasmworkflow.DefaultStreamName + 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 *streampb.StreamOwner, + name string, +) (ownedTarget, error) { + if err := checkOwner(owner); err != nil { + return ownedTarget{}, err + } + name = ownedStreamName(name) + switch owner.GetKind() { + case streampb.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 streampb.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 streampb.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 + } + 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 *streampb.StreamOwner) error { + if owner.GetId() == "" { + return serviceerror.NewInvalidArgument("owner id is required") + } + switch owner.GetKind() { + case streampb.STREAM_OWNER_KIND_WORKFLOW, streampb.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 streampb.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 *streampb.CreateStreamRequest, @@ -183,8 +269,9 @@ func (h *handler) AddMessages( }, nil } -// AddWorkflowMessages appends to a stream a workflow owns, from outside that -// workflow. +// 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 @@ -205,21 +292,24 @@ func (h *handler) AddWorkflowMessages( } ctx = h.withCallerInfo(ctx, req.GetNamespaceId()) + target, err := resolveOwned(req.GetNamespaceId(), in.GetOwner(), in.GetStreamName()) + if 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. - name := ownedStreamName(in.GetStreamName()) addReq := stream.AddMessagesRequest{ Records: in.GetRecords(), ProducerID: in.GetProducerId(), Sequence: in.GetSequence(), Limits: h.limitsFor(req.GetNamespaceId()), } - result, _, err := chasm.UpdateComponent(ctx, - workflowRef(req.GetNamespaceId(), in.GetWorkflowId(), in.GetOwnerRunId()), + result, _, err := chasm.UpdateComponent(ctx, target.ref, func( - wf *chasmworkflow.Workflow, mctx chasm.MutableContext, r stream.AddMessagesRequest, + owner stream.Owner, mctx chasm.MutableContext, r stream.AddMessagesRequest, ) (stream.AddMessagesResult, error) { - return wf.AppendToOwnedStream(mctx, name, r) + return owner.AppendToOwnedStream(mctx, target.key, r) }, addReq) if err != nil { return nil, err @@ -592,10 +682,10 @@ func (h *handler) PollMessages( return &streampb.PollMessagesResponse{FrontendResponse: out}, nil } -// PollWorkflowMessages reads a stream a workflow owns. +// PollWorkflowMessages reads a stream an execution owns. // // An attached stream is reached through its owner, so this call routes on the -// workflow id and both the frontier and the batches come out of the owner's +// owner and both the frontier and the batches come out of the owner's // component. func (h *handler) PollWorkflowMessages( ctx context.Context, @@ -604,24 +694,26 @@ func (h *handler) PollWorkflowMessages( in := req.GetFrontendRequest() ctx = h.withCallerInfo(ctx, req.GetNamespaceId()) - ref := workflowRef(req.GetNamespaceId(), in.GetWorkflowId(), in.GetOwnerRunId()) - name := ownedStreamName(in.GetStreamName()) + target, err := resolveOwned(req.GetNamespaceId(), in.GetOwner(), in.GetStreamName()) + if err != nil { + return nil, err + } from := in.GetFromOffset() - state, err := h.ownedStreamState(ctx, ref, name) + state, err := h.ownedStreamState(ctx, target) if err != nil { return nil, err } if in.GetWaitNewMessages() && from == state.GetHeadOffset() && !state.GetClosed() { - if _, err := h.waitForOwnedMessages(ctx, ref, name, from, state); err != nil { + if _, err := h.waitForOwnedMessages(ctx, target, from, state); err != nil { return nil, err } } wreq := stream.WindowRequest{From: from, MaxMessages: in.GetMaxMessages(), Topics: in.GetTopics()} - w, err := chasm.ReadComponent(ctx, ref, readOwnedWindow, - ownedWindowRequest{Name: name, Window: wreq}) + w, err := chasm.ReadComponent(ctx, target.ref, readOwnedWindow, + ownedWindowRequest{Key: target.key, Window: wreq}) if err != nil { return nil, err } @@ -664,26 +756,27 @@ func formatWindow(w stream.Window, req stream.WindowRequest) (*streampb.PollMess // ownedWindowRequest names which attached stream to read and what to read. type ownedWindowRequest struct { - Name string + Key string Window stream.WindowRequest } -// readOwnedWindow reads a stream attached to a workflow. +// readOwnedWindow reads a stream attached to an owner. // -// A closed execution can take no more publishes, from its own Workflow Task or -// from anywhere else, so its stream is finished whether or not a producer said -// so. Without that a reader tailing a workflow that ended stays parked forever. +// 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( - wf *chasmworkflow.Workflow, + owner stream.Owner, cctx chasm.Context, req ownedWindowRequest, ) (stream.Window, error) { - s := wf.OwnedStream(cctx, req.Name) + 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. return stream.Window{ - State: &streampb.StreamState{Closed: !cctx.ExecutionInfo().CloseTime.IsZero()}, + State: &streampb.StreamState{Closed: ended}, To: req.Window.From, }, nil } @@ -691,46 +784,37 @@ func readOwnedWindow( if err != nil { return stream.Window{}, err } - if !cctx.ExecutionInfo().CloseTime.IsZero() { + if ended { w.State.Closed = true } return w, nil } // ownedStreamState snapshots an attached stream through the component that -// owns it. A stream the workflow has not published to yet reads as an empty -// one, so a reader may attach before the first event. +// 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, - ref chasm.ComponentRef, - name string, + target ownedTarget, ) (*streampb.StreamState, error) { - state, err := chasm.ReadComponent(ctx, ref, readOwnedStream, name) - if err != nil { - return nil, err - } - return state, nil + return chasm.ReadComponent(ctx, target.ref, readOwnedStream, target.key) } // readOwnedStream snapshots an attached stream and reports whether anything -// can still be added to it. -// -// A closed execution can take no more publishes, from its own Workflow Task or -// from anywhere else, so its stream is finished whether or not a producer said -// so. Without this a reader tailing a workflow that ended stays parked forever. +// can still be added to it, which is not the case once its owner has ended. func readOwnedStream( - wf *chasmworkflow.Workflow, + owner stream.Owner, cctx chasm.Context, - name string, + key string, ) (*streampb.StreamState, error) { - state, err := wf.OwnedStreamState(cctx, name) - if err != nil { - return nil, err - } - if state == nil { - state = &streampb.StreamState{} + state := &streampb.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 !cctx.ExecutionInfo().CloseTime.IsZero() { + if owner.OwnedStreamEnded(cctx, key) { state.Closed = true } return state, nil @@ -767,8 +851,7 @@ func (h *handler) waitForMessages( // because what the poll observes is the owning execution. func (h *handler) waitForOwnedMessages( ctx context.Context, - ref chasm.ComponentRef, - name string, + target ownedTarget, from int64, current *streampb.StreamState, ) (*streampb.StreamState, error) { @@ -776,11 +859,11 @@ func (h *handler) waitForOwnedMessages( ctx, stream.LongPollTimeout, stream.LongPollBuffer) defer cancel() - state, _, err := chasm.PollComponent(pollCtx, ref, + state, _, err := chasm.PollComponent(pollCtx, target.ref, func( - wf *chasmworkflow.Workflow, cctx chasm.Context, offset int64, + owner stream.Owner, cctx chasm.Context, offset int64, ) (*streampb.StreamState, bool, error) { - owned, err := readOwnedStream(wf, cctx, name) + owned, err := readOwnedStream(owner, cctx, target.key) if err != nil { return nil, false, err } @@ -838,7 +921,7 @@ func (h *handler) DescribeStream( }, nil } -// DescribeWorkflowStream reports the frontier of a stream a workflow owns. A +// 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( @@ -848,9 +931,11 @@ func (h *handler) DescribeWorkflowStream( in := req.GetFrontendRequest() ctx = h.withCallerInfo(ctx, req.GetNamespaceId()) - state, err := h.ownedStreamState(ctx, - workflowRef(req.GetNamespaceId(), in.GetWorkflowId(), in.GetOwnerRunId()), - ownedStreamName(in.GetStreamName())) + 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 } diff --git a/chasm/lib/stream/service/handler_test.go b/chasm/lib/stream/service/handler_test.go index 68d4259859c..6a7a784bdde 100644 --- a/chasm/lib/stream/service/handler_test.go +++ b/chasm/lib/stream/service/handler_test.go @@ -6,9 +6,11 @@ import ( "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" ) @@ -53,3 +55,54 @@ func TestFormatWindowAdvancesOnlyOverExaminedOffsets(t *testing.T) { 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/workflow/stream_admission_test.go b/chasm/lib/workflow/stream_admission_test.go index f0e85bb3037..5b4905c1567 100644 --- a/chasm/lib/workflow/stream_admission_test.go +++ b/chasm/lib/workflow/stream_admission_test.go @@ -114,3 +114,35 @@ func TestKnownHeadIsOnlyPushedIntoAnExternalCursor(t *testing.T) { 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 index 8bfdc131469..6d78a1b73cc 100644 --- a/chasm/lib/workflow/stream_commands.go +++ b/chasm/lib/workflow/stream_commands.go @@ -1,14 +1,52 @@ package workflow import ( + "net/url" + "strings" + "go.temporal.io/api/serviceerror" "go.temporal.io/server/chasm" "go.temporal.io/server/chasm/lib/stream" - streamlib "go.temporal.io/server/chasm/lib/stream/gen/streampb/v1" ) // DefaultStreamName is the stream a command addresses when it names none. -const DefaultStreamName = "output" +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 @@ -18,61 +56,14 @@ func (w *Workflow) streamNamed( name string, limits stream.Limits, ) (*stream.Stream, error) { - if w.Streams == nil { - w.Streams = make(chasm.Map[string, *stream.Stream]) - } - if field, ok := w.Streams[name]; ok { - return field.Get(ctx), nil - } - - // Checked only on the create path, so an existing stream is never refused - // for room. The name 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 terminates the - // workflow. - if len(name) > stream.MaxStreamNameLength { - return nil, serviceerror.NewInvalidArgumentf( - "stream name is %d characters, over the %d limit", len(name), stream.MaxStreamNameLength) - } - if len(w.Streams) >= limits.MaxOwnedStreamsPerWorkflow { - return nil, serviceerror.NewFailedPreconditionf( - "workflow already owns %d streams, the limit", limits.MaxOwnedStreamsPerWorkflow) - } - - // Budgeted, because the batches live in this execution's mutable state and - // the size limit on that terminates the workflow instead of refusing. - created, err := stream.NewStream(ctx, stream.NewStreamRequest{ - Attached: true, - Budget: &streamlib.StreamBudget{ - MaxItems: int64(limits.OwnedStreamMaxItems), - MaxBytes: int64(limits.OwnedStreamMaxBytes), - }, - }) - if err != nil { + if err := CheckWorkflowStreamName(name); err != nil { return nil, err } - w.Streams[name] = chasm.NewComponentField(ctx, created) - return created, nil + return w.ownedStreams().Named(ctx, name, limits) } -// siblingStreamBytes is what every other stream this workflow owns 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 the 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. +// 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 { - if len(w.Streams) < 2 { - return 0 - } - var total int64 - for other, field := range w.Streams { - if other == name { - continue - } - total += field.Get(ctx).State.GetAppendedBytes() - } - return total + return w.ownedStreams().SiblingBytes(ctx, name) } diff --git a/chasm/lib/workflow/workflow.go b/chasm/lib/workflow/workflow.go index c32d2f31629..ff796251490 100644 --- a/chasm/lib/workflow/workflow.go +++ b/chasm/lib/workflow/workflow.go @@ -12,7 +12,6 @@ import ( 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" - streamlib "go.temporal.io/server/chasm/lib/stream/gen/streampb/v1" 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" @@ -459,58 +458,39 @@ func (w *Workflow) WorkflowTypeName() string { return w.GetWorkflowTypeName() } -// OwnedStreamState returns the state of a stream this workflow owns, or nil if -// it owns none by that name. -// -// An attached stream has no id of its own, so this is the only way to see its -// frontier from outside the execution. Reading it needs the owner's component, -// which is why it lives here rather than on the stream. -// -// Absent is not an error. An owned stream is created by the first publish to -// it, so a reader that arrives before the workflow has published anything is -// the ordinary case rather than a mistake, and from outside the execution -// "not created yet" and "never will be" are the same observation. -func (w *Workflow) OwnedStreamState( - ctx chasm.Context, - name string, -) (*streamlib.StreamState, error) { - field, ok := w.Streams[name] - if !ok { - return nil, nil - } - return field.Get(ctx).Snapshot(ctx, struct{}{}) -} +var _ stream.Owner = (*Workflow)(nil) -// OwnedStream returns the attached stream itself, or nil when the workflow has -// not created it yet. Reads that need the payload and not just the frontier go +// 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, - name string, + key string, ) *stream.Stream { - field, ok := w.Streams[name] - if !ok { - return nil - } - return field.Get(ctx) + 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 to a stream this workflow owns on behalf of a -// writer outside the execution. +// 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, - name string, + key string, req stream.AddMessagesRequest, ) (stream.AddMessagesResult, error) { - s, err := w.streamNamed(mctx, name, req.Limits) - if err != nil { - return stream.AddMessagesResult{}, err - } - req.SiblingBytes = w.siblingStreamBytes(mctx, name) - return s.AddMessages(mctx, req) + return w.ownedStreams().Append(mctx, key, req) } From 72dbd5302dbb39b2fe7e2932dc9fe3a63502951f Mon Sep 17 00:00:00 2001 From: Moe Dashti Date: Mon, 28 Sep 2026 15:40:17 -0700 Subject: [PATCH 05/17] Tested streams owned by standalone and workflow activities. Covers the default and named streams of both owners, outside producers and readers, retry inheritance, and a standalone activity's streams ending at its terminal status. --- tests/stream_owner_test.go | 400 +++++++++++++++++++++++++++++++++++++ 1 file changed, 400 insertions(+) create mode 100644 tests/stream_owner_test.go diff --git a/tests/stream_owner_test.go b/tests/stream_owner_test.go new file mode 100644 index 00000000000..9de7f32c898 --- /dev/null +++ b/tests/stream_owner_test.go @@ -0,0 +1,400 @@ +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. + time.Sleep(500 * time.Millisecond) + 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())) +} From 5e7624a9235f98851ad1dd4095531c8fc5cedd0f Mon Sep 17 00:00:00 2001 From: Moe Dashti Date: Mon, 28 Sep 2026 16:23:24 -0700 Subject: [PATCH 06/17] Resolved stream start positions where the reader is registered. Earliest, tail and last N are resolved against the frontier of the transaction that registers a consumer or serves a first poll, so none of them races with truncation. The resolved absolute offset is what gets pinned and recorded, which leaves replay and reset untouched. --- .../gen/streampb/v1/request_response.pb.go | 197 +++++++++++------- .../stream/proto/v1/request_response.proto | 23 +- chasm/lib/stream/service/frontend.go | 21 ++ chasm/lib/stream/service/handler.go | 87 ++++++-- chasm/lib/stream/service/tasks.go | 2 +- chasm/lib/stream/start.go | 107 ++++++++++ chasm/lib/stream/start_test.go | 139 ++++++++++++ chasm/lib/stream/stream.go | 37 +++- chasm/lib/stream/stream_test.go | 44 ++-- chasm/lib/workflow/stream_admission_test.go | 7 +- chasm/lib/workflow/workflow.go | 13 +- 11 files changed, 538 insertions(+), 139 deletions(-) create mode 100644 chasm/lib/stream/start.go create mode 100644 chasm/lib/stream/start_test.go diff --git a/chasm/lib/stream/gen/streampb/v1/request_response.pb.go b/chasm/lib/stream/gen/streampb/v1/request_response.pb.go index b10fee610da..042f616daf4 100644 --- a/chasm/lib/stream/gen/streampb/v1/request_response.pb.go +++ b/chasm/lib/stream/gen/streampb/v1/request_response.pb.go @@ -12,7 +12,8 @@ import ( sync "sync" unsafe "unsafe" - v1 "go.temporal.io/api/common/v1" + 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" ) @@ -483,9 +484,13 @@ type SubscribeWorkflowInput struct { // 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, and a negative value means the head. + 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. - StartOffset int64 `protobuf:"varint,4,opt,name=start_offset,json=startOffset,proto3" json:"start_offset,omitempty"` + // 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 } @@ -562,6 +567,13 @@ func (x *SubscribeWorkflowInput) GetStartOffset() int64 { 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"` @@ -611,9 +623,14 @@ type PollMessagesInput struct { 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"` - MaxMessages int32 `protobuf:"varint,4,opt,name=max_messages,json=maxMessages,proto3" json:"max_messages,omitempty"` + 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"` @@ -683,6 +700,13 @@ func (x *PollMessagesInput) GetFromOffset() int64 { 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 @@ -710,7 +734,7 @@ type PollMessagesOutput struct { 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 *v1.Payload `protobuf:"bytes,5,opt,name=close_reason,json=closeReason,proto3" json:"close_reason,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"` @@ -776,7 +800,7 @@ func (x *PollMessagesOutput) GetClosed() bool { return false } -func (x *PollMessagesOutput) GetCloseReason() *v1.Payload { +func (x *PollMessagesOutput) GetCloseReason() *v11.Payload { if x != nil { return x.CloseReason } @@ -929,9 +953,11 @@ type PollWorkflowMessagesInput struct { 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"` - MaxMessages int32 `protobuf:"varint,5,opt,name=max_messages,json=maxMessages,proto3" json:"max_messages,omitempty"` + 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"` @@ -1011,6 +1037,13 @@ func (x *PollWorkflowMessagesInput) GetFromOffset() int64 { 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 @@ -1262,7 +1295,7 @@ 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 *v1.Payload `protobuf:"bytes,3,opt,name=reason,proto3" json:"reason,omitempty"` + Reason *v11.Payload `protobuf:"bytes,3,opt,name=reason,proto3" json:"reason,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -1311,7 +1344,7 @@ func (x *CloseStreamInput) GetStreamId() string { return "" } -func (x *CloseStreamInput) GetReason() *v1.Payload { +func (x *CloseStreamInput) GetReason() *v11.Payload { if x != nil { return x.Reason } @@ -2330,9 +2363,12 @@ type RegisterStreamConsumerInput struct { // 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"` - // Negative means from wherever the stream is when the pin is taken. Resolved - // here, where the frontier is, and returned so the cursor records a fact. - StartOffset int64 `protobuf:"varint,4,opt,name=start_offset,json=startOffset,proto3" json:"start_offset,omitempty"` + // Negative means from wherever the stream is when the pin is taken. Read + // only when start_position is unset. + 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 } @@ -2402,6 +2438,13 @@ func (x *RegisterStreamConsumerInput) GetStartOffset() int64 { 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"` @@ -3463,7 +3506,7 @@ var File_temporal_server_chasm_lib_stream_proto_v1_request_response_proto protor 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.StreamLifecycle 61, // 1: temporal.server.chasm.lib.stream.proto.v1.AddMessagesInput.records:type_name -> temporal.server.chasm.lib.stream.proto.v1.StreamRecord - 61, // 2: temporal.server.chasm.lib.stream.proto.v1.PollMessagesOutput.records:type_name -> temporal.server.chasm.lib.stream.proto.v1.StreamRecord - 62, // 3: temporal.server.chasm.lib.stream.proto.v1.PollMessagesOutput.close_reason:type_name -> temporal.api.common.v1.Payload - 0, // 4: temporal.server.chasm.lib.stream.proto.v1.StreamOwner.kind:type_name -> temporal.server.chasm.lib.stream.proto.v1.StreamOwnerKind - 12, // 5: temporal.server.chasm.lib.stream.proto.v1.PollWorkflowMessagesInput.owner:type_name -> temporal.server.chasm.lib.stream.proto.v1.StreamOwner - 12, // 6: temporal.server.chasm.lib.stream.proto.v1.DescribeWorkflowStreamInput.owner:type_name -> temporal.server.chasm.lib.stream.proto.v1.StreamOwner - 12, // 7: temporal.server.chasm.lib.stream.proto.v1.AddWorkflowMessagesInput.owner:type_name -> temporal.server.chasm.lib.stream.proto.v1.StreamOwner - 61, // 8: temporal.server.chasm.lib.stream.proto.v1.AddWorkflowMessagesInput.records:type_name -> temporal.server.chasm.lib.stream.proto.v1.StreamRecord - 63, // 9: temporal.server.chasm.lib.stream.proto.v1.DescribeStreamOutput.state:type_name -> temporal.server.chasm.lib.stream.proto.v1.StreamState - 62, // 10: temporal.server.chasm.lib.stream.proto.v1.CloseStreamInput.reason:type_name -> temporal.api.common.v1.Payload - 1, // 11: temporal.server.chasm.lib.stream.proto.v1.CreateStreamRequest.frontend_request:type_name -> temporal.server.chasm.lib.stream.proto.v1.CreateStreamInput - 2, // 12: temporal.server.chasm.lib.stream.proto.v1.CreateStreamResponse.frontend_response:type_name -> temporal.server.chasm.lib.stream.proto.v1.CreateStreamOutput - 3, // 13: temporal.server.chasm.lib.stream.proto.v1.AddMessagesRequest.frontend_request:type_name -> temporal.server.chasm.lib.stream.proto.v1.AddMessagesInput - 4, // 14: temporal.server.chasm.lib.stream.proto.v1.AddMessagesResponse.frontend_response:type_name -> temporal.server.chasm.lib.stream.proto.v1.AddMessagesOutput - 5, // 15: temporal.server.chasm.lib.stream.proto.v1.FinishWritingRequest.frontend_request:type_name -> temporal.server.chasm.lib.stream.proto.v1.FinishWritingInput - 6, // 16: temporal.server.chasm.lib.stream.proto.v1.FinishWritingResponse.frontend_response:type_name -> temporal.server.chasm.lib.stream.proto.v1.FinishWritingOutput - 7, // 17: temporal.server.chasm.lib.stream.proto.v1.SubscribeWorkflowRequest.frontend_request:type_name -> temporal.server.chasm.lib.stream.proto.v1.SubscribeWorkflowInput - 8, // 18: temporal.server.chasm.lib.stream.proto.v1.SubscribeWorkflowResponse.frontend_response:type_name -> temporal.server.chasm.lib.stream.proto.v1.SubscribeWorkflowOutput - 9, // 19: temporal.server.chasm.lib.stream.proto.v1.PollMessagesRequest.frontend_request:type_name -> temporal.server.chasm.lib.stream.proto.v1.PollMessagesInput - 10, // 20: temporal.server.chasm.lib.stream.proto.v1.PollMessagesResponse.frontend_response:type_name -> temporal.server.chasm.lib.stream.proto.v1.PollMessagesOutput - 11, // 21: temporal.server.chasm.lib.stream.proto.v1.DescribeStreamRequest.frontend_request:type_name -> temporal.server.chasm.lib.stream.proto.v1.DescribeStreamInput - 16, // 22: temporal.server.chasm.lib.stream.proto.v1.DescribeStreamResponse.frontend_response:type_name -> temporal.server.chasm.lib.stream.proto.v1.DescribeStreamOutput - 13, // 23: temporal.server.chasm.lib.stream.proto.v1.PollWorkflowMessagesRequest.frontend_request:type_name -> temporal.server.chasm.lib.stream.proto.v1.PollWorkflowMessagesInput - 10, // 24: temporal.server.chasm.lib.stream.proto.v1.PollWorkflowMessagesResponse.frontend_response:type_name -> temporal.server.chasm.lib.stream.proto.v1.PollMessagesOutput - 14, // 25: temporal.server.chasm.lib.stream.proto.v1.DescribeWorkflowStreamRequest.frontend_request:type_name -> temporal.server.chasm.lib.stream.proto.v1.DescribeWorkflowStreamInput - 16, // 26: temporal.server.chasm.lib.stream.proto.v1.DescribeWorkflowStreamResponse.frontend_response:type_name -> temporal.server.chasm.lib.stream.proto.v1.DescribeStreamOutput - 15, // 27: temporal.server.chasm.lib.stream.proto.v1.AddWorkflowMessagesRequest.frontend_request:type_name -> temporal.server.chasm.lib.stream.proto.v1.AddWorkflowMessagesInput - 4, // 28: temporal.server.chasm.lib.stream.proto.v1.AddWorkflowMessagesResponse.frontend_response:type_name -> temporal.server.chasm.lib.stream.proto.v1.AddMessagesOutput - 39, // 29: temporal.server.chasm.lib.stream.proto.v1.RegisterStreamConsumerRequest.frontend_request:type_name -> temporal.server.chasm.lib.stream.proto.v1.RegisterStreamConsumerInput - 40, // 30: temporal.server.chasm.lib.stream.proto.v1.RegisterStreamConsumerResponse.frontend_response:type_name -> temporal.server.chasm.lib.stream.proto.v1.RegisterStreamConsumerOutput - 41, // 31: temporal.server.chasm.lib.stream.proto.v1.AdvanceConsumerHeadRequest.frontend_request:type_name -> temporal.server.chasm.lib.stream.proto.v1.AdvanceConsumerHeadInput - 42, // 32: temporal.server.chasm.lib.stream.proto.v1.AdvanceConsumerHeadResponse.frontend_response:type_name -> temporal.server.chasm.lib.stream.proto.v1.AdvanceConsumerHeadOutput - 17, // 33: temporal.server.chasm.lib.stream.proto.v1.CloseStreamRequest.frontend_request:type_name -> temporal.server.chasm.lib.stream.proto.v1.CloseStreamInput - 18, // 34: temporal.server.chasm.lib.stream.proto.v1.CloseStreamResponse.frontend_response:type_name -> temporal.server.chasm.lib.stream.proto.v1.CloseStreamOutput - 19, // 35: temporal.server.chasm.lib.stream.proto.v1.TruncateStreamRequest.frontend_request:type_name -> temporal.server.chasm.lib.stream.proto.v1.TruncateStreamInput - 20, // 36: temporal.server.chasm.lib.stream.proto.v1.TruncateStreamResponse.frontend_response:type_name -> temporal.server.chasm.lib.stream.proto.v1.TruncateStreamOutput - 54, // 37: temporal.server.chasm.lib.stream.proto.v1.ListStreamsOutput.streams:type_name -> temporal.server.chasm.lib.stream.proto.v1.StreamListEntry - 53, // 38: temporal.server.chasm.lib.stream.proto.v1.ListStreamsRequest.frontend_request:type_name -> temporal.server.chasm.lib.stream.proto.v1.ListStreamsInput - 55, // 39: temporal.server.chasm.lib.stream.proto.v1.ListStreamsResponse.frontend_response:type_name -> temporal.server.chasm.lib.stream.proto.v1.ListStreamsOutput - 21, // 40: temporal.server.chasm.lib.stream.proto.v1.DeleteStreamRequest.frontend_request:type_name -> temporal.server.chasm.lib.stream.proto.v1.DeleteStreamInput - 22, // 41: temporal.server.chasm.lib.stream.proto.v1.DeleteStreamResponse.frontend_response:type_name -> temporal.server.chasm.lib.stream.proto.v1.DeleteStreamOutput - 42, // [42:42] is the sub-list for method output_type - 42, // [42:42] is the sub-list for method input_type - 42, // [42:42] is the sub-list for extension type_name - 42, // [42:42] is the sub-list for extension extendee - 0, // [0:42] is the sub-list for field type_name + 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() } diff --git a/chasm/lib/stream/proto/v1/request_response.proto b/chasm/lib/stream/proto/v1/request_response.proto index 31d2f9c46b0..7b4aeb2f2d0 100644 --- a/chasm/lib/stream/proto/v1/request_response.proto +++ b/chasm/lib/stream/proto/v1/request_response.proto @@ -5,6 +5,7 @@ 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"; @@ -75,9 +76,13 @@ message SubscribeWorkflowInput { // Id of a standalone stream in another execution. Exactly one of this and // stream_name is set. string stream_id = 5; - // Where to start. Resolved here rather than at delivery, so the first - // recorded range starts from a fact instead of a reading. + // Where to start, as an absolute offset. Read only when start_position is + // unset, and a negative value means the head. 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 { @@ -90,6 +95,11 @@ message PollMessagesInput { // 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. @@ -156,6 +166,8 @@ message PollWorkflowMessagesInput { // 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; @@ -296,9 +308,12 @@ message RegisterStreamConsumerInput { // inheriting a closed run's floor. string consumer_workflow_id = 3; string consumer_run_id = 5; - // Negative means from wherever the stream is when the pin is taken. Resolved - // here, where the frontier is, and returned so the cursor records a fact. + // Negative means from wherever the stream is when the pin is taken. Read + // only when start_position is unset. 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 { diff --git a/chasm/lib/stream/service/frontend.go b/chasm/lib/stream/service/frontend.go index ecf3f5d3bc4..43e5a09d25f 100644 --- a/chasm/lib/stream/service/frontend.go +++ b/chasm/lib/stream/service/frontend.go @@ -5,6 +5,7 @@ import ( "strings" "go.temporal.io/api/serviceerror" + apistreampb "go.temporal.io/api/stream/v1" "go.temporal.io/server/chasm" "go.temporal.io/server/chasm/lib/stream" streampb "go.temporal.io/server/chasm/lib/stream/gen/streampb/v1" @@ -213,6 +214,16 @@ func checkOffset(field string, value int64) error { 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 *apistreampb.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 { @@ -301,6 +312,10 @@ func (h *FrontendHandler) SubscribeWorkflow( 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, &streampb.SubscribeWorkflowRequest{ NamespaceId: id, FrontendRequest: in, }) @@ -320,6 +335,9 @@ func (h *FrontendHandler) PollMessages( 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, &streampb.PollMessagesRequest{ NamespaceId: id, FrontendRequest: in, @@ -340,6 +358,9 @@ func (h *FrontendHandler) PollWorkflowMessages( 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 diff --git a/chasm/lib/stream/service/handler.go b/chasm/lib/stream/service/handler.go index 75dd345350b..0fbaeba7543 100644 --- a/chasm/lib/stream/service/handler.go +++ b/chasm/lib/stream/service/handler.go @@ -6,6 +6,7 @@ import ( commonpb "go.temporal.io/api/common/v1" "go.temporal.io/api/serviceerror" + apistreampb "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" @@ -357,6 +358,10 @@ func (h *handler) SubscribeWorkflow( req *streampb.SubscribeWorkflowRequest, ) (*streampb.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() != "" { @@ -364,7 +369,7 @@ func (h *handler) SubscribeWorkflow( "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) + return h.subscribeToExternalStream(ctx, req.GetNamespaceId(), in, start) } limits := h.limitsFor(req.GetNamespaceId()) @@ -374,8 +379,7 @@ func (h *handler) SubscribeWorkflow( func( wf *chasmworkflow.Workflow, mctx chasm.MutableContext, input *streampb.SubscribeWorkflowInput, ) (int64, error) { - return wf.SubscribeToOwnedStream( - mctx, ownedStreamName(input.GetStreamName()), input.GetStartOffset(), limits) + return wf.SubscribeToOwnedStream(mctx, ownedStreamName(input.GetStreamName()), start, limits) }, in, ) @@ -400,6 +404,7 @@ func (h *handler) subscribeToExternalStream( ctx context.Context, namespaceID string, in *streampb.SubscribeWorkflowInput, + start *apistreampb.StreamStartPosition, ) (*streampb.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 @@ -429,7 +434,7 @@ func (h *handler) subscribeToExternalStream( StreamId: in.GetStreamId(), ConsumerWorkflowId: in.GetWorkflowId(), ConsumerRunId: consumerRunID, - StartOffset: in.GetStartOffset(), + StartPosition: start, }, }) if err != nil { @@ -477,19 +482,23 @@ func (h *handler) RegisterStreamConsumer( ) (*streampb.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, offset int64, + s *stream.Stream, mctx chasm.MutableContext, start *apistreampb.StreamStartPosition, ) (*streampb.RegisterStreamConsumerOutput, error) { startOffset, err := s.RegisterConsumer(mctx, stream.ConsumerRegistration{ ConsumerID: externalConsumerID(in.GetConsumerWorkflowId(), in.GetConsumerRunId()), WorkflowID: in.GetConsumerWorkflowId(), RunID: in.GetConsumerRunId(), - Offset: offset, + Start: start, External: true, MaxConsumers: limits.MaxConsumersPerStream, }) @@ -501,7 +510,7 @@ func (h *handler) RegisterStreamConsumer( KnownHead: s.State.GetHeadOffset(), }, nil }, - in.GetStartOffset(), + start, ) if err != nil { // Only the absence of the execution itself is turned into an answer. @@ -650,7 +659,7 @@ func (h *handler) PollMessages( ctx = h.withCallerInfo(ctx, req.GetNamespaceId()) ref := refForRun(req.GetNamespaceId(), in.GetStreamId(), in.GetRunId()) - from := in.GetFromOffset() + 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 @@ -661,16 +670,23 @@ func (h *handler) PollMessages( 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 from == state.GetHeadOffset() && !state.GetClosed() { + if waitFrom == state.GetHeadOffset() && !state.GetClosed() { // The window is re-read below, so only the blocking matters here. - if _, err := h.waitForMessages(ctx, ref, from, state); err != nil { + if _, err := h.waitForMessages(ctx, ref, waitFrom, state); err != nil { return nil, err } + from, start = waitFrom, nil } } - wreq := stream.WindowRequest{From: from, MaxMessages: in.GetMaxMessages(), Topics: in.GetTopics()} + 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 @@ -698,20 +714,29 @@ func (h *handler) PollWorkflowMessages( if err != nil { return nil, err } - from := in.GetFromOffset() + from, start := in.GetFromOffset(), in.GetStartPosition() state, err := h.ownedStreamState(ctx, target) if err != nil { return nil, err } - if in.GetWaitNewMessages() && from == state.GetHeadOffset() && !state.GetClosed() { - if _, err := h.waitForOwnedMessages(ctx, target, from, state); err != nil { + if in.GetWaitNewMessages() { + waitFrom, err := waitOffset(from, start, state) + if err != nil { return nil, err } + if waitFrom == state.GetHeadOffset() && !state.GetClosed() { + if _, err := h.waitForOwnedMessages(ctx, target, waitFrom, state); err != nil { + return nil, err + } + from, start = waitFrom, nil + } } - wreq := stream.WindowRequest{From: from, MaxMessages: in.GetMaxMessages(), Topics: in.GetTopics()} + 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 { @@ -734,18 +759,18 @@ func (h *handler) PollWorkflowMessages( // must not be reported as read to the end. func formatWindow(w stream.Window, req stream.WindowRequest) (*streampb.PollMessagesOutput, error) { out := &streampb.PollMessagesOutput{ - NextOffset: req.From, + NextOffset: w.From, HeadOffset: w.State.GetHeadOffset(), Closed: w.State.GetClosed(), CloseReason: w.State.GetCloseReason(), RunId: w.RunID, } - if req.From == w.State.GetHeadOffset() { + if w.From == w.State.GetHeadOffset() { return out, nil } records, next, err := stream.CollectRecords( - w.Blobs, w.Starts, req.From, w.To, w.Limit, req.Topics) + w.Blobs, w.Starts, w.From, w.To, w.Limit, req.Topics) if err != nil { return nil, err } @@ -775,10 +800,15 @@ func readOwnedWindow( if s == nil { // Nothing published yet, which reads as an empty stream so a reader can // attach before the first append. - return stream.Window{ - State: &streampb.StreamState{Closed: ended}, - To: req.Window.From, - }, nil + state := &streampb.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 { @@ -875,6 +905,19 @@ func (h *handler) waitForOwnedMessages( return pollOutcome(pollCtx, ctx, 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 *apistreampb.StreamStartPosition, state *streampb.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 *streampb.StreamState, from int64) bool { diff --git a/chasm/lib/stream/service/tasks.go b/chasm/lib/stream/service/tasks.go index ceb1e313b81..ecc2beb415d 100644 --- a/chasm/lib/stream/service/tasks.go +++ b/chasm/lib/stream/service/tasks.go @@ -143,7 +143,7 @@ func rekeyConsumer( ConsumerID: externalConsumerID(consumer.GetWorkflowId(), out.GetSuccessorRunId()), WorkflowID: consumer.GetWorkflowId(), RunID: out.GetSuccessorRunId(), - Offset: out.GetSuccessorStartOffset(), + Start: stream.AtOffset(out.GetSuccessorStartOffset()), External: true, } _, _, err := chasm.UpdateComponent(ctx, ref, diff --git a/chasm/lib/stream/start.go b/chasm/lib/stream/start.go new file mode 100644 index 00000000000..29dfe0a7078 --- /dev/null +++ b/chasm/lib/stream/start.go @@ -0,0 +1,107 @@ +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. +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 Tail(), nil + } + 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..c49b19ac9b1 --- /dev/null +++ b/chasm/lib/stream/start_test.go @@ -0,0 +1,139 @@ +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(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 03ff5fd04db..2475090bde0 100644 --- a/chasm/lib/stream/stream.go +++ b/chasm/lib/stream/stream.go @@ -511,7 +511,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 } @@ -522,8 +525,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 @@ -533,6 +539,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{}, serviceerror.NewFailedPreconditionf( "offset %d has been truncated, the stream starts at %d", req.From, s.State.BaseOffset) @@ -550,6 +563,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, @@ -675,8 +689,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 @@ -687,9 +702,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 @@ -703,9 +718,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, serviceerror.NewFailedPreconditionf( diff --git a/chasm/lib/stream/stream_test.go b/chasm/lib/stream/stream_test.go index d9dc9ca040b..7e6d2983089 100644 --- a/chasm/lib/stream/stream_test.go +++ b/chasm/lib/stream/stream_test.go @@ -218,7 +218,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) @@ -265,7 +265,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) @@ -307,7 +307,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) @@ -321,7 +321,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) @@ -338,7 +338,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) @@ -356,7 +356,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") } @@ -368,13 +368,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) @@ -386,7 +386,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) @@ -421,7 +421,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} @@ -436,7 +436,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") @@ -456,7 +456,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") @@ -465,7 +465,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") } @@ -516,20 +516,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) } @@ -603,12 +604,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) @@ -619,7 +622,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) @@ -635,7 +638,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/stream_admission_test.go b/chasm/lib/workflow/stream_admission_test.go index 5b4905c1567..9a5c19209a1 100644 --- a/chasm/lib/workflow/stream_admission_test.go +++ b/chasm/lib/workflow/stream_admission_test.go @@ -82,7 +82,7 @@ func TestSubscriptionsOfBothOriginsCannotShareOneKey(t *testing.T) { limits := stream.Limits{MaxOwnedStreamsPerWorkflow: 10, OwnedStreamMaxItems: 100} owned := &Workflow{} - _, err := owned.SubscribeToOwnedStream(ctx, "x", 0, limits) + _, err := owned.SubscribeToOwnedStream(ctx, "x", stream.AtOffset(0), limits) require.NoError(t, err) _, err = owned.SubscribeToExternalStream(ctx, ExternalStreamSubscription{ StreamID: "x", StartOffset: 7, KnownHead: 9, @@ -95,7 +95,7 @@ func TestSubscriptionsOfBothOriginsCannotShareOneKey(t *testing.T) { StreamID: "x", StartOffset: 7, KnownHead: 9, }) require.NoError(t, err) - _, err = external.SubscribeToOwnedStream(ctx, "x", 0, limits) + _, err = external.SubscribeToOwnedStream(ctx, "x", stream.AtOffset(0), limits) require.ErrorAs(t, err, &refused) } @@ -107,7 +107,8 @@ func TestKnownHeadIsOnlyPushedIntoAnExternalCursor(t *testing.T) { ctx := newStreamBudgetTestContext() w := &Workflow{} _, err := w.SubscribeToOwnedStream( - ctx, "x", 0, stream.Limits{MaxOwnedStreamsPerWorkflow: 10, OwnedStreamMaxItems: 100}) + ctx, "x", stream.AtOffset(0), + stream.Limits{MaxOwnedStreamsPerWorkflow: 10, OwnedStreamMaxItems: 100}) require.NoError(t, err) err = w.AdvanceKnownHead(ctx, "x", 42) diff --git a/chasm/lib/workflow/workflow.go b/chasm/lib/workflow/workflow.go index ff796251490..747148698a0 100644 --- a/chasm/lib/workflow/workflow.go +++ b/chasm/lib/workflow/workflow.go @@ -7,6 +7,7 @@ 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" @@ -66,13 +67,13 @@ func streamConsumerID(streamName string) string { // SubscribeToOwnedStream registers this workflow as a consumer of a stream it // owns, returning the offset the subscription actually starts from. // -// A negative start offset means "from wherever the stream is now". That is -// resolved here and stored, so the first recorded range begins at a fact rather -// than at a reading that would land somewhere else on replay. +// 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, - startOffset int64, + start *streampb.StreamStartPosition, limits stream.Limits, ) (int64, error) { // Created here as well as on first write, so a workflow can start reading a @@ -102,11 +103,11 @@ func (w *Workflow) SubscribeToOwnedStream( // 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{ + startOffset, err := owned.RegisterConsumer(mctx, stream.ConsumerRegistration{ ConsumerID: streamConsumerID(name), WorkflowID: key.BusinessID, RunID: key.RunID, - Offset: startOffset, + Start: start, MaxConsumers: limits.MaxConsumersPerStream, }) if err != nil { From 67cf5d9189252bd638dbb07236b13de2cb6ae501 Mon Sep 17 00:00:00 2001 From: Moe Dashti Date: Mon, 28 Sep 2026 16:23:24 -0700 Subject: [PATCH 07/17] Tested first polls from each start position, on activity streams too. --- tests/stream_start_test.go | 221 +++++++++++++++++++++++++++++++++++++ 1 file changed, 221 insertions(+) create mode 100644 tests/stream_start_test.go diff --git a/tests/stream_start_test.go b/tests/stream_start_test.go new file mode 100644 index 00000000000..e913e7b5947 --- /dev/null +++ b/tests/stream_start_test.go @@ -0,0 +1,221 @@ +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())) +} From 3b15cce35c54301030f944a181d7ef2eaa5bc230 Mon Sep 17 00:00:00 2001 From: Moe Dashti Date: Mon, 28 Sep 2026 16:24:00 -0700 Subject: [PATCH 08/17] Tested that a subscription records where its start position resolved. --- tests/stream_start_test.go | 54 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/tests/stream_start_test.go b/tests/stream_start_test.go index e913e7b5947..6f125cb017e 100644 --- a/tests/stream_start_test.go +++ b/tests/stream_start_test.go @@ -219,3 +219,57 @@ func TestStreamWorkflowActivityStreamStartPositions(t *testing.T) { 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) +} From c2fa4de46b5e18edd9c4418e4f2906896aac485e Mon Sep 17 00:00:00 2001 From: Moe Dashti Date: Mon, 28 Sep 2026 16:39:57 -0700 Subject: [PATCH 09/17] Refused a negative start offset instead of reading it as the head. The Core and Python chains now send a start position, so nothing depends on the sentinel. Refusing it with the field to use tells a caller still sending it, where reinterpreting it would not. --- chasm/lib/stream/gen/streampb/v1/request_response.pb.go | 6 +++--- chasm/lib/stream/proto/v1/request_response.proto | 6 +++--- chasm/lib/stream/start.go | 8 +++++++- chasm/lib/stream/start_test.go | 4 ++++ tests/stream_start_test.go | 5 +++++ 5 files changed, 22 insertions(+), 7 deletions(-) diff --git a/chasm/lib/stream/gen/streampb/v1/request_response.pb.go b/chasm/lib/stream/gen/streampb/v1/request_response.pb.go index 042f616daf4..e2e6f6ac2c9 100644 --- a/chasm/lib/stream/gen/streampb/v1/request_response.pb.go +++ b/chasm/lib/stream/gen/streampb/v1/request_response.pb.go @@ -485,7 +485,7 @@ type SubscribeWorkflowInput struct { // 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, and a negative value means the head. + // 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 @@ -2363,8 +2363,8 @@ type RegisterStreamConsumerInput struct { // 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"` - // Negative means from wherever the stream is when the pin is taken. Read - // only when start_position is unset. + // 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. diff --git a/chasm/lib/stream/proto/v1/request_response.proto b/chasm/lib/stream/proto/v1/request_response.proto index 7b4aeb2f2d0..24b242745e8 100644 --- a/chasm/lib/stream/proto/v1/request_response.proto +++ b/chasm/lib/stream/proto/v1/request_response.proto @@ -77,7 +77,7 @@ message SubscribeWorkflowInput { // stream_name is set. string stream_id = 5; // Where to start, as an absolute offset. Read only when start_position is - // unset, and a negative value means the head. + // 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 @@ -308,8 +308,8 @@ message RegisterStreamConsumerInput { // inheriting a closed run's floor. string consumer_workflow_id = 3; string consumer_run_id = 5; - // Negative means from wherever the stream is when the pin is taken. Read - // only when start_position is unset. + // 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. diff --git a/chasm/lib/stream/start.go b/chasm/lib/stream/start.go index 29dfe0a7078..65bd54733b9 100644 --- a/chasm/lib/stream/start.go +++ b/chasm/lib/stream/start.go @@ -37,6 +37,10 @@ func LastN(n int64) *streampb.StreamStartPosition { // 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) { @@ -48,7 +52,9 @@ func RequestedStart( return pos, CheckStart(pos) } if offset < 0 { - return Tail(), nil + return nil, serviceerror.NewInvalidArgumentf( + "%s cannot be negative, got %d: ask for the head with start_position.tail", + offsetField, offset) } return AtOffset(offset), nil } diff --git a/chasm/lib/stream/start_test.go b/chasm/lib/stream/start_test.go index c49b19ac9b1..0eb888e4486 100644 --- a/chasm/lib/stream/start_test.go +++ b/chasm/lib/stream/start_test.go @@ -82,6 +82,10 @@ func TestRequestedStartReadsEitherField(t *testing.T) { 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") diff --git a/tests/stream_start_test.go b/tests/stream_start_test.go index 6f125cb017e..1eef8a41e4a 100644 --- a/tests/stream_start_test.go +++ b/tests/stream_start_test.go @@ -272,4 +272,9 @@ func TestStreamSubscribeWorkflowResolvesItsStartPosition(t *testing.T) { 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") } From 6c2c61cc97b27b5478080f397833721346d71941 Mon Sep 17 00:00:00 2001 From: Moe Dashti Date: Tue, 29 Sep 2026 10:12:15 -0700 Subject: [PATCH 10/17] Fixed the lint findings in the stream service and its owner test. The public stream protos take the `streampb` alias the lint config requires, so the generated ones move to `streamlib` as in the tests. The two switches got their default cases, and the owner test says why it waits. --- chasm/lib/stream/service/frontend.go | 164 ++++++++++++----------- chasm/lib/stream/service/handler.go | 193 ++++++++++++++------------- tests/stream_owner_test.go | 5 +- 3 files changed, 183 insertions(+), 179 deletions(-) diff --git a/chasm/lib/stream/service/frontend.go b/chasm/lib/stream/service/frontend.go index 43e5a09d25f..16a82ea3a99 100644 --- a/chasm/lib/stream/service/frontend.go +++ b/chasm/lib/stream/service/frontend.go @@ -5,10 +5,10 @@ import ( "strings" "go.temporal.io/api/serviceerror" - apistreampb "go.temporal.io/api/stream/v1" + streampb "go.temporal.io/api/stream/v1" "go.temporal.io/server/chasm" "go.temporal.io/server/chasm/lib/stream" - streampb "go.temporal.io/server/chasm/lib/stream/gen/streampb/v1" + 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" @@ -28,16 +28,16 @@ import ( // or page size that history would only clamp or refuse later is refused here // before the request is routed. type FrontendHandler struct { - streampb.UnimplementedStreamServiceServer + streamlib.UnimplementedStreamServiceServer - client streampb.StreamServiceClient + client streamlib.StreamServiceClient namespaceRegistry namespace.Registry logger log.Logger config *stream.Config } func NewFrontendHandler( - client streampb.StreamServiceClient, + client streamlib.StreamServiceClient, namespaceRegistry namespace.Registry, logger log.Logger, config *stream.Config, @@ -61,44 +61,44 @@ func NewFrontendHandler( // them with Unimplemented wherever they land. func RedirectableMethods() map[string]func() any { return map[string]func() any{ - streampb.StreamService_CreateStream_FullMethodName: func() any { - return &streampb.CreateStreamResponse{} + streamlib.StreamService_CreateStream_FullMethodName: func() any { + return &streamlib.CreateStreamResponse{} }, - streampb.StreamService_AddMessages_FullMethodName: func() any { - return &streampb.AddMessagesResponse{} + streamlib.StreamService_AddMessages_FullMethodName: func() any { + return &streamlib.AddMessagesResponse{} }, - streampb.StreamService_FinishWriting_FullMethodName: func() any { - return &streampb.FinishWritingResponse{} + streamlib.StreamService_FinishWriting_FullMethodName: func() any { + return &streamlib.FinishWritingResponse{} }, - streampb.StreamService_SubscribeWorkflow_FullMethodName: func() any { - return &streampb.SubscribeWorkflowResponse{} + streamlib.StreamService_SubscribeWorkflow_FullMethodName: func() any { + return &streamlib.SubscribeWorkflowResponse{} }, - streampb.StreamService_PollMessages_FullMethodName: func() any { - return &streampb.PollMessagesResponse{} + streamlib.StreamService_PollMessages_FullMethodName: func() any { + return &streamlib.PollMessagesResponse{} }, - streampb.StreamService_DescribeStream_FullMethodName: func() any { - return &streampb.DescribeStreamResponse{} + streamlib.StreamService_DescribeStream_FullMethodName: func() any { + return &streamlib.DescribeStreamResponse{} }, - streampb.StreamService_PollWorkflowMessages_FullMethodName: func() any { - return &streampb.PollWorkflowMessagesResponse{} + streamlib.StreamService_PollWorkflowMessages_FullMethodName: func() any { + return &streamlib.PollWorkflowMessagesResponse{} }, - streampb.StreamService_DescribeWorkflowStream_FullMethodName: func() any { - return &streampb.DescribeWorkflowStreamResponse{} + streamlib.StreamService_DescribeWorkflowStream_FullMethodName: func() any { + return &streamlib.DescribeWorkflowStreamResponse{} }, - streampb.StreamService_AddWorkflowMessages_FullMethodName: func() any { - return &streampb.AddWorkflowMessagesResponse{} + streamlib.StreamService_AddWorkflowMessages_FullMethodName: func() any { + return &streamlib.AddWorkflowMessagesResponse{} }, - streampb.StreamService_CloseStream_FullMethodName: func() any { - return &streampb.CloseStreamResponse{} + streamlib.StreamService_CloseStream_FullMethodName: func() any { + return &streamlib.CloseStreamResponse{} }, - streampb.StreamService_TruncateStream_FullMethodName: func() any { - return &streampb.TruncateStreamResponse{} + streamlib.StreamService_TruncateStream_FullMethodName: func() any { + return &streamlib.TruncateStreamResponse{} }, - streampb.StreamService_ListStreams_FullMethodName: func() any { - return &streampb.ListStreamsResponse{} + streamlib.StreamService_ListStreams_FullMethodName: func() any { + return &streamlib.ListStreamsResponse{} }, - streampb.StreamService_DeleteStream_FullMethodName: func() any { - return &streampb.DeleteStreamResponse{} + streamlib.StreamService_DeleteStream_FullMethodName: func() any { + return &streamlib.DeleteStreamResponse{} }, } } @@ -133,14 +133,14 @@ func (h *FrontendHandler) namespaceID(name string) (string, error) { // should not be the exception because the caller left a field empty. func (h *FrontendHandler) checkLifecycle( namespaceName string, - lifecycle *streampb.StreamLifecycle, -) (*streampb.StreamLifecycle, error) { + lifecycle *streamlib.StreamLifecycle, +) (*streamlib.StreamLifecycle, error) { ns, err := h.namespaceRegistry.GetNamespace(namespace.Name(namespaceName)) if err != nil { return nil, err } if lifecycle == nil { - lifecycle = &streampb.StreamLifecycle{} + lifecycle = &streamlib.StreamLifecycle{} } lifecycle = common.CloneProto(lifecycle) @@ -159,6 +159,8 @@ func (h *FrontendHandler) checkLifecycle( 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 @@ -179,12 +181,12 @@ func (h *FrontendHandler) checkID(field, value string) error { // 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 *streampb.StreamOwner, + owner *streamlib.StreamOwner, workflowID, runID string, -) (*streampb.StreamOwner, error) { +) (*streamlib.StreamOwner, error) { if owner == nil { - owner = &streampb.StreamOwner{ - Kind: streampb.STREAM_OWNER_KIND_WORKFLOW, + owner = &streamlib.StreamOwner{ + Kind: streamlib.STREAM_OWNER_KIND_WORKFLOW, Id: workflowID, RunId: runID, } @@ -216,7 +218,7 @@ func checkOffset(field string, value int64) error { // 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 *apistreampb.StreamStartPosition, from int64) error { +func checkFirstPoll(pos *streampb.StreamStartPosition, from int64) error { if pos == nil { return nil } @@ -234,8 +236,8 @@ func clampMaxMessages(requested int32) int32 { } func (h *FrontendHandler) CreateStream( - ctx context.Context, req *streampb.CreateStreamRequest, -) (*streampb.CreateStreamResponse, error) { + ctx context.Context, req *streamlib.CreateStreamRequest, +) (*streamlib.CreateStreamResponse, error) { in := req.GetFrontendRequest() id, err := h.namespaceID(in.GetNamespace()) if err != nil { @@ -252,14 +254,14 @@ func (h *FrontendHandler) CreateStream( return nil, err } in.Lifecycle = lifecycle - return h.client.CreateStream(ctx, &streampb.CreateStreamRequest{ + return h.client.CreateStream(ctx, &streamlib.CreateStreamRequest{ NamespaceId: id, FrontendRequest: in, }) } func (h *FrontendHandler) AddMessages( - ctx context.Context, req *streampb.AddMessagesRequest, -) (*streampb.AddMessagesResponse, error) { + ctx context.Context, req *streamlib.AddMessagesRequest, +) (*streamlib.AddMessagesResponse, error) { in := req.GetFrontendRequest() id, err := h.namespaceID(in.GetNamespace()) if err != nil { @@ -271,14 +273,14 @@ func (h *FrontendHandler) AddMessages( if err := h.checkID("producer id", in.GetProducerId()); err != nil { return nil, err } - return h.client.AddMessages(ctx, &streampb.AddMessagesRequest{ + return h.client.AddMessages(ctx, &streamlib.AddMessagesRequest{ NamespaceId: id, FrontendRequest: in, }) } func (h *FrontendHandler) FinishWriting( - ctx context.Context, req *streampb.FinishWritingRequest, -) (*streampb.FinishWritingResponse, error) { + ctx context.Context, req *streamlib.FinishWritingRequest, +) (*streamlib.FinishWritingResponse, error) { in := req.GetFrontendRequest() id, err := h.namespaceID(in.GetNamespace()) if err != nil { @@ -290,14 +292,14 @@ func (h *FrontendHandler) FinishWriting( if err := h.checkID("producer id", in.GetProducerId()); err != nil { return nil, err } - return h.client.FinishWriting(ctx, &streampb.FinishWritingRequest{ + return h.client.FinishWriting(ctx, &streamlib.FinishWritingRequest{ NamespaceId: id, FrontendRequest: in, }) } func (h *FrontendHandler) SubscribeWorkflow( - ctx context.Context, req *streampb.SubscribeWorkflowRequest, -) (*streampb.SubscribeWorkflowResponse, error) { + ctx context.Context, req *streamlib.SubscribeWorkflowRequest, +) (*streamlib.SubscribeWorkflowResponse, error) { in := req.GetFrontendRequest() id, err := h.namespaceID(in.GetNamespace()) if err != nil { @@ -316,14 +318,14 @@ func (h *FrontendHandler) SubscribeWorkflow( in.GetStartPosition(), "start_offset", in.GetStartOffset()); err != nil { return nil, err } - return h.client.SubscribeWorkflow(ctx, &streampb.SubscribeWorkflowRequest{ + return h.client.SubscribeWorkflow(ctx, &streamlib.SubscribeWorkflowRequest{ NamespaceId: id, FrontendRequest: in, }) } func (h *FrontendHandler) PollMessages( - ctx context.Context, req *streampb.PollMessagesRequest, -) (*streampb.PollMessagesResponse, error) { + ctx context.Context, req *streamlib.PollMessagesRequest, +) (*streamlib.PollMessagesResponse, error) { in := req.GetFrontendRequest() id, err := h.namespaceID(in.GetNamespace()) if err != nil { @@ -339,14 +341,14 @@ func (h *FrontendHandler) PollMessages( return nil, err } in.MaxMessages = clampMaxMessages(in.GetMaxMessages()) - return h.client.PollMessages(ctx, &streampb.PollMessagesRequest{ + return h.client.PollMessages(ctx, &streamlib.PollMessagesRequest{ NamespaceId: id, FrontendRequest: in, }) } func (h *FrontendHandler) PollWorkflowMessages( - ctx context.Context, req *streampb.PollWorkflowMessagesRequest, -) (*streampb.PollWorkflowMessagesResponse, error) { + ctx context.Context, req *streamlib.PollWorkflowMessagesRequest, +) (*streamlib.PollWorkflowMessagesResponse, error) { in := req.GetFrontendRequest() id, err := h.namespaceID(in.GetNamespace()) if err != nil { @@ -367,14 +369,14 @@ func (h *FrontendHandler) PollWorkflowMessages( } in.Owner, in.WorkflowId, in.OwnerRunId = owner, "", "" in.MaxMessages = clampMaxMessages(in.GetMaxMessages()) - return h.client.PollWorkflowMessages(ctx, &streampb.PollWorkflowMessagesRequest{ + return h.client.PollWorkflowMessages(ctx, &streamlib.PollWorkflowMessagesRequest{ NamespaceId: id, FrontendRequest: in, }) } func (h *FrontendHandler) DescribeWorkflowStream( - ctx context.Context, req *streampb.DescribeWorkflowStreamRequest, -) (*streampb.DescribeWorkflowStreamResponse, error) { + ctx context.Context, req *streamlib.DescribeWorkflowStreamRequest, +) (*streamlib.DescribeWorkflowStreamResponse, error) { in := req.GetFrontendRequest() id, err := h.namespaceID(in.GetNamespace()) if err != nil { @@ -388,14 +390,14 @@ func (h *FrontendHandler) DescribeWorkflowStream( return nil, err } in.Owner, in.WorkflowId, in.OwnerRunId = owner, "", "" - return h.client.DescribeWorkflowStream(ctx, &streampb.DescribeWorkflowStreamRequest{ + return h.client.DescribeWorkflowStream(ctx, &streamlib.DescribeWorkflowStreamRequest{ NamespaceId: id, FrontendRequest: in, }) } func (h *FrontendHandler) AddWorkflowMessages( - ctx context.Context, req *streampb.AddWorkflowMessagesRequest, -) (*streampb.AddWorkflowMessagesResponse, error) { + ctx context.Context, req *streamlib.AddWorkflowMessagesRequest, +) (*streamlib.AddWorkflowMessagesResponse, error) { in := req.GetFrontendRequest() id, err := h.namespaceID(in.GetNamespace()) if err != nil { @@ -412,14 +414,14 @@ func (h *FrontendHandler) AddWorkflowMessages( if err := h.checkID("producer id", in.GetProducerId()); err != nil { return nil, err } - return h.client.AddWorkflowMessages(ctx, &streampb.AddWorkflowMessagesRequest{ + return h.client.AddWorkflowMessages(ctx, &streamlib.AddWorkflowMessagesRequest{ NamespaceId: id, FrontendRequest: in, }) } func (h *FrontendHandler) DescribeStream( - ctx context.Context, req *streampb.DescribeStreamRequest, -) (*streampb.DescribeStreamResponse, error) { + ctx context.Context, req *streamlib.DescribeStreamRequest, +) (*streamlib.DescribeStreamResponse, error) { in := req.GetFrontendRequest() id, err := h.namespaceID(in.GetNamespace()) if err != nil { @@ -428,14 +430,14 @@ func (h *FrontendHandler) DescribeStream( if err := h.checkID("stream id", in.GetStreamId()); err != nil { return nil, err } - return h.client.DescribeStream(ctx, &streampb.DescribeStreamRequest{ + return h.client.DescribeStream(ctx, &streamlib.DescribeStreamRequest{ NamespaceId: id, FrontendRequest: in, }) } func (h *FrontendHandler) CloseStream( - ctx context.Context, req *streampb.CloseStreamRequest, -) (*streampb.CloseStreamResponse, error) { + ctx context.Context, req *streamlib.CloseStreamRequest, +) (*streamlib.CloseStreamResponse, error) { in := req.GetFrontendRequest() id, err := h.namespaceID(in.GetNamespace()) if err != nil { @@ -444,14 +446,14 @@ func (h *FrontendHandler) CloseStream( if err := h.checkID("stream id", in.GetStreamId()); err != nil { return nil, err } - return h.client.CloseStream(ctx, &streampb.CloseStreamRequest{ + return h.client.CloseStream(ctx, &streamlib.CloseStreamRequest{ NamespaceId: id, FrontendRequest: in, }) } func (h *FrontendHandler) TruncateStream( - ctx context.Context, req *streampb.TruncateStreamRequest, -) (*streampb.TruncateStreamResponse, error) { + ctx context.Context, req *streamlib.TruncateStreamRequest, +) (*streamlib.TruncateStreamResponse, error) { in := req.GetFrontendRequest() id, err := h.namespaceID(in.GetNamespace()) if err != nil { @@ -463,14 +465,14 @@ func (h *FrontendHandler) TruncateStream( if err := checkOffset("new base offset", in.GetNewBaseOffset()); err != nil { return nil, err } - return h.client.TruncateStream(ctx, &streampb.TruncateStreamRequest{ + return h.client.TruncateStream(ctx, &streamlib.TruncateStreamRequest{ NamespaceId: id, FrontendRequest: in, }) } func (h *FrontendHandler) DeleteStream( - ctx context.Context, req *streampb.DeleteStreamRequest, -) (*streampb.DeleteStreamResponse, error) { + ctx context.Context, req *streamlib.DeleteStreamRequest, +) (*streamlib.DeleteStreamResponse, error) { in := req.GetFrontendRequest() id, err := h.namespaceID(in.GetNamespace()) if err != nil { @@ -479,7 +481,7 @@ func (h *FrontendHandler) DeleteStream( if err := h.checkID("stream id", in.GetStreamId()); err != nil { return nil, err } - return h.client.DeleteStream(ctx, &streampb.DeleteStreamRequest{ + return h.client.DeleteStream(ctx, &streamlib.DeleteStreamRequest{ NamespaceId: id, FrontendRequest: in, }) } @@ -487,8 +489,8 @@ func (h *FrontendHandler) DeleteStream( // 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 *streampb.ListStreamsRequest, -) (*streampb.ListStreamsResponse, error) { + ctx context.Context, req *streamlib.ListStreamsRequest, +) (*streamlib.ListStreamsResponse, error) { in := req.GetFrontendRequest() if _, err := h.namespaceID(in.GetNamespace()); err != nil { return nil, err @@ -521,15 +523,15 @@ func (h *FrontendHandler) ListStreams( return nil, err } - entries := make([]*streampb.StreamListEntry, 0, len(resp.Executions)) + entries := make([]*streamlib.StreamListEntry, 0, len(resp.Executions)) for _, e := range resp.Executions { - entries = append(entries, &streampb.StreamListEntry{ + entries = append(entries, &streamlib.StreamListEntry{ StreamId: e.BusinessID, RunId: e.RunID, }) } - return &streampb.ListStreamsResponse{ - FrontendResponse: &streampb.ListStreamsOutput{ + return &streamlib.ListStreamsResponse{ + FrontendResponse: &streamlib.ListStreamsOutput{ Streams: entries, NextPageToken: resp.NextPageToken, }, diff --git a/chasm/lib/stream/service/handler.go b/chasm/lib/stream/service/handler.go index 0fbaeba7543..f36413cebb3 100644 --- a/chasm/lib/stream/service/handler.go +++ b/chasm/lib/stream/service/handler.go @@ -6,11 +6,11 @@ import ( commonpb "go.temporal.io/api/common/v1" "go.temporal.io/api/serviceerror" - apistreampb "go.temporal.io/api/stream/v1" + 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" - streampb "go.temporal.io/server/chasm/lib/stream/gen/streampb/v1" + 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/contextutil" @@ -21,7 +21,7 @@ import ( ) type handler struct { - streampb.UnimplementedStreamServiceServer + streamlib.UnimplementedStreamServiceServer shardController shard.Controller namespaceRegistry namespace.Registry @@ -32,7 +32,7 @@ type handler struct { // 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 streampb.StreamServiceClient + routed streamlib.StreamServiceClient } func newHandler( @@ -40,7 +40,7 @@ func newHandler( namespaceRegistry namespace.Registry, logger log.Logger, config *stream.Config, - routed streampb.StreamServiceClient, + routed streamlib.StreamServiceClient, ) *handler { return &handler{ shardController: shardController, @@ -142,7 +142,7 @@ type ownedTarget struct { // a stream its activity owns. func resolveOwned( namespaceID string, - owner *streampb.StreamOwner, + owner *streamlib.StreamOwner, name string, ) (ownedTarget, error) { if err := checkOwner(owner); err != nil { @@ -150,7 +150,7 @@ func resolveOwned( } name = ownedStreamName(name) switch owner.GetKind() { - case streampb.STREAM_OWNER_KIND_WORKFLOW: + case streamlib.STREAM_OWNER_KIND_WORKFLOW: if err := chasmworkflow.CheckWorkflowStreamName(name); err != nil { return ownedTarget{}, err } @@ -158,7 +158,7 @@ func resolveOwned( ref: workflowRef(namespaceID, owner.GetId(), owner.GetRunId()), key: name, }, nil - case streampb.STREAM_OWNER_KIND_ACTIVITY: + case streamlib.STREAM_OWNER_KIND_ACTIVITY: if err := stream.CheckStreamName(name); err != nil { return ownedTarget{}, err } @@ -166,7 +166,7 @@ func resolveOwned( ref: activityRef(namespaceID, owner.GetId(), owner.GetRunId()), key: name, }, nil - case streampb.STREAM_OWNER_KIND_WORKFLOW_ACTIVITY: + case streamlib.STREAM_OWNER_KIND_WORKFLOW_ACTIVITY: if err := stream.CheckStreamName(name); err != nil { return ownedTarget{}, err } @@ -174,24 +174,25 @@ func resolveOwned( ref: workflowRef(namespaceID, owner.GetId(), owner.GetRunId()), key: chasmworkflow.ActivityStreamKey(owner.GetActivityId(), name), }, nil + default: + return ownedTarget{}, serviceerror.NewInvalidArgument("owner kind is required") } - 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 *streampb.StreamOwner) error { +func checkOwner(owner *streamlib.StreamOwner) error { if owner.GetId() == "" { return serviceerror.NewInvalidArgument("owner id is required") } switch owner.GetKind() { - case streampb.STREAM_OWNER_KIND_WORKFLOW, streampb.STREAM_OWNER_KIND_ACTIVITY: + 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 streampb.STREAM_OWNER_KIND_WORKFLOW_ACTIVITY: + case streamlib.STREAM_OWNER_KIND_WORKFLOW_ACTIVITY: if owner.GetActivityId() == "" { return serviceerror.NewInvalidArgument( "owner activity id is required for a workflow's activity") @@ -204,8 +205,8 @@ func checkOwner(owner *streampb.StreamOwner) error { func (h *handler) CreateStream( ctx context.Context, - req *streampb.CreateStreamRequest, -) (*streampb.CreateStreamResponse, error) { + req *streamlib.CreateStreamRequest, +) (*streamlib.CreateStreamResponse, error) { in := req.GetFrontendRequest() ctx = h.withCallerInfo(ctx, req.GetNamespaceId()) if in.GetStreamId() == "" { @@ -215,7 +216,7 @@ func (h *handler) CreateStream( result, err := chasm.StartExecution( ctx, chasm.ExecutionKey{NamespaceID: req.GetNamespaceId(), BusinessID: in.GetStreamId()}, - func(mctx chasm.MutableContext, input *streampb.CreateStreamInput) (*stream.Stream, error) { + func(mctx chasm.MutableContext, input *streamlib.CreateStreamInput) (*stream.Stream, error) { return stream.NewStream(mctx, stream.NewStreamRequest{Lifecycle: input.GetLifecycle()}) }, in, @@ -223,15 +224,15 @@ func (h *handler) CreateStream( if err != nil { return nil, err } - return &streampb.CreateStreamResponse{ - FrontendResponse: &streampb.CreateStreamOutput{RunId: result.ExecutionKey.RunID}, + return &streamlib.CreateStreamResponse{ + FrontendResponse: &streamlib.CreateStreamOutput{RunId: result.ExecutionKey.RunID}, }, nil } func (h *handler) AddMessages( ctx context.Context, - req *streampb.AddMessagesRequest, -) (*streampb.AddMessagesResponse, error) { + req *streamlib.AddMessagesRequest, +) (*streamlib.AddMessagesResponse, error) { in := req.GetFrontendRequest() if len(in.GetRecords()) == 0 { return nil, serviceerror.NewInvalidArgument("no records to append") @@ -260,8 +261,8 @@ func (h *handler) AddMessages( return nil, err } - return &streampb.AddMessagesResponse{ - FrontendResponse: &streampb.AddMessagesOutput{ + return &streamlib.AddMessagesResponse{ + FrontendResponse: &streamlib.AddMessagesOutput{ FirstOffset: result.FirstOffset, NextOffset: result.NextOffset, Count: result.Count, @@ -285,8 +286,8 @@ func (h *handler) AddMessages( // execution, so neither producer needs to pin the head against the other. func (h *handler) AddWorkflowMessages( ctx context.Context, - req *streampb.AddWorkflowMessagesRequest, -) (*streampb.AddWorkflowMessagesResponse, error) { + req *streamlib.AddWorkflowMessagesRequest, +) (*streamlib.AddWorkflowMessagesResponse, error) { in := req.GetFrontendRequest() if len(in.GetRecords()) == 0 { return nil, serviceerror.NewInvalidArgument("no records to append") @@ -316,8 +317,8 @@ func (h *handler) AddWorkflowMessages( return nil, err } - return &streampb.AddWorkflowMessagesResponse{ - FrontendResponse: &streampb.AddMessagesOutput{ + return &streamlib.AddWorkflowMessagesResponse{ + FrontendResponse: &streamlib.AddMessagesOutput{ FirstOffset: result.FirstOffset, NextOffset: result.NextOffset, Count: result.Count, @@ -328,8 +329,8 @@ func (h *handler) AddWorkflowMessages( func (h *handler) FinishWriting( ctx context.Context, - req *streampb.FinishWritingRequest, -) (*streampb.FinishWritingResponse, error) { + req *streamlib.FinishWritingRequest, +) (*streamlib.FinishWritingResponse, error) { in := req.GetFrontendRequest() ctx = h.withCallerInfo(ctx, req.GetNamespaceId()) _, _, err := chasm.UpdateComponent( @@ -343,7 +344,7 @@ func (h *handler) FinishWriting( if err != nil { return nil, err } - return &streampb.FinishWritingResponse{FrontendResponse: &streampb.FinishWritingOutput{}}, nil + return &streamlib.FinishWritingResponse{FrontendResponse: &streamlib.FinishWritingOutput{}}, nil } // SubscribeWorkflow registers a workflow as a consumer of a stream it owns. @@ -355,8 +356,8 @@ func (h *handler) FinishWriting( // consuming workflow's transaction is a separate problem. func (h *handler) SubscribeWorkflow( ctx context.Context, - req *streampb.SubscribeWorkflowRequest, -) (*streampb.SubscribeWorkflowResponse, error) { + req *streamlib.SubscribeWorkflowRequest, +) (*streamlib.SubscribeWorkflowResponse, error) { in := req.GetFrontendRequest() start, err := stream.RequestedStart(in.GetStartPosition(), "start_offset", in.GetStartOffset()) if err != nil { @@ -377,7 +378,7 @@ func (h *handler) SubscribeWorkflow( ctx, workflowRef(req.GetNamespaceId(), in.GetWorkflowId(), in.GetOwnerRunId()), func( - wf *chasmworkflow.Workflow, mctx chasm.MutableContext, input *streampb.SubscribeWorkflowInput, + wf *chasmworkflow.Workflow, mctx chasm.MutableContext, input *streamlib.SubscribeWorkflowInput, ) (int64, error) { return wf.SubscribeToOwnedStream(mctx, ownedStreamName(input.GetStreamName()), start, limits) }, @@ -387,8 +388,8 @@ func (h *handler) SubscribeWorkflow( return nil, err } - return &streampb.SubscribeWorkflowResponse{ - FrontendResponse: &streampb.SubscribeWorkflowOutput{StartOffset: startOffset}, + return &streamlib.SubscribeWorkflowResponse{ + FrontendResponse: &streamlib.SubscribeWorkflowOutput{StartOffset: startOffset}, }, nil } @@ -403,9 +404,9 @@ func (h *handler) SubscribeWorkflow( func (h *handler) subscribeToExternalStream( ctx context.Context, namespaceID string, - in *streampb.SubscribeWorkflowInput, - start *apistreampb.StreamStartPosition, -) (*streampb.SubscribeWorkflowResponse, error) { + 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. @@ -427,9 +428,9 @@ func (h *handler) subscribeToExternalStream( // 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, &streampb.RegisterStreamConsumerRequest{ + registered, err := h.routed.RegisterStreamConsumer(ctx, &streamlib.RegisterStreamConsumerRequest{ NamespaceId: namespaceID, - FrontendRequest: &streampb.RegisterStreamConsumerInput{ + FrontendRequest: &streamlib.RegisterStreamConsumerInput{ Namespace: in.GetNamespace(), StreamId: in.GetStreamId(), ConsumerWorkflowId: in.GetWorkflowId(), @@ -464,8 +465,8 @@ func (h *handler) subscribeToExternalStream( return nil, err } - return &streampb.SubscribeWorkflowResponse{ - FrontendResponse: &streampb.SubscribeWorkflowOutput{StartOffset: startOffset}, + return &streamlib.SubscribeWorkflowResponse{ + FrontendResponse: &streamlib.SubscribeWorkflowOutput{StartOffset: startOffset}, }, nil } @@ -478,8 +479,8 @@ func (h *handler) subscribeToExternalStream( // facts rather than readings. func (h *handler) RegisterStreamConsumer( ctx context.Context, - req *streampb.RegisterStreamConsumerRequest, -) (*streampb.RegisterStreamConsumerResponse, error) { + 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()) @@ -492,8 +493,8 @@ func (h *handler) RegisterStreamConsumer( ctx, refFor(req.GetNamespaceId(), in.GetStreamId()), func( - s *stream.Stream, mctx chasm.MutableContext, start *apistreampb.StreamStartPosition, - ) (*streampb.RegisterStreamConsumerOutput, error) { + 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(), @@ -505,7 +506,7 @@ func (h *handler) RegisterStreamConsumer( if err != nil { return nil, err } - return &streampb.RegisterStreamConsumerOutput{ + return &streamlib.RegisterStreamConsumerOutput{ StartOffset: startOffset, KnownHead: s.State.GetHeadOffset(), }, nil @@ -518,13 +519,13 @@ func (h *handler) RegisterStreamConsumer( // 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 &streampb.RegisterStreamConsumerResponse{ - FrontendResponse: &streampb.RegisterStreamConsumerOutput{StreamAbsent: true}, + return &streamlib.RegisterStreamConsumerResponse{ + FrontendResponse: &streamlib.RegisterStreamConsumerOutput{StreamAbsent: true}, }, nil } return nil, err } - return &streampb.RegisterStreamConsumerResponse{FrontendResponse: pin}, nil + return &streamlib.RegisterStreamConsumerResponse{FrontendResponse: pin}, nil } // executionAbsent reports whether an error means no execution holds the stream. @@ -609,8 +610,8 @@ func (h *handler) pushHead( // the stream re-keys its pin, or nothing does, so the stream releases it. func (h *handler) AdvanceConsumerHead( ctx context.Context, - req *streampb.AdvanceConsumerHeadRequest, -) (*streampb.AdvanceConsumerHeadResponse, error) { + req *streamlib.AdvanceConsumerHeadRequest, +) (*streamlib.AdvanceConsumerHeadResponse, error) { in := req.GetFrontendRequest() ctx = h.withCallerInfo(ctx, req.GetNamespaceId()) namespaceID, workflowID, streamID := req.GetNamespaceId(), in.GetWorkflowId(), in.GetStreamId() @@ -619,17 +620,17 @@ func (h *handler) AdvanceConsumerHead( if err != nil { return nil, err } - out := &streampb.AdvanceConsumerHeadOutput{} + out := &streamlib.AdvanceConsumerHeadOutput{} if !pinned.closed { if !pinned.consumes { out.ConsumerClosed = true - return &streampb.AdvanceConsumerHeadResponse{FrontendResponse: out}, nil + return &streamlib.AdvanceConsumerHeadResponse{FrontendResponse: out}, nil } err := h.pushHead(ctx, namespaceID, workflowID, pinned.runID, streamID, in.GetHeadOffset()) if err != nil { return nil, err } - return &streampb.AdvanceConsumerHeadResponse{FrontendResponse: out}, nil + return &streamlib.AdvanceConsumerHeadResponse{FrontendResponse: out}, nil } // The pinned run is over. A continue-as-new carries the subscription to the @@ -640,7 +641,7 @@ func (h *handler) AdvanceConsumerHead( } if current.closed || current.runID == pinned.runID || !current.consumes { out.ConsumerClosed = true - return &streampb.AdvanceConsumerHeadResponse{FrontendResponse: out}, nil + return &streamlib.AdvanceConsumerHeadResponse{FrontendResponse: out}, nil } err = h.pushHead(ctx, namespaceID, workflowID, current.runID, streamID, in.GetHeadOffset()) if err != nil { @@ -648,13 +649,13 @@ func (h *handler) AdvanceConsumerHead( } out.SuccessorRunId = current.runID out.SuccessorStartOffset = current.startOffset - return &streampb.AdvanceConsumerHeadResponse{FrontendResponse: out}, nil + return &streamlib.AdvanceConsumerHeadResponse{FrontendResponse: out}, nil } func (h *handler) PollMessages( ctx context.Context, - req *streampb.PollMessagesRequest, -) (*streampb.PollMessagesResponse, error) { + req *streamlib.PollMessagesRequest, +) (*streamlib.PollMessagesResponse, error) { in := req.GetFrontendRequest() ctx = h.withCallerInfo(ctx, req.GetNamespaceId()) @@ -695,7 +696,7 @@ func (h *handler) PollMessages( if err != nil { return nil, err } - return &streampb.PollMessagesResponse{FrontendResponse: out}, nil + return &streamlib.PollMessagesResponse{FrontendResponse: out}, nil } // PollWorkflowMessages reads a stream an execution owns. @@ -705,8 +706,8 @@ func (h *handler) PollMessages( // component. func (h *handler) PollWorkflowMessages( ctx context.Context, - req *streampb.PollWorkflowMessagesRequest, -) (*streampb.PollWorkflowMessagesResponse, error) { + req *streamlib.PollWorkflowMessagesRequest, +) (*streamlib.PollWorkflowMessagesResponse, error) { in := req.GetFrontendRequest() ctx = h.withCallerInfo(ctx, req.GetNamespaceId()) @@ -746,7 +747,7 @@ func (h *handler) PollWorkflowMessages( if err != nil { return nil, err } - return &streampb.PollWorkflowMessagesResponse{FrontendResponse: out}, nil + return &streamlib.PollWorkflowMessagesResponse{FrontendResponse: out}, nil } // formatWindow turns a component read into the wire response. The read happens @@ -757,8 +758,8 @@ func (h *handler) PollWorkflowMessages( // 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) (*streampb.PollMessagesOutput, error) { - out := &streampb.PollMessagesOutput{ +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(), @@ -800,7 +801,7 @@ func readOwnedWindow( if s == nil { // Nothing published yet, which reads as an empty stream so a reader can // attach before the first append. - state := &streampb.StreamState{Closed: ended} + state := &streamlib.StreamState{Closed: ended} from := req.Window.From if req.Window.Start != nil { var err error @@ -826,7 +827,7 @@ func readOwnedWindow( func (h *handler) ownedStreamState( ctx context.Context, target ownedTarget, -) (*streampb.StreamState, error) { +) (*streamlib.StreamState, error) { return chasm.ReadComponent(ctx, target.ref, readOwnedStream, target.key) } @@ -836,8 +837,8 @@ func readOwnedStream( owner stream.Owner, cctx chasm.Context, key string, -) (*streampb.StreamState, error) { - state := &streampb.StreamState{} +) (*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 { @@ -858,14 +859,14 @@ func (h *handler) waitForMessages( ctx context.Context, ref chasm.ComponentRef, from int64, - current *streampb.StreamState, -) (*streampb.StreamState, error) { + current *streamlib.StreamState, +) (*streamlib.StreamState, error) { pollCtx, cancel := contextutil.WithDeadlineBuffer( ctx, stream.LongPollTimeout, stream.LongPollBuffer) defer cancel() state, _, err := chasm.PollComponent(pollCtx, ref, - func(s *stream.Stream, _ chasm.Context, offset int64) (*streampb.StreamState, bool, error) { + 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) { @@ -883,8 +884,8 @@ func (h *handler) waitForOwnedMessages( ctx context.Context, target ownedTarget, from int64, - current *streampb.StreamState, -) (*streampb.StreamState, error) { + current *streamlib.StreamState, +) (*streamlib.StreamState, error) { pollCtx, cancel := contextutil.WithDeadlineBuffer( ctx, stream.LongPollTimeout, stream.LongPollBuffer) defer cancel() @@ -892,7 +893,7 @@ func (h *handler) waitForOwnedMessages( state, _, err := chasm.PollComponent(pollCtx, target.ref, func( owner stream.Owner, cctx chasm.Context, offset int64, - ) (*streampb.StreamState, bool, error) { + ) (*streamlib.StreamState, bool, error) { owned, err := readOwnedStream(owner, cctx, target.key) if err != nil { return nil, false, err @@ -910,7 +911,7 @@ func (h *handler) waitForOwnedMessages( // 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 *apistreampb.StreamStartPosition, state *streampb.StreamState, + from int64, start *streampb.StreamStartPosition, state *streamlib.StreamState, ) (int64, error) { if start == nil { return from, nil @@ -920,7 +921,7 @@ func waitOffset( // pollSatisfied is the monotonic condition PollComponent requires: the head // only advances and closed never clears. -func pollSatisfied(state *streampb.StreamState, from int64) bool { +func pollSatisfied(state *streamlib.StreamState, from int64) bool { return state.GetHeadOffset() > from || state.GetClosed() } @@ -928,10 +929,10 @@ func pollSatisfied(state *streampb.StreamState, from int64) bool { // served. func pollOutcome( pollCtx, callerCtx context.Context, - state *streampb.StreamState, + state *streamlib.StreamState, err error, - current *streampb.StreamState, -) (*streampb.StreamState, 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 @@ -950,8 +951,8 @@ func pollOutcome( func (h *handler) DescribeStream( ctx context.Context, - req *streampb.DescribeStreamRequest, -) (*streampb.DescribeStreamResponse, error) { + req *streamlib.DescribeStreamRequest, +) (*streamlib.DescribeStreamResponse, error) { in := req.GetFrontendRequest() ctx = h.withCallerInfo(ctx, req.GetNamespaceId()) state, err := chasm.ReadComponent(ctx, @@ -959,8 +960,8 @@ func (h *handler) DescribeStream( if err != nil { return nil, err } - return &streampb.DescribeStreamResponse{ - FrontendResponse: &streampb.DescribeStreamOutput{State: state}, + return &streamlib.DescribeStreamResponse{ + FrontendResponse: &streamlib.DescribeStreamOutput{State: state}, }, nil } @@ -969,8 +970,8 @@ func (h *handler) DescribeStream( // attached stream offers no other way to find. func (h *handler) DescribeWorkflowStream( ctx context.Context, - req *streampb.DescribeWorkflowStreamRequest, -) (*streampb.DescribeWorkflowStreamResponse, error) { + req *streamlib.DescribeWorkflowStreamRequest, +) (*streamlib.DescribeWorkflowStreamResponse, error) { in := req.GetFrontendRequest() ctx = h.withCallerInfo(ctx, req.GetNamespaceId()) @@ -982,15 +983,15 @@ func (h *handler) DescribeWorkflowStream( if err != nil { return nil, err } - return &streampb.DescribeWorkflowStreamResponse{ - FrontendResponse: &streampb.DescribeStreamOutput{State: state}, + return &streamlib.DescribeWorkflowStreamResponse{ + FrontendResponse: &streamlib.DescribeStreamOutput{State: state}, }, nil } func (h *handler) CloseStream( ctx context.Context, - req *streampb.CloseStreamRequest, -) (*streampb.CloseStreamResponse, error) { + req *streamlib.CloseStreamRequest, +) (*streamlib.CloseStreamResponse, error) { in := req.GetFrontendRequest() ctx = h.withCallerInfo(ctx, req.GetNamespaceId()) _, _, err := chasm.UpdateComponent( @@ -1004,7 +1005,7 @@ func (h *handler) CloseStream( if err != nil { return nil, err } - return &streampb.CloseStreamResponse{FrontendResponse: &streampb.CloseStreamOutput{}}, nil + return &streamlib.CloseStreamResponse{FrontendResponse: &streamlib.CloseStreamOutput{}}, nil } // TruncateStream advances the readable floor. @@ -1017,8 +1018,8 @@ func (h *handler) CloseStream( // tries once more, and what survives that is a consumer that really is there. func (h *handler) TruncateStream( ctx context.Context, - req *streampb.TruncateStreamRequest, -) (*streampb.TruncateStreamResponse, error) { + req *streamlib.TruncateStreamRequest, +) (*streamlib.TruncateStreamResponse, error) { in := req.GetFrontendRequest() ctx = h.withCallerInfo(ctx, req.GetNamespaceId()) ref := refFor(req.GetNamespaceId(), in.GetStreamId()) @@ -1039,7 +1040,7 @@ func (h *handler) TruncateStream( if err != nil { return nil, err } - return &streampb.TruncateStreamResponse{FrontendResponse: &streampb.TruncateStreamOutput{}}, nil + return &streamlib.TruncateStreamResponse{FrontendResponse: &streamlib.TruncateStreamOutput{}}, nil } func (h *handler) truncate(ctx context.Context, ref chasm.ComponentRef, newBase int64) error { @@ -1059,8 +1060,8 @@ func (h *handler) truncate(ctx context.Context, ref chasm.ComponentRef, newBase func (h *handler) DeleteStream( ctx context.Context, - req *streampb.DeleteStreamRequest, -) (*streampb.DeleteStreamResponse, error) { + req *streamlib.DeleteStreamRequest, +) (*streamlib.DeleteStreamResponse, error) { in := req.GetFrontendRequest() ctx = h.withCallerInfo(ctx, req.GetNamespaceId()) key := chasm.ExecutionKey{NamespaceID: req.GetNamespaceId(), BusinessID: in.GetStreamId()} @@ -1090,5 +1091,5 @@ func (h *handler) DeleteStream( if err != nil { return nil, err } - return &streampb.DeleteStreamResponse{FrontendResponse: &streampb.DeleteStreamOutput{}}, nil + return &streamlib.DeleteStreamResponse{FrontendResponse: &streamlib.DeleteStreamOutput{}}, nil } diff --git a/tests/stream_owner_test.go b/tests/stream_owner_test.go index 9de7f32c898..574b49f9cc6 100644 --- a/tests/stream_owner_test.go +++ b/tests/stream_owner_test.go @@ -233,8 +233,9 @@ func TestStreamStandaloneActivityOwnsStreams(t *testing.T) { }) done <- result{resp.GetFrontendResponse(), err} }() - // Give the reader time to park, so the wake is what releases it. - time.Sleep(500 * time.Millisecond) + // 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 { From 8a9a458737a47d5a2c942d008821aebbf0dd26e7 Mon Sep 17 00:00:00 2001 From: Moe Dashti Date: Wed, 30 Sep 2026 02:10:41 -0700 Subject: [PATCH 11/17] Asserted the reason tokens on the stream service's refusals. The component carries the tokens; the functional tests now read them the way an SDK does, by status code and message prefix over a raw gRPC client, so a change to the prose cannot silently break the mapping. --- tests/stream_test.go | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/stream_test.go b/tests/stream_test.go index 9eae35308c3..8baa5531000 100644 --- a/tests/stream_test.go +++ b/tests/stream_test.go @@ -15,9 +15,19 @@ import ( "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 @@ -211,6 +221,19 @@ func TestStreamProducerDedup(t *testing.T) { 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) { @@ -279,6 +302,7 @@ func TestStreamCloseAndTruncate(t *testing.T) { 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}, @@ -477,6 +501,7 @@ func TestStreamCapTruncatesAndReclaims(t *testing.T) { 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) { From 49c5856b172876e8e9ae323511d0d37995b77dd5 Mon Sep 17 00:00:00 2001 From: Moe Dashti Date: Wed, 30 Sep 2026 02:13:30 -0700 Subject: [PATCH 12/17] Parked a blocking poll on a standalone stream until it is created. A reader that attaches before the producer's first write got NotFound and had to retry on its own schedule, while a reader of an owned stream could wait on the owner. The poll now rechecks on stream.createWaitRecheckInterval for its wait budget and reads the first batch when the stream appears; NotFound still comes back when the wait expires or the reference pins a run. --- chasm/lib/stream/config.go | 8 ++++ chasm/lib/stream/service/handler.go | 74 +++++++++++++++++++++++------ tests/stream_test.go | 66 +++++++++++++++++++++++++ 3 files changed, 134 insertions(+), 14 deletions(-) diff --git a/chasm/lib/stream/config.go b/chasm/lib/stream/config.go index 24601649651..630910a3932 100644 --- a/chasm/lib/stream/config.go +++ b/chasm/lib/stream/config.go @@ -176,6 +176,12 @@ terminate the workflow.`, `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.`, + ) ) // Config holds the settings as live property functions. @@ -185,6 +191,7 @@ type Config struct { // 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 @@ -204,6 +211,7 @@ func NewConfig(dc *dynamicconfig.Collection) *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), diff --git a/chasm/lib/stream/service/handler.go b/chasm/lib/stream/service/handler.go index f36413cebb3..dbd2abd6c8c 100644 --- a/chasm/lib/stream/service/handler.go +++ b/chasm/lib/stream/service/handler.go @@ -3,6 +3,7 @@ package service import ( "context" "errors" + "time" commonpb "go.temporal.io/api/common/v1" "go.temporal.io/api/serviceerror" @@ -667,7 +668,15 @@ func (h *handler) PollMessages( // 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 } @@ -678,7 +687,7 @@ func (h *handler) PollMessages( // 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(ctx, ref, waitFrom, state); err != nil { + if _, err := h.waitForMessages(pollCtx, ctx, ref, waitFrom, state); err != nil { return nil, err } from, start = waitFrom, nil @@ -728,7 +737,9 @@ func (h *handler) PollWorkflowMessages( return nil, err } if waitFrom == state.GetHeadOffset() && !state.GetClosed() { - if _, err := h.waitForOwnedMessages(ctx, target, waitFrom, state); err != nil { + pollCtx, cancel := pollBudget(ctx) + defer cancel() + if _, err := h.waitForOwnedMessages(pollCtx, ctx, target, waitFrom, state); err != nil { return nil, err } from, start = waitFrom, nil @@ -851,20 +862,59 @@ func readOwnedStream( 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( - ctx context.Context, + pollCtx, callerCtx context.Context, ref chasm.ComponentRef, from int64, current *streamlib.StreamState, ) (*streamlib.StreamState, error) { - pollCtx, cancel := contextutil.WithDeadlineBuffer( - ctx, stream.LongPollTimeout, stream.LongPollBuffer) - defer cancel() - 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 @@ -874,22 +924,18 @@ func (h *handler) waitForMessages( } return common.CloneProto(s.State), true, nil }, from) - return pollOutcome(pollCtx, ctx, state, err, current) + 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( - ctx context.Context, + pollCtx, callerCtx context.Context, target ownedTarget, from int64, current *streamlib.StreamState, ) (*streamlib.StreamState, error) { - pollCtx, cancel := contextutil.WithDeadlineBuffer( - ctx, stream.LongPollTimeout, stream.LongPollBuffer) - defer cancel() - state, _, err := chasm.PollComponent(pollCtx, target.ref, func( owner stream.Owner, cctx chasm.Context, offset int64, @@ -903,7 +949,7 @@ func (h *handler) waitForOwnedMessages( } return owned, true, nil }, from) - return pollOutcome(pollCtx, ctx, state, err, current) + return pollOutcome(pollCtx, callerCtx, state, err, current) } // waitOffset is the offset a blocking poll waits past. A first poll that names diff --git a/tests/stream_test.go b/tests/stream_test.go index 8baa5531000..112a265e8a5 100644 --- a/tests/stream_test.go +++ b/tests/stream_test.go @@ -472,6 +472,72 @@ func TestStreamLongPollReturnsImmediatelyWhenBehind(t *testing.T) { 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) From e0a62964e18132ddb409865cf923ee2300f14693 Mon Sep 17 00:00:00 2001 From: Moe Dashti Date: Wed, 30 Sep 2026 02:20:31 -0700 Subject: [PATCH 13/17] Rate-limited and metered the stream service per namespace. The poll RPCs sat on the frontend's long-poll quota and dynamic config bounded sizes, but nothing bounded how fast one namespace appends or polls and nothing counted what it moved. Three namespace rates now answer ResourceExhausted with the RPS cause, and five namespace-tagged counters give metering a unit to read. --- chasm/lib/stream/config.go | 39 ++++++++ chasm/lib/stream/service/handler.go | 111 +++++++++++++++++++-- chasm/lib/stream/service/ratelimit.go | 103 +++++++++++++++++++ chasm/lib/stream/service/ratelimit_test.go | 97 ++++++++++++++++++ common/metrics/metric_defs.go | 22 ++++ tests/stream_metering_test.go | 109 ++++++++++++++++++++ 6 files changed, 474 insertions(+), 7 deletions(-) create mode 100644 chasm/lib/stream/service/ratelimit.go create mode 100644 chasm/lib/stream/service/ratelimit_test.go create mode 100644 tests/stream_metering_test.go diff --git a/chasm/lib/stream/config.go b/chasm/lib/stream/config.go index 630910a3932..ed535d2c1c5 100644 --- a/chasm/lib/stream/config.go +++ b/chasm/lib/stream/config.go @@ -182,6 +182,37 @@ consumers holding it are still running.`, `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. @@ -204,6 +235,10 @@ type Config struct { // 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 { @@ -223,6 +258,10 @@ func NewConfig(dc *dynamicconfig.Collection) *Config { OwnedStreamMaxItems: OwnedStreamMaxItemsSetting.Get(dc), OwnedStreamsMaxBytesPerWorkflow: OwnedStreamsMaxBytesPerWorkflowSetting.Get(dc), + + AppendRecordsPerSecond: AppendRecordsPerSecondSetting.Get(dc), + AppendBytesPerSecond: AppendBytesPerSecondSetting.Get(dc), + PollsPerSecond: PollsPerSecondSetting.Get(dc), } } diff --git a/chasm/lib/stream/service/handler.go b/chasm/lib/stream/service/handler.go index dbd2abd6c8c..f8b628612f5 100644 --- a/chasm/lib/stream/service/handler.go +++ b/chasm/lib/stream/service/handler.go @@ -14,11 +14,14 @@ import ( 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 { @@ -27,7 +30,10 @@ type handler struct { 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 @@ -40,6 +46,8 @@ func newHandler( shardController shard.Controller, namespaceRegistry namespace.Registry, logger log.Logger, + metricsHandler metrics.Handler, + timeSource clock.TimeSource, config *stream.Config, routed streamlib.StreamServiceClient, ) *handler { @@ -47,20 +55,87 @@ func newHandler( shardController: shardController, namespaceRegistry: namespaceRegistry, logger: logger, + metricsHandler: metricsHandler, + timeSource: timeSource, config: config, + limiters: newNamespaceLimiters(config), routed: routed, } } -// limitsFor resolves the namespace's limits. An id the registry cannot name -// falls back to the defaults; the interceptors have already refused requests -// for namespaces that do not exist. -func (h *handler) limitsFor(namespaceID string) stream.Limits { +// 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.String()) + 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 @@ -239,6 +314,11 @@ func (h *handler) AddMessages( 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 @@ -248,7 +328,7 @@ func (h *handler) AddMessages( Records: in.GetRecords(), ProducerID: in.GetProducerId(), Sequence: in.GetSequence(), - Limits: h.limitsFor(req.GetNamespaceId()), + Limits: limits, } if in.GetUseExpectedOffset() { expected := in.GetExpectedOffset() @@ -261,6 +341,7 @@ func (h *handler) AddMessages( if err != nil { return nil, err } + h.meterAppend(ns, in.GetRecords(), result) return &streamlib.AddMessagesResponse{ FrontendResponse: &streamlib.AddMessagesOutput{ @@ -299,6 +380,11 @@ func (h *handler) AddWorkflowMessages( 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. @@ -306,7 +392,7 @@ func (h *handler) AddWorkflowMessages( Records: in.GetRecords(), ProducerID: in.GetProducerId(), Sequence: in.GetSequence(), - Limits: h.limitsFor(req.GetNamespaceId()), + Limits: limits, } result, _, err := chasm.UpdateComponent(ctx, target.ref, func( @@ -317,6 +403,7 @@ func (h *handler) AddWorkflowMessages( if err != nil { return nil, err } + h.meterAppend(ns, in.GetRecords(), result) return &streamlib.AddWorkflowMessagesResponse{ FrontendResponse: &streamlib.AddMessagesOutput{ @@ -659,6 +746,10 @@ func (h *handler) PollMessages( ) (*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() @@ -705,6 +796,7 @@ func (h *handler) PollMessages( if err != nil { return nil, err } + h.meterDelivery(ns, out) return &streamlib.PollMessagesResponse{FrontendResponse: out}, nil } @@ -724,6 +816,10 @@ func (h *handler) PollWorkflowMessages( 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) @@ -758,6 +854,7 @@ func (h *handler) PollWorkflowMessages( if err != nil { return nil, err } + h.meterDelivery(ns, out) return &streamlib.PollWorkflowMessagesResponse{FrontendResponse: out}, nil } 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/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/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()) +} From 1bc33b7b1599560e0e42c7c6813e8d7861965826 Mon Sep 17 00:00:00 2001 From: Moe Dashti Date: Wed, 30 Sep 2026 02:51:19 -0700 Subject: [PATCH 14/17] Carried STREAM_CLOSED on an append to an ended activity's stream. An activity's streams take no more records once it is terminal, which is the same refusal a sealed standalone stream gives, so it carries the same token. The functional test reads the token off a sealed stream too. --- chasm/lib/activity/streams.go | 3 +-- chasm/lib/activity/streams_test.go | 2 ++ tests/stream_test.go | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/chasm/lib/activity/streams.go b/chasm/lib/activity/streams.go index f0c32a25975..f1cf80c3e5b 100644 --- a/chasm/lib/activity/streams.go +++ b/chasm/lib/activity/streams.go @@ -1,7 +1,6 @@ package activity import ( - "go.temporal.io/api/serviceerror" "go.temporal.io/server/chasm" "go.temporal.io/server/chasm/lib/stream" ) @@ -37,7 +36,7 @@ func (a *Activity) AppendToOwnedStream( req stream.AddMessagesRequest, ) (stream.AddMessagesResult, error) { if a.isTerminal() { - return stream.AddMessagesResult{}, serviceerror.NewFailedPreconditionf( + return stream.AddMessagesResult{}, stream.Refusal(stream.ReasonStreamClosed, "activity execution closed with status %v, so its streams take no more records", InternalStatusToAPIStatus(a.GetStatus())) } diff --git a/chasm/lib/activity/streams_test.go b/chasm/lib/activity/streams_test.go index fe294c03128..7d515d21878 100644 --- a/chasm/lib/activity/streams_test.go +++ b/chasm/lib/activity/streams_test.go @@ -76,6 +76,8 @@ func TestActivityStreamsEndAtTerminalStatus(t *testing.T) { }) 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") }) diff --git a/tests/stream_test.go b/tests/stream_test.go index 112a265e8a5..1469137694c 100644 --- a/tests/stream_test.go +++ b/tests/stream_test.go @@ -310,7 +310,7 @@ func TestStreamCloseAndTruncate(t *testing.T) { require.NoError(t, err) _, err = s.add(ctx, t, id, &streamlib.AddMessagesInput{Records: streamMsgs("", "d")}) - require.Error(t, err) + 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. From b0b82fbdba3406cb2fa602c0569b324c0425f898 Mon Sep 17 00:00:00 2001 From: Moe Dashti Date: Wed, 30 Sep 2026 03:24:29 -0700 Subject: [PATCH 15/17] Told a repeated CreateStream from a policy change on an existing id. The engine's already-started error is not a service error and reached the caller as Unknown. A create that repeats the existing lifecycle now answers AlreadyExists, and one that differs is refused with STREAM_POLICY_MISMATCH, so an SDK tells an idempotent retry from a change without a describe. --- chasm/lib/stream/doc.go | 3 ++ chasm/lib/stream/reason.go | 3 +- chasm/lib/stream/service/handler.go | 33 +++++++++++++++ tests/stream_policy_test.go | 65 +++++++++++++++++++++++++++++ 4 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 tests/stream_policy_test.go 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/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/handler.go b/chasm/lib/stream/service/handler.go index f8b628612f5..d09e67534f2 100644 --- a/chasm/lib/stream/service/handler.go +++ b/chasm/lib/stream/service/handler.go @@ -298,6 +298,9 @@ func (h *handler) CreateStream( 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{ @@ -305,6 +308,36 @@ func (h *handler) CreateStream( }, 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, 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()) +} From f8117a338289968f57cdf12804c8022251d0f723 Mon Sep 17 00:00:00 2001 From: Moe Dashti Date: Wed, 30 Sep 2026 03:24:29 -0700 Subject: [PATCH 16/17] Validated the lifecycle's byte cap on the frontend and covered it end to end. A negative cap is refused before the request is routed, like a negative record cap. The functional case appends until the cap refuses, reads the cap and the held bytes back from describe, and gets the room back by truncating. --- chasm/lib/stream/service/frontend.go | 4 ++ tests/stream_lifecycle_test.go | 79 ++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+) create mode 100644 tests/stream_lifecycle_test.go diff --git a/chasm/lib/stream/service/frontend.go b/chasm/lib/stream/service/frontend.go index 16a82ea3a99..5f66e8a8045 100644 --- a/chasm/lib/stream/service/frontend.go +++ b/chasm/lib/stream/service/frontend.go @@ -148,6 +148,10 @@ func (h *FrontendHandler) checkLifecycle( 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 { 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) +} From 74b5cd4a3e12943c94cf8a97dd4b7ec72fd61d4e Mon Sep 17 00:00:00 2001 From: Moe Dashti Date: Wed, 30 Sep 2026 03:24:29 -0700 Subject: [PATCH 17/17] Ran the stream age check as a pure task on the recheck interval. The component arms the check and decides when to re-arm it; the task only supplies the cadence from stream.retentionRecheckInterval. The functional case watches a record age out of an open stream while a workflow's pin holds an equally old one on another. --- chasm/lib/stream/service/age_task.go | 44 ++++++++++ chasm/lib/stream/service/fx.go | 1 + chasm/lib/stream/service/library.go | 8 +- tests/stream_age_test.go | 118 +++++++++++++++++++++++++++ 4 files changed, 170 insertions(+), 1 deletion(-) create mode 100644 chasm/lib/stream/service/age_task.go create mode 100644 tests/stream_age_test.go 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/fx.go b/chasm/lib/stream/service/fx.go index bbbd4778537..030be46eeee 100644 --- a/chasm/lib/stream/service/fx.go +++ b/chasm/lib/stream/service/fx.go @@ -17,6 +17,7 @@ var HistoryModule = fx.Module( newHandler, newRetentionTaskHandler, newNotifyConsumersTaskHandler, + newAgeTaskHandler, newLibrary, ), fx.Invoke(func(l *library, registry *chasm.Registry) error { diff --git a/chasm/lib/stream/service/library.go b/chasm/lib/stream/service/library.go index 8abf549cbbe..825c295e126 100644 --- a/chasm/lib/stream/service/library.go +++ b/chasm/lib/stream/service/library.go @@ -22,14 +22,16 @@ type library struct { 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} + return &library{handler: h, retention: retention, notifyConsumers: notifyConsumers, age: age} } // componentOnlyLibrary registers the component without the service, which is @@ -81,6 +83,10 @@ func (l *library) Tasks() []*chasm.RegistrableTask { "streamNotifyConsumers", l.notifyConsumers, ), + chasm.NewRegistrablePureTask( + "streamAge", + l.age, + ), } } 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())) +}