diff --git a/chasm/lib/channel/channel.go b/chasm/lib/channel/channel.go index f974a894809..f94b0268e52 100644 --- a/chasm/lib/channel/channel.go +++ b/chasm/lib/channel/channel.go @@ -194,7 +194,7 @@ func (c *Channel) RetainedCount() int64 { } func (c *Channel) scheduleFanOut(mctx chasm.MutableContext) { - if c.State.FanOutPending || c.ListenerCount() == 0 { + if c.State.GetLinked() || c.State.FanOutPending || c.ListenerCount() == 0 { return } c.State.FanOutPending = true @@ -516,6 +516,8 @@ type CallbackDelivery struct { Callback *commonpb.Callback Notification *channelpb.Notification Attempt int32 + // The channel is linked to a workflow, for the metrics. + Linked bool } // CallbackDeliveryFor reads what the delivery task should post. It reports @@ -538,6 +540,7 @@ func (c *Channel) CallbackDeliveryFor( Callback: common.CloneProto(listener.GetCallback()), Notification: common.CloneProto(listener.GetInFlight()), Attempt: listener.GetAttempt(), + Linked: c.State.GetLinked(), }, true } @@ -740,7 +743,7 @@ func (c *Channel) PollNeedsTouch(now time.Time, limits Limits) bool { // scheduleIdleCheck arms the deletion check for a channel with no listeners. // One is outstanding at a time; it re-arms itself while the channel is in use. func (c *Channel) scheduleIdleCheck(mctx chasm.MutableContext, limits Limits) { - if c.State.IdleCheckPending || c.ListenerCount() > 0 { + if c.State.GetLinked() || c.State.IdleCheckPending || c.ListenerCount() > 0 { return } c.State.IdleCheckPending = true diff --git a/chasm/lib/channel/config.go b/chasm/lib/channel/config.go index 7efff2424c0..1f0c87c8865 100644 --- a/chasm/lib/channel/config.go +++ b/chasm/lib/channel/config.go @@ -24,12 +24,14 @@ const RoutedCallTimeout = 5 * time.Second // The defaults below are also what component code falls back on when it is // driven without a config, as the unit tests do. const ( - DefaultMaxListeners = 1000 - DefaultRetainedNotifications = 1000 - DefaultMaxMetadataBytes = 2048 - DefaultRetention = time.Hour - DefaultNotifyPerSecond = 10000 - DefaultMaxSubscriptionsPerWorkflow = 32 + DefaultMaxListeners = 1000 + DefaultRetainedNotifications = 1000 + DefaultMaxMetadataBytes = 2048 + DefaultRetention = time.Hour + DefaultNotifyPerSecond = 10000 + DefaultMaxSubscriptionsPerWorkflow = 32 + DefaultLinkedRetainedNotifications = 100 + DefaultMaxLinkedChannelsPerWorkflow = 32 ) var ( @@ -68,6 +70,31 @@ means no limit.`, `Most notification channels one workflow run subscribes to. A subscribe command past it fails the workflow task.`, ) + LinkedRetainedNotificationsSetting = dynamicconfig.NewNamespaceIntSetting( + "channel.linkedRetainedNotifications", + DefaultLinkedRetainedNotifications, + `Most notifications a channel linked to a workflow keeps for pollers. Held in the +workflow's own state, so smaller than an independent channel's ring.`, + ) + MaxLinkedChannelsPerWorkflowSetting = dynamicconfig.NewNamespaceIntSetting( + "channel.maxLinkedChannelsPerWorkflow", + DefaultMaxLinkedChannelsPerWorkflow, + `Most channels linked to one workflow run. A notify or a registration that would +create one past it is refused with ResourceExhausted.`, + ) + // LinkedKindEnabledSetting exists so the independent kind can be exercised + // live on a server that has the linked kind: a client that sends + // workflow_execution is routed to the independent channel of that name, as + // before the linked kind existed, and a probe lands on INDEPENDENT. + LinkedKindEnabledSetting = dynamicconfig.NewNamespaceBoolSetting( + "channel.linkedKindEnabled", + true, + `Whether the public channel calls honour workflow_execution and reach the channel +linked to that workflow. Off, they ignore it and reach the independent channel of +the name, so DescribeChannel on an untouched linked name answers NotFound. The +channels a native stream drives in its owner's state are not affected: they are +written by the server, not addressed through these calls.`, + ) ) // Limits are the per-namespace bounds a channel transition applies, resolved @@ -77,15 +104,20 @@ type Limits struct { RetainedNotifications int MaxMetadataBytes int Retention time.Duration + // The linked kind's ring and how many linked channels one run may hold. + LinkedRetainedNotifications int + MaxLinkedChannelsPerWorkflow int } // DefaultLimits is what applies when nothing is configured. func DefaultLimits() Limits { return Limits{ - MaxListeners: DefaultMaxListeners, - RetainedNotifications: DefaultRetainedNotifications, - MaxMetadataBytes: DefaultMaxMetadataBytes, - Retention: DefaultRetention, + MaxListeners: DefaultMaxListeners, + RetainedNotifications: DefaultRetainedNotifications, + MaxMetadataBytes: DefaultMaxMetadataBytes, + Retention: DefaultRetention, + LinkedRetainedNotifications: DefaultLinkedRetainedNotifications, + MaxLinkedChannelsPerWorkflow: DefaultMaxLinkedChannelsPerWorkflow, } } @@ -104,35 +136,49 @@ func (l Limits) withDefaults() Limits { if l.Retention <= 0 { l.Retention = d.Retention } + if l.LinkedRetainedNotifications <= 0 { + l.LinkedRetainedNotifications = d.LinkedRetainedNotifications + } + if l.MaxLinkedChannelsPerWorkflow <= 0 { + l.MaxLinkedChannelsPerWorkflow = d.MaxLinkedChannelsPerWorkflow + } return l } type Config struct { - MaxListeners dynamicconfig.IntPropertyFnWithNamespaceFilter - RetainedNotifications dynamicconfig.IntPropertyFnWithNamespaceFilter - MaxMetadataBytes dynamicconfig.IntPropertyFnWithNamespaceFilter - Retention dynamicconfig.DurationPropertyFnWithNamespaceFilter - NotifyPerSecond dynamicconfig.IntPropertyFnWithNamespaceFilter - MaxSubscriptionsPerWorkflow dynamicconfig.IntPropertyFnWithNamespaceFilter + MaxListeners dynamicconfig.IntPropertyFnWithNamespaceFilter + RetainedNotifications dynamicconfig.IntPropertyFnWithNamespaceFilter + MaxMetadataBytes dynamicconfig.IntPropertyFnWithNamespaceFilter + Retention dynamicconfig.DurationPropertyFnWithNamespaceFilter + NotifyPerSecond dynamicconfig.IntPropertyFnWithNamespaceFilter + MaxSubscriptionsPerWorkflow dynamicconfig.IntPropertyFnWithNamespaceFilter + LinkedRetainedNotifications dynamicconfig.IntPropertyFnWithNamespaceFilter + MaxLinkedChannelsPerWorkflow dynamicconfig.IntPropertyFnWithNamespaceFilter + LinkedKindEnabled dynamicconfig.BoolPropertyFnWithNamespaceFilter } func NewConfig(dc *dynamicconfig.Collection) *Config { return &Config{ - MaxListeners: MaxListenersSetting.Get(dc), - RetainedNotifications: RetainedNotificationsSetting.Get(dc), - MaxMetadataBytes: MaxMetadataBytesSetting.Get(dc), - Retention: RetentionSetting.Get(dc), - NotifyPerSecond: NotifyPerSecondSetting.Get(dc), - MaxSubscriptionsPerWorkflow: MaxSubscriptionsPerWorkflowSetting.Get(dc), + LinkedKindEnabled: LinkedKindEnabledSetting.Get(dc), + MaxListeners: MaxListenersSetting.Get(dc), + RetainedNotifications: RetainedNotificationsSetting.Get(dc), + MaxMetadataBytes: MaxMetadataBytesSetting.Get(dc), + Retention: RetentionSetting.Get(dc), + NotifyPerSecond: NotifyPerSecondSetting.Get(dc), + MaxSubscriptionsPerWorkflow: MaxSubscriptionsPerWorkflowSetting.Get(dc), + LinkedRetainedNotifications: LinkedRetainedNotificationsSetting.Get(dc), + MaxLinkedChannelsPerWorkflow: MaxLinkedChannelsPerWorkflowSetting.Get(dc), } } // LimitsFor resolves the namespace's limits. func (c *Config) LimitsFor(namespaceName string) Limits { return Limits{ - MaxListeners: c.MaxListeners(namespaceName), - RetainedNotifications: c.RetainedNotifications(namespaceName), - MaxMetadataBytes: c.MaxMetadataBytes(namespaceName), - Retention: c.Retention(namespaceName), + MaxListeners: c.MaxListeners(namespaceName), + RetainedNotifications: c.RetainedNotifications(namespaceName), + MaxMetadataBytes: c.MaxMetadataBytes(namespaceName), + Retention: c.Retention(namespaceName), + LinkedRetainedNotifications: c.LinkedRetainedNotifications(namespaceName), + MaxLinkedChannelsPerWorkflow: c.MaxLinkedChannelsPerWorkflow(namespaceName), } } diff --git a/chasm/lib/channel/convert.go b/chasm/lib/channel/convert.go index 2a25f6f2cd5..6e058d6f4b2 100644 --- a/chasm/lib/channel/convert.go +++ b/chasm/lib/channel/convert.go @@ -1,13 +1,17 @@ package channel import ( + commonpb "go.temporal.io/api/common/v1" + enumspb "go.temporal.io/api/enums/v1" notificationpb "go.temporal.io/api/notification/v1" channelpb "go.temporal.io/server/chasm/lib/channel/gen/channelpb/v1" "go.temporal.io/server/common" ) // The stored notification has the public message's fields under the same -// names, so converting is a field copy in either direction. +// names, so converting is a field copy in either direction. The owner of a +// linked channel is the one exception: the public message names it as an +// Execution, the stored one as the workflow run it is. func FromAPINotification(n *notificationpb.Notification) *channelpb.Notification { if n == nil { @@ -19,6 +23,7 @@ func FromAPINotification(n *notificationpb.Notification) *channelpb.Notification Position: n.GetPosition(), Counter: n.GetCounter(), Metadata: n.GetMetadata(), + LinkedTo: FromAPIExecution(n.GetLinkedTo()), } } @@ -32,6 +37,7 @@ func ToAPINotification(n *channelpb.Notification) *notificationpb.Notification { Position: n.GetPosition(), Counter: n.GetCounter(), Metadata: n.GetMetadata(), + LinkedTo: ToAPIExecution(n.GetLinkedTo()), } } @@ -46,6 +52,27 @@ func ToAPINotifications(ns []*channelpb.Notification) []*notificationpb.Notifica return out } +// ToAPIExecution names a workflow run as the public Execution. +func ToAPIExecution(e *commonpb.WorkflowExecution) *commonpb.Execution { + if e == nil { + return nil + } + return &commonpb.Execution{ + Type: enumspb.EXECUTION_TYPE_WORKFLOW, + BusinessId: e.GetWorkflowId(), + RunId: e.GetRunId(), + } +} + +// FromAPIExecution reads an Execution as the workflow run it names. The type +// is not checked here: only workflows hold linked channels. +func FromAPIExecution(e *commonpb.Execution) *commonpb.WorkflowExecution { + if e == nil { + return nil + } + return &commonpb.WorkflowExecution{WorkflowId: e.GetBusinessId(), RunId: e.GetRunId()} +} + // ToAPIListener converts a listener from describe. func ToAPIListener(l *channelpb.ChannelListenerInfo) *notificationpb.ChannelListener { out := ¬ificationpb.ChannelListener{ diff --git a/chasm/lib/channel/gen/channelpb/v1/channel_state.pb.go b/chasm/lib/channel/gen/channelpb/v1/channel_state.pb.go index 2886f33b0ee..11c8c4a1801 100644 --- a/chasm/lib/channel/gen/channelpb/v1/channel_state.pb.go +++ b/chasm/lib/channel/gen/channelpb/v1/channel_state.pb.go @@ -46,9 +46,20 @@ type ChannelState struct { // Set by the idle task just before it deletes the channel, so a notify or a // registration that arrives in between starts a new channel rather than // landing on the one being deleted. - Closed bool `protobuf:"varint,8,opt,name=closed,proto3" json:"closed,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + Closed bool `protobuf:"varint,8,opt,name=closed,proto3" json:"closed,omitempty"` + // The channel lives in a workflow's mutable state and that workflow is its + // listener. No fan-out and no idle check: a notification is handed to the + // owner in the same write, and the channel dies with the run. + Linked bool `protobuf:"varint,9,opt,name=linked,proto3" json:"linked,omitempty"` + // The latest notification the owner has not been handed on a scheduled + // event yet. Cleared when one carries it. + OwnerPending *Notification `protobuf:"bytes,10,opt,name=owner_pending,json=ownerPending,proto3" json:"owner_pending,omitempty"` + // Counter the owner's scheduled, not yet started task carries. A repeat at + // or below it folds into that task. Zero once the task starts, fails or + // times out. + OwnerScheduledCounter int64 `protobuf:"varint,11,opt,name=owner_scheduled_counter,json=ownerScheduledCounter,proto3" json:"owner_scheduled_counter,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ChannelState) Reset() { @@ -137,11 +148,32 @@ func (x *ChannelState) GetClosed() bool { return false } +func (x *ChannelState) GetLinked() bool { + if x != nil { + return x.Linked + } + return false +} + +func (x *ChannelState) GetOwnerPending() *Notification { + if x != nil { + return x.OwnerPending + } + return nil +} + +func (x *ChannelState) GetOwnerScheduledCounter() int64 { + if x != nil { + return x.OwnerScheduledCounter + } + return 0 +} + var File_temporal_server_chasm_lib_channel_proto_v1_channel_state_proto protoreflect.FileDescriptor const file_temporal_server_chasm_lib_channel_proto_v1_channel_state_proto_rawDesc = "" + "\n" + - ">temporal/server/chasm/lib/channel/proto/v1/channel_state.proto\x12*temporal.server.chasm.lib.channel.proto.v1\x1a8temporal/server/chasm/lib/channel/proto/v1/message.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\x8b\x03\n" + + ">temporal/server/chasm/lib/channel/proto/v1/channel_state.proto\x12*temporal.server.chasm.lib.channel.proto.v1\x1a8temporal/server/chasm/lib/channel/proto/v1/message.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\xba\x04\n" + "\fChannelState\x12P\n" + "\x06latest\x18\x01 \x01(\v28.temporal.server.chasm.lib.channel.proto.v1.NotificationR\x06latest\x12%\n" + "\x0eretained_first\x18\x02 \x01(\x03R\rretainedFirst\x12#\n" + @@ -150,7 +182,11 @@ const file_temporal_server_chasm_lib_channel_proto_v1_channel_state_proto_rawDes "\x12last_activity_time\x18\x05 \x01(\v2\x1a.google.protobuf.TimestampR\x10lastActivityTime\x12,\n" + "\x12idle_check_pending\x18\x06 \x01(\bR\x10idleCheckPending\x12%\n" + "\x0eaccepted_count\x18\a \x01(\x03R\racceptedCount\x12\x16\n" + - "\x06closed\x18\b \x01(\bR\x06closedBAZ?go.temporal.io/server/chasm/lib/channel/gen/channelpb;channelpbb\x06proto3" + "\x06closed\x18\b \x01(\bR\x06closed\x12\x16\n" + + "\x06linked\x18\t \x01(\bR\x06linked\x12]\n" + + "\rowner_pending\x18\n" + + " \x01(\v28.temporal.server.chasm.lib.channel.proto.v1.NotificationR\fownerPending\x126\n" + + "\x17owner_scheduled_counter\x18\v \x01(\x03R\x15ownerScheduledCounterBAZ?go.temporal.io/server/chasm/lib/channel/gen/channelpb;channelpbb\x06proto3" var ( file_temporal_server_chasm_lib_channel_proto_v1_channel_state_proto_rawDescOnce sync.Once @@ -173,11 +209,12 @@ var file_temporal_server_chasm_lib_channel_proto_v1_channel_state_proto_goTypes var file_temporal_server_chasm_lib_channel_proto_v1_channel_state_proto_depIdxs = []int32{ 1, // 0: temporal.server.chasm.lib.channel.proto.v1.ChannelState.latest:type_name -> temporal.server.chasm.lib.channel.proto.v1.Notification 2, // 1: temporal.server.chasm.lib.channel.proto.v1.ChannelState.last_activity_time:type_name -> google.protobuf.Timestamp - 2, // [2:2] is the sub-list for method output_type - 2, // [2:2] is the sub-list for method input_type - 2, // [2:2] is the sub-list for extension type_name - 2, // [2:2] is the sub-list for extension extendee - 0, // [0:2] is the sub-list for field type_name + 1, // 2: temporal.server.chasm.lib.channel.proto.v1.ChannelState.owner_pending:type_name -> temporal.server.chasm.lib.channel.proto.v1.Notification + 3, // [3:3] is the sub-list for method output_type + 3, // [3:3] is the sub-list for method input_type + 3, // [3:3] is the sub-list for extension type_name + 3, // [3:3] is the sub-list for extension extendee + 0, // [0:3] is the sub-list for field type_name } func init() { file_temporal_server_chasm_lib_channel_proto_v1_channel_state_proto_init() } diff --git a/chasm/lib/channel/gen/channelpb/v1/message.pb.go b/chasm/lib/channel/gen/channelpb/v1/message.pb.go index dd8e8b7bff7..bb43ce83f22 100644 --- a/chasm/lib/channel/gen/channelpb/v1/message.pb.go +++ b/chasm/lib/channel/gen/channelpb/v1/message.pb.go @@ -34,8 +34,11 @@ type Notification struct { // Opaque to the server: where the writer stood when it notified. Position []byte `protobuf:"bytes,2,opt,name=position,proto3" json:"position,omitempty"` // Set by the writer and expected to grow. Folding keeps the highest. - Counter int64 `protobuf:"varint,3,opt,name=counter,proto3" json:"counter,omitempty"` - Metadata map[string]*v1.Payload `protobuf:"bytes,4,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + Counter int64 `protobuf:"varint,3,opt,name=counter,proto3" json:"counter,omitempty"` + Metadata map[string]*v1.Payload `protobuf:"bytes,4,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + // The workflow a linked channel belongs to and the run that received the + // notification. Empty for an independent channel. + LinkedTo *v1.WorkflowExecution `protobuf:"bytes,5,opt,name=linked_to,json=linkedTo,proto3" json:"linked_to,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -98,6 +101,13 @@ func (x *Notification) GetMetadata() map[string]*v1.Payload { return nil } +func (x *Notification) GetLinkedTo() *v1.WorkflowExecution { + if x != nil { + return x.LinkedTo + } + return nil +} + // WorkflowListener is a workflow run that subscribed to the channel. type WorkflowListener struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -308,12 +318,13 @@ var File_temporal_server_chasm_lib_channel_proto_v1_message_proto protoreflect.F const file_temporal_server_chasm_lib_channel_proto_v1_message_proto_rawDesc = "" + "\n" + - "8temporal/server/chasm/lib/channel/proto/v1/message.proto\x12*temporal.server.chasm.lib.channel.proto.v1\x1a\x1fgoogle/protobuf/timestamp.proto\x1a$temporal/api/common/v1/message.proto\x1a%temporal/api/failure/v1/message.proto\"\xa0\x02\n" + + "8temporal/server/chasm/lib/channel/proto/v1/message.proto\x12*temporal.server.chasm.lib.channel.proto.v1\x1a\x1fgoogle/protobuf/timestamp.proto\x1a$temporal/api/common/v1/message.proto\x1a%temporal/api/failure/v1/message.proto\"\xe8\x02\n" + "\fNotification\x12\x18\n" + "\achannel\x18\x01 \x01(\tR\achannel\x12\x1a\n" + "\bposition\x18\x02 \x01(\fR\bposition\x12\x18\n" + "\acounter\x18\x03 \x01(\x03R\acounter\x12b\n" + - "\bmetadata\x18\x04 \x03(\v2F.temporal.server.chasm.lib.channel.proto.v1.Notification.MetadataEntryR\bmetadata\x1a\\\n" + + "\bmetadata\x18\x04 \x03(\v2F.temporal.server.chasm.lib.channel.proto.v1.Notification.MetadataEntryR\bmetadata\x12F\n" + + "\tlinked_to\x18\x05 \x01(\v2).temporal.api.common.v1.WorkflowExecutionR\blinkedTo\x1a\\\n" + "\rMetadataEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x125\n" + "\x05value\x18\x02 \x01(\v2\x1f.temporal.api.common.v1.PayloadR\x05value:\x028\x01\"\xc4\x01\n" + @@ -357,26 +368,28 @@ var file_temporal_server_chasm_lib_channel_proto_v1_message_proto_goTypes = []an (*WorkflowListener)(nil), // 1: temporal.server.chasm.lib.channel.proto.v1.WorkflowListener (*CallbackListener)(nil), // 2: temporal.server.chasm.lib.channel.proto.v1.CallbackListener nil, // 3: temporal.server.chasm.lib.channel.proto.v1.Notification.MetadataEntry - (*timestamppb.Timestamp)(nil), // 4: google.protobuf.Timestamp - (*v1.Callback)(nil), // 5: temporal.api.common.v1.Callback - (*v11.Failure)(nil), // 6: temporal.api.failure.v1.Failure - (*v1.Payload)(nil), // 7: temporal.api.common.v1.Payload + (*v1.WorkflowExecution)(nil), // 4: temporal.api.common.v1.WorkflowExecution + (*timestamppb.Timestamp)(nil), // 5: google.protobuf.Timestamp + (*v1.Callback)(nil), // 6: temporal.api.common.v1.Callback + (*v11.Failure)(nil), // 7: temporal.api.failure.v1.Failure + (*v1.Payload)(nil), // 8: temporal.api.common.v1.Payload } var file_temporal_server_chasm_lib_channel_proto_v1_message_proto_depIdxs = []int32{ - 3, // 0: temporal.server.chasm.lib.channel.proto.v1.Notification.metadata:type_name -> temporal.server.chasm.lib.channel.proto.v1.Notification.MetadataEntry - 4, // 1: temporal.server.chasm.lib.channel.proto.v1.WorkflowListener.registered_time:type_name -> google.protobuf.Timestamp - 5, // 2: temporal.server.chasm.lib.channel.proto.v1.CallbackListener.callback:type_name -> temporal.api.common.v1.Callback - 4, // 3: temporal.server.chasm.lib.channel.proto.v1.CallbackListener.registered_time:type_name -> google.protobuf.Timestamp - 0, // 4: temporal.server.chasm.lib.channel.proto.v1.CallbackListener.in_flight:type_name -> temporal.server.chasm.lib.channel.proto.v1.Notification - 0, // 5: temporal.server.chasm.lib.channel.proto.v1.CallbackListener.pending:type_name -> temporal.server.chasm.lib.channel.proto.v1.Notification - 4, // 6: temporal.server.chasm.lib.channel.proto.v1.CallbackListener.next_attempt_time:type_name -> google.protobuf.Timestamp - 6, // 7: temporal.server.chasm.lib.channel.proto.v1.CallbackListener.last_attempt_failure:type_name -> temporal.api.failure.v1.Failure - 7, // 8: temporal.server.chasm.lib.channel.proto.v1.Notification.MetadataEntry.value:type_name -> temporal.api.common.v1.Payload - 9, // [9:9] is the sub-list for method output_type - 9, // [9:9] is the sub-list for method input_type - 9, // [9:9] is the sub-list for extension type_name - 9, // [9:9] is the sub-list for extension extendee - 0, // [0:9] is the sub-list for field type_name + 3, // 0: temporal.server.chasm.lib.channel.proto.v1.Notification.metadata:type_name -> temporal.server.chasm.lib.channel.proto.v1.Notification.MetadataEntry + 4, // 1: temporal.server.chasm.lib.channel.proto.v1.Notification.linked_to:type_name -> temporal.api.common.v1.WorkflowExecution + 5, // 2: temporal.server.chasm.lib.channel.proto.v1.WorkflowListener.registered_time:type_name -> google.protobuf.Timestamp + 6, // 3: temporal.server.chasm.lib.channel.proto.v1.CallbackListener.callback:type_name -> temporal.api.common.v1.Callback + 5, // 4: temporal.server.chasm.lib.channel.proto.v1.CallbackListener.registered_time:type_name -> google.protobuf.Timestamp + 0, // 5: temporal.server.chasm.lib.channel.proto.v1.CallbackListener.in_flight:type_name -> temporal.server.chasm.lib.channel.proto.v1.Notification + 0, // 6: temporal.server.chasm.lib.channel.proto.v1.CallbackListener.pending:type_name -> temporal.server.chasm.lib.channel.proto.v1.Notification + 5, // 7: temporal.server.chasm.lib.channel.proto.v1.CallbackListener.next_attempt_time:type_name -> google.protobuf.Timestamp + 7, // 8: temporal.server.chasm.lib.channel.proto.v1.CallbackListener.last_attempt_failure:type_name -> temporal.api.failure.v1.Failure + 8, // 9: temporal.server.chasm.lib.channel.proto.v1.Notification.MetadataEntry.value:type_name -> temporal.api.common.v1.Payload + 10, // [10:10] is the sub-list for method output_type + 10, // [10:10] is the sub-list for method input_type + 10, // [10:10] is the sub-list for extension type_name + 10, // [10:10] is the sub-list for extension extendee + 0, // [0:10] is the sub-list for field type_name } func init() { file_temporal_server_chasm_lib_channel_proto_v1_message_proto_init() } diff --git a/chasm/lib/channel/gen/channelpb/v1/request_response.pb.go b/chasm/lib/channel/gen/channelpb/v1/request_response.pb.go index d907286eb39..d3683d5b39e 100644 --- a/chasm/lib/channel/gen/channelpb/v1/request_response.pb.go +++ b/chasm/lib/channel/gen/channelpb/v1/request_response.pb.go @@ -26,13 +26,16 @@ const ( ) type NotifyChannelInput struct { - state protoimpl.MessageState `protogen:"open.v1"` - Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"` - Notification *Notification `protobuf:"bytes,2,opt,name=notification,proto3" json:"notification,omitempty"` - Identity string `protobuf:"bytes,3,opt,name=identity,proto3" json:"identity,omitempty"` - RequestId string `protobuf:"bytes,4,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"` + Notification *Notification `protobuf:"bytes,2,opt,name=notification,proto3" json:"notification,omitempty"` + Identity string `protobuf:"bytes,3,opt,name=identity,proto3" json:"identity,omitempty"` + RequestId string `protobuf:"bytes,4,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + // Set for a channel linked to a workflow: the owner, whose run id may be + // empty for the current run. Unset for an independent channel. + WorkflowExecution *v1.WorkflowExecution `protobuf:"bytes,5,opt,name=workflow_execution,json=workflowExecution,proto3" json:"workflow_execution,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *NotifyChannelInput) Reset() { @@ -93,6 +96,13 @@ func (x *NotifyChannelInput) GetRequestId() string { return "" } +func (x *NotifyChannelInput) GetWorkflowExecution() *v1.WorkflowExecution { + if x != nil { + return x.WorkflowExecution + } + return nil +} + type NotifyChannelOutput struct { state protoimpl.MessageState `protogen:"open.v1"` // Listeners registered when the notification was accepted. @@ -235,14 +245,17 @@ func (x *NotifyChannelResponse) GetFrontendResponse() *NotifyChannelOutput { } type RegisterChannelListenerInput struct { - state protoimpl.MessageState `protogen:"open.v1"` - Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"` - Channel string `protobuf:"bytes,2,opt,name=channel,proto3" json:"channel,omitempty"` - Callback *v1.Callback `protobuf:"bytes,3,opt,name=callback,proto3" json:"callback,omitempty"` - RequestId string `protobuf:"bytes,4,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` - Identity string `protobuf:"bytes,5,opt,name=identity,proto3" json:"identity,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"` + Channel string `protobuf:"bytes,2,opt,name=channel,proto3" json:"channel,omitempty"` + Callback *v1.Callback `protobuf:"bytes,3,opt,name=callback,proto3" json:"callback,omitempty"` + RequestId string `protobuf:"bytes,4,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + Identity string `protobuf:"bytes,5,opt,name=identity,proto3" json:"identity,omitempty"` + // Set for a channel linked to a workflow: the owner, whose run id may be + // empty for the current run. Unset for an independent channel. + WorkflowExecution *v1.WorkflowExecution `protobuf:"bytes,6,opt,name=workflow_execution,json=workflowExecution,proto3" json:"workflow_execution,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *RegisterChannelListenerInput) Reset() { @@ -310,6 +323,13 @@ func (x *RegisterChannelListenerInput) GetIdentity() string { return "" } +func (x *RegisterChannelListenerInput) GetWorkflowExecution() *v1.WorkflowExecution { + if x != nil { + return x.WorkflowExecution + } + return nil +} + type RegisterChannelListenerOutput struct { state protoimpl.MessageState `protogen:"open.v1"` ListenerId string `protobuf:"bytes,1,opt,name=listener_id,json=listenerId,proto3" json:"listener_id,omitempty"` @@ -451,13 +471,16 @@ func (x *RegisterChannelListenerResponse) GetFrontendResponse() *RegisterChannel } type UnregisterChannelListenerInput struct { - state protoimpl.MessageState `protogen:"open.v1"` - Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"` - Channel string `protobuf:"bytes,2,opt,name=channel,proto3" json:"channel,omitempty"` - ListenerId string `protobuf:"bytes,3,opt,name=listener_id,json=listenerId,proto3" json:"listener_id,omitempty"` - Identity string `protobuf:"bytes,4,opt,name=identity,proto3" json:"identity,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"` + Channel string `protobuf:"bytes,2,opt,name=channel,proto3" json:"channel,omitempty"` + ListenerId string `protobuf:"bytes,3,opt,name=listener_id,json=listenerId,proto3" json:"listener_id,omitempty"` + Identity string `protobuf:"bytes,4,opt,name=identity,proto3" json:"identity,omitempty"` + // Set for a channel linked to a workflow: the owner, whose run id may be + // empty for the current run. Unset for an independent channel. + WorkflowExecution *v1.WorkflowExecution `protobuf:"bytes,5,opt,name=workflow_execution,json=workflowExecution,proto3" json:"workflow_execution,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *UnregisterChannelListenerInput) Reset() { @@ -518,6 +541,13 @@ func (x *UnregisterChannelListenerInput) GetIdentity() string { return "" } +func (x *UnregisterChannelListenerInput) GetWorkflowExecution() *v1.WorkflowExecution { + if x != nil { + return x.WorkflowExecution + } + return nil +} + type UnregisterChannelListenerOutput struct { state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields @@ -661,8 +691,11 @@ type PollChannelInput struct { Wait *durationpb.Duration `protobuf:"bytes,4,opt,name=wait,proto3" json:"wait,omitempty"` // Zero means the server's page size. MaxNotifications int32 `protobuf:"varint,5,opt,name=max_notifications,json=maxNotifications,proto3" json:"max_notifications,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Set for a channel linked to a workflow: the owner, whose run id may be + // empty for the current run. Unset for an independent channel. + WorkflowExecution *v1.WorkflowExecution `protobuf:"bytes,6,opt,name=workflow_execution,json=workflowExecution,proto3" json:"workflow_execution,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *PollChannelInput) Reset() { @@ -730,6 +763,13 @@ func (x *PollChannelInput) GetMaxNotifications() int32 { return 0 } +func (x *PollChannelInput) GetWorkflowExecution() *v1.WorkflowExecution { + if x != nil { + return x.WorkflowExecution + } + return nil +} + type PollChannelOutput struct { state protoimpl.MessageState `protogen:"open.v1"` Notifications []*Notification `protobuf:"bytes,1,rep,name=notifications,proto3" json:"notifications,omitempty"` @@ -871,11 +911,14 @@ func (x *PollChannelResponse) GetFrontendResponse() *PollChannelOutput { } type DescribeChannelInput struct { - state protoimpl.MessageState `protogen:"open.v1"` - Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"` - Channel string `protobuf:"bytes,2,opt,name=channel,proto3" json:"channel,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"` + Channel string `protobuf:"bytes,2,opt,name=channel,proto3" json:"channel,omitempty"` + // Set for a channel linked to a workflow: the owner, whose run id may be + // empty for the current run. Unset for an independent channel. + WorkflowExecution *v1.WorkflowExecution `protobuf:"bytes,3,opt,name=workflow_execution,json=workflowExecution,proto3" json:"workflow_execution,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *DescribeChannelInput) Reset() { @@ -922,6 +965,13 @@ func (x *DescribeChannelInput) GetChannel() string { return "" } +func (x *DescribeChannelInput) GetWorkflowExecution() *v1.WorkflowExecution { + if x != nil { + return x.WorkflowExecution + } + return nil +} + // ChannelListenerInfo has the fields of temporal.api.notification.v1.ChannelListener. type ChannelListenerInfo struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -1026,6 +1076,9 @@ type DescribeChannelOutput struct { Listeners []*ChannelListenerInfo `protobuf:"bytes,1,rep,name=listeners,proto3" json:"listeners,omitempty"` Latest *Notification `protobuf:"bytes,2,opt,name=latest,proto3" json:"latest,omitempty"` RetainedCount int64 `protobuf:"varint,3,opt,name=retained_count,json=retainedCount,proto3" json:"retained_count,omitempty"` + // The channel is linked to a workflow, and which one. + Linked bool `protobuf:"varint,4,opt,name=linked,proto3" json:"linked,omitempty"` + LinkedTo *v1.WorkflowExecution `protobuf:"bytes,5,opt,name=linked_to,json=linkedTo,proto3" json:"linked_to,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -1081,6 +1134,20 @@ func (x *DescribeChannelOutput) GetRetainedCount() int64 { return 0 } +func (x *DescribeChannelOutput) GetLinked() bool { + if x != nil { + return x.Linked + } + return false +} + +func (x *DescribeChannelOutput) GetLinkedTo() *v1.WorkflowExecution { + if x != nil { + return x.LinkedTo + } + return nil +} + type DescribeChannelRequest struct { state protoimpl.MessageState `protogen:"open.v1"` NamespaceId string `protobuf:"bytes,1,opt,name=namespace_id,json=namespaceId,proto3" json:"namespace_id,omitempty"` @@ -1693,27 +1760,29 @@ var File_temporal_server_chasm_lib_channel_proto_v1_request_response_proto proto const file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_rawDesc = "" + "\n" + - "Atemporal/server/chasm/lib/channel/proto/v1/request_response.proto\x12*temporal.server.chasm.lib.channel.proto.v1\x1a8temporal/server/chasm/lib/channel/proto/v1/message.proto\x1a\x1egoogle/protobuf/duration.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a$temporal/api/common/v1/message.proto\"\xcb\x01\n" + + "Atemporal/server/chasm/lib/channel/proto/v1/request_response.proto\x12*temporal.server.chasm.lib.channel.proto.v1\x1a8temporal/server/chasm/lib/channel/proto/v1/message.proto\x1a\x1egoogle/protobuf/duration.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a$temporal/api/common/v1/message.proto\"\xa5\x02\n" + "\x12NotifyChannelInput\x12\x1c\n" + "\tnamespace\x18\x01 \x01(\tR\tnamespace\x12\\\n" + "\fnotification\x18\x02 \x01(\v28.temporal.server.chasm.lib.channel.proto.v1.NotificationR\fnotification\x12\x1a\n" + "\bidentity\x18\x03 \x01(\tR\bidentity\x12\x1d\n" + "\n" + - "request_id\x18\x04 \x01(\tR\trequestId\"<\n" + + "request_id\x18\x04 \x01(\tR\trequestId\x12X\n" + + "\x12workflow_execution\x18\x05 \x01(\v2).temporal.api.common.v1.WorkflowExecutionR\x11workflowExecution\"<\n" + "\x13NotifyChannelOutput\x12%\n" + "\x0elistener_count\x18\x01 \x01(\x03R\rlistenerCount\"\xa4\x01\n" + "\x14NotifyChannelRequest\x12!\n" + "\fnamespace_id\x18\x01 \x01(\tR\vnamespaceId\x12i\n" + "\x10frontend_request\x18\x02 \x01(\v2>.temporal.server.chasm.lib.channel.proto.v1.NotifyChannelInputR\x0ffrontendRequest\"\x85\x01\n" + "\x15NotifyChannelResponse\x12l\n" + - "\x11frontend_response\x18\x01 \x01(\v2?.temporal.server.chasm.lib.channel.proto.v1.NotifyChannelOutputR\x10frontendResponse\"\xcf\x01\n" + + "\x11frontend_response\x18\x01 \x01(\v2?.temporal.server.chasm.lib.channel.proto.v1.NotifyChannelOutputR\x10frontendResponse\"\xa9\x02\n" + "\x1cRegisterChannelListenerInput\x12\x1c\n" + "\tnamespace\x18\x01 \x01(\tR\tnamespace\x12\x18\n" + "\achannel\x18\x02 \x01(\tR\achannel\x12<\n" + "\bcallback\x18\x03 \x01(\v2 .temporal.api.common.v1.CallbackR\bcallback\x12\x1d\n" + "\n" + "request_id\x18\x04 \x01(\tR\trequestId\x12\x1a\n" + - "\bidentity\x18\x05 \x01(\tR\bidentity\"@\n" + + "\bidentity\x18\x05 \x01(\tR\bidentity\x12X\n" + + "\x12workflow_execution\x18\x06 \x01(\v2).temporal.api.common.v1.WorkflowExecutionR\x11workflowExecution\"@\n" + "\x1dRegisterChannelListenerOutput\x12\x1f\n" + "\vlistener_id\x18\x01 \x01(\tR\n" + "listenerId\"\xb8\x01\n" + @@ -1721,35 +1790,38 @@ const file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_raw "\fnamespace_id\x18\x01 \x01(\tR\vnamespaceId\x12s\n" + "\x10frontend_request\x18\x02 \x01(\v2H.temporal.server.chasm.lib.channel.proto.v1.RegisterChannelListenerInputR\x0ffrontendRequest\"\x99\x01\n" + "\x1fRegisterChannelListenerResponse\x12v\n" + - "\x11frontend_response\x18\x01 \x01(\v2I.temporal.server.chasm.lib.channel.proto.v1.RegisterChannelListenerOutputR\x10frontendResponse\"\x95\x01\n" + + "\x11frontend_response\x18\x01 \x01(\v2I.temporal.server.chasm.lib.channel.proto.v1.RegisterChannelListenerOutputR\x10frontendResponse\"\xef\x01\n" + "\x1eUnregisterChannelListenerInput\x12\x1c\n" + "\tnamespace\x18\x01 \x01(\tR\tnamespace\x12\x18\n" + "\achannel\x18\x02 \x01(\tR\achannel\x12\x1f\n" + "\vlistener_id\x18\x03 \x01(\tR\n" + "listenerId\x12\x1a\n" + - "\bidentity\x18\x04 \x01(\tR\bidentity\"!\n" + + "\bidentity\x18\x04 \x01(\tR\bidentity\x12X\n" + + "\x12workflow_execution\x18\x05 \x01(\v2).temporal.api.common.v1.WorkflowExecutionR\x11workflowExecution\"!\n" + "\x1fUnregisterChannelListenerOutput\"\xbc\x01\n" + " UnregisterChannelListenerRequest\x12!\n" + "\fnamespace_id\x18\x01 \x01(\tR\vnamespaceId\x12u\n" + "\x10frontend_request\x18\x02 \x01(\v2J.temporal.server.chasm.lib.channel.proto.v1.UnregisterChannelListenerInputR\x0ffrontendRequest\"\x9d\x01\n" + "!UnregisterChannelListenerResponse\x12x\n" + - "\x11frontend_response\x18\x01 \x01(\v2K.temporal.server.chasm.lib.channel.proto.v1.UnregisterChannelListenerOutputR\x10frontendResponse\"\xcb\x01\n" + + "\x11frontend_response\x18\x01 \x01(\v2K.temporal.server.chasm.lib.channel.proto.v1.UnregisterChannelListenerOutputR\x10frontendResponse\"\xa5\x02\n" + "\x10PollChannelInput\x12\x1c\n" + "\tnamespace\x18\x01 \x01(\tR\tnamespace\x12\x18\n" + "\achannel\x18\x02 \x01(\tR\achannel\x12#\n" + "\rafter_counter\x18\x03 \x01(\x03R\fafterCounter\x12-\n" + "\x04wait\x18\x04 \x01(\v2\x19.google.protobuf.DurationR\x04wait\x12+\n" + - "\x11max_notifications\x18\x05 \x01(\x05R\x10maxNotifications\"s\n" + + "\x11max_notifications\x18\x05 \x01(\x05R\x10maxNotifications\x12X\n" + + "\x12workflow_execution\x18\x06 \x01(\v2).temporal.api.common.v1.WorkflowExecutionR\x11workflowExecution\"s\n" + "\x11PollChannelOutput\x12^\n" + "\rnotifications\x18\x01 \x03(\v28.temporal.server.chasm.lib.channel.proto.v1.NotificationR\rnotifications\"\xa0\x01\n" + "\x12PollChannelRequest\x12!\n" + "\fnamespace_id\x18\x01 \x01(\tR\vnamespaceId\x12g\n" + "\x10frontend_request\x18\x02 \x01(\v2<.temporal.server.chasm.lib.channel.proto.v1.PollChannelInputR\x0ffrontendRequest\"\x81\x01\n" + "\x13PollChannelResponse\x12j\n" + - "\x11frontend_response\x18\x01 \x01(\v2=.temporal.server.chasm.lib.channel.proto.v1.PollChannelOutputR\x10frontendResponse\"N\n" + + "\x11frontend_response\x18\x01 \x01(\v2=.temporal.server.chasm.lib.channel.proto.v1.PollChannelOutputR\x10frontendResponse\"\xa8\x01\n" + "\x14DescribeChannelInput\x12\x1c\n" + "\tnamespace\x18\x01 \x01(\tR\tnamespace\x12\x18\n" + - "\achannel\x18\x02 \x01(\tR\achannel\"\xf2\x02\n" + + "\achannel\x18\x02 \x01(\tR\achannel\x12X\n" + + "\x12workflow_execution\x18\x03 \x01(\v2).temporal.api.common.v1.WorkflowExecutionR\x11workflowExecution\"\xf2\x02\n" + "\x13ChannelListenerInfo\x12\x1f\n" + "\vlistener_id\x18\x01 \x01(\tR\n" + "listenerId\x12f\n" + @@ -1760,11 +1832,13 @@ const file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_raw "\vworkflow_id\x18\x01 \x01(\tR\n" + "workflowId\x12\x15\n" + "\x06run_id\x18\x02 \x01(\tR\x05runIdB\t\n" + - "\avariant\"\xef\x01\n" + + "\avariant\"\xcf\x02\n" + "\x15DescribeChannelOutput\x12]\n" + "\tlisteners\x18\x01 \x03(\v2?.temporal.server.chasm.lib.channel.proto.v1.ChannelListenerInfoR\tlisteners\x12P\n" + "\x06latest\x18\x02 \x01(\v28.temporal.server.chasm.lib.channel.proto.v1.NotificationR\x06latest\x12%\n" + - "\x0eretained_count\x18\x03 \x01(\x03R\rretainedCount\"\xa8\x01\n" + + "\x0eretained_count\x18\x03 \x01(\x03R\rretainedCount\x12\x16\n" + + "\x06linked\x18\x04 \x01(\bR\x06linked\x12F\n" + + "\tlinked_to\x18\x05 \x01(\v2).temporal.api.common.v1.WorkflowExecutionR\blinkedTo\"\xa8\x01\n" + "\x16DescribeChannelRequest\x12!\n" + "\fnamespace_id\x18\x01 \x01(\tR\vnamespaceId\x12k\n" + "\x10frontend_request\x18\x02 \x01(\v2@.temporal.server.chasm.lib.channel.proto.v1.DescribeChannelInputR\x0ffrontendRequest\"\x89\x01\n" + @@ -1846,41 +1920,48 @@ var file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_goTyp (*DeliverChannelNotificationResponse)(nil), // 28: temporal.server.chasm.lib.channel.proto.v1.DeliverChannelNotificationResponse (*ChannelListenerInfo_Workflow)(nil), // 29: temporal.server.chasm.lib.channel.proto.v1.ChannelListenerInfo.Workflow (*Notification)(nil), // 30: temporal.server.chasm.lib.channel.proto.v1.Notification - (*v1.Callback)(nil), // 31: temporal.api.common.v1.Callback - (*durationpb.Duration)(nil), // 32: google.protobuf.Duration - (*timestamppb.Timestamp)(nil), // 33: google.protobuf.Timestamp + (*v1.WorkflowExecution)(nil), // 31: temporal.api.common.v1.WorkflowExecution + (*v1.Callback)(nil), // 32: temporal.api.common.v1.Callback + (*durationpb.Duration)(nil), // 33: google.protobuf.Duration + (*timestamppb.Timestamp)(nil), // 34: google.protobuf.Timestamp } var file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_depIdxs = []int32{ 30, // 0: temporal.server.chasm.lib.channel.proto.v1.NotifyChannelInput.notification:type_name -> temporal.server.chasm.lib.channel.proto.v1.Notification - 0, // 1: temporal.server.chasm.lib.channel.proto.v1.NotifyChannelRequest.frontend_request:type_name -> temporal.server.chasm.lib.channel.proto.v1.NotifyChannelInput - 1, // 2: temporal.server.chasm.lib.channel.proto.v1.NotifyChannelResponse.frontend_response:type_name -> temporal.server.chasm.lib.channel.proto.v1.NotifyChannelOutput - 31, // 3: temporal.server.chasm.lib.channel.proto.v1.RegisterChannelListenerInput.callback:type_name -> temporal.api.common.v1.Callback - 4, // 4: temporal.server.chasm.lib.channel.proto.v1.RegisterChannelListenerRequest.frontend_request:type_name -> temporal.server.chasm.lib.channel.proto.v1.RegisterChannelListenerInput - 5, // 5: temporal.server.chasm.lib.channel.proto.v1.RegisterChannelListenerResponse.frontend_response:type_name -> temporal.server.chasm.lib.channel.proto.v1.RegisterChannelListenerOutput - 8, // 6: temporal.server.chasm.lib.channel.proto.v1.UnregisterChannelListenerRequest.frontend_request:type_name -> temporal.server.chasm.lib.channel.proto.v1.UnregisterChannelListenerInput - 9, // 7: temporal.server.chasm.lib.channel.proto.v1.UnregisterChannelListenerResponse.frontend_response:type_name -> temporal.server.chasm.lib.channel.proto.v1.UnregisterChannelListenerOutput - 32, // 8: temporal.server.chasm.lib.channel.proto.v1.PollChannelInput.wait:type_name -> google.protobuf.Duration - 30, // 9: temporal.server.chasm.lib.channel.proto.v1.PollChannelOutput.notifications:type_name -> temporal.server.chasm.lib.channel.proto.v1.Notification - 12, // 10: temporal.server.chasm.lib.channel.proto.v1.PollChannelRequest.frontend_request:type_name -> temporal.server.chasm.lib.channel.proto.v1.PollChannelInput - 13, // 11: temporal.server.chasm.lib.channel.proto.v1.PollChannelResponse.frontend_response:type_name -> temporal.server.chasm.lib.channel.proto.v1.PollChannelOutput - 29, // 12: temporal.server.chasm.lib.channel.proto.v1.ChannelListenerInfo.workflow:type_name -> temporal.server.chasm.lib.channel.proto.v1.ChannelListenerInfo.Workflow - 31, // 13: temporal.server.chasm.lib.channel.proto.v1.ChannelListenerInfo.callback:type_name -> temporal.api.common.v1.Callback - 33, // 14: temporal.server.chasm.lib.channel.proto.v1.ChannelListenerInfo.registered_time:type_name -> google.protobuf.Timestamp - 17, // 15: temporal.server.chasm.lib.channel.proto.v1.DescribeChannelOutput.listeners:type_name -> temporal.server.chasm.lib.channel.proto.v1.ChannelListenerInfo - 30, // 16: temporal.server.chasm.lib.channel.proto.v1.DescribeChannelOutput.latest:type_name -> temporal.server.chasm.lib.channel.proto.v1.Notification - 16, // 17: temporal.server.chasm.lib.channel.proto.v1.DescribeChannelRequest.frontend_request:type_name -> temporal.server.chasm.lib.channel.proto.v1.DescribeChannelInput - 18, // 18: temporal.server.chasm.lib.channel.proto.v1.DescribeChannelResponse.frontend_response:type_name -> temporal.server.chasm.lib.channel.proto.v1.DescribeChannelOutput - 30, // 19: temporal.server.chasm.lib.channel.proto.v1.RegisterWorkflowListenerOutput.latest:type_name -> temporal.server.chasm.lib.channel.proto.v1.Notification - 21, // 20: temporal.server.chasm.lib.channel.proto.v1.RegisterWorkflowListenerRequest.frontend_request:type_name -> temporal.server.chasm.lib.channel.proto.v1.RegisterWorkflowListenerInput - 22, // 21: temporal.server.chasm.lib.channel.proto.v1.RegisterWorkflowListenerResponse.frontend_response:type_name -> temporal.server.chasm.lib.channel.proto.v1.RegisterWorkflowListenerOutput - 30, // 22: temporal.server.chasm.lib.channel.proto.v1.DeliverChannelNotificationInput.notification:type_name -> temporal.server.chasm.lib.channel.proto.v1.Notification - 25, // 23: temporal.server.chasm.lib.channel.proto.v1.DeliverChannelNotificationRequest.frontend_request:type_name -> temporal.server.chasm.lib.channel.proto.v1.DeliverChannelNotificationInput - 26, // 24: temporal.server.chasm.lib.channel.proto.v1.DeliverChannelNotificationResponse.frontend_response:type_name -> temporal.server.chasm.lib.channel.proto.v1.DeliverChannelNotificationOutput - 25, // [25:25] is the sub-list for method output_type - 25, // [25:25] is the sub-list for method input_type - 25, // [25:25] is the sub-list for extension type_name - 25, // [25:25] is the sub-list for extension extendee - 0, // [0:25] is the sub-list for field type_name + 31, // 1: temporal.server.chasm.lib.channel.proto.v1.NotifyChannelInput.workflow_execution:type_name -> temporal.api.common.v1.WorkflowExecution + 0, // 2: temporal.server.chasm.lib.channel.proto.v1.NotifyChannelRequest.frontend_request:type_name -> temporal.server.chasm.lib.channel.proto.v1.NotifyChannelInput + 1, // 3: temporal.server.chasm.lib.channel.proto.v1.NotifyChannelResponse.frontend_response:type_name -> temporal.server.chasm.lib.channel.proto.v1.NotifyChannelOutput + 32, // 4: temporal.server.chasm.lib.channel.proto.v1.RegisterChannelListenerInput.callback:type_name -> temporal.api.common.v1.Callback + 31, // 5: temporal.server.chasm.lib.channel.proto.v1.RegisterChannelListenerInput.workflow_execution:type_name -> temporal.api.common.v1.WorkflowExecution + 4, // 6: temporal.server.chasm.lib.channel.proto.v1.RegisterChannelListenerRequest.frontend_request:type_name -> temporal.server.chasm.lib.channel.proto.v1.RegisterChannelListenerInput + 5, // 7: temporal.server.chasm.lib.channel.proto.v1.RegisterChannelListenerResponse.frontend_response:type_name -> temporal.server.chasm.lib.channel.proto.v1.RegisterChannelListenerOutput + 31, // 8: temporal.server.chasm.lib.channel.proto.v1.UnregisterChannelListenerInput.workflow_execution:type_name -> temporal.api.common.v1.WorkflowExecution + 8, // 9: temporal.server.chasm.lib.channel.proto.v1.UnregisterChannelListenerRequest.frontend_request:type_name -> temporal.server.chasm.lib.channel.proto.v1.UnregisterChannelListenerInput + 9, // 10: temporal.server.chasm.lib.channel.proto.v1.UnregisterChannelListenerResponse.frontend_response:type_name -> temporal.server.chasm.lib.channel.proto.v1.UnregisterChannelListenerOutput + 33, // 11: temporal.server.chasm.lib.channel.proto.v1.PollChannelInput.wait:type_name -> google.protobuf.Duration + 31, // 12: temporal.server.chasm.lib.channel.proto.v1.PollChannelInput.workflow_execution:type_name -> temporal.api.common.v1.WorkflowExecution + 30, // 13: temporal.server.chasm.lib.channel.proto.v1.PollChannelOutput.notifications:type_name -> temporal.server.chasm.lib.channel.proto.v1.Notification + 12, // 14: temporal.server.chasm.lib.channel.proto.v1.PollChannelRequest.frontend_request:type_name -> temporal.server.chasm.lib.channel.proto.v1.PollChannelInput + 13, // 15: temporal.server.chasm.lib.channel.proto.v1.PollChannelResponse.frontend_response:type_name -> temporal.server.chasm.lib.channel.proto.v1.PollChannelOutput + 31, // 16: temporal.server.chasm.lib.channel.proto.v1.DescribeChannelInput.workflow_execution:type_name -> temporal.api.common.v1.WorkflowExecution + 29, // 17: temporal.server.chasm.lib.channel.proto.v1.ChannelListenerInfo.workflow:type_name -> temporal.server.chasm.lib.channel.proto.v1.ChannelListenerInfo.Workflow + 32, // 18: temporal.server.chasm.lib.channel.proto.v1.ChannelListenerInfo.callback:type_name -> temporal.api.common.v1.Callback + 34, // 19: temporal.server.chasm.lib.channel.proto.v1.ChannelListenerInfo.registered_time:type_name -> google.protobuf.Timestamp + 17, // 20: temporal.server.chasm.lib.channel.proto.v1.DescribeChannelOutput.listeners:type_name -> temporal.server.chasm.lib.channel.proto.v1.ChannelListenerInfo + 30, // 21: temporal.server.chasm.lib.channel.proto.v1.DescribeChannelOutput.latest:type_name -> temporal.server.chasm.lib.channel.proto.v1.Notification + 31, // 22: temporal.server.chasm.lib.channel.proto.v1.DescribeChannelOutput.linked_to:type_name -> temporal.api.common.v1.WorkflowExecution + 16, // 23: temporal.server.chasm.lib.channel.proto.v1.DescribeChannelRequest.frontend_request:type_name -> temporal.server.chasm.lib.channel.proto.v1.DescribeChannelInput + 18, // 24: temporal.server.chasm.lib.channel.proto.v1.DescribeChannelResponse.frontend_response:type_name -> temporal.server.chasm.lib.channel.proto.v1.DescribeChannelOutput + 30, // 25: temporal.server.chasm.lib.channel.proto.v1.RegisterWorkflowListenerOutput.latest:type_name -> temporal.server.chasm.lib.channel.proto.v1.Notification + 21, // 26: temporal.server.chasm.lib.channel.proto.v1.RegisterWorkflowListenerRequest.frontend_request:type_name -> temporal.server.chasm.lib.channel.proto.v1.RegisterWorkflowListenerInput + 22, // 27: temporal.server.chasm.lib.channel.proto.v1.RegisterWorkflowListenerResponse.frontend_response:type_name -> temporal.server.chasm.lib.channel.proto.v1.RegisterWorkflowListenerOutput + 30, // 28: temporal.server.chasm.lib.channel.proto.v1.DeliverChannelNotificationInput.notification:type_name -> temporal.server.chasm.lib.channel.proto.v1.Notification + 25, // 29: temporal.server.chasm.lib.channel.proto.v1.DeliverChannelNotificationRequest.frontend_request:type_name -> temporal.server.chasm.lib.channel.proto.v1.DeliverChannelNotificationInput + 26, // 30: temporal.server.chasm.lib.channel.proto.v1.DeliverChannelNotificationResponse.frontend_response:type_name -> temporal.server.chasm.lib.channel.proto.v1.DeliverChannelNotificationOutput + 31, // [31:31] is the sub-list for method output_type + 31, // [31:31] is the sub-list for method input_type + 31, // [31:31] is the sub-list for extension type_name + 31, // [31:31] is the sub-list for extension extendee + 0, // [0:31] is the sub-list for field type_name } func init() { file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_init() } diff --git a/chasm/lib/channel/gen/channelpb/v1/service.pb.go b/chasm/lib/channel/gen/channelpb/v1/service.pb.go index db157c165da..6c668b92abe 100644 --- a/chasm/lib/channel/gen/channelpb/v1/service.pb.go +++ b/chasm/lib/channel/gen/channelpb/v1/service.pb.go @@ -27,13 +27,18 @@ var File_temporal_server_chasm_lib_channel_proto_v1_service_proto protoreflect.F const file_temporal_server_chasm_lib_channel_proto_v1_service_proto_rawDesc = "" + "\n" + - "8temporal/server/chasm/lib/channel/proto/v1/service.proto\x12*temporal.server.chasm.lib.channel.proto.v1\x1aAtemporal/server/chasm/lib/channel/proto/v1/request_response.proto\x1a0temporal/server/api/common/v1/api_category.proto\x1a.temporal/server/api/routing/v1/extension.proto2\xd6\v\n" + + "8temporal/server/chasm/lib/channel/proto/v1/service.proto\x12*temporal.server.chasm.lib.channel.proto.v1\x1aAtemporal/server/chasm/lib/channel/proto/v1/request_response.proto\x1a0temporal/server/api/common/v1/api_category.proto\x1a.temporal/server/api/routing/v1/extension.proto2\xda\x14\n" + "\x0eChannelService\x12\xc7\x01\n" + "\rNotifyChannel\x12@.temporal.server.chasm.lib.channel.proto.v1.NotifyChannelRequest\x1aA.temporal.server.chasm.lib.channel.proto.v1.NotifyChannelResponse\"1\x8a\xb5\x18\x02\b\x01\xd2\xc3\x18'\x1a%frontend_request.notification.channel\x12\xd8\x01\n" + "\x17RegisterChannelListener\x12J.temporal.server.chasm.lib.channel.proto.v1.RegisterChannelListenerRequest\x1aK.temporal.server.chasm.lib.channel.proto.v1.RegisterChannelListenerResponse\"$\x8a\xb5\x18\x02\b\x01\xd2\xc3\x18\x1a\x1a\x18frontend_request.channel\x12\xde\x01\n" + "\x19UnregisterChannelListener\x12L.temporal.server.chasm.lib.channel.proto.v1.UnregisterChannelListenerRequest\x1aM.temporal.server.chasm.lib.channel.proto.v1.UnregisterChannelListenerResponse\"$\x8a\xb5\x18\x02\b\x01\xd2\xc3\x18\x1a\x1a\x18frontend_request.channel\x12\xb4\x01\n" + "\vPollChannel\x12>.temporal.server.chasm.lib.channel.proto.v1.PollChannelRequest\x1a?.temporal.server.chasm.lib.channel.proto.v1.PollChannelResponse\"$\x8a\xb5\x18\x02\b\x02\xd2\xc3\x18\x1a\x1a\x18frontend_request.channel\x12\xc0\x01\n" + - "\x0fDescribeChannel\x12B.temporal.server.chasm.lib.channel.proto.v1.DescribeChannelRequest\x1aC.temporal.server.chasm.lib.channel.proto.v1.DescribeChannelResponse\"$\x8a\xb5\x18\x02\b\x01\xd2\xc3\x18\x1a\x1a\x18frontend_request.channel\x12\xdb\x01\n" + + "\x0fDescribeChannel\x12B.temporal.server.chasm.lib.channel.proto.v1.DescribeChannelRequest\x1aC.temporal.server.chasm.lib.channel.proto.v1.DescribeChannelResponse\"$\x8a\xb5\x18\x02\b\x01\xd2\xc3\x18\x1a\x1a\x18frontend_request.channel\x12\xd7\x01\n" + + "\x13NotifyLinkedChannel\x12@.temporal.server.chasm.lib.channel.proto.v1.NotifyChannelRequest\x1aA.temporal.server.chasm.lib.channel.proto.v1.NotifyChannelResponse\";\x8a\xb5\x18\x02\b\x01\xd2\xc3\x181\x1a/frontend_request.workflow_execution.workflow_id\x12\xf5\x01\n" + + "\x1dRegisterLinkedChannelListener\x12J.temporal.server.chasm.lib.channel.proto.v1.RegisterChannelListenerRequest\x1aK.temporal.server.chasm.lib.channel.proto.v1.RegisterChannelListenerResponse\";\x8a\xb5\x18\x02\b\x01\xd2\xc3\x181\x1a/frontend_request.workflow_execution.workflow_id\x12\xfb\x01\n" + + "\x1fUnregisterLinkedChannelListener\x12L.temporal.server.chasm.lib.channel.proto.v1.UnregisterChannelListenerRequest\x1aM.temporal.server.chasm.lib.channel.proto.v1.UnregisterChannelListenerResponse\";\x8a\xb5\x18\x02\b\x01\xd2\xc3\x181\x1a/frontend_request.workflow_execution.workflow_id\x12\xd1\x01\n" + + "\x11PollLinkedChannel\x12>.temporal.server.chasm.lib.channel.proto.v1.PollChannelRequest\x1a?.temporal.server.chasm.lib.channel.proto.v1.PollChannelResponse\";\x8a\xb5\x18\x02\b\x02\xd2\xc3\x181\x1a/frontend_request.workflow_execution.workflow_id\x12\xdd\x01\n" + + "\x15DescribeLinkedChannel\x12B.temporal.server.chasm.lib.channel.proto.v1.DescribeChannelRequest\x1aC.temporal.server.chasm.lib.channel.proto.v1.DescribeChannelResponse\";\x8a\xb5\x18\x02\b\x01\xd2\xc3\x181\x1a/frontend_request.workflow_execution.workflow_id\x12\xdb\x01\n" + "\x18RegisterWorkflowListener\x12K.temporal.server.chasm.lib.channel.proto.v1.RegisterWorkflowListenerRequest\x1aL.temporal.server.chasm.lib.channel.proto.v1.RegisterWorkflowListenerResponse\"$\x8a\xb5\x18\x02\b\x01\xd2\xc3\x18\x1a\x1a\x18frontend_request.channel\x12\xe5\x01\n" + "\x1aDeliverChannelNotification\x12M.temporal.server.chasm.lib.channel.proto.v1.DeliverChannelNotificationRequest\x1aN.temporal.server.chasm.lib.channel.proto.v1.DeliverChannelNotificationResponse\"(\x8a\xb5\x18\x02\b\x01\xd2\xc3\x18\x1e\x1a\x1cfrontend_request.workflow_idBAZ?go.temporal.io/server/chasm/lib/channel/gen/channelpb;channelpbb\x06proto3" @@ -59,17 +64,27 @@ var file_temporal_server_chasm_lib_channel_proto_v1_service_proto_depIdxs = []in 2, // 2: temporal.server.chasm.lib.channel.proto.v1.ChannelService.UnregisterChannelListener:input_type -> temporal.server.chasm.lib.channel.proto.v1.UnregisterChannelListenerRequest 3, // 3: temporal.server.chasm.lib.channel.proto.v1.ChannelService.PollChannel:input_type -> temporal.server.chasm.lib.channel.proto.v1.PollChannelRequest 4, // 4: temporal.server.chasm.lib.channel.proto.v1.ChannelService.DescribeChannel:input_type -> temporal.server.chasm.lib.channel.proto.v1.DescribeChannelRequest - 5, // 5: temporal.server.chasm.lib.channel.proto.v1.ChannelService.RegisterWorkflowListener:input_type -> temporal.server.chasm.lib.channel.proto.v1.RegisterWorkflowListenerRequest - 6, // 6: temporal.server.chasm.lib.channel.proto.v1.ChannelService.DeliverChannelNotification:input_type -> temporal.server.chasm.lib.channel.proto.v1.DeliverChannelNotificationRequest - 7, // 7: temporal.server.chasm.lib.channel.proto.v1.ChannelService.NotifyChannel:output_type -> temporal.server.chasm.lib.channel.proto.v1.NotifyChannelResponse - 8, // 8: temporal.server.chasm.lib.channel.proto.v1.ChannelService.RegisterChannelListener:output_type -> temporal.server.chasm.lib.channel.proto.v1.RegisterChannelListenerResponse - 9, // 9: temporal.server.chasm.lib.channel.proto.v1.ChannelService.UnregisterChannelListener:output_type -> temporal.server.chasm.lib.channel.proto.v1.UnregisterChannelListenerResponse - 10, // 10: temporal.server.chasm.lib.channel.proto.v1.ChannelService.PollChannel:output_type -> temporal.server.chasm.lib.channel.proto.v1.PollChannelResponse - 11, // 11: temporal.server.chasm.lib.channel.proto.v1.ChannelService.DescribeChannel:output_type -> temporal.server.chasm.lib.channel.proto.v1.DescribeChannelResponse - 12, // 12: temporal.server.chasm.lib.channel.proto.v1.ChannelService.RegisterWorkflowListener:output_type -> temporal.server.chasm.lib.channel.proto.v1.RegisterWorkflowListenerResponse - 13, // 13: temporal.server.chasm.lib.channel.proto.v1.ChannelService.DeliverChannelNotification:output_type -> temporal.server.chasm.lib.channel.proto.v1.DeliverChannelNotificationResponse - 7, // [7:14] is the sub-list for method output_type - 0, // [0:7] is the sub-list for method input_type + 0, // 5: temporal.server.chasm.lib.channel.proto.v1.ChannelService.NotifyLinkedChannel:input_type -> temporal.server.chasm.lib.channel.proto.v1.NotifyChannelRequest + 1, // 6: temporal.server.chasm.lib.channel.proto.v1.ChannelService.RegisterLinkedChannelListener:input_type -> temporal.server.chasm.lib.channel.proto.v1.RegisterChannelListenerRequest + 2, // 7: temporal.server.chasm.lib.channel.proto.v1.ChannelService.UnregisterLinkedChannelListener:input_type -> temporal.server.chasm.lib.channel.proto.v1.UnregisterChannelListenerRequest + 3, // 8: temporal.server.chasm.lib.channel.proto.v1.ChannelService.PollLinkedChannel:input_type -> temporal.server.chasm.lib.channel.proto.v1.PollChannelRequest + 4, // 9: temporal.server.chasm.lib.channel.proto.v1.ChannelService.DescribeLinkedChannel:input_type -> temporal.server.chasm.lib.channel.proto.v1.DescribeChannelRequest + 5, // 10: temporal.server.chasm.lib.channel.proto.v1.ChannelService.RegisterWorkflowListener:input_type -> temporal.server.chasm.lib.channel.proto.v1.RegisterWorkflowListenerRequest + 6, // 11: temporal.server.chasm.lib.channel.proto.v1.ChannelService.DeliverChannelNotification:input_type -> temporal.server.chasm.lib.channel.proto.v1.DeliverChannelNotificationRequest + 7, // 12: temporal.server.chasm.lib.channel.proto.v1.ChannelService.NotifyChannel:output_type -> temporal.server.chasm.lib.channel.proto.v1.NotifyChannelResponse + 8, // 13: temporal.server.chasm.lib.channel.proto.v1.ChannelService.RegisterChannelListener:output_type -> temporal.server.chasm.lib.channel.proto.v1.RegisterChannelListenerResponse + 9, // 14: temporal.server.chasm.lib.channel.proto.v1.ChannelService.UnregisterChannelListener:output_type -> temporal.server.chasm.lib.channel.proto.v1.UnregisterChannelListenerResponse + 10, // 15: temporal.server.chasm.lib.channel.proto.v1.ChannelService.PollChannel:output_type -> temporal.server.chasm.lib.channel.proto.v1.PollChannelResponse + 11, // 16: temporal.server.chasm.lib.channel.proto.v1.ChannelService.DescribeChannel:output_type -> temporal.server.chasm.lib.channel.proto.v1.DescribeChannelResponse + 7, // 17: temporal.server.chasm.lib.channel.proto.v1.ChannelService.NotifyLinkedChannel:output_type -> temporal.server.chasm.lib.channel.proto.v1.NotifyChannelResponse + 8, // 18: temporal.server.chasm.lib.channel.proto.v1.ChannelService.RegisterLinkedChannelListener:output_type -> temporal.server.chasm.lib.channel.proto.v1.RegisterChannelListenerResponse + 9, // 19: temporal.server.chasm.lib.channel.proto.v1.ChannelService.UnregisterLinkedChannelListener:output_type -> temporal.server.chasm.lib.channel.proto.v1.UnregisterChannelListenerResponse + 10, // 20: temporal.server.chasm.lib.channel.proto.v1.ChannelService.PollLinkedChannel:output_type -> temporal.server.chasm.lib.channel.proto.v1.PollChannelResponse + 11, // 21: temporal.server.chasm.lib.channel.proto.v1.ChannelService.DescribeLinkedChannel:output_type -> temporal.server.chasm.lib.channel.proto.v1.DescribeChannelResponse + 12, // 22: temporal.server.chasm.lib.channel.proto.v1.ChannelService.RegisterWorkflowListener:output_type -> temporal.server.chasm.lib.channel.proto.v1.RegisterWorkflowListenerResponse + 13, // 23: temporal.server.chasm.lib.channel.proto.v1.ChannelService.DeliverChannelNotification:output_type -> temporal.server.chasm.lib.channel.proto.v1.DeliverChannelNotificationResponse + 12, // [12:24] is the sub-list for method output_type + 0, // [0:12] 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 diff --git a/chasm/lib/channel/gen/channelpb/v1/service_client.pb.go b/chasm/lib/channel/gen/channelpb/v1/service_client.pb.go index 35982975c8d..5053adc1220 100644 --- a/chasm/lib/channel/gen/channelpb/v1/service_client.pb.go +++ b/chasm/lib/channel/gen/channelpb/v1/service_client.pb.go @@ -280,6 +280,221 @@ func (c *ChannelServiceLayeredClient) DescribeChannel( } return backoff.ThrottleRetryContextWithReturn(ctx, call, c.retryPolicy, common.IsServiceClientTransientError) } +func (c *ChannelServiceLayeredClient) callNotifyLinkedChannelNoRetry( + ctx context.Context, + request *NotifyChannelRequest, + opts ...grpc.CallOption, +) (*NotifyChannelResponse, error) { + var response *NotifyChannelResponse + 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("ChannelService.NotifyLinkedChannel"), + 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().GetWorkflowExecution().GetWorkflowId(), c.numShards) + op := func(ctx context.Context, client ChannelServiceClient) error { + var err error + ctx, cancel := context.WithTimeout(ctx, history.DefaultTimeout) + defer cancel() + response, err = client.NotifyLinkedChannel(ctx, request, opts...) + return err + } + err = c.redirector.Execute(ctx, shardID, op) + return response, err +} +func (c *ChannelServiceLayeredClient) NotifyLinkedChannel( + ctx context.Context, + request *NotifyChannelRequest, + opts ...grpc.CallOption, +) (*NotifyChannelResponse, error) { + call := func(ctx context.Context) (*NotifyChannelResponse, error) { + return c.callNotifyLinkedChannelNoRetry(ctx, request, opts...) + } + return backoff.ThrottleRetryContextWithReturn(ctx, call, c.retryPolicy, common.IsServiceClientTransientError) +} +func (c *ChannelServiceLayeredClient) callRegisterLinkedChannelListenerNoRetry( + ctx context.Context, + request *RegisterChannelListenerRequest, + opts ...grpc.CallOption, +) (*RegisterChannelListenerResponse, error) { + var response *RegisterChannelListenerResponse + 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("ChannelService.RegisterLinkedChannelListener"), + 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().GetWorkflowExecution().GetWorkflowId(), c.numShards) + op := func(ctx context.Context, client ChannelServiceClient) error { + var err error + ctx, cancel := context.WithTimeout(ctx, history.DefaultTimeout) + defer cancel() + response, err = client.RegisterLinkedChannelListener(ctx, request, opts...) + return err + } + err = c.redirector.Execute(ctx, shardID, op) + return response, err +} +func (c *ChannelServiceLayeredClient) RegisterLinkedChannelListener( + ctx context.Context, + request *RegisterChannelListenerRequest, + opts ...grpc.CallOption, +) (*RegisterChannelListenerResponse, error) { + call := func(ctx context.Context) (*RegisterChannelListenerResponse, error) { + return c.callRegisterLinkedChannelListenerNoRetry(ctx, request, opts...) + } + return backoff.ThrottleRetryContextWithReturn(ctx, call, c.retryPolicy, common.IsServiceClientTransientError) +} +func (c *ChannelServiceLayeredClient) callUnregisterLinkedChannelListenerNoRetry( + ctx context.Context, + request *UnregisterChannelListenerRequest, + opts ...grpc.CallOption, +) (*UnregisterChannelListenerResponse, error) { + var response *UnregisterChannelListenerResponse + 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("ChannelService.UnregisterLinkedChannelListener"), + 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().GetWorkflowExecution().GetWorkflowId(), c.numShards) + op := func(ctx context.Context, client ChannelServiceClient) error { + var err error + ctx, cancel := context.WithTimeout(ctx, history.DefaultTimeout) + defer cancel() + response, err = client.UnregisterLinkedChannelListener(ctx, request, opts...) + return err + } + err = c.redirector.Execute(ctx, shardID, op) + return response, err +} +func (c *ChannelServiceLayeredClient) UnregisterLinkedChannelListener( + ctx context.Context, + request *UnregisterChannelListenerRequest, + opts ...grpc.CallOption, +) (*UnregisterChannelListenerResponse, error) { + call := func(ctx context.Context) (*UnregisterChannelListenerResponse, error) { + return c.callUnregisterLinkedChannelListenerNoRetry(ctx, request, opts...) + } + return backoff.ThrottleRetryContextWithReturn(ctx, call, c.retryPolicy, common.IsServiceClientTransientError) +} +func (c *ChannelServiceLayeredClient) callPollLinkedChannelNoRetry( + ctx context.Context, + request *PollChannelRequest, + opts ...grpc.CallOption, +) (*PollChannelResponse, error) { + var response *PollChannelResponse + 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("ChannelService.PollLinkedChannel"), + 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().GetWorkflowExecution().GetWorkflowId(), c.numShards) + op := func(ctx context.Context, client ChannelServiceClient) error { + var err error + ctx, cancel := context.WithTimeout(ctx, history.DefaultTimeout) + defer cancel() + response, err = client.PollLinkedChannel(ctx, request, opts...) + return err + } + err = c.redirector.Execute(ctx, shardID, op) + return response, err +} +func (c *ChannelServiceLayeredClient) PollLinkedChannel( + ctx context.Context, + request *PollChannelRequest, + opts ...grpc.CallOption, +) (*PollChannelResponse, error) { + call := func(ctx context.Context) (*PollChannelResponse, error) { + return c.callPollLinkedChannelNoRetry(ctx, request, opts...) + } + return backoff.ThrottleRetryContextWithReturn(ctx, call, c.retryPolicy, common.IsServiceClientTransientError) +} +func (c *ChannelServiceLayeredClient) callDescribeLinkedChannelNoRetry( + ctx context.Context, + request *DescribeChannelRequest, + opts ...grpc.CallOption, +) (*DescribeChannelResponse, error) { + var response *DescribeChannelResponse + 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("ChannelService.DescribeLinkedChannel"), + 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().GetWorkflowExecution().GetWorkflowId(), c.numShards) + op := func(ctx context.Context, client ChannelServiceClient) error { + var err error + ctx, cancel := context.WithTimeout(ctx, history.DefaultTimeout) + defer cancel() + response, err = client.DescribeLinkedChannel(ctx, request, opts...) + return err + } + err = c.redirector.Execute(ctx, shardID, op) + return response, err +} +func (c *ChannelServiceLayeredClient) DescribeLinkedChannel( + ctx context.Context, + request *DescribeChannelRequest, + opts ...grpc.CallOption, +) (*DescribeChannelResponse, error) { + call := func(ctx context.Context) (*DescribeChannelResponse, error) { + return c.callDescribeLinkedChannelNoRetry(ctx, request, opts...) + } + return backoff.ThrottleRetryContextWithReturn(ctx, call, c.retryPolicy, common.IsServiceClientTransientError) +} func (c *ChannelServiceLayeredClient) callRegisterWorkflowListenerNoRetry( ctx context.Context, request *RegisterWorkflowListenerRequest, diff --git a/chasm/lib/channel/gen/channelpb/v1/service_grpc.pb.go b/chasm/lib/channel/gen/channelpb/v1/service_grpc.pb.go index e37f6203c59..9f0b12f8935 100644 --- a/chasm/lib/channel/gen/channelpb/v1/service_grpc.pb.go +++ b/chasm/lib/channel/gen/channelpb/v1/service_grpc.pb.go @@ -20,13 +20,18 @@ import ( const _ = grpc.SupportPackageIsVersion7 const ( - ChannelService_NotifyChannel_FullMethodName = "/temporal.server.chasm.lib.channel.proto.v1.ChannelService/NotifyChannel" - ChannelService_RegisterChannelListener_FullMethodName = "/temporal.server.chasm.lib.channel.proto.v1.ChannelService/RegisterChannelListener" - ChannelService_UnregisterChannelListener_FullMethodName = "/temporal.server.chasm.lib.channel.proto.v1.ChannelService/UnregisterChannelListener" - ChannelService_PollChannel_FullMethodName = "/temporal.server.chasm.lib.channel.proto.v1.ChannelService/PollChannel" - ChannelService_DescribeChannel_FullMethodName = "/temporal.server.chasm.lib.channel.proto.v1.ChannelService/DescribeChannel" - ChannelService_RegisterWorkflowListener_FullMethodName = "/temporal.server.chasm.lib.channel.proto.v1.ChannelService/RegisterWorkflowListener" - ChannelService_DeliverChannelNotification_FullMethodName = "/temporal.server.chasm.lib.channel.proto.v1.ChannelService/DeliverChannelNotification" + ChannelService_NotifyChannel_FullMethodName = "/temporal.server.chasm.lib.channel.proto.v1.ChannelService/NotifyChannel" + ChannelService_RegisterChannelListener_FullMethodName = "/temporal.server.chasm.lib.channel.proto.v1.ChannelService/RegisterChannelListener" + ChannelService_UnregisterChannelListener_FullMethodName = "/temporal.server.chasm.lib.channel.proto.v1.ChannelService/UnregisterChannelListener" + ChannelService_PollChannel_FullMethodName = "/temporal.server.chasm.lib.channel.proto.v1.ChannelService/PollChannel" + ChannelService_DescribeChannel_FullMethodName = "/temporal.server.chasm.lib.channel.proto.v1.ChannelService/DescribeChannel" + ChannelService_NotifyLinkedChannel_FullMethodName = "/temporal.server.chasm.lib.channel.proto.v1.ChannelService/NotifyLinkedChannel" + ChannelService_RegisterLinkedChannelListener_FullMethodName = "/temporal.server.chasm.lib.channel.proto.v1.ChannelService/RegisterLinkedChannelListener" + ChannelService_UnregisterLinkedChannelListener_FullMethodName = "/temporal.server.chasm.lib.channel.proto.v1.ChannelService/UnregisterLinkedChannelListener" + ChannelService_PollLinkedChannel_FullMethodName = "/temporal.server.chasm.lib.channel.proto.v1.ChannelService/PollLinkedChannel" + ChannelService_DescribeLinkedChannel_FullMethodName = "/temporal.server.chasm.lib.channel.proto.v1.ChannelService/DescribeLinkedChannel" + ChannelService_RegisterWorkflowListener_FullMethodName = "/temporal.server.chasm.lib.channel.proto.v1.ChannelService/RegisterWorkflowListener" + ChannelService_DeliverChannelNotification_FullMethodName = "/temporal.server.chasm.lib.channel.proto.v1.ChannelService/DeliverChannelNotification" ) // ChannelServiceClient is the client API for ChannelService service. @@ -38,6 +43,13 @@ type ChannelServiceClient interface { UnregisterChannelListener(ctx context.Context, in *UnregisterChannelListenerRequest, opts ...grpc.CallOption) (*UnregisterChannelListenerResponse, error) PollChannel(ctx context.Context, in *PollChannelRequest, opts ...grpc.CallOption) (*PollChannelResponse, error) DescribeChannel(ctx context.Context, in *DescribeChannelRequest, opts ...grpc.CallOption) (*DescribeChannelResponse, error) + // The linked kind. Same requests, with workflow_execution set, routed to the + // shard of the workflow that holds the channel. + NotifyLinkedChannel(ctx context.Context, in *NotifyChannelRequest, opts ...grpc.CallOption) (*NotifyChannelResponse, error) + RegisterLinkedChannelListener(ctx context.Context, in *RegisterChannelListenerRequest, opts ...grpc.CallOption) (*RegisterChannelListenerResponse, error) + UnregisterLinkedChannelListener(ctx context.Context, in *UnregisterChannelListenerRequest, opts ...grpc.CallOption) (*UnregisterChannelListenerResponse, error) + PollLinkedChannel(ctx context.Context, in *PollChannelRequest, opts ...grpc.CallOption) (*PollChannelResponse, error) + DescribeLinkedChannel(ctx context.Context, in *DescribeChannelRequest, opts ...grpc.CallOption) (*DescribeChannelResponse, error) // Internal. The completion of a Workflow Task that subscribed calls this on // the channel's shard, creating the channel if it is absent. RegisterWorkflowListener(ctx context.Context, in *RegisterWorkflowListenerRequest, opts ...grpc.CallOption) (*RegisterWorkflowListenerResponse, error) @@ -98,6 +110,51 @@ func (c *channelServiceClient) DescribeChannel(ctx context.Context, in *Describe return out, nil } +func (c *channelServiceClient) NotifyLinkedChannel(ctx context.Context, in *NotifyChannelRequest, opts ...grpc.CallOption) (*NotifyChannelResponse, error) { + out := new(NotifyChannelResponse) + err := c.cc.Invoke(ctx, ChannelService_NotifyLinkedChannel_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *channelServiceClient) RegisterLinkedChannelListener(ctx context.Context, in *RegisterChannelListenerRequest, opts ...grpc.CallOption) (*RegisterChannelListenerResponse, error) { + out := new(RegisterChannelListenerResponse) + err := c.cc.Invoke(ctx, ChannelService_RegisterLinkedChannelListener_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *channelServiceClient) UnregisterLinkedChannelListener(ctx context.Context, in *UnregisterChannelListenerRequest, opts ...grpc.CallOption) (*UnregisterChannelListenerResponse, error) { + out := new(UnregisterChannelListenerResponse) + err := c.cc.Invoke(ctx, ChannelService_UnregisterLinkedChannelListener_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *channelServiceClient) PollLinkedChannel(ctx context.Context, in *PollChannelRequest, opts ...grpc.CallOption) (*PollChannelResponse, error) { + out := new(PollChannelResponse) + err := c.cc.Invoke(ctx, ChannelService_PollLinkedChannel_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *channelServiceClient) DescribeLinkedChannel(ctx context.Context, in *DescribeChannelRequest, opts ...grpc.CallOption) (*DescribeChannelResponse, error) { + out := new(DescribeChannelResponse) + err := c.cc.Invoke(ctx, ChannelService_DescribeLinkedChannel_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *channelServiceClient) RegisterWorkflowListener(ctx context.Context, in *RegisterWorkflowListenerRequest, opts ...grpc.CallOption) (*RegisterWorkflowListenerResponse, error) { out := new(RegisterWorkflowListenerResponse) err := c.cc.Invoke(ctx, ChannelService_RegisterWorkflowListener_FullMethodName, in, out, opts...) @@ -125,6 +182,13 @@ type ChannelServiceServer interface { UnregisterChannelListener(context.Context, *UnregisterChannelListenerRequest) (*UnregisterChannelListenerResponse, error) PollChannel(context.Context, *PollChannelRequest) (*PollChannelResponse, error) DescribeChannel(context.Context, *DescribeChannelRequest) (*DescribeChannelResponse, error) + // The linked kind. Same requests, with workflow_execution set, routed to the + // shard of the workflow that holds the channel. + NotifyLinkedChannel(context.Context, *NotifyChannelRequest) (*NotifyChannelResponse, error) + RegisterLinkedChannelListener(context.Context, *RegisterChannelListenerRequest) (*RegisterChannelListenerResponse, error) + UnregisterLinkedChannelListener(context.Context, *UnregisterChannelListenerRequest) (*UnregisterChannelListenerResponse, error) + PollLinkedChannel(context.Context, *PollChannelRequest) (*PollChannelResponse, error) + DescribeLinkedChannel(context.Context, *DescribeChannelRequest) (*DescribeChannelResponse, error) // Internal. The completion of a Workflow Task that subscribed calls this on // the channel's shard, creating the channel if it is absent. RegisterWorkflowListener(context.Context, *RegisterWorkflowListenerRequest) (*RegisterWorkflowListenerResponse, error) @@ -152,6 +216,21 @@ func (UnimplementedChannelServiceServer) PollChannel(context.Context, *PollChann func (UnimplementedChannelServiceServer) DescribeChannel(context.Context, *DescribeChannelRequest) (*DescribeChannelResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method DescribeChannel not implemented") } +func (UnimplementedChannelServiceServer) NotifyLinkedChannel(context.Context, *NotifyChannelRequest) (*NotifyChannelResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method NotifyLinkedChannel not implemented") +} +func (UnimplementedChannelServiceServer) RegisterLinkedChannelListener(context.Context, *RegisterChannelListenerRequest) (*RegisterChannelListenerResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method RegisterLinkedChannelListener not implemented") +} +func (UnimplementedChannelServiceServer) UnregisterLinkedChannelListener(context.Context, *UnregisterChannelListenerRequest) (*UnregisterChannelListenerResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method UnregisterLinkedChannelListener not implemented") +} +func (UnimplementedChannelServiceServer) PollLinkedChannel(context.Context, *PollChannelRequest) (*PollChannelResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method PollLinkedChannel not implemented") +} +func (UnimplementedChannelServiceServer) DescribeLinkedChannel(context.Context, *DescribeChannelRequest) (*DescribeChannelResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method DescribeLinkedChannel not implemented") +} func (UnimplementedChannelServiceServer) RegisterWorkflowListener(context.Context, *RegisterWorkflowListenerRequest) (*RegisterWorkflowListenerResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method RegisterWorkflowListener not implemented") } @@ -261,6 +340,96 @@ func _ChannelService_DescribeChannel_Handler(srv interface{}, ctx context.Contex return interceptor(ctx, in, info, handler) } +func _ChannelService_NotifyLinkedChannel_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(NotifyChannelRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ChannelServiceServer).NotifyLinkedChannel(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ChannelService_NotifyLinkedChannel_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ChannelServiceServer).NotifyLinkedChannel(ctx, req.(*NotifyChannelRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ChannelService_RegisterLinkedChannelListener_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RegisterChannelListenerRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ChannelServiceServer).RegisterLinkedChannelListener(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ChannelService_RegisterLinkedChannelListener_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ChannelServiceServer).RegisterLinkedChannelListener(ctx, req.(*RegisterChannelListenerRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ChannelService_UnregisterLinkedChannelListener_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UnregisterChannelListenerRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ChannelServiceServer).UnregisterLinkedChannelListener(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ChannelService_UnregisterLinkedChannelListener_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ChannelServiceServer).UnregisterLinkedChannelListener(ctx, req.(*UnregisterChannelListenerRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ChannelService_PollLinkedChannel_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(PollChannelRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ChannelServiceServer).PollLinkedChannel(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ChannelService_PollLinkedChannel_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ChannelServiceServer).PollLinkedChannel(ctx, req.(*PollChannelRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ChannelService_DescribeLinkedChannel_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DescribeChannelRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ChannelServiceServer).DescribeLinkedChannel(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ChannelService_DescribeLinkedChannel_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ChannelServiceServer).DescribeLinkedChannel(ctx, req.(*DescribeChannelRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _ChannelService_RegisterWorkflowListener_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(RegisterWorkflowListenerRequest) if err := dec(in); err != nil { @@ -324,6 +493,26 @@ var ChannelService_ServiceDesc = grpc.ServiceDesc{ MethodName: "DescribeChannel", Handler: _ChannelService_DescribeChannel_Handler, }, + { + MethodName: "NotifyLinkedChannel", + Handler: _ChannelService_NotifyLinkedChannel_Handler, + }, + { + MethodName: "RegisterLinkedChannelListener", + Handler: _ChannelService_RegisterLinkedChannelListener_Handler, + }, + { + MethodName: "UnregisterLinkedChannelListener", + Handler: _ChannelService_UnregisterLinkedChannelListener_Handler, + }, + { + MethodName: "PollLinkedChannel", + Handler: _ChannelService_PollLinkedChannel_Handler, + }, + { + MethodName: "DescribeLinkedChannel", + Handler: _ChannelService_DescribeLinkedChannel_Handler, + }, { MethodName: "RegisterWorkflowListener", Handler: _ChannelService_RegisterWorkflowListener_Handler, diff --git a/chasm/lib/channel/linked.go b/chasm/lib/channel/linked.go new file mode 100644 index 00000000000..5f354d5d9dc --- /dev/null +++ b/chasm/lib/channel/linked.go @@ -0,0 +1,154 @@ +package channel + +import ( + commonpb "go.temporal.io/api/common/v1" + "go.temporal.io/server/chasm" + channelpb "go.temporal.io/server/chasm/lib/channel/gen/channelpb/v1" + "go.temporal.io/server/common" +) + +// A linked channel lives in the mutable state of one workflow, which is its +// listener by construction: no subscription, no event and no registration +// race. A notification reaches the owner in the write that accepts it and +// rides the owner's next scheduled event, with linked_to naming the owner. +// Callback listeners and pollers attach to a linked channel as they do to an +// independent one. The channel dies with the run and nothing of it reaches a +// successor: a continue-as-new successor starts with no linked channels, so +// callbacks register again and pollers start over on an empty ring. + +// NewLinkedChannel makes an empty channel linked to the workflow that holds +// it. +func NewLinkedChannel(mctx chasm.MutableContext) *Channel { + c := NewChannel(mctx) + c.State.Linked = true + return c +} + +// Linked reports whether the channel lives in a workflow's mutable state. +func (c *Channel) Linked() bool { + return c.State.GetLinked() +} + +// LinkedNotifyResult is what a notify on a linked channel reports. +type LinkedNotifyResult struct { + // The owner and the callback listeners. + ListenerCount int + // The notification raised the latest counter and joined the ring. + Advanced bool + // The owner already held it, pending or on a task that has not started, + // so nothing changed for the owner. + OwnerHeld bool + // It replaced a pending notification the owner had not been handed yet. + OwnerFolded bool + CallbackStarted int + CallbackFolded int +} + +// NotifyLinked accepts a notification on a linked channel. A counter above +// the latest joins the ring. Any counter reaches the owner and the callback +// listeners, each folding it against what it holds, since a repeat after the +// owner's task ran is a new reason to run. The owner's part is the pending +// entry here, on its own execution, which the transaction close turns into a +// Workflow Task. +func (c *Channel) NotifyLinked( + mctx chasm.MutableContext, + n *channelpb.Notification, + limits Limits, +) (LinkedNotifyResult, error) { + limits = limits.withDefaults() + if err := CheckNotification(n, limits.MaxMetadataBytes); err != nil { + return LinkedNotifyResult{}, err + } + n = common.CloneProto(n) + n.LinkedTo = c.LinkedTo(mctx) + out := LinkedNotifyResult{ListenerCount: c.LinkedListenerCount()} + if c.Advances(n.GetCounter()) { + c.retain(mctx, n, limits.LinkedRetainedNotifications) + c.State.AcceptedCount++ + c.State.Latest = n + out.Advanced = true + } + c.touch(mctx) + if c.OwnerHolds(n.GetCounter()) { + out.OwnerHeld = true + } else { + out.OwnerFolded = c.State.GetOwnerPending() != nil + c.State.OwnerPending = n + } + started, folded, err := c.HandToCallbacks(mctx, n) + if err != nil { + return LinkedNotifyResult{}, err + } + out.CallbackStarted, out.CallbackFolded = started, folded + return out, nil +} + +// LinkedTo names the owner: the workflow the channel lives in and the run +// that holds it. +func (c *Channel) LinkedTo(ctx chasm.Context) *commonpb.WorkflowExecution { + key := ctx.ExecutionKey() + return &commonpb.WorkflowExecution{WorkflowId: key.BusinessID, RunId: key.RunID} +} + +// LinkedListenerCount counts the owner and the callback listeners. +func (c *Channel) LinkedListenerCount() int { + return 1 + len(c.CallbackListeners) +} + +// OwnerHolds reports whether the owner already has a notification at this +// counter or above: pending for its next scheduled event, or on the scheduled +// event of a task that has not started. A started task does not count: it +// may have read its source before the write the repeat stands for. +func (c *Channel) OwnerHolds(counter int64) bool { + if counter <= c.State.GetOwnerScheduledCounter() { + return true + } + pending := c.State.GetOwnerPending() + return pending != nil && counter <= pending.GetCounter() +} + +// HasOwnerPending reports whether a notification waits for the owner's next +// scheduled event. +func (c *Channel) HasOwnerPending() bool { + return c.State.GetOwnerPending() != nil +} + +// TakeOwnerPending hands the pending notification to a scheduled event being +// written and clears it. The channel remembers the counter until the task +// starts, so a repeat in that window folds into the task. +func (c *Channel) TakeOwnerPending() *channelpb.Notification { + n := c.State.GetOwnerPending() + if n == nil { + return nil + } + c.State.OwnerPending = nil + c.State.OwnerScheduledCounter = n.GetCounter() + return n +} + +// HasOwnerScheduledCounter reports whether the channel remembers what the +// owner's scheduled task carries. +func (c *Channel) HasOwnerScheduledCounter() bool { + return c.State.GetOwnerScheduledCounter() > 0 +} + +// ClearOwnerScheduledCounter forgets what the scheduled task carries, once +// that task starts, fails or times out. +func (c *Channel) ClearOwnerScheduledCounter() { + c.State.OwnerScheduledCounter = 0 +} + +// OwnerListenerInfo describes the owner as the linked channel's workflow +// listener, for describe. +func (c *Channel) OwnerListenerInfo(ctx chasm.Context) *channelpb.ChannelListenerInfo { + owner := c.LinkedTo(ctx) + return &channelpb.ChannelListenerInfo{ + ListenerId: owner.GetWorkflowId(), + Variant: &channelpb.ChannelListenerInfo_Workflow_{ + Workflow: &channelpb.ChannelListenerInfo_Workflow{ + WorkflowId: owner.GetWorkflowId(), + RunId: owner.GetRunId(), + }, + }, + } +} diff --git a/chasm/lib/channel/linked_test.go b/chasm/lib/channel/linked_test.go new file mode 100644 index 00000000000..0a6422e1486 --- /dev/null +++ b/chasm/lib/channel/linked_test.go @@ -0,0 +1,143 @@ +package channel + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + "go.temporal.io/server/chasm" + channelpb "go.temporal.io/server/chasm/lib/channel/gen/channelpb/v1" +) + +func newTestLinkedChannel(t *testing.T) (*Channel, *chasm.MockMutableContext) { + t.Helper() + now := time.Unix(1_700_000_000, 0).UTC() + mctx := &chasm.MockMutableContext{MockContext: chasm.MockContext{ + HandleNow: func(chasm.Component) time.Time { return now }, + HandleExecutionKey: func() chasm.ExecutionKey { + return chasm.ExecutionKey{NamespaceID: "ns", BusinessID: "owner", RunID: "run-1"} + }, + }} + return NewLinkedChannel(mctx), mctx +} + +// A notify on a linked channel names the owner, joins the ring, and waits for +// the owner's next scheduled event. No fan-out and no idle check: the owner +// is reached in this write, and the channel dies with the run. +func TestLinkedNotifyReachesTheOwnerInOneWrite(t *testing.T) { + c, mctx := newTestLinkedChannel(t) + require.True(t, c.Linked()) + + result, err := c.NotifyLinked(mctx, note(1), Limits{}) + require.NoError(t, err) + require.Equal(t, 1, result.ListenerCount, "the owner") + require.True(t, result.Advanced) + require.False(t, result.OwnerHeld) + require.False(t, result.OwnerFolded) + require.Empty(t, mctx.Tasks, "nothing to fan out to and nothing to expire") + + require.True(t, c.HasOwnerPending()) + pending := c.State.GetOwnerPending() + require.Equal(t, "owner", pending.GetLinkedTo().GetWorkflowId()) + require.Equal(t, "run-1", pending.GetLinkedTo().GetRunId()) + polled, err := c.Poll(mctx, PollRequest{}) + require.NoError(t, err) + require.Equal(t, []int64{1}, counters(polled)) + require.Equal(t, "owner", polled[0].GetLinkedTo().GetWorkflowId()) +} + +// The owner folds like a subscribed listener: a repeat at or below what it +// holds pending, or what its unstarted task carries, changes nothing; a +// higher counter replaces the pending entry; and once the task has started +// the same counter is news again. +func TestLinkedOwnerFoldRules(t *testing.T) { + c, mctx := newTestLinkedChannel(t) + _, err := c.NotifyLinked(mctx, note(2), Limits{}) + require.NoError(t, err) + + result, err := c.NotifyLinked(mctx, note(1), Limits{}) + require.NoError(t, err) + require.False(t, result.Advanced, "below the latest, so not retained") + require.True(t, result.OwnerHeld) + require.Equal(t, int64(2), c.State.GetOwnerPending().GetCounter()) + + result, err = c.NotifyLinked(mctx, note(3), Limits{}) + require.NoError(t, err) + require.True(t, result.Advanced) + require.True(t, result.OwnerFolded, "replaced the pending 2") + require.Equal(t, int64(3), c.State.GetOwnerPending().GetCounter()) + + taken := c.TakeOwnerPending() + require.Equal(t, int64(3), taken.GetCounter()) + require.False(t, c.HasOwnerPending()) + require.Nil(t, c.TakeOwnerPending()) + require.True(t, c.HasOwnerScheduledCounter()) + require.True(t, c.OwnerHolds(3), "the unstarted task carries it") + require.True(t, c.OwnerHolds(2)) + require.False(t, c.OwnerHolds(4)) + + result, err = c.NotifyLinked(mctx, note(3), Limits{}) + require.NoError(t, err) + require.True(t, result.OwnerHeld) + require.False(t, c.HasOwnerPending()) + + c.ClearOwnerScheduledCounter() + require.False(t, c.HasOwnerScheduledCounter()) + result, err = c.NotifyLinked(mctx, note(3), Limits{}) + require.NoError(t, err) + require.False(t, result.Advanced) + require.False(t, result.OwnerHeld, "the task started, so the repeat is a new reason to run") + require.True(t, c.HasOwnerPending()) + require.Equal(t, int64(2), c.RetainedCount(), "2 and 3 joined the ring, the repeats did not") +} + +// The linked ring has its own, smaller bound. +func TestLinkedRingBound(t *testing.T) { + c, mctx := newTestLinkedChannel(t) + for counter := int64(1); counter <= 5; counter++ { + _, err := c.NotifyLinked(mctx, note(counter), Limits{LinkedRetainedNotifications: 2}) + require.NoError(t, err) + } + require.Equal(t, int64(2), c.RetainedCount()) + polled, err := c.Poll(mctx, PollRequest{}) + require.NoError(t, err) + require.Equal(t, []int64{4, 5}, counters(polled)) +} + +// Callback listeners on a linked channel are handed each notification in the +// notify itself, folding while busy as on an independent channel, and a new +// one is posted the latest. They count as listeners beside the owner, and +// dropping the last one arms no idle check. +func TestLinkedCallbackListeners(t *testing.T) { + c, mctx := newTestLinkedChannel(t) + id, err := c.RegisterCallbackListener(mctx, CallbackRegistration{RequestID: "r1", Callback: testCallback()}) + require.NoError(t, err) + require.Empty(t, tasksOf[*channelpb.ChannelCallbackTask](mctx), "nothing to hand over yet") + + result, err := c.NotifyLinked(mctx, note(1), Limits{}) + require.NoError(t, err) + require.Equal(t, 2, result.ListenerCount) + require.Equal(t, 1, result.CallbackStarted) + require.Len(t, tasksOf[*channelpb.ChannelCallbackTask](mctx), 1) + require.Empty(t, tasksOf[*channelpb.ChannelFanOutTask](mctx)) + delivery, ok := c.CallbackDeliveryFor(mctx, &channelpb.ChannelCallbackTask{ListenerId: id, Sequence: 1}) + require.True(t, ok) + require.True(t, delivery.Linked) + require.Equal(t, "owner", delivery.Notification.GetLinkedTo().GetWorkflowId()) + + result, err = c.NotifyLinked(mctx, note(2), Limits{}) + require.NoError(t, err) + require.Equal(t, 0, result.CallbackStarted, "busy, so it folds into pending") + require.Equal(t, int64(2), c.CallbackListeners[id].Get(mctx).GetPending().GetCounter()) + + late, err := c.RegisterCallbackListener(mctx, CallbackRegistration{RequestID: "r2", Callback: testCallback()}) + require.NoError(t, err) + require.Equal(t, int64(2), c.CallbackListeners[late].Get(mctx).GetInFlight().GetCounter(), + "handed the latest on registration") + require.Equal(t, 3, c.LinkedListenerCount()) + + require.NoError(t, c.UnregisterListener(mctx, id, Limits{})) + require.NoError(t, c.UnregisterListener(mctx, late, Limits{})) + require.Equal(t, 1, c.LinkedListenerCount()) + require.Empty(t, tasksOf[*channelpb.ChannelIdleTask](mctx)) +} diff --git a/chasm/lib/channel/proto/v1/channel_state.proto b/chasm/lib/channel/proto/v1/channel_state.proto index 273205f52b5..f170968022b 100644 --- a/chasm/lib/channel/proto/v1/channel_state.proto +++ b/chasm/lib/channel/proto/v1/channel_state.proto @@ -34,4 +34,16 @@ message ChannelState { // registration that arrives in between starts a new channel rather than // landing on the one being deleted. bool closed = 8; + + // The channel lives in a workflow's mutable state and that workflow is its + // listener. No fan-out and no idle check: a notification is handed to the + // owner in the same write, and the channel dies with the run. + bool linked = 9; + // The latest notification the owner has not been handed on a scheduled + // event yet. Cleared when one carries it. + Notification owner_pending = 10; + // Counter the owner's scheduled, not yet started task carries. A repeat at + // or below it folds into that task. Zero once the task starts, fails or + // times out. + int64 owner_scheduled_counter = 11; } diff --git a/chasm/lib/channel/proto/v1/message.proto b/chasm/lib/channel/proto/v1/message.proto index 8b95dc1b8e3..edfda374254 100644 --- a/chasm/lib/channel/proto/v1/message.proto +++ b/chasm/lib/channel/proto/v1/message.proto @@ -18,6 +18,9 @@ message Notification { // Set by the writer and expected to grow. Folding keeps the highest. int64 counter = 3; map metadata = 4; + // The workflow a linked channel belongs to and the run that received the + // notification. Empty for an independent channel. + temporal.api.common.v1.WorkflowExecution linked_to = 5; } // WorkflowListener is a workflow run that subscribed to the channel. diff --git a/chasm/lib/channel/proto/v1/request_response.proto b/chasm/lib/channel/proto/v1/request_response.proto index 445446283ed..ba0751b9494 100644 --- a/chasm/lib/channel/proto/v1/request_response.proto +++ b/chasm/lib/channel/proto/v1/request_response.proto @@ -19,6 +19,9 @@ message NotifyChannelInput { Notification notification = 2; string identity = 3; string request_id = 4; + // Set for a channel linked to a workflow: the owner, whose run id may be + // empty for the current run. Unset for an independent channel. + temporal.api.common.v1.WorkflowExecution workflow_execution = 5; } message NotifyChannelOutput { @@ -41,6 +44,9 @@ message RegisterChannelListenerInput { temporal.api.common.v1.Callback callback = 3; string request_id = 4; string identity = 5; + // Set for a channel linked to a workflow: the owner, whose run id may be + // empty for the current run. Unset for an independent channel. + temporal.api.common.v1.WorkflowExecution workflow_execution = 6; } message RegisterChannelListenerOutput { @@ -61,6 +67,9 @@ message UnregisterChannelListenerInput { string channel = 2; string listener_id = 3; string identity = 4; + // Set for a channel linked to a workflow: the owner, whose run id may be + // empty for the current run. Unset for an independent channel. + temporal.api.common.v1.WorkflowExecution workflow_execution = 5; } message UnregisterChannelListenerOutput {} @@ -84,6 +93,9 @@ message PollChannelInput { google.protobuf.Duration wait = 4; // Zero means the server's page size. int32 max_notifications = 5; + // Set for a channel linked to a workflow: the owner, whose run id may be + // empty for the current run. Unset for an independent channel. + temporal.api.common.v1.WorkflowExecution workflow_execution = 6; } message PollChannelOutput { @@ -102,6 +114,9 @@ message PollChannelResponse { message DescribeChannelInput { string namespace = 1; string channel = 2; + // Set for a channel linked to a workflow: the owner, whose run id may be + // empty for the current run. Unset for an independent channel. + temporal.api.common.v1.WorkflowExecution workflow_execution = 3; } // ChannelListenerInfo has the fields of temporal.api.notification.v1.ChannelListener. @@ -122,6 +137,9 @@ message DescribeChannelOutput { repeated ChannelListenerInfo listeners = 1; Notification latest = 2; int64 retained_count = 3; + // The channel is linked to a workflow, and which one. + bool linked = 4; + temporal.api.common.v1.WorkflowExecution linked_to = 5; } message DescribeChannelRequest { diff --git a/chasm/lib/channel/proto/v1/service.proto b/chasm/lib/channel/proto/v1/service.proto index 22c1a4d21bc..9a17c1f658b 100644 --- a/chasm/lib/channel/proto/v1/service.proto +++ b/chasm/lib/channel/proto/v1/service.proto @@ -37,6 +37,33 @@ service ChannelService { option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; } + // The linked kind. Same requests, with workflow_execution set, routed to the + // shard of the workflow that holds the channel. + rpc NotifyLinkedChannel(NotifyChannelRequest) returns (NotifyChannelResponse) { + option (temporal.server.api.routing.v1.routing).business_id = "frontend_request.workflow_execution.workflow_id"; + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + rpc RegisterLinkedChannelListener(RegisterChannelListenerRequest) returns (RegisterChannelListenerResponse) { + option (temporal.server.api.routing.v1.routing).business_id = "frontend_request.workflow_execution.workflow_id"; + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + rpc UnregisterLinkedChannelListener(UnregisterChannelListenerRequest) returns (UnregisterChannelListenerResponse) { + option (temporal.server.api.routing.v1.routing).business_id = "frontend_request.workflow_execution.workflow_id"; + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + rpc PollLinkedChannel(PollChannelRequest) returns (PollChannelResponse) { + option (temporal.server.api.routing.v1.routing).business_id = "frontend_request.workflow_execution.workflow_id"; + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_LONG_POLL; + } + + rpc DescribeLinkedChannel(DescribeChannelRequest) returns (DescribeChannelResponse) { + option (temporal.server.api.routing.v1.routing).business_id = "frontend_request.workflow_execution.workflow_id"; + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + // Internal. The completion of a Workflow Task that subscribed calls this on // the channel's shard, creating the channel if it is absent. rpc RegisterWorkflowListener(RegisterWorkflowListenerRequest) returns (RegisterWorkflowListenerResponse) { diff --git a/chasm/lib/channel/service/handler.go b/chasm/lib/channel/service/handler.go index cf6f7c0e634..19e7720e487 100644 --- a/chasm/lib/channel/service/handler.go +++ b/chasm/lib/channel/service/handler.go @@ -191,7 +191,8 @@ func (h *handler) NotifyChannel( if err != nil { return nil, err } - metrics.ChannelNotificationsAccepted.With(h.metricsHandler).Record(1, metrics.NamespaceTag(ns)) + metrics.ChannelNotificationsAccepted.With(h.metricsHandler).Record( + 1, metrics.NamespaceTag(ns), independentKindTag) return &channelpb.NotifyChannelResponse{ FrontendResponse: &channelpb.NotifyChannelOutput{ ListenerCount: int64(count), @@ -258,12 +259,12 @@ func (h *handler) redeliver( } if folded > 0 { metrics.ChannelNotificationsFolded.With(h.metricsHandler).Record( - int64(folded), metrics.NamespaceTag(ns), callbackKindTag) + int64(folded), metrics.NamespaceTag(ns), independentKindTag, callbackKindTag) } } if holders := read.listeners - len(read.workflows); !read.callbacksNeed && holders > 0 { metrics.ChannelNotificationsFolded.With(h.metricsHandler).Record( - int64(holders), metrics.NamespaceTag(ns), callbackKindTag) + int64(holders), metrics.NamespaceTag(ns), independentKindTag, callbackKindTag) } return h.deliverer.deliverAll(ctx, ref, ns, read.workflows, n) } @@ -361,7 +362,7 @@ func (h *handler) PollChannel( in := req.GetFrontendRequest() ns := h.namespaceName(req.GetNamespaceId()) ctx = withCallerInfo(ctx, ns) - metrics.ChannelPollers.With(h.metricsHandler).Record(1, metrics.NamespaceTag(ns)) + metrics.ChannelPollers.With(h.metricsHandler).Record(1, metrics.NamespaceTag(ns), independentKindTag) ref := channelRef(req.GetNamespaceId(), in.GetChannel()) limits := h.limitsFor(ns) @@ -622,7 +623,7 @@ func (h *handler) DeliverChannelNotification( if target.duplicate { out.Duplicate = true metrics.ChannelNotificationsFolded.With(h.metricsHandler).Record( - 1, metrics.NamespaceTag(ns), workflowKindTag) + 1, metrics.NamespaceTag(ns), independentKindTag, workflowKindTag) return &channelpb.DeliverChannelNotificationResponse{FrontendResponse: out}, nil } folded, err := h.accept(ctx, namespaceID, workflowID, target.runID, n) @@ -631,9 +632,11 @@ func (h *handler) DeliverChannelNotification( } out.Folded = folded tag := metrics.NamespaceTag(ns) - metrics.ChannelNotificationsDelivered.With(h.metricsHandler).Record(1, tag, workflowKindTag) + metrics.ChannelNotificationsDelivered.With(h.metricsHandler).Record( + 1, tag, independentKindTag, workflowKindTag) if folded { - metrics.ChannelNotificationsFolded.With(h.metricsHandler).Record(1, tag, workflowKindTag) + metrics.ChannelNotificationsFolded.With(h.metricsHandler).Record( + 1, tag, independentKindTag, workflowKindTag) } return &channelpb.DeliverChannelNotificationResponse{FrontendResponse: out}, nil } diff --git a/chasm/lib/channel/service/linked.go b/chasm/lib/channel/service/linked.go new file mode 100644 index 00000000000..9357f60a4a5 --- /dev/null +++ b/chasm/lib/channel/service/linked.go @@ -0,0 +1,348 @@ +package service + +import ( + "context" + + commonpb "go.temporal.io/api/common/v1" + "go.temporal.io/api/serviceerror" + "go.temporal.io/server/chasm" + "go.temporal.io/server/chasm/lib/channel" + channelpb "go.temporal.io/server/chasm/lib/channel/gen/channelpb/v1" + chasmworkflow "go.temporal.io/server/chasm/lib/workflow" + "go.temporal.io/server/common/metrics" +) + +// The linked kind is served on the shard of the workflow that holds the +// channel. The requests are the independent kind's with workflow_execution +// set; an empty run id means the chain's current run, as it does for a +// Signal. A workflow that has closed took its linked channels with it, so a +// call on one answers NotFound, and a name nobody has notified yet on a +// running workflow is a linked channel with nothing in it rather than one +// that does not exist, which is how a client probes for the linked kind. + +var ( + independentKindTag = metrics.StringTag("kind", "independent") + linkedKindTag = metrics.StringTag("kind", "linked") +) + +func ownerRef(namespaceID string, owner *commonpb.WorkflowExecution) chasm.ComponentRef { + return workflowRef(namespaceID, owner.GetWorkflowId(), owner.GetRunId()) +} + +func checkOwner(owner *commonpb.WorkflowExecution) error { + if owner.GetWorkflowId() == "" { + return serviceerror.NewInvalidArgument("a linked channel call needs the owner's workflow id") + } + return nil +} + +func ownerClosedError(owner *commonpb.WorkflowExecution) error { + return serviceerror.NewNotFoundf( + "workflow %q has closed and its linked channels are gone with it", owner.GetWorkflowId()) +} + +func ownerClosed(ctx chasm.Context) bool { + return !ctx.ExecutionInfo().CloseTime.IsZero() +} + +// ownerRead is what a call learns from reading the owner first. +type ownerRead struct { + closed bool + // The linked channel exists on the run. + exists bool + latest int64 + // For a notify at the counter asked about. + advances bool + ownerHolds bool + callbacksNeed bool + listeners int +} + +// readOwner reads the owner without touching it. An absent workflow comes +// back as NotFound. +func (h *handler) readOwner( + ctx context.Context, + namespaceID string, + owner *commonpb.WorkflowExecution, + name string, + counter int64, +) (ownerRead, error) { + return chasm.ReadComponent(ctx, ownerRef(namespaceID, owner), + func(wf *chasmworkflow.Workflow, cctx chasm.Context, _ struct{}) (ownerRead, error) { + r := ownerRead{closed: ownerClosed(cctx), advances: true, listeners: 1} + c, ok := wf.LinkedChannel(cctx, name) + if !ok { + return r, nil + } + r.exists = true + r.latest = c.LatestCounter() + r.advances = c.Advances(counter) + r.ownerHolds = c.OwnerHolds(counter) + r.callbacksNeed = c.CallbacksNeed(cctx, counter) + r.listeners = c.LinkedListenerCount() + return r, nil + }, struct{}{}) +} + +// NotifyLinkedChannel accepts a notification on a channel linked to a +// workflow, creating the channel on the run on first use. The owner is the +// listener: the notification waits on its run for the next scheduled event, +// and the same write hands it to the callback listeners. +// +// Read first, as the independent kind does. A notification the owner already +// holds, pending or on a task that has not started, and that every callback +// listener holds too, changes nothing and writes nothing. +func (h *handler) NotifyLinkedChannel( + ctx context.Context, + req *channelpb.NotifyChannelRequest, +) (*channelpb.NotifyChannelResponse, error) { + in := req.GetFrontendRequest() + owner := in.GetWorkflowExecution() + ns := h.namespaceName(req.GetNamespaceId()) + ctx = withCallerInfo(ctx, ns) + limits := h.limitsFor(ns) + if err := checkOwner(owner); err != nil { + return nil, err + } + n := in.GetNotification() + if err := channel.CheckNotification(n, limits.MaxMetadataBytes); err != nil { + return nil, err + } + if err := h.limiters.allowNotify(ns, h.timeSource.Now()); err != nil { + return nil, err + } + + read, err := h.readOwner(ctx, req.GetNamespaceId(), owner, n.GetChannel(), n.GetCounter()) + if err != nil { + return nil, err + } + if read.closed { + return nil, ownerClosedError(owner) + } + nsTag := metrics.NamespaceTag(ns) + if read.exists && !read.advances && read.ownerHolds && !read.callbacksNeed { + metrics.ChannelNotificationsFolded.With(h.metricsHandler).Record( + 1, nsTag, linkedKindTag, workflowKindTag) + return &channelpb.NotifyChannelResponse{ + FrontendResponse: &channelpb.NotifyChannelOutput{ListenerCount: int64(read.listeners)}, + }, nil + } + + result, _, err := chasm.UpdateComponent(ctx, ownerRef(req.GetNamespaceId(), owner), + func( + wf *chasmworkflow.Workflow, + mctx chasm.MutableContext, + n *channelpb.Notification, + ) (channel.LinkedNotifyResult, error) { + if ownerClosed(mctx) { + return channel.LinkedNotifyResult{}, ownerClosedError(owner) + } + return wf.NotifyLinkedChannel(mctx, n, limits) + }, n) + if err != nil { + return nil, err + } + if result.Advanced { + metrics.ChannelNotificationsAccepted.With(h.metricsHandler).Record(1, nsTag, linkedKindTag) + } + if result.OwnerHeld || result.OwnerFolded { + metrics.ChannelNotificationsFolded.With(h.metricsHandler).Record( + 1, nsTag, linkedKindTag, workflowKindTag) + } + if !result.OwnerHeld { + metrics.ChannelNotificationsDelivered.With(h.metricsHandler).Record( + 1, nsTag, linkedKindTag, workflowKindTag) + } + if result.CallbackFolded > 0 { + metrics.ChannelNotificationsFolded.With(h.metricsHandler).Record( + int64(result.CallbackFolded), nsTag, linkedKindTag, callbackKindTag) + } + return &channelpb.NotifyChannelResponse{ + FrontendResponse: &channelpb.NotifyChannelOutput{ListenerCount: int64(result.ListenerCount)}, + }, nil +} + +// RegisterLinkedChannelListener adds a callback listener to a linked channel, +// creating the channel on the run if it has none by that name. A new +// listener is posted the latest notification, as on an independent channel. +func (h *handler) RegisterLinkedChannelListener( + ctx context.Context, + req *channelpb.RegisterChannelListenerRequest, +) (*channelpb.RegisterChannelListenerResponse, error) { + in := req.GetFrontendRequest() + owner := in.GetWorkflowExecution() + ns := h.namespaceName(req.GetNamespaceId()) + ctx = withCallerInfo(ctx, ns) + if err := checkOwner(owner); err != nil { + return nil, err + } + if err := channel.CheckChannelName(in.GetChannel(), 0); err != nil { + return nil, err + } + limits := h.limitsFor(ns) + reg := channel.CallbackRegistration{ + RequestID: in.GetRequestId(), + Callback: in.GetCallback(), + Limits: limits, + } + listenerID, _, err := chasm.UpdateComponent(ctx, ownerRef(req.GetNamespaceId(), owner), + func(wf *chasmworkflow.Workflow, mctx chasm.MutableContext, name string) (string, error) { + if ownerClosed(mctx) { + return "", ownerClosedError(owner) + } + c, err := wf.LinkedChannelOrNew(mctx, name, limits.MaxLinkedChannelsPerWorkflow) + if err != nil { + return "", err + } + return c.RegisterCallbackListener(mctx, reg) + }, in.GetChannel()) + if err != nil { + return nil, err + } + return &channelpb.RegisterChannelListenerResponse{ + FrontendResponse: &channelpb.RegisterChannelListenerOutput{ListenerId: listenerID}, + }, nil +} + +// UnregisterLinkedChannelListener drops a callback listener from a linked +// channel. A workflow, channel or listener that is already gone is not an +// error, so a retry succeeds. +func (h *handler) UnregisterLinkedChannelListener( + ctx context.Context, + req *channelpb.UnregisterChannelListenerRequest, +) (*channelpb.UnregisterChannelListenerResponse, error) { + in := req.GetFrontendRequest() + owner := in.GetWorkflowExecution() + ns := h.namespaceName(req.GetNamespaceId()) + ctx = withCallerInfo(ctx, ns) + if err := checkOwner(owner); err != nil { + return nil, err + } + limits := h.limitsFor(ns) + done := &channelpb.UnregisterChannelListenerResponse{ + FrontendResponse: &channelpb.UnregisterChannelListenerOutput{}, + } + read, err := h.readOwner(ctx, req.GetNamespaceId(), owner, in.GetChannel(), 0) + if executionAbsent(err) { + return done, nil + } + if err != nil { + return nil, err + } + if read.closed || !read.exists { + return done, nil + } + _, _, err = chasm.UpdateComponent(ctx, ownerRef(req.GetNamespaceId(), owner), + func(wf *chasmworkflow.Workflow, mctx chasm.MutableContext, id string) (struct{}, error) { + c, ok := wf.LinkedChannel(mctx, in.GetChannel()) + if !ok { + return struct{}{}, nil + } + return struct{}{}, c.UnregisterListener(mctx, id, limits) + }, in.GetListenerId()) + if err != nil && !executionAbsent(err) { + return nil, err + } + return done, nil +} + +// PollLinkedChannel returns the retained notifications of a linked channel +// above the caller's counter, waiting on the owner's execution for one when +// none is there yet. A name nobody has notified is a channel with nothing in +// it: the poll waits for the first notify, which creates it. +func (h *handler) PollLinkedChannel( + ctx context.Context, + req *channelpb.PollChannelRequest, +) (*channelpb.PollChannelResponse, error) { + in := req.GetFrontendRequest() + owner := in.GetWorkflowExecution() + ns := h.namespaceName(req.GetNamespaceId()) + ctx = withCallerInfo(ctx, ns) + if err := checkOwner(owner); err != nil { + return nil, err + } + metrics.ChannelPollers.With(h.metricsHandler).Record(1, metrics.NamespaceTag(ns), linkedKindTag) + ref := ownerRef(req.GetNamespaceId(), owner) + name := in.GetChannel() + + read, err := h.readOwner(ctx, req.GetNamespaceId(), owner, name, 0) + if err != nil { + return nil, err + } + if read.closed { + return nil, ownerClosedError(owner) + } + if wait := in.GetWait().AsDuration(); wait > 0 && read.latest <= in.GetAfterCounter() { + pollCtx, cancel := pollBudget(ctx, wait) + defer cancel() + _, _, err := chasm.PollComponent(pollCtx, ref, + func(wf *chasmworkflow.Workflow, cctx chasm.Context, after int64) (struct{}, bool, error) { + // Monotonic, as PollComponent requires: a linked channel is + // never removed from a run, and its latest counter only grows. + c, ok := wf.LinkedChannel(cctx, name) + return struct{}{}, ok && c.LatestCounter() > after, nil + }, in.GetAfterCounter()) + if err != nil && (pollCtx.Err() == nil || ctx.Err() != nil) { + return nil, err + } + } + + notifications, err := chasm.ReadComponent(ctx, ref, + func(wf *chasmworkflow.Workflow, cctx chasm.Context, r channel.PollRequest) ([]*channelpb.Notification, error) { + c, ok := wf.LinkedChannel(cctx, name) + if !ok { + return nil, nil + } + return c.Poll(cctx, r) + }, channel.PollRequest{AfterCounter: in.GetAfterCounter(), Max: int(in.GetMaxNotifications())}) + if err != nil { + return nil, err + } + return &channelpb.PollChannelResponse{ + FrontendResponse: &channelpb.PollChannelOutput{Notifications: notifications}, + }, nil +} + +// DescribeLinkedChannel reports a linked channel: the owner and the callback +// listeners, the latest notification and how many are retained. A name +// nobody has notified yet on a running workflow answers as linked with +// nothing in it. +func (h *handler) DescribeLinkedChannel( + ctx context.Context, + req *channelpb.DescribeChannelRequest, +) (*channelpb.DescribeChannelResponse, error) { + in := req.GetFrontendRequest() + owner := in.GetWorkflowExecution() + ctx = withCallerInfo(ctx, h.namespaceName(req.GetNamespaceId())) + if err := checkOwner(owner); err != nil { + return nil, err + } + out, err := chasm.ReadComponent(ctx, ownerRef(req.GetNamespaceId(), owner), + func(wf *chasmworkflow.Workflow, cctx chasm.Context, name string) (*channelpb.DescribeChannelOutput, error) { + if ownerClosed(cctx) { + return nil, ownerClosedError(owner) + } + key := cctx.ExecutionKey() + res := &channelpb.DescribeChannelOutput{ + Linked: true, + LinkedTo: &commonpb.WorkflowExecution{WorkflowId: key.BusinessID, RunId: key.RunID}, + } + c, ok := wf.LinkedChannel(cctx, name) + if !ok { + return res, nil + } + snapshot, err := c.Describe(cctx, struct{}{}) + if err != nil { + return nil, err + } + res.Listeners = append([]*channelpb.ChannelListenerInfo{c.OwnerListenerInfo(cctx)}, + snapshot.Listeners...) + res.Latest = snapshot.Latest + res.RetainedCount = snapshot.RetainedCount + return res, nil + }, in.GetChannel()) + if err != nil { + return nil, err + } + return &channelpb.DescribeChannelResponse{FrontendResponse: out}, nil +} diff --git a/chasm/lib/channel/service/tasks.go b/chasm/lib/channel/service/tasks.go index 828fa430ede..7bc23eeac14 100644 --- a/chasm/lib/channel/service/tasks.go +++ b/chasm/lib/channel/service/tasks.go @@ -99,7 +99,7 @@ func (h *fanOutTaskHandler) Execute( } if fanOut.CallbackFolded > 0 { metrics.ChannelNotificationsFolded.With(h.metricsHandler).Record( - int64(fanOut.CallbackFolded), metrics.NamespaceTag(ns), callbackKindTag) + int64(fanOut.CallbackFolded), metrics.NamespaceTag(ns), independentKindTag, callbackKindTag) } if fanOut.Latest == nil { return nil @@ -212,8 +212,12 @@ func (h *callbackTaskHandler) Execute( RetryPolicy: h.callbackConfig.RetryPolicy(), }) if saveErr == nil && delivered { + kind := independentKindTag + if delivery.Linked { + kind = linkedKindTag + } metrics.ChannelNotificationsDelivered.With(h.metricsHandler).Record( - 1, metrics.NamespaceTag(ns), callbackKindTag) + 1, metrics.NamespaceTag(ns), kind, callbackKindTag) } if postErr != nil && retryable { // Reported to the queue as the destination being down, which feeds its diff --git a/chasm/lib/workflow/linked_channel.go b/chasm/lib/workflow/linked_channel.go new file mode 100644 index 00000000000..87a06241e3c --- /dev/null +++ b/chasm/lib/workflow/linked_channel.go @@ -0,0 +1,73 @@ +package workflow + +import ( + enumspb "go.temporal.io/api/enums/v1" + "go.temporal.io/api/serviceerror" + "go.temporal.io/server/chasm" + "go.temporal.io/server/chasm/lib/channel" + channelpb "go.temporal.io/server/chasm/lib/channel/gen/channelpb/v1" +) + +// LinkedChannel returns the channel linked to this run under the name. +func (w *Workflow) LinkedChannel(ctx chasm.Context, name string) (*channel.Channel, bool) { + field, ok := w.LinkedChannels[name] + if !ok { + return nil, false + } + return field.Get(ctx), true +} + +// LinkedChannelCount is how many channels are linked to this run. +func (w *Workflow) LinkedChannelCount() int { + return len(w.LinkedChannels) +} + +// LinkedChannelOrNew returns the linked channel, creating it when the run +// has none by that name. The limit bounds how many one run holds, since each +// is state the run carries; zero means the default. +func (w *Workflow) LinkedChannelOrNew( + mctx chasm.MutableContext, + name string, + limit int, +) (*channel.Channel, error) { + if field, ok := w.LinkedChannels[name]; ok { + return field.Get(mctx), nil + } + if limit <= 0 { + limit = channel.DefaultMaxLinkedChannelsPerWorkflow + } + if len(w.LinkedChannels) >= limit { + return nil, serviceerror.NewResourceExhaustedf( + enumspb.RESOURCE_EXHAUSTED_CAUSE_CONCURRENT_LIMIT, + "workflow already holds %d linked notification channels, which is the limit", + len(w.LinkedChannels)) + } + if w.LinkedChannels == nil { + w.LinkedChannels = make(chasm.Map[string, *channel.Channel]) + } + c := channel.NewLinkedChannel(mctx) + w.LinkedChannels[name] = chasm.NewComponentField(mctx, c) + return c, nil +} + +// NotifyLinkedChannel accepts a notification on the channel linked to this run +// under the notification's channel name, creating the channel on first use. +func (w *Workflow) NotifyLinkedChannel( + mctx chasm.MutableContext, + n *channelpb.Notification, + limits channel.Limits, +) (channel.LinkedNotifyResult, error) { + c, err := w.LinkedChannelOrNew(mctx, n.GetChannel(), limits.MaxLinkedChannelsPerWorkflow) + if err != nil { + return channel.LinkedNotifyResult{}, err + } + return c.NotifyLinked(mctx, n, limits) +} + +// LinkedChannelHolds reports whether the channel linked under the name already +// holds a notification at this counter or above for the owner. A channel the +// run does not have holds nothing. +func (w *Workflow) LinkedChannelHolds(ctx chasm.Context, name string, counter int64) bool { + c, ok := w.LinkedChannel(readOnly(ctx), name) + return ok && c.OwnerHolds(counter) +} diff --git a/chasm/lib/workflow/linked_channel_test.go b/chasm/lib/workflow/linked_channel_test.go new file mode 100644 index 00000000000..5fa23020cec --- /dev/null +++ b/chasm/lib/workflow/linked_channel_test.go @@ -0,0 +1,81 @@ +package workflow + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + "go.temporal.io/api/serviceerror" + "go.temporal.io/server/chasm" + "go.temporal.io/server/chasm/lib/channel" +) + +func newOwnerContext() *chasm.MockMutableContext { + now := time.Unix(1_700_000_000, 0).UTC() + return &chasm.MockMutableContext{MockContext: chasm.MockContext{ + HandleNow: func(chasm.Component) time.Time { return now }, + HandleExecutionKey: func() chasm.ExecutionKey { + return chasm.ExecutionKey{NamespaceID: "ns", BusinessID: "owner", RunID: "run-1"} + }, + }} +} + +// A linked channel is created on the run by its first notify, bounded in +// number, and its pending notification is taken by the scheduled event +// beside those of the channels the run subscribed to, naming the owner. +func TestLinkedChannelsOnTheOwner(t *testing.T) { + ctx := newOwnerContext() + w := &Workflow{} + require.False(t, w.HasPendingChannelNotifications(ctx)) + + result, err := w.NotifyLinkedChannel(ctx, channelNote("orders", 1), channel.Limits{}) + require.NoError(t, err) + require.True(t, result.Advanced) + require.Equal(t, 1, w.LinkedChannelCount()) + require.True(t, w.HasPendingChannelNotifications(ctx)) + require.True(t, w.LinkedChannelHolds(ctx, "orders", 1)) + require.False(t, w.LinkedChannelHolds(ctx, "orders", 2)) + require.False(t, w.LinkedChannelHolds(ctx, "billing", 1), "no such channel on the run") + + subscribe(t, w, ctx, "billing") + _, err = w.AcceptChannelNotification(ctx, channelNote("billing", 7)) + require.NoError(t, err) + + taken := w.TakeChannelNotifications(ctx) + require.Len(t, taken, 2) + require.Equal(t, "billing", taken[0].GetChannel()) + require.Nil(t, taken[0].GetLinkedTo(), "a subscribed channel's notification names no owner") + require.Equal(t, "orders", taken[1].GetChannel()) + require.Equal(t, int64(1), taken[1].GetCounter()) + require.Equal(t, "owner", taken[1].GetLinkedTo().GetWorkflowId()) + require.Equal(t, "run-1", taken[1].GetLinkedTo().GetRunId()) + require.False(t, w.HasPendingChannelNotifications(ctx)) + + require.True(t, w.HasScheduledChannelCounters(ctx)) + require.True(t, w.LinkedChannelHolds(ctx, "orders", 1), "the unstarted task carries it") + w.ClearScheduledChannelCounters(ctx) + require.False(t, w.HasScheduledChannelCounters(ctx)) + require.False(t, w.LinkedChannelHolds(ctx, "orders", 1)) +} + +// A run holds a bounded number of linked channels. The bound refuses a new +// channel and leaves the existing ones taking notifications. +func TestLinkedChannelLimitPerRun(t *testing.T) { + ctx := newOwnerContext() + w := &Workflow{} + limits := channel.Limits{MaxLinkedChannelsPerWorkflow: 1} + + _, err := w.NotifyLinkedChannel(ctx, channelNote("orders", 1), limits) + require.NoError(t, err) + _, err = w.NotifyLinkedChannel(ctx, channelNote("billing", 1), limits) + var exhausted *serviceerror.ResourceExhausted + require.ErrorAs(t, err, &exhausted) + require.Equal(t, 1, w.LinkedChannelCount()) + + result, err := w.NotifyLinkedChannel(ctx, channelNote("orders", 2), limits) + require.NoError(t, err) + require.True(t, result.OwnerFolded, "replaced the pending 1") + c, ok := w.LinkedChannel(ctx, "orders") + require.True(t, ok) + require.Equal(t, int64(2), c.LatestCounter()) +} diff --git a/chasm/lib/workflow/notification.go b/chasm/lib/workflow/notification.go index 6a514ddee3d..c41d241ba04 100644 --- a/chasm/lib/workflow/notification.go +++ b/chasm/lib/workflow/notification.go @@ -145,9 +145,20 @@ func (w *Workflow) AcceptChannelNotification( } // HasPendingChannelNotifications reports whether a notification is waiting -// for a scheduled event. Only the keys are read, so nothing is loaded. -func (w *Workflow) HasPendingChannelNotifications() bool { - return len(w.ChannelNotifications) > 0 +// for a scheduled event, from a channel the run subscribed to or one linked +// to it. The subscribed kind is answered from the keys alone; the linked +// kind keeps its pending entry on the channel, so those are read. +func (w *Workflow) HasPendingChannelNotifications(ctx chasm.Context) bool { + if len(w.ChannelNotifications) > 0 { + return true + } + view := readOnly(ctx) + for _, field := range w.LinkedChannels { + if field.Get(view).HasOwnerPending() { + return true + } + } + return false } // TakeChannelNotifications returns the pending notifications, one per @@ -158,7 +169,7 @@ func (w *Workflow) HasPendingChannelNotifications() bool { // the task starts, so a repeat in that window folds into the task. func (w *Workflow) TakeChannelNotifications(mctx chasm.MutableContext) []*channelpb.Notification { if len(w.ChannelNotifications) == 0 { - return nil + return w.takeLinkedNotifications(mctx) } view := readOnly(mctx) channels := slices.Sorted(maps.Keys(w.ChannelNotifications)) @@ -176,6 +187,22 @@ func (w *Workflow) TakeChannelNotifications(mctx chasm.MutableContext) []*channe sub.Get(mctx).ScheduledCounter = entry.GetCounter() } } + return append(out, w.takeLinkedNotifications(mctx)...) +} + +// takeLinkedNotifications takes the pending notification of every linked +// channel that holds one, sorted by channel. Each names its owner, so a +// worker can tell it from one of a channel the run subscribed to. +func (w *Workflow) takeLinkedNotifications(mctx chasm.MutableContext) []*channelpb.Notification { + view := readOnly(mctx) + var out []*channelpb.Notification + for _, name := range slices.Sorted(maps.Keys(w.LinkedChannels)) { + field := w.LinkedChannels[name] + if !field.Get(view).HasOwnerPending() { + continue + } + out = append(out, field.Get(mctx).TakeOwnerPending()) + } return out } @@ -188,6 +215,11 @@ func (w *Workflow) HasScheduledChannelCounters(ctx chasm.Context) bool { return true } } + for _, field := range w.LinkedChannels { + if field.Get(view).HasOwnerScheduledCounter() { + return true + } + } return false } @@ -201,4 +233,9 @@ func (w *Workflow) ClearScheduledChannelCounters(mctx chasm.MutableContext) { field.Get(mctx).ScheduledCounter = 0 } } + for _, field := range w.LinkedChannels { + if field.Get(view).HasOwnerScheduledCounter() { + field.Get(mctx).ClearOwnerScheduledCounter() + } + } } diff --git a/chasm/lib/workflow/notification_test.go b/chasm/lib/workflow/notification_test.go index 30133bbf4bd..a8da4168aeb 100644 --- a/chasm/lib/workflow/notification_test.go +++ b/chasm/lib/workflow/notification_test.go @@ -45,7 +45,7 @@ func TestChannelNotificationsFoldAndSnapshot(t *testing.T) { require.True(t, folded, "a lower counter folds into the pending one and changes nothing") _, err = w.AcceptChannelNotification(ctx, channelNote("billing", 7)) require.NoError(t, err) - require.True(t, w.HasPendingChannelNotifications()) + require.True(t, w.HasPendingChannelNotifications(ctx)) taken := w.TakeChannelNotifications(ctx) require.Len(t, taken, 2) @@ -54,7 +54,7 @@ func TestChannelNotificationsFoldAndSnapshot(t *testing.T) { require.Equal(t, int64(3), taken[1].GetCounter()) require.Equal(t, []byte{3}, taken[1].GetPosition()) require.Equal(t, []byte{3}, taken[1].GetMetadata()["k"].GetData()) - require.False(t, w.HasPendingChannelNotifications()) + require.False(t, w.HasPendingChannelNotifications(ctx)) require.Empty(t, w.TakeChannelNotifications(ctx)) } @@ -79,7 +79,7 @@ func TestChannelNotificationFoldsOnlyWhilePending(t *testing.T) { folded, err := w.AcceptChannelNotification(ctx, channelNote("orders", 4)) require.NoError(t, err) require.False(t, folded) - require.True(t, w.HasPendingChannelNotifications()) + require.True(t, w.HasPendingChannelNotifications(ctx)) require.Equal(t, int64(4), w.ChannelSubscriptions["orders"].Get(ctx).GetLastCounter()) } @@ -125,7 +125,7 @@ func TestChannelNotificationFoldsIntoUnstartedTask(t *testing.T) { folded, err := w.AcceptChannelNotification(ctx, channelNote("orders", 4)) require.NoError(t, err) require.True(t, folded, "the unstarted task carries it") - require.False(t, w.HasPendingChannelNotifications()) + require.False(t, w.HasPendingChannelNotifications(ctx)) require.False(t, w.ChannelNotificationIsDuplicate(ctx, "orders", 5)) w.ClearScheduledChannelCounters(ctx) @@ -133,5 +133,5 @@ func TestChannelNotificationFoldsIntoUnstartedTask(t *testing.T) { folded, err = w.AcceptChannelNotification(ctx, channelNote("orders", 4)) require.NoError(t, err) require.False(t, folded, "the task started, so the repeat goes to the next one") - require.True(t, w.HasPendingChannelNotifications()) + require.True(t, w.HasPendingChannelNotifications(ctx)) } diff --git a/chasm/lib/workflow/workflow.go b/chasm/lib/workflow/workflow.go index 937b18b5bba..2bbcaca38b1 100644 --- a/chasm/lib/workflow/workflow.go +++ b/chasm/lib/workflow/workflow.go @@ -13,6 +13,7 @@ import ( "go.temporal.io/server/chasm" "go.temporal.io/server/chasm/lib/callback" callbackspb "go.temporal.io/server/chasm/lib/callback/gen/callbackpb/v1" + "go.temporal.io/server/chasm/lib/channel" "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" @@ -71,6 +72,12 @@ type Workflow struct { // this run on. In memory only, drained before commit. pendingChannelRegistrations []string + // Channels linked to this run, keyed by channel name. The run is their + // listener by construction, so there is no subscription and no event: a + // notification waits on the channel for the run's next scheduled event. + // Nothing of them reaches a successor run. + LinkedChannels chasm.Map[string, *channel.Channel] + // Subscribe commands whose stream is in another execution, so the addressing // has to be looked up before a cursor can be made. In memory only, drained // by the flush before commit. diff --git a/service/frontend/service.go b/service/frontend/service.go index 5c33b287b7a..fb276f733c7 100644 --- a/service/frontend/service.go +++ b/service/frontend/service.go @@ -12,6 +12,7 @@ import ( "go.temporal.io/server/api/adminservice/v1" "go.temporal.io/server/chasm/lib/activity" chasmcallback "go.temporal.io/server/chasm/lib/callback" + channellib "go.temporal.io/server/chasm/lib/channel" 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" @@ -205,6 +206,10 @@ type Config struct { // Enable deployment version RPCs EnableDeploymentVersions dynamicconfig.BoolPropertyFnWithNamespaceFilter + // Whether the channel calls honour workflow_execution and reach the + // channel linked to that workflow. + LinkedChannelKindEnabled dynamicconfig.BoolPropertyFnWithNamespaceFilter + // Enable batcher RPCs EnableBatcher dynamicconfig.BoolPropertyFnWithNamespaceFilter // Batch operation dynamic configs @@ -408,6 +413,7 @@ func NewConfig( // [cleanup-wv-pre-release] EnableDeployments: dynamicconfig.EnableDeployments.Get(dc), EnableDeploymentVersions: dynamicconfig.EnableDeploymentVersions.Get(dc), + LinkedChannelKindEnabled: channellib.LinkedKindEnabledSetting.Get(dc), EnableBatcher: dynamicconfig.FrontendEnableBatcher.Get(dc), MaxConcurrentBatchOperation: dynamicconfig.FrontendMaxConcurrentBatchOperationPerNamespace.Get(dc), diff --git a/service/frontend/workflow_handler.go b/service/frontend/workflow_handler.go index 5de6ad1e8cd..88da9477d0e 100644 --- a/service/frontend/workflow_handler.go +++ b/service/frontend/workflow_handler.go @@ -2401,9 +2401,39 @@ func (wh *WorkflowHandler) validateCallerFields(identity, requestID string) erro return nil } +// validateLinkedOwner checks the workflow a linked channel call names. The +// run id is optional and means the chain's current run when empty. +func (wh *WorkflowHandler) validateLinkedOwner(owner *commonpb.WorkflowExecution) error { + if err := validateExecution(owner); err != nil { + return err + } + if len(owner.GetWorkflowId()) > wh.config.MaxIDLengthLimit() { + return errWorkflowIDTooLong + } + return nil +} + +// linkedChannelOwner is the workflow a channel call names, or nil when the +// linked kind is off for the namespace: the call then reaches the independent +// channel of that name, as it did before the linked kind existed. The owner +// is dropped from the routed request too, so the independent handlers see +// the call a client without the field would make. Only workflows hold linked +// channels, so the execution's type is not consulted. +func (wh *WorkflowHandler) linkedChannelOwner( + namespaceName string, + owner *commonpb.Execution, +) *commonpb.WorkflowExecution { + if owner == nil || !wh.config.LinkedChannelKindEnabled(namespaceName) { + return nil + } + return channel.FromAPIExecution(owner) +} + // NotifyChannel tells every listener of a channel that a source they consume // moved. The writer never learns who listens; the channel's shard wakes each // one. A channel with no listeners retains the notification for pollers. +// With execution set the channel is the one linked to that workflow, whose +// run is the listener. func (wh *WorkflowHandler) NotifyChannel( ctx context.Context, request *workflowservice.NotifyChannelRequest, @@ -2434,15 +2464,26 @@ func (wh *WorkflowHandler) NotifyChannel( return nil, err } - resp, err := wh.channelClient.NotifyChannel(ctx, &channelpb.NotifyChannelRequest{ + owner := wh.linkedChannelOwner(request.GetNamespace(), request.GetExecution()) + routed := &channelpb.NotifyChannelRequest{ NamespaceId: namespaceID, FrontendRequest: &channelpb.NotifyChannelInput{ - Namespace: request.GetNamespace(), - Notification: channel.FromAPINotification(n), - Identity: request.GetIdentity(), - RequestId: request.GetRequestId(), + Namespace: request.GetNamespace(), + Notification: channel.FromAPINotification(n), + Identity: request.GetIdentity(), + RequestId: request.GetRequestId(), + WorkflowExecution: owner, }, - }) + } + var resp *channelpb.NotifyChannelResponse + if owner != nil { + if err := wh.validateLinkedOwner(owner); err != nil { + return nil, err + } + resp, err = wh.channelClient.NotifyLinkedChannel(ctx, routed) + } else { + resp, err = wh.channelClient.NotifyChannel(ctx, routed) + } if err != nil { return nil, err } @@ -2488,16 +2529,27 @@ func (wh *WorkflowHandler) RegisterChannelListener( requestID = uuid.NewString() } - resp, err := wh.channelClient.RegisterChannelListener(ctx, &channelpb.RegisterChannelListenerRequest{ + owner := wh.linkedChannelOwner(request.GetNamespace(), request.GetExecution()) + routed := &channelpb.RegisterChannelListenerRequest{ NamespaceId: namespaceID, FrontendRequest: &channelpb.RegisterChannelListenerInput{ - Namespace: request.GetNamespace(), - Channel: request.GetChannel(), - Callback: request.GetCallback(), - RequestId: requestID, - Identity: request.GetIdentity(), + Namespace: request.GetNamespace(), + Channel: request.GetChannel(), + Callback: request.GetCallback(), + RequestId: requestID, + Identity: request.GetIdentity(), + WorkflowExecution: owner, }, - }) + } + var resp *channelpb.RegisterChannelListenerResponse + if owner != nil { + if err := wh.validateLinkedOwner(owner); err != nil { + return nil, err + } + resp, err = wh.channelClient.RegisterLinkedChannelListener(ctx, routed) + } else { + resp, err = wh.channelClient.RegisterChannelListener(ctx, routed) + } if err != nil { return nil, err } @@ -2534,15 +2586,25 @@ func (wh *WorkflowHandler) UnregisterChannelListener( return nil, err } - _, err = wh.channelClient.UnregisterChannelListener(ctx, &channelpb.UnregisterChannelListenerRequest{ + owner := wh.linkedChannelOwner(request.GetNamespace(), request.GetExecution()) + routed := &channelpb.UnregisterChannelListenerRequest{ NamespaceId: namespaceID, FrontendRequest: &channelpb.UnregisterChannelListenerInput{ - Namespace: request.GetNamespace(), - Channel: request.GetChannel(), - ListenerId: request.GetListenerId(), - Identity: request.GetIdentity(), + Namespace: request.GetNamespace(), + Channel: request.GetChannel(), + ListenerId: request.GetListenerId(), + Identity: request.GetIdentity(), + WorkflowExecution: owner, }, - }) + } + if owner != nil { + if err := wh.validateLinkedOwner(owner); err != nil { + return nil, err + } + _, err = wh.channelClient.UnregisterLinkedChannelListener(ctx, routed) + } else { + _, err = wh.channelClient.UnregisterChannelListener(ctx, routed) + } if err != nil { return nil, err } @@ -2574,16 +2636,27 @@ func (wh *WorkflowHandler) PollChannel( return nil, err } - resp, err := wh.channelClient.PollChannel(ctx, &channelpb.PollChannelRequest{ + owner := wh.linkedChannelOwner(request.GetNamespace(), request.GetExecution()) + routed := &channelpb.PollChannelRequest{ NamespaceId: namespaceID, FrontendRequest: &channelpb.PollChannelInput{ - Namespace: request.GetNamespace(), - Channel: request.GetChannel(), - AfterCounter: request.GetAfterCounter(), - Wait: request.GetWait(), - MaxNotifications: request.GetMaxNotifications(), + Namespace: request.GetNamespace(), + Channel: request.GetChannel(), + AfterCounter: request.GetAfterCounter(), + Wait: request.GetWait(), + MaxNotifications: request.GetMaxNotifications(), + WorkflowExecution: owner, }, - }) + } + var resp *channelpb.PollChannelResponse + if owner != nil { + if err := wh.validateLinkedOwner(owner); err != nil { + return nil, err + } + resp, err = wh.channelClient.PollLinkedChannel(ctx, routed) + } else { + resp, err = wh.channelClient.PollChannel(ctx, routed) + } if err != nil { return nil, err } @@ -2611,13 +2684,24 @@ func (wh *WorkflowHandler) DescribeChannel( return nil, err } - resp, err := wh.channelClient.DescribeChannel(ctx, &channelpb.DescribeChannelRequest{ + owner := wh.linkedChannelOwner(request.GetNamespace(), request.GetExecution()) + routed := &channelpb.DescribeChannelRequest{ NamespaceId: namespaceID, FrontendRequest: &channelpb.DescribeChannelInput{ - Namespace: request.GetNamespace(), - Channel: request.GetChannel(), + Namespace: request.GetNamespace(), + Channel: request.GetChannel(), + WorkflowExecution: owner, }, - }) + } + var resp *channelpb.DescribeChannelResponse + if owner != nil { + if err := wh.validateLinkedOwner(owner); err != nil { + return nil, err + } + resp, err = wh.channelClient.DescribeLinkedChannel(ctx, routed) + } else { + resp, err = wh.channelClient.DescribeChannel(ctx, routed) + } if err != nil { return nil, err } @@ -2626,10 +2710,16 @@ func (wh *WorkflowHandler) DescribeChannel( for _, l := range out.GetListeners() { listeners = append(listeners, channel.ToAPIListener(l)) } + kind := notificationpb.CHANNEL_KIND_INDEPENDENT + if out.GetLinked() { + kind = notificationpb.CHANNEL_KIND_LINKED + } return &workflowservice.DescribeChannelResponse{ Listeners: listeners, Latest: channel.ToAPINotification(out.GetLatest()), RetainedCount: int32(out.GetRetainedCount()), + Kind: kind, + LinkedTo: channel.ToAPIExecution(out.GetLinkedTo()), }, nil } diff --git a/service/history/workflow/mutable_state_impl.go b/service/history/workflow/mutable_state_impl.go index d758b5525cf..2cb0bec52a0 100644 --- a/service/history/workflow/mutable_state_impl.go +++ b/service/history/workflow/mutable_state_impl.go @@ -760,7 +760,7 @@ func (ms *MutableStateImpl) HasPendingStreamData() bool { if wf.StreamCursorsBehind(chasmCtx) { return true } - if !wf.HasPendingChannelNotifications() { + if !wf.HasPendingChannelNotifications(chasmCtx) { return false } return !ms.IsWorkflowPendingOnWorkflowTaskBackoff() @@ -821,7 +821,7 @@ func (ms *MutableStateImpl) takeChannelNotifications() []*channelpb.Notification if err != nil { return nil } - if wf, ok := rootComponent.(*chasmworkflow.Workflow); !ok || !wf.HasPendingChannelNotifications() { + if wf, ok := rootComponent.(*chasmworkflow.Workflow); !ok || !wf.HasPendingChannelNotifications(view) { return nil } wf, chasmCtx, err := ms.ChasmWorkflowComponent(context.Background()) diff --git a/tests/linked_channel_test.go b/tests/linked_channel_test.go new file mode 100644 index 00000000000..d8475c41a8e --- /dev/null +++ b/tests/linked_channel_test.go @@ -0,0 +1,445 @@ +package tests + +import ( + "testing" + "time" + + "github.com/google/uuid" + "github.com/stretchr/testify/require" + commonpb "go.temporal.io/api/common/v1" + enumspb "go.temporal.io/api/enums/v1" + notificationpb "go.temporal.io/api/notification/v1" + "go.temporal.io/api/serviceerror" + "go.temporal.io/api/workflowservice/v1" + "go.temporal.io/server/chasm/lib/channel" + "go.temporal.io/server/common/testing/await" + "go.temporal.io/server/tests/testcore" + "google.golang.org/protobuf/types/known/durationpb" +) + +// A linked channel lives in the mutable state of the workflow named on the +// call. These cases drive it with the raw client the way the independent +// cases do, so each scheduled event can be inspected. + +func linkedOwner(id, runID string) *commonpb.Execution { + return &commonpb.Execution{Type: enumspb.EXECUTION_TYPE_WORKFLOW, BusinessId: id, RunId: runID} +} + +func (c *channelTestEnv) notifyLinked( + owner *commonpb.Execution, + name string, + counter int64, +) (*workflowservice.NotifyChannelResponse, error) { + return c.env.FrontendClient().NotifyChannel(c.ctx(), &workflowservice.NotifyChannelRequest{ + Namespace: c.ns, + Notification: channelNotification(name, counter), + Identity: "tester", + RequestId: uuid.NewString(), + Execution: owner, + }) +} + +func (c *channelTestEnv) mustNotifyLinked(id, name string, counter int64) int32 { + c.t.Helper() + resp, err := c.notifyLinked(linkedOwner(id, ""), name, counter) + require.NoError(c.t, err) + return resp.GetListenerCount() +} + +func (c *channelTestEnv) describeLinked( + owner *commonpb.Execution, + name string, +) (*workflowservice.DescribeChannelResponse, error) { + return c.env.FrontendClient().DescribeChannel(c.ctx(), &workflowservice.DescribeChannelRequest{ + Namespace: c.ns, + Channel: name, + Execution: owner, + }) +} + +func (c *channelTestEnv) mustDescribeLinked(id, name string) *workflowservice.DescribeChannelResponse { + c.t.Helper() + resp, err := c.describeLinked(linkedOwner(id, ""), name) + require.NoError(c.t, err) + return resp +} + +func (c *channelTestEnv) pollLinked( + owner *commonpb.Execution, + name string, + after int64, + wait time.Duration, + limit int32, +) (*workflowservice.PollChannelResponse, error) { + return c.env.FrontendClient().PollChannel(c.ctx(), &workflowservice.PollChannelRequest{ + Namespace: c.ns, + Channel: name, + AfterCounter: after, + Wait: durationpb.New(wait), + MaxNotifications: limit, + Execution: owner, + }) +} + +// startIdle starts a workflow and completes its first task, so the next task +// it gets is one a notification scheduled. +func (c *channelTestEnv) startIdle(id string) string { + c.t.Helper() + runID := c.start(id) + c.complete(c.poll(id), false) + require.False(c.t, c.hasPendingTask(id)) + return runID +} + +func requireLinkedTo(t *testing.T, ns []*notificationpb.Notification, id, runID string) { + t.Helper() + for _, n := range ns { + require.Equal(t, id, n.GetLinkedTo().GetBusinessId(), "linked_to of %q", n.GetChannel()) + require.Equal(t, runID, n.GetLinkedTo().GetRunId(), "linked_to run of %q", n.GetChannel()) + } +} + +func requireResourceExhausted(t *testing.T, err error) { + t.Helper() + var exhausted *serviceerror.ResourceExhausted + require.ErrorAs(t, err, &exhausted) +} + +// A notify addressed to a workflow lands on that workflow's own linked +// channel: the run is the listener with no subscription, the notification +// rides its next scheduled event naming the owner, and describe reports the +// kind. Before anyone notifies, the channel exists on a running workflow with +// nothing in it, which is what a client probes for. +func TestLinkedChannelNotifyCarriesOnScheduledEvent(t *testing.T) { + c := newChannelTestEnv(t) + id := "linked-basic-" + uuid.NewString() + name := "orders-" + uuid.NewString() + runID := c.startIdle(id) + + untouched := c.mustDescribeLinked(id, name) + require.Equal(t, notificationpb.CHANNEL_KIND_LINKED, untouched.GetKind()) + require.Equal(t, id, untouched.GetLinkedTo().GetBusinessId()) + require.Equal(t, runID, untouched.GetLinkedTo().GetRunId()) + require.Empty(t, untouched.GetListeners()) + require.Nil(t, untouched.GetLatest()) + require.Zero(t, untouched.GetRetainedCount()) + + require.Equal(t, int32(1), c.mustNotifyLinked(id, name, 1), "the owner listens") + task := c.poll(id) + scheduled := c.scheduledNotifications(id, task) + requireNotifications(t, scheduled, map[string]int64{name: 1}) + requireLinkedTo(t, scheduled, id, runID) + c.complete(task, false) + require.False(t, c.hasPendingTask(id), "History acknowledged it, so nothing is redelivered") + + desc := c.mustDescribeLinked(id, name) + require.Equal(t, notificationpb.CHANNEL_KIND_LINKED, desc.GetKind()) + require.Len(t, desc.GetListeners(), 1) + require.Equal(t, id, desc.GetListeners()[0].GetWorkflow().GetWorkflowId()) + require.Equal(t, runID, desc.GetListeners()[0].GetWorkflow().GetRunId()) + require.Equal(t, int64(1), desc.GetLatest().GetCounter()) + require.Equal(t, id, desc.GetLatest().GetLinkedTo().GetBusinessId()) + require.Equal(t, int32(1), desc.GetRetainedCount()) + + // The independent channel of the same name is untouched by all of this. + _, err := c.env.FrontendClient().DescribeChannel(c.ctx(), &workflowservice.DescribeChannelRequest{ + Namespace: c.ns, Channel: name, + }) + requireNotFound(t, err) +} + +// The owner folds as a subscribed listener does: a notification waiting for +// a scheduled event, or carried by a task that has not started, absorbs a +// repeat at or below its counter with no write, and a higher one replaces +// the pending entry. +func TestLinkedChannelFoldsWhilePendingAndIntoUnstartedTask(t *testing.T) { + c := newChannelTestEnv(t) + id := "linked-fold-" + uuid.NewString() + name := "orders-" + uuid.NewString() + c.startIdle(id) + + c.mustNotifyLinked(id, name, 1) + require.True(t, c.hasPendingTask(id), "the notify scheduled a task carrying 1") + c.mustNotifyLinked(id, name, 2) + transitions := c.stateTransitions(id) + require.Equal(t, int32(1), c.mustNotifyLinked(id, name, 1), "the unstarted task carries it") + require.Equal(t, transitions, c.stateTransitions(id), "a fold into the unstarted task writes nothing") + + first := c.poll(id) + requireNotifications(t, c.scheduledNotifications(id, first), map[string]int64{name: 1}) + c.complete(first, false) + + require.True(t, c.hasPendingTask(id), "2 arrived while the first task was open") + transitions = c.stateTransitions(id) + c.mustNotifyLinked(id, name, 2) + require.Equal(t, transitions, c.stateTransitions(id), "held by the scheduled task, so no write") + second := c.poll(id) + requireNotifications(t, c.scheduledNotifications(id, second), map[string]int64{name: 2}) + c.complete(second, false) + require.False(t, c.hasPendingTask(id)) + + require.Equal(t, int32(2), c.mustDescribeLinked(id, name).GetRetainedCount(), "repeats do not join the ring") +} + +// Once the task carrying a counter has run, the same counter is a new reason +// to run: a watcher that finds the record with no task open notifies again, +// and the owner gets a task whose scheduled event carries it. +func TestLinkedChannelRepeatAfterTaskRunsAgain(t *testing.T) { + c := newChannelTestEnv(t) + id := "linked-repeat-" + uuid.NewString() + name := "orders-" + uuid.NewString() + c.startIdle(id) + + c.mustNotifyLinked(id, name, 1) + task := c.poll(id) + requireNotifications(t, c.scheduledNotifications(id, task), map[string]int64{name: 1}) + c.complete(task, false) + require.False(t, c.hasPendingTask(id)) + + c.mustNotifyLinked(id, name, 1) + again := c.poll(id) + requireNotifications(t, c.scheduledNotifications(id, again), map[string]int64{name: 1}) + c.complete(again, false) + require.Equal(t, int32(1), c.mustDescribeLinked(id, name).GetRetainedCount()) +} + +// A linked channel is addressed by workflow id, so after continue-as-new the +// same calls reach the new run. Nothing carries over: the successor starts +// with an empty channel, and the run that closed answers NotFound. +func TestLinkedChannelFollowsContinueAsNew(t *testing.T) { + c := newChannelTestEnv(t) + id := "linked-can-" + uuid.NewString() + name := "orders-" + uuid.NewString() + firstRun := c.startIdle(id) + + c.mustNotifyLinked(id, name, 1) + task := c.poll(id) + c.complete(task, false, c.continueAsNewCommand(id)) + c.complete(c.poll(id), false) + + desc := c.mustDescribeLinked(id, name) + secondRun := desc.GetLinkedTo().GetRunId() + require.NotEqual(t, firstRun, secondRun) + require.Empty(t, desc.GetListeners(), "the successor starts with an empty channel") + require.Zero(t, desc.GetRetainedCount()) + + require.Equal(t, int32(1), c.mustNotifyLinked(id, name, 2)) + next := c.poll(id) + scheduled := c.scheduledNotifications(id, next) + requireNotifications(t, scheduled, map[string]int64{name: 2}) + requireLinkedTo(t, scheduled, id, secondRun) + c.complete(next, false) + + _, err := c.notifyLinked(linkedOwner(id, firstRun), name, 3) + requireNotFound(t, err) + _, err = c.describeLinked(linkedOwner(id, firstRun), name) + requireNotFound(t, err) +} + +// A poll on a linked channel reads the owner's ring: counters above the +// caller's, oldest first, up to the page, and a long poll returns one +// published while it waits. A name nobody has notified answers empty. +func TestLinkedChannelPoll(t *testing.T) { + c := newChannelTestEnv(t) + id := "linked-poll-" + uuid.NewString() + name := "orders-" + uuid.NewString() + runID := c.startIdle(id) + owner := linkedOwner(id, "") + + empty, err := c.pollLinked(owner, name, 0, 0, 0) + require.NoError(t, err) + require.Empty(t, empty.GetNotifications()) + + c.mustNotifyLinked(id, name, 1) + c.mustNotifyLinked(id, name, 2) + c.mustNotifyLinked(id, name, 3) + + resp, err := c.pollLinked(owner, name, 1, 0, 0) + require.NoError(t, err) + requireNotifications(t, resp.GetNotifications()[:1], map[string]int64{name: 2}) + require.Len(t, resp.GetNotifications(), 2) + requireLinkedTo(t, resp.GetNotifications(), id, runID) + + resp, err = c.pollLinked(owner, name, 1, 0, 1) + require.NoError(t, err) + require.Len(t, resp.GetNotifications(), 1) + require.Equal(t, int64(2), resp.GetNotifications()[0].GetCounter()) + + done := make(chan *workflowservice.PollChannelResponse, 1) + errs := make(chan error, 1) + go func() { + resp, err := c.pollLinked(owner, name, 3, 10*time.Second, 0) + if err != nil { + errs <- err + return + } + done <- resp + }() + // Nothing says when the poll has parked, so it is given a moment. Were the + // notify to land first, the poll would return it all the same. + time.Sleep(time.Second) //nolint:forbidigo // no signal says the poll is waiting + c.mustNotifyLinked(id, name, 4) + select { + case resp := <-done: + requireNotifications(t, resp.GetNotifications(), map[string]int64{name: 4}) + case err := <-errs: + t.Fatalf("the long poll failed: %v", err) + case <-time.After(20 * time.Second): + t.Fatal("the long poll did not return on the notify") + } +} + +// A callback listener on a linked channel is posted each notification, with +// linked_to naming the owner, and one that registers after a notify is handed +// the latest once. The owner counts as a listener beside it. +func TestLinkedChannelCallbackListener(t *testing.T) { + c := newChannelTestEnv(t) + id := "linked-callback-" + uuid.NewString() + name := "orders-" + uuid.NewString() + c.startIdle(id) + recorder, url := newCallbackRecorder(t) + + register := func(requestID string) string { + resp, err := c.env.FrontendClient().RegisterChannelListener(c.ctx(), + &workflowservice.RegisterChannelListenerRequest{ + Namespace: c.ns, + Channel: name, + Callback: &commonpb.Callback{Variant: &commonpb.Callback_Nexus_{ + Nexus: &commonpb.Callback_Nexus{Url: url}, + }}, + RequestId: requestID, + Identity: "tester", + Execution: linkedOwner(id, ""), + }) + require.NoError(t, err) + return resp.GetListenerId() + } + requestID := uuid.NewString() + listenerID := register(requestID) + require.Equal(t, listenerID, register(requestID), "a retried registration finds its listener") + require.Len(t, c.mustDescribeLinked(id, name).GetListeners(), 2, "the owner and the callback") + + require.Equal(t, int32(2), c.mustNotifyLinked(id, name, 1)) + select { + case <-recorder.arrived: + case <-time.After(20 * time.Second): + t.Fatal("the callback was not posted") + } + await.Require(c.ctx(), t, func(t *await.T) { + require.Equal(t, []string{"1"}, recorder.counters()) + }, 5*time.Second, 20*time.Millisecond) + recorder.mu.Lock() + require.Equal(t, name, recorder.channels[0]) + linkedTo, _ := recorder.bodies[0]["linkedTo"].(map[string]any) + require.Equal(t, id, linkedTo["workflowId"]) + recorder.mu.Unlock() + task := c.poll(id) + requireNotifications(t, c.scheduledNotifications(id, task), map[string]int64{name: 1}) + c.complete(task, false) + + late := register(uuid.NewString()) + select { + case <-recorder.arrived: + case <-time.After(20 * time.Second): + t.Fatal("the late listener was not handed the latest") + } + await.Require(c.ctx(), t, func(t *await.T) { + require.Equal(t, []string{"1", "1"}, recorder.counters()) + }, 5*time.Second, 20*time.Millisecond) + require.False(t, c.hasPendingTask(id), "the handoff is the callback's, not the owner's") + + for _, l := range []string{listenerID, late} { + _, err := c.env.FrontendClient().UnregisterChannelListener(c.ctx(), + &workflowservice.UnregisterChannelListenerRequest{ + Namespace: c.ns, Channel: name, ListenerId: l, Identity: "tester", + Execution: linkedOwner(id, ""), + }) + require.NoError(t, err) + } + require.Len(t, c.mustDescribeLinked(id, name).GetListeners(), 1, "the owner remains") + require.Equal(t, int32(1), c.mustNotifyLinked(id, name, 2)) +} + +// A workflow that has closed took its linked channels with it, and a +// workflow that never existed has none. +func TestLinkedChannelClosedChainIsNotFound(t *testing.T) { + c := newChannelTestEnv(t) + id := "linked-closed-" + uuid.NewString() + name := "orders-" + uuid.NewString() + runID := c.start(id) + c.complete(c.poll(id), false, completeWorkflowCommand()...) + + for _, owner := range []*commonpb.Execution{linkedOwner(id, ""), linkedOwner(id, runID)} { + _, err := c.describeLinked(owner, name) + requireNotFound(t, err) + _, err = c.notifyLinked(owner, name, 1) + requireNotFound(t, err) + _, err = c.pollLinked(owner, name, 0, 0, 0) + requireNotFound(t, err) + } + _, err := c.describeLinked(linkedOwner("absent-"+uuid.NewString(), ""), name) + requireNotFound(t, err) + _, err = c.notifyLinked(linkedOwner("absent-"+uuid.NewString(), ""), name, 1) + requireNotFound(t, err) +} + +// With the linked kind switched off, the channel calls ignore the workflow +// they name and reach the independent channel of that name, as before the +// kind existed: a notify with an owner wakes the workflow that subscribed +// with the command, describe answers the independent kind, and an untouched +// name with an owner is NotFound rather than an empty linked channel. +func TestLinkedChannelKindSwitchedOff(t *testing.T) { + c := newChannelTestEnv(t, testcore.WithDynamicConfig(channel.LinkedKindEnabledSetting, false)) + id := "linked-off-" + uuid.NewString() + name := "orders-" + uuid.NewString() + runID := c.subscribe(id, name) + + _, err := c.describeLinked(linkedOwner(id, ""), "untouched-"+uuid.NewString()) + requireNotFound(t, err) + + resp, err := c.notifyLinked(linkedOwner(id, ""), name, 1) + require.NoError(t, err) + require.Equal(t, int32(1), resp.GetListenerCount(), "the subscribed run, on the independent channel") + task := c.poll(id) + scheduled := c.scheduledNotifications(id, task) + requireNotifications(t, scheduled, map[string]int64{name: 1}) + require.Nil(t, scheduled[0].GetLinkedTo(), "delivered by the independent channel") + c.complete(task, false) + + desc := c.mustDescribeLinked(id, name) + require.Equal(t, notificationpb.CHANNEL_KIND_INDEPENDENT, desc.GetKind()) + require.Nil(t, desc.GetLinkedTo()) + require.Len(t, desc.GetListeners(), 1) + require.Equal(t, runID, desc.GetListeners()[0].GetWorkflow().GetRunId()) + require.Equal(t, int32(1), desc.GetRetainedCount()) + + polled, err := c.pollLinked(linkedOwner(id, ""), name, 0, 0, 0) + require.NoError(t, err) + requireNotifications(t, polled.GetNotifications(), map[string]int64{name: 1}) +} + +// One run holds a bounded number of linked channels, and each keeps a +// bounded ring of its own. +func TestLinkedChannelLimits(t *testing.T) { + c := newChannelTestEnv(t, + testcore.WithDynamicConfig(channel.MaxLinkedChannelsPerWorkflowSetting, 1), + testcore.WithDynamicConfig(channel.LinkedRetainedNotificationsSetting, 2), + ) + id := "linked-limits-" + uuid.NewString() + first := "orders-" + uuid.NewString() + second := "billing-" + uuid.NewString() + c.startIdle(id) + + require.Equal(t, int32(1), c.mustNotifyLinked(id, first, 1)) + _, err := c.notifyLinked(linkedOwner(id, ""), second, 1) + requireResourceExhausted(t, err) + require.Equal(t, int32(1), c.mustNotifyLinked(id, first, 2), "the first channel still takes notifications") + c.mustNotifyLinked(id, first, 3) + + require.Equal(t, int32(2), c.mustDescribeLinked(id, first).GetRetainedCount()) + resp, err := c.pollLinked(linkedOwner(id, ""), first, 0, 0, 0) + require.NoError(t, err) + require.Equal(t, []int64{2, 3}, []int64{ + resp.GetNotifications()[0].GetCounter(), resp.GetNotifications()[1].GetCounter(), + }) +}