diff --git a/chasm/lib/channel/channel.go b/chasm/lib/channel/channel.go new file mode 100644 index 00000000000..f974a894809 --- /dev/null +++ b/chasm/lib/channel/channel.go @@ -0,0 +1,780 @@ +package channel + +import ( + "maps" + "net/url" + "slices" + "time" + + commonpb "go.temporal.io/api/common/v1" + enumspb "go.temporal.io/api/enums/v1" + failurepb "go.temporal.io/api/failure/v1" + "go.temporal.io/api/serviceerror" + "go.temporal.io/server/chasm" + channelpb "go.temporal.io/server/chasm/lib/channel/gen/channelpb/v1" + "go.temporal.io/server/common" + "go.temporal.io/server/common/backoff" + "google.golang.org/protobuf/types/known/timestamppb" +) + +// Channel is a named pub/sub point in a namespace, held as an execution of its +// own keyed by the channel name. A writer notifies the channel and never +// learns who listens. The channel keeps the listener table and wakes every +// listener: a workflow run through its next scheduled Workflow Task, a +// callback through an HTTP post. A bounded ring of recent notifications serves +// clients that poll. +type Channel struct { + chasm.UnimplementedComponent + + State *channelpb.ChannelState + + // Keyed by sequence, oldest first. Nodes of their own, so a notify writes + // one of them rather than rewriting the whole ring. + Retained chasm.Map[int64, *channelpb.Notification] + + // Keyed by workflow id. One workflow id has one open run, so a later run + // that subscribes replaces the entry of the run before it. + WorkflowListeners chasm.Map[string, *channelpb.WorkflowListener] + + // Keyed by listener id. + CallbackListeners chasm.Map[string, *channelpb.CallbackListener] +} + +// readOnly hides the mutable half of a context. Reaching a data node through a +// mutable context marks it for persistence, and a notify must not rewrite +// every listener it only looked at. +func readOnly(ctx chasm.Context) chasm.Context { + return struct{ chasm.Context }{ctx} +} + +// NewChannel makes an empty channel. The transition that creates it is always +// a notify or a registration, which stamps the first activity. +func NewChannel(_ chasm.MutableContext) *Channel { + return &Channel{ + State: &channelpb.ChannelState{}, + Retained: make(chasm.Map[int64, *channelpb.Notification]), + WorkflowListeners: make(chasm.Map[string, *channelpb.WorkflowListener]), + CallbackListeners: make(chasm.Map[string, *channelpb.CallbackListener]), + } +} + +// ContextMetadata satisfies chasm.RootComponent. A channel has nothing to +// propagate to the request context. +func (c *Channel) ContextMetadata(_ chasm.Context) map[string]string { + return nil +} + +// Terminate drops the listeners, so nothing is woken by a channel an operator +// shut down. What it retained stays readable until it is deleted. +func (c *Channel) Terminate( + mctx chasm.MutableContext, + _ chasm.TerminateComponentRequest, +) (chasm.TerminateComponentResponse, error) { + for id := range c.WorkflowListeners { + delete(c.WorkflowListeners, id) + } + for id := range c.CallbackListeners { + delete(c.CallbackListeners, id) + } + c.scheduleIdleCheck(mctx, DefaultLimits()) + return chasm.TerminateComponentResponse{}, nil +} + +// LifecycleState reports a channel as running until the idle task closes it +// on the way to deleting it. +func (c *Channel) LifecycleState(_ chasm.Context) chasm.LifecycleState { + if c.State.GetClosed() { + return chasm.LifecycleStateCompleted + } + return chasm.LifecycleStateRunning +} + +// ListenerCount is how many listeners the channel holds, of both kinds. +func (c *Channel) ListenerCount() int { + return len(c.WorkflowListeners) + len(c.CallbackListeners) +} + +// CheckNotification refuses a notification the channel cannot accept. The +// frontend checks the same things first; this is what the component itself +// guarantees. +func CheckNotification(n *channelpb.Notification, maxMetadataBytes int) error { + if n == nil { + return serviceerror.NewInvalidArgument("notification is required") + } + if n.GetChannel() == "" { + return serviceerror.NewInvalidArgument("channel is required") + } + if n.GetCounter() <= 0 { + return serviceerror.NewInvalidArgument("notification counter must be greater than zero") + } + if len(n.GetPosition()) > MaxPositionBytes { + return serviceerror.NewInvalidArgumentf( + "notification position exceeds %d bytes", MaxPositionBytes) + } + if size := MetadataSize(n.GetMetadata()); maxMetadataBytes > 0 && size > maxMetadataBytes { + return serviceerror.NewInvalidArgumentf( + "notification metadata is %d bytes, more than the limit of %d", size, maxMetadataBytes) + } + return nil +} + +// MetadataSize is the metered size of a notification's metadata: the keys and +// the payloads as the writer sent them. +func MetadataSize(metadata map[string]*commonpb.Payload) int { + size := 0 + for key, p := range metadata { + size += len(key) + p.Size() + } + return size +} + +// NotifyResult is what a notify reports back to the writer. +type NotifyResult struct { + ListenerCount int + // The notification raised the channel's latest counter and was accepted. + // One that did not changed nothing. + Advanced bool +} + +// Notify accepts a notification: it joins the retained ring for pollers and +// is fanned out to the listeners. One whose counter is not above the latest +// changes nothing, since every listener and poller already has a newer one; +// the caller checks that first so such a notify writes nothing at all. +// +// The fan-out is one task at a time. A notify that lands while one is +// outstanding schedules nothing: the task reads the latest when it runs, so a +// burst reaches each listener as its newest notification rather than as one +// delivery per notify. +func (c *Channel) Notify( + mctx chasm.MutableContext, + n *channelpb.Notification, + limits Limits, +) (NotifyResult, error) { + limits = limits.withDefaults() + if err := CheckNotification(n, limits.MaxMetadataBytes); err != nil { + return NotifyResult{}, err + } + if !c.Advances(n.GetCounter()) { + return NotifyResult{ListenerCount: c.ListenerCount()}, nil + } + n = common.CloneProto(n) + + c.retain(mctx, n, limits.RetainedNotifications) + c.State.AcceptedCount++ + c.touch(mctx) + c.State.Latest = n + c.scheduleFanOut(mctx) + c.scheduleIdleCheck(mctx, limits) + return NotifyResult{ListenerCount: c.ListenerCount(), Advanced: true}, nil +} + +// retain appends to the ring and drops the oldest past the bound. +func (c *Channel) retain(mctx chasm.MutableContext, n *channelpb.Notification, bound int) { + if c.Retained == nil { + c.Retained = make(chasm.Map[int64, *channelpb.Notification]) + } + seq := c.State.RetainedNext + c.Retained[seq] = chasm.NewDataField(mctx, n) + c.State.RetainedNext = seq + 1 + for c.State.RetainedNext-c.State.RetainedFirst > int64(bound) { + delete(c.Retained, c.State.RetainedFirst) + c.State.RetainedFirst++ + } +} + +// Advances reports whether a notification at this counter would be news to +// the channel. +func (c *Channel) Advances(counter int64) bool { + return counter > c.State.GetLatest().GetCounter() +} + +// RetainedCount is how many notifications the ring holds. +func (c *Channel) RetainedCount() int64 { + return c.State.GetRetainedNext() - c.State.GetRetainedFirst() +} + +func (c *Channel) scheduleFanOut(mctx chasm.MutableContext) { + if c.State.FanOutPending || c.ListenerCount() == 0 { + return + } + c.State.FanOutPending = true + mctx.AddTask(c, chasm.TaskAttributes{ScheduledTime: mctx.Now(c)}, &channelpb.ChannelFanOutTask{}) +} + +func (c *Channel) touch(mctx chasm.MutableContext) { + c.State.LastActivityTime = timestamppb.New(mctx.Now(c)) +} + +// FanOut is what the fan-out task delivers and to whom. +type FanOut struct { + Latest *channelpb.Notification + WorkflowListeners []*channelpb.WorkflowListener + // Callback listeners that already held the notification, in flight or + // pending, or whose pending one it replaced. + CallbackFolded int + // Callback listeners a delivery was started for. + CallbackStarted int +} + +// TakeFanOut is the fan-out task's transition. It lowers the coalescing flag in +// the same transition that reads the latest, so a notify that commits after it +// schedules the next task and nothing is missed. +// +// Callback listeners are handed the latest here, since handing it over is a +// write on this execution: a listener with nothing in flight starts a delivery, +// and one that is busy keeps the latest as its pending notification, replacing +// any older one. The workflow listeners come back for the task to reach on +// their own shards. +func (c *Channel) TakeFanOut(mctx chasm.MutableContext, _ struct{}) (FanOut, error) { + c.State.FanOutPending = false + latest := c.State.GetLatest() + out := FanOut{Latest: common.CloneProto(latest)} + if latest == nil { + return out, nil + } + + started, folded, err := c.HandToCallbacks(mctx, latest) + if err != nil { + return FanOut{}, err + } + out.CallbackStarted, out.CallbackFolded = started, folded + + view := readOnly(mctx) + for _, id := range slices.Sorted(maps.Keys(c.WorkflowListeners)) { + out.WorkflowListeners = append(out.WorkflowListeners, + common.CloneProto(c.WorkflowListeners[id].Get(view))) + } + return out, nil +} + +// callbackHolds reports whether a callback listener already holds a +// notification at this counter or above, in flight or pending. Only what it +// holds counts: once a post is done, the same counter is news again. +func callbackHolds(listener *channelpb.CallbackListener, counter int64) bool { + return (listener.GetInFlight() != nil && listener.GetInFlight().GetCounter() >= counter) || + (listener.GetPending() != nil && listener.GetPending().GetCounter() >= counter) +} + +// CallbacksNeed reports whether any callback listener does not hold the +// notification, which is when handing it over writes the channel. +func (c *Channel) CallbacksNeed(ctx chasm.Context, counter int64) bool { + view := readOnly(ctx) + for _, field := range c.CallbackListeners { + if !callbackHolds(field.Get(view), counter) { + return true + } + } + return false +} + +// HandToCallbacks gives the notification to every callback listener that +// does not hold it: one with nothing in flight starts a post, and one that is +// busy keeps it as its pending notification, replacing an older one. A +// listener that holds it already is left untouched. It reports how many posts +// it started and how many listeners folded it. +func (c *Channel) HandToCallbacks( + mctx chasm.MutableContext, + n *channelpb.Notification, +) (started int, folded int, err error) { + view := readOnly(mctx) + for _, id := range slices.Sorted(maps.Keys(c.CallbackListeners)) { + if callbackHolds(c.CallbackListeners[id].Get(view), n.GetCounter()) { + folded++ + continue + } + listener := c.CallbackListeners[id].Get(mctx) + listener.HandedCounter = max(listener.GetHandedCounter(), n.GetCounter()) + if listener.GetInFlight() != nil { + if listener.GetPending() != nil { + folded++ + } + listener.Pending = common.CloneProto(n) + continue + } + listener.InFlight = common.CloneProto(n) + listener.Attempt = 0 + if err = c.scheduleCallback(mctx, listener); err != nil { + return 0, 0, err + } + started++ + } + return started, folded, nil +} + +// WorkflowListenerList returns the workflow listeners in workflow id order. +func (c *Channel) WorkflowListenerList(ctx chasm.Context) []*channelpb.WorkflowListener { + view := readOnly(ctx) + out := make([]*channelpb.WorkflowListener, 0, len(c.WorkflowListeners)) + for _, id := range slices.Sorted(maps.Keys(c.WorkflowListeners)) { + out = append(out, common.CloneProto(c.WorkflowListeners[id].Get(view))) + } + return out +} + +// CallbackDestination keys a callback's deliveries on the outbound queue, +// where rate limits and the circuit breaker are per destination. +func CallbackDestination(cb *commonpb.Callback) (string, error) { + nexus := cb.GetNexus() + if nexus == nil { + return "", serviceerror.NewInvalidArgument("a channel listener callback needs a URL") + } + u, err := url.Parse(nexus.GetUrl()) + if err != nil { + return "", serviceerror.NewInvalidArgumentf("callback URL does not parse: %v", err) + } + return u.Scheme + "://" + u.Host, nil +} + +func (c *Channel) scheduleCallback( + mctx chasm.MutableContext, + listener *channelpb.CallbackListener, +) error { + destination, err := CallbackDestination(listener.GetCallback()) + if err != nil { + return err + } + listener.TaskSequence++ + mctx.AddTask(c, chasm.TaskAttributes{Destination: destination}, &channelpb.ChannelCallbackTask{ + ListenerId: listener.GetListenerId(), + Sequence: listener.GetTaskSequence(), + }) + return nil +} + +// WorkflowRegistration describes a workflow run that subscribed. +type WorkflowRegistration struct { + WorkflowID string + RunID string + FirstExecutionRunID string + Limits Limits +} + +// RegisterWorkflowListener records a run as a listener. A run of the same +// workflow already on record is replaced: one workflow id has one open run, so +// the entry belongs to a run that has closed. +// +// A run new to the channel gets the latest notification back, for it to take +// as pending. A reader that checked its source, found nothing and subscribed +// can otherwise miss a write that landed in between, since the notify that +// followed found nobody to tell. +func (c *Channel) RegisterWorkflowListener( + mctx chasm.MutableContext, + reg WorkflowRegistration, +) (*channelpb.Notification, error) { + if reg.WorkflowID == "" || reg.RunID == "" { + return nil, serviceerror.NewInvalidArgument("workflow id and run id are required") + } + limits := reg.Limits.withDefaults() + c.touch(mctx) + latest := common.CloneProto(c.State.GetLatest()) + if field, ok := c.WorkflowListeners[reg.WorkflowID]; ok { + if field.Get(readOnly(mctx)).GetRunId() == reg.RunID { + return nil, nil + } + listener := field.Get(mctx) + listener.RunId = reg.RunID + listener.FirstExecutionRunId = reg.FirstExecutionRunID + listener.RegisteredTime = timestamppb.New(mctx.Now(c)) + return latest, nil + } + if err := c.checkListenerRoom(limits.MaxListeners); err != nil { + return nil, err + } + if c.WorkflowListeners == nil { + c.WorkflowListeners = make(chasm.Map[string, *channelpb.WorkflowListener]) + } + c.WorkflowListeners[reg.WorkflowID] = chasm.NewDataField(mctx, &channelpb.WorkflowListener{ + WorkflowId: reg.WorkflowID, + RunId: reg.RunID, + FirstExecutionRunId: reg.FirstExecutionRunID, + RegisteredTime: timestamppb.New(mctx.Now(c)), + }) + return latest, nil +} + +func (c *Channel) checkListenerRoom(maxListeners int) error { + if c.ListenerCount() < maxListeners { + return nil + } + return serviceerror.NewResourceExhaustedf( + enumspb.RESOURCE_EXHAUSTED_CAUSE_CONCURRENT_LIMIT, + "channel already has %d listeners, which is the limit", maxListeners) +} + +// RekeyWorkflowListener moves a listener to the run that continued the one on +// record. Left alone when the entry no longer names that run, because the +// successor may have registered itself in the meantime. +func (c *Channel) RekeyWorkflowListener( + mctx chasm.MutableContext, + workflowID, fromRunID, toRunID string, +) { + field, ok := c.WorkflowListeners[workflowID] + if !ok || field.Get(readOnly(mctx)).GetRunId() != fromRunID { + return + } + field.Get(mctx).RunId = toRunID +} + +// ForgetWorkflowListener drops a listener no run of its workflow answers for +// any more, unless a newer run has registered under the same workflow id. +func (c *Channel) ForgetWorkflowListener( + mctx chasm.MutableContext, + workflowID, runID string, + limits Limits, +) { + field, ok := c.WorkflowListeners[workflowID] + if !ok || field.Get(readOnly(mctx)).GetRunId() != runID { + return + } + delete(c.WorkflowListeners, workflowID) + c.scheduleIdleCheck(mctx, limits.withDefaults()) +} + +// CallbackRegistration describes a callback being registered. +type CallbackRegistration struct { + RequestID string + Callback *commonpb.Callback + Limits Limits +} + +// RegisterCallbackListener adds a callback listener and returns its id. A +// registration whose request id the channel has seen returns the listener +// that request made, so a retry adds nothing. +// +// A new listener is posted the latest notification the channel holds, once, +// for the same reason a new workflow listener is handed it. +func (c *Channel) RegisterCallbackListener( + mctx chasm.MutableContext, + reg CallbackRegistration, +) (string, error) { + if reg.RequestID == "" { + return "", serviceerror.NewInvalidArgument("request id is required") + } + if _, err := CallbackDestination(reg.Callback); err != nil { + return "", err + } + limits := reg.Limits.withDefaults() + c.touch(mctx) + + view := readOnly(mctx) + for id, field := range c.CallbackListeners { + if field.Get(view).GetRequestId() == reg.RequestID { + return id, nil + } + } + if err := c.checkListenerRoom(limits.MaxListeners); err != nil { + return "", err + } + if c.CallbackListeners == nil { + c.CallbackListeners = make(chasm.Map[string, *channelpb.CallbackListener]) + } + id := reg.RequestID + listener := &channelpb.CallbackListener{ + ListenerId: id, + Callback: common.CloneProto(reg.Callback), + RequestId: reg.RequestID, + RegisteredTime: timestamppb.New(mctx.Now(c)), + } + if latest := c.State.GetLatest(); latest != nil { + listener.InFlight = common.CloneProto(latest) + listener.HandedCounter = latest.GetCounter() + if err := c.scheduleCallback(mctx, listener); err != nil { + return "", err + } + } + c.CallbackListeners[id] = chasm.NewDataField(mctx, listener) + return id, nil +} + +// UnregisterListener drops a callback listener. An id the channel does not +// hold is not an error, so a retried unregister succeeds. Workflow listeners +// are not dropped this way: the run believes it is subscribed, and only the +// run ending ends that. +func (c *Channel) UnregisterListener( + mctx chasm.MutableContext, + listenerID string, + limits Limits, +) error { + if listenerID == "" { + return serviceerror.NewInvalidArgument("listener id is required") + } + if _, ok := c.WorkflowListeners[listenerID]; ok { + if _, isCallback := c.CallbackListeners[listenerID]; !isCallback { + return serviceerror.NewInvalidArgumentf( + "listener %q is a workflow; it stops listening when its run ends", listenerID) + } + } + c.touch(mctx) + delete(c.CallbackListeners, listenerID) + c.scheduleIdleCheck(mctx, limits.withDefaults()) + return nil +} + +// CallbackDelivery is one attempt at posting a callback listener's in-flight +// notification. +type CallbackDelivery struct { + Callback *commonpb.Callback + Notification *channelpb.Notification + Attempt int32 +} + +// CallbackDeliveryFor reads what the delivery task should post. It reports +// false when the listener is gone or the attempt the task was made for is +// over, so a stale task does nothing. +func (c *Channel) CallbackDeliveryFor( + ctx chasm.Context, + task *channelpb.ChannelCallbackTask, +) (CallbackDelivery, bool) { + field, ok := c.CallbackListeners[task.GetListenerId()] + if !ok { + return CallbackDelivery{}, false + } + listener := field.Get(readOnly(ctx)) + if listener.GetInFlight() == nil || listener.GetTaskSequence() != task.GetSequence() || + listener.GetNextAttemptTime() != nil { + return CallbackDelivery{}, false + } + return CallbackDelivery{ + Callback: common.CloneProto(listener.GetCallback()), + Notification: common.CloneProto(listener.GetInFlight()), + Attempt: listener.GetAttempt(), + }, true +} + +// CallbackOutcome is how one delivery attempt ended. +type CallbackOutcome struct { + ListenerID string + // The task sequence the attempt ran under. + Sequence int64 + // Nil on success. + Err error + Retryable bool + // Applied to a retryable failure. + RetryPolicy backoff.RetryPolicy +} + +// CompleteCallbackDelivery records an attempt. A success or a failure that +// will not succeed on retry ends the in-flight delivery, and the pending +// notification, if one arrived meanwhile, goes out next. A retryable failure +// backs off and tries the same notification again. +// +// It reports whether the attempt delivered. +func (c *Channel) CompleteCallbackDelivery( + mctx chasm.MutableContext, + outcome CallbackOutcome, +) (bool, error) { + field, ok := c.CallbackListeners[outcome.ListenerID] + if !ok { + return false, nil + } + view := field.Get(readOnly(mctx)) + if view.GetInFlight() == nil || view.GetTaskSequence() != outcome.Sequence || + view.GetNextAttemptTime() != nil { + return false, nil + } + listener := field.Get(mctx) + now := mctx.Now(c) + + if outcome.Err != nil && outcome.Retryable { + listener.Attempt++ + delay := outcome.RetryPolicy.ComputeNextDelay(0, int(listener.Attempt), outcome.Err) + next := now.Add(delay) + listener.NextAttemptTime = timestamppb.New(next) + listener.LastAttemptFailure = attemptFailure(outcome.Err, false) + mctx.AddTask(c, chasm.TaskAttributes{ScheduledTime: next}, &channelpb.ChannelCallbackBackoffTask{ + ListenerId: listener.GetListenerId(), + Sequence: listener.GetTaskSequence(), + }) + return false, nil + } + + if outcome.Err != nil { + listener.LastAttemptFailure = attemptFailure(outcome.Err, true) + } else { + listener.LastAttemptFailure = nil + } + listener.InFlight = listener.GetPending() + listener.Pending = nil + listener.Attempt = 0 + listener.NextAttemptTime = nil + if listener.GetInFlight() != nil { + if err := c.scheduleCallback(mctx, listener); err != nil { + return false, err + } + } + return outcome.Err == nil, nil +} + +func attemptFailure(err error, nonRetryable bool) *failurepb.Failure { + return &failurepb.Failure{ + Message: err.Error(), + FailureInfo: &failurepb.Failure_ApplicationFailureInfo{ + ApplicationFailureInfo: &failurepb.ApplicationFailureInfo{NonRetryable: nonRetryable}, + }, + } +} + +// BackoffDone reports whether a backoff task still has an attempt to resume. +func (c *Channel) BackoffDone(ctx chasm.Context, task *channelpb.ChannelCallbackBackoffTask) bool { + field, ok := c.CallbackListeners[task.GetListenerId()] + if !ok { + return false + } + listener := field.Get(readOnly(ctx)) + return listener.GetInFlight() != nil && listener.GetTaskSequence() == task.GetSequence() && + listener.GetNextAttemptTime() != nil +} + +// ResumeCallback schedules the next attempt once a backoff has run out. +func (c *Channel) ResumeCallback( + mctx chasm.MutableContext, + task *channelpb.ChannelCallbackBackoffTask, +) error { + if !c.BackoffDone(mctx, task) { + return nil + } + listener := c.CallbackListeners[task.GetListenerId()].Get(mctx) + listener.NextAttemptTime = nil + return c.scheduleCallback(mctx, listener) +} + +// PollRequest asks for the retained notifications above a counter. +type PollRequest struct { + AfterCounter int64 + Max int +} + +// Poll returns the retained notifications with a counter above the request's, +// oldest first, at most Max of them. +func (c *Channel) Poll(ctx chasm.Context, req PollRequest) ([]*channelpb.Notification, error) { + limit := req.Max + if limit <= 0 || limit > DefaultRetainedNotifications { + limit = DefaultRetainedNotifications + } + view := readOnly(ctx) + var out []*channelpb.Notification + for seq := c.State.GetRetainedFirst(); seq < c.State.GetRetainedNext(); seq++ { + field, ok := c.Retained[seq] + if !ok { + continue + } + n := field.Get(view) + if n.GetCounter() <= req.AfterCounter { + continue + } + out = append(out, common.CloneProto(n)) + if len(out) >= limit { + break + } + } + return out, nil +} + +// LatestCounter is the highest counter the channel has accepted. It only +// grows, which is what makes it a condition a long poll can wait on. +func (c *Channel) LatestCounter() int64 { + return c.State.GetLatest().GetCounter() +} + +// Snapshot is a read of the channel for describe and for the poll path. +type Snapshot struct { + Listeners []*channelpb.ChannelListenerInfo + Latest *channelpb.Notification + RetainedCount int64 + LastActivityTime time.Time +} + +// Describe lists the listeners, workflow listeners first, each kind in id +// order, and reports the latest notification and how many are retained. +func (c *Channel) Describe(ctx chasm.Context, _ struct{}) (Snapshot, error) { + view := readOnly(ctx) + out := Snapshot{ + Latest: common.CloneProto(c.State.GetLatest()), + RetainedCount: c.RetainedCount(), + LastActivityTime: c.State.GetLastActivityTime().AsTime(), + } + for _, id := range slices.Sorted(maps.Keys(c.WorkflowListeners)) { + listener := c.WorkflowListeners[id].Get(view) + out.Listeners = append(out.Listeners, &channelpb.ChannelListenerInfo{ + ListenerId: id, + Variant: &channelpb.ChannelListenerInfo_Workflow_{ + Workflow: &channelpb.ChannelListenerInfo_Workflow{ + WorkflowId: listener.GetWorkflowId(), + RunId: listener.GetRunId(), + }, + }, + RegisteredTime: common.CloneProto(listener.GetRegisteredTime()), + }) + } + for _, id := range slices.Sorted(maps.Keys(c.CallbackListeners)) { + listener := c.CallbackListeners[id].Get(view) + out.Listeners = append(out.Listeners, &channelpb.ChannelListenerInfo{ + ListenerId: id, + Variant: &channelpb.ChannelListenerInfo_Callback{ + Callback: common.CloneProto(listener.GetCallback()), + }, + RegisteredTime: common.CloneProto(listener.GetRegisteredTime()), + }) + } + return out, nil +} + +// TouchForPoll records a poll as activity, so a channel only pollers use is +// not deleted under them. Skipped while the last activity is recent, since a +// poll is otherwise a read and this keeps it one on all but a few calls. +func (c *Channel) TouchForPoll(mctx chasm.MutableContext, limits Limits) (struct{}, error) { + limits = limits.withDefaults() + if mctx.Now(c).Sub(c.State.GetLastActivityTime().AsTime()) < limits.Retention/2 { + return struct{}{}, nil + } + c.touch(mctx) + return struct{}{}, nil +} + +// PollNeedsTouch reports whether a poll should record itself as activity. +func (c *Channel) PollNeedsTouch(now time.Time, limits Limits) bool { + limits = limits.withDefaults() + return now.Sub(c.State.GetLastActivityTime().AsTime()) >= limits.Retention/2 +} + +// 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 { + return + } + c.State.IdleCheckPending = true + at := c.State.GetLastActivityTime().AsTime().Add(limits.Retention) + mctx.AddTask(c, chasm.TaskAttributes{ScheduledTime: at}, &channelpb.ChannelIdleTask{}) +} + +// RunIdleCheck is the idle task's transition. It reports whether the channel +// has gone unused for a full retention with no listeners, which is when the +// task deletes it. Otherwise it re-arms for when that could next be true, or +// stands down while there are listeners, whose departure arms it again. +func (c *Channel) RunIdleCheck(mctx chasm.MutableContext, limits Limits) (bool, error) { + limits = limits.withDefaults() + c.State.IdleCheckPending = false + if c.ListenerCount() > 0 { + return false, nil + } + deadline := c.State.GetLastActivityTime().AsTime().Add(limits.Retention) + if !mctx.Now(c).Before(deadline) { + c.State.Closed = true + return true, nil + } + c.scheduleIdleCheck(mctx, limits) + return false, nil +} + +// CheckChannelName refuses a channel name that cannot key an execution. +func CheckChannelName(name string, maxLength int) error { + if name == "" { + return serviceerror.NewInvalidArgument("channel is required") + } + if maxLength > 0 && len(name) > maxLength { + return serviceerror.NewInvalidArgumentf( + "channel name is %d bytes, more than the limit of %d", len(name), maxLength) + } + return nil +} diff --git a/chasm/lib/channel/channel_test.go b/chasm/lib/channel/channel_test.go new file mode 100644 index 00000000000..4dd2b0b3905 --- /dev/null +++ b/chasm/lib/channel/channel_test.go @@ -0,0 +1,415 @@ +package channel + +import ( + "errors" + "testing" + "time" + + "github.com/stretchr/testify/require" + commonpb "go.temporal.io/api/common/v1" + "go.temporal.io/api/serviceerror" + "go.temporal.io/server/chasm" + channelpb "go.temporal.io/server/chasm/lib/channel/gen/channelpb/v1" + "go.temporal.io/server/common/backoff" +) + +func newTestChannel(t *testing.T) (*Channel, *chasm.MockMutableContext, *time.Time) { + 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 }, + }} + return NewChannel(mctx), mctx, &now +} + +func note(counter int64) *channelpb.Notification { + return &channelpb.Notification{Channel: "orders", Counter: counter, Position: []byte{byte(counter)}} +} + +func tasksOf[T any](mctx *chasm.MockMutableContext) []chasm.MockTask { + var out []chasm.MockTask + for _, task := range mctx.Tasks { + if _, ok := task.Payload.(T); ok { + out = append(out, task) + } + } + return out +} + +func counters(ns []*channelpb.Notification) []int64 { + out := make([]int64, len(ns)) + for i, n := range ns { + out[i] = n.GetCounter() + } + return out +} + +func testCallback() *commonpb.Callback { + return &commonpb.Callback{Variant: &commonpb.Callback_Nexus_{ + Nexus: &commonpb.Callback_Nexus{Url: "http://localhost:7243/notify"}, + }} +} + +// A channel nobody listens to still keeps what it is told, for a poller that +// arrives later, and reports that nobody was there. +func TestNotifyWithoutListenersRetains(t *testing.T) { + c, mctx, _ := newTestChannel(t) + result, err := c.Notify(mctx, note(1), Limits{}) + require.NoError(t, err) + require.Zero(t, result.ListenerCount) + require.Empty(t, tasksOf[*channelpb.ChannelFanOutTask](mctx), "nobody to fan out to") + + polled, err := c.Poll(mctx, PollRequest{}) + require.NoError(t, err) + require.Equal(t, []int64{1}, counters(polled)) + require.Equal(t, int64(1), c.LatestCounter()) +} + +// The ring keeps the newest notifications up to its bound, and a poll returns +// those above the caller's counter, oldest first, up to its page size. +func TestRingDropsOldestAndPollFilters(t *testing.T) { + c, mctx, _ := newTestChannel(t) + limits := Limits{RetainedNotifications: 3} + for counter := int64(1); counter <= 5; counter++ { + _, err := c.Notify(mctx, note(counter), limits) + require.NoError(t, err) + } + require.Equal(t, int64(3), c.RetainedCount()) + require.Len(t, c.Retained, 3) + + polled, err := c.Poll(mctx, PollRequest{}) + require.NoError(t, err) + require.Equal(t, []int64{3, 4, 5}, counters(polled)) + + polled, err = c.Poll(mctx, PollRequest{AfterCounter: 3, Max: 1}) + require.NoError(t, err) + require.Equal(t, []int64{4}, counters(polled)) +} + +func TestNotifyRefusesWhatItCannotAccept(t *testing.T) { + c, mctx, _ := newTestChannel(t) + cases := map[string]*channelpb.Notification{ + "no counter": {Channel: "orders"}, + "no channel": {Counter: 1}, + "long position": {Channel: "orders", Counter: 1, Position: make([]byte, MaxPositionBytes+1)}, + "large metadata": {Channel: "orders", Counter: 1, Metadata: map[string]*commonpb.Payload{"k": {Data: make([]byte, 64)}}}, + } + for name, n := range cases { + _, err := c.Notify(mctx, n, Limits{MaxMetadataBytes: 32}) + var invalid *serviceerror.InvalidArgument + require.ErrorAs(t, err, &invalid, name) + } + require.Zero(t, c.RetainedCount()) +} + +// A burst schedules one fan-out, and the fan-out hands each listener only the +// highest counter. +func TestBurstCoalescesIntoOneFanOut(t *testing.T) { + c, mctx, _ := newTestChannel(t) + require.NoError(t, registerWorkflow(c, mctx, WorkflowRegistration{ + WorkflowID: "wf", RunID: "run-1", + })) + for _, counter := range []int64{1, 3, 2} { + result, err := c.Notify(mctx, note(counter), Limits{}) + require.NoError(t, err) + require.Equal(t, 1, result.ListenerCount) + } + require.Len(t, tasksOf[*channelpb.ChannelFanOutTask](mctx), 1) + require.Equal(t, int64(3), c.LatestCounter(), "a lower counter does not replace the latest") + require.Equal(t, int64(2), c.RetainedCount(), "nor is it retained: it changes nothing") + require.False(t, c.Advances(3)) + require.True(t, c.Advances(4)) + + fanOut, err := c.TakeFanOut(mctx, struct{}{}) + require.NoError(t, err) + require.Equal(t, int64(3), fanOut.Latest.GetCounter()) + require.Len(t, fanOut.WorkflowListeners, 1) + require.False(t, c.State.GetFanOutPending()) + + _, err = c.Notify(mctx, note(4), Limits{}) + require.NoError(t, err) + require.Len(t, tasksOf[*channelpb.ChannelFanOutTask](mctx), 2, "the flag came down") +} + +// A callback listener has one delivery in flight. What arrives meanwhile folds +// into a single pending notification, which goes out when the in-flight one +// is done. +func TestCallbackFoldsWhileBusy(t *testing.T) { + c, mctx, _ := newTestChannel(t) + id, err := c.RegisterCallbackListener(mctx, CallbackRegistration{ + RequestID: "req-1", Callback: testCallback(), + }) + require.NoError(t, err) + + notifyAndFanOut := func(counter int64) FanOut { + _, err := c.Notify(mctx, note(counter), Limits{}) + require.NoError(t, err) + fanOut, err := c.TakeFanOut(mctx, struct{}{}) + require.NoError(t, err) + return fanOut + } + + require.Equal(t, 1, notifyAndFanOut(1).CallbackStarted) + deliveries := tasksOf[*channelpb.ChannelCallbackTask](mctx) + require.Len(t, deliveries, 1) + require.Equal(t, "http://localhost:7243", deliveries[0].Attributes.Destination) + + require.Zero(t, notifyAndFanOut(2).CallbackFolded, "first one waiting is not a fold") + require.Equal(t, 1, notifyAndFanOut(3).CallbackFolded) + require.Len(t, tasksOf[*channelpb.ChannelCallbackTask](mctx), 1, "still one in flight") + + task := deliveries[0].Payload.(*channelpb.ChannelCallbackTask) + delivery, ok := c.CallbackDeliveryFor(mctx, task) + require.True(t, ok) + require.Equal(t, int64(1), delivery.Notification.GetCounter()) + + delivered, err := c.CompleteCallbackDelivery(mctx, CallbackOutcome{ + ListenerID: id, Sequence: task.GetSequence(), + }) + require.NoError(t, err) + require.True(t, delivered) + deliveries = tasksOf[*channelpb.ChannelCallbackTask](mctx) + require.Len(t, deliveries, 2, "the pending one goes out next") + next, ok := c.CallbackDeliveryFor(mctx, deliveries[1].Payload.(*channelpb.ChannelCallbackTask)) + require.True(t, ok) + require.Equal(t, int64(3), next.Notification.GetCounter()) + _, ok = c.CallbackDeliveryFor(mctx, task) + require.False(t, ok, "the first delivery's task is stale") + + stale, err := c.CompleteCallbackDelivery(mctx, CallbackOutcome{ + ListenerID: id, Sequence: task.GetSequence(), + }) + require.NoError(t, err) + require.False(t, stale, "a stale outcome changes nothing") + delivered, err = c.CompleteCallbackDelivery(mctx, CallbackOutcome{ + ListenerID: id, Sequence: deliveries[1].Payload.(*channelpb.ChannelCallbackTask).GetSequence(), + }) + require.NoError(t, err) + require.True(t, delivered) + require.Nil(t, c.CallbackListeners[id].Get(mctx).GetInFlight()) +} + +// A retryable failure backs off and tries the same notification again; a +// failure that will not succeed on retry drops it. +func TestCallbackRetryBacksOff(t *testing.T) { + c, mctx, now := newTestChannel(t) + id, err := c.RegisterCallbackListener(mctx, CallbackRegistration{ + RequestID: "req-1", Callback: testCallback(), + }) + require.NoError(t, err) + _, err = c.Notify(mctx, note(1), Limits{}) + require.NoError(t, err) + _, err = c.TakeFanOut(mctx, struct{}{}) + require.NoError(t, err) + + policy := backoff.NewExponentialRetryPolicy(time.Second).WithExpirationInterval(backoff.NoInterval) + delivered, err := c.CompleteCallbackDelivery(mctx, CallbackOutcome{ + ListenerID: id, Sequence: 1, Err: errors.New("503"), Retryable: true, RetryPolicy: policy, + }) + require.NoError(t, err) + require.False(t, delivered) + backoffs := tasksOf[*channelpb.ChannelCallbackBackoffTask](mctx) + require.Len(t, backoffs, 1) + require.True(t, backoffs[0].Attributes.ScheduledTime.After(*now)) + backoffTask := backoffs[0].Payload.(*channelpb.ChannelCallbackBackoffTask) + require.Equal(t, int64(1), backoffTask.GetSequence()) + _, ok := c.CallbackDeliveryFor(mctx, &channelpb.ChannelCallbackTask{ListenerId: id, Sequence: 1}) + require.False(t, ok, "nothing to post while backing off") + + require.True(t, c.BackoffDone(mctx, backoffTask)) + require.NoError(t, c.ResumeCallback(mctx, backoffTask)) + retry, ok := c.CallbackDeliveryFor(mctx, &channelpb.ChannelCallbackTask{ListenerId: id, Sequence: 2}) + require.True(t, ok) + require.Equal(t, int64(1), retry.Notification.GetCounter()) + require.Equal(t, int32(1), retry.Attempt) + + delivered, err = c.CompleteCallbackDelivery(mctx, CallbackOutcome{ + ListenerID: id, Sequence: 2, Err: errors.New("400"), + }) + require.NoError(t, err) + require.False(t, delivered) + listener := c.CallbackListeners[id].Get(mctx) + require.Nil(t, listener.GetInFlight()) + require.True(t, listener.GetLastAttemptFailure().GetApplicationFailureInfo().GetNonRetryable()) +} + +// A retried registration returns the listener it made; a new listener is not +// handed what was accepted before it registered; the table is bounded. +func TestListenerTable(t *testing.T) { + c, mctx, _ := newTestChannel(t) + _, err := c.Notify(mctx, note(5), Limits{}) + require.NoError(t, err) + + limits := Limits{MaxListeners: 2} + first, err := c.RegisterCallbackListener(mctx, CallbackRegistration{ + RequestID: "req-1", Callback: testCallback(), Limits: limits, + }) + require.NoError(t, err) + again, err := c.RegisterCallbackListener(mctx, CallbackRegistration{ + RequestID: "req-1", Callback: testCallback(), Limits: limits, + }) + require.NoError(t, err) + require.Equal(t, first, again) + listener := c.CallbackListeners[first].Get(mctx) + require.Equal(t, int64(5), listener.GetInFlight().GetCounter(), "a new listener is posted the latest") + require.Equal(t, int64(5), listener.GetHandedCounter()) + require.Len(t, tasksOf[*channelpb.ChannelCallbackTask](mctx), 1, "once, not per retry") + + require.NoError(t, registerWorkflow(c, mctx, WorkflowRegistration{ + WorkflowID: "wf", RunID: "run-1", Limits: limits, + })) + _, err = c.RegisterCallbackListener(mctx, CallbackRegistration{ + RequestID: "req-2", Callback: testCallback(), Limits: limits, + }) + var exhausted *serviceerror.ResourceExhausted + require.ErrorAs(t, err, &exhausted) + require.NoError(t, registerWorkflow(c, mctx, WorkflowRegistration{ + WorkflowID: "wf", RunID: "run-2", Limits: limits, + }), "a later run of a known workflow takes its entry, not a new one") + require.Equal(t, "run-2", c.WorkflowListeners["wf"].Get(mctx).GetRunId()) + + _, err = c.RegisterCallbackListener(mctx, CallbackRegistration{ + RequestID: "req-3", Callback: &commonpb.Callback{}, + }) + require.Error(t, err, "a callback with no URL cannot be reached") + + require.Error(t, c.UnregisterListener(mctx, "wf", Limits{}), "workflows leave by ending") + require.NoError(t, c.UnregisterListener(mctx, first, Limits{})) + require.NoError(t, c.UnregisterListener(mctx, first, Limits{}), "a retry succeeds") + require.Equal(t, 1, c.ListenerCount()) + + snapshot, err := c.Describe(mctx, struct{}{}) + require.NoError(t, err) + require.Len(t, snapshot.Listeners, 1) + require.Equal(t, "run-2", snapshot.Listeners[0].GetWorkflow().GetRunId()) + require.Equal(t, int64(5), snapshot.Latest.GetCounter()) +} + +// The chain rule: a listener moves to the run that continued the one on +// record, and goes when no run listens. Neither touches an entry a newer run +// has already taken. +func TestWorkflowListenerRekeyAndForget(t *testing.T) { + c, mctx, _ := newTestChannel(t) + require.NoError(t, registerWorkflow(c, mctx, WorkflowRegistration{ + WorkflowID: "wf", RunID: "run-1", + })) + + c.RekeyWorkflowListener(mctx, "wf", "run-1", "run-2") + require.Equal(t, "run-2", c.WorkflowListeners["wf"].Get(mctx).GetRunId()) + c.RekeyWorkflowListener(mctx, "wf", "run-1", "run-3") + require.Equal(t, "run-2", c.WorkflowListeners["wf"].Get(mctx).GetRunId(), "stale re-key") + + c.ForgetWorkflowListener(mctx, "wf", "run-1", Limits{}) + require.Equal(t, 1, c.ListenerCount(), "stale forget") + c.ForgetWorkflowListener(mctx, "wf", "run-2", Limits{}) + require.Zero(t, c.ListenerCount()) + require.Len(t, tasksOf[*channelpb.ChannelIdleTask](mctx), 1, "the last listener leaving arms the idle check") +} + +// A channel with no listeners is closed for deletion a full retention after +// its last activity, and not before. +func TestIdleCheck(t *testing.T) { + c, mctx, now := newTestChannel(t) + limits := Limits{Retention: time.Hour} + _, err := c.Notify(mctx, note(1), limits) + require.NoError(t, err) + idle := tasksOf[*channelpb.ChannelIdleTask](mctx) + require.Len(t, idle, 1) + require.Equal(t, now.Add(time.Hour), idle[0].Attributes.ScheduledTime) + + *now = now.Add(30 * time.Minute) + _, err = c.Notify(mctx, note(2), limits) + require.NoError(t, err) + require.Len(t, tasksOf[*channelpb.ChannelIdleTask](mctx), 1, "one check outstanding") + + *now = now.Add(30 * time.Minute) + expired, err := c.RunIdleCheck(mctx, limits) + require.NoError(t, err) + require.False(t, expired, "the second notify moved the deadline") + idle = tasksOf[*channelpb.ChannelIdleTask](mctx) + require.Len(t, idle, 2) + require.Equal(t, now.Add(30*time.Minute), idle[1].Attributes.ScheduledTime) + + *now = now.Add(30 * time.Minute) + require.True(t, c.PollNeedsTouch(*now, limits)) + expired, err = c.RunIdleCheck(mctx, limits) + require.NoError(t, err) + require.True(t, expired) + require.Equal(t, chasm.LifecycleStateCompleted, c.LifecycleState(mctx)) +} + +// A listener holds the channel: the idle check stands down while one is +// registered. +func TestIdleCheckStandsDownWithListeners(t *testing.T) { + c, mctx, now := newTestChannel(t) + limits := Limits{Retention: time.Minute} + _, err := c.Notify(mctx, note(1), limits) + require.NoError(t, err) + require.NoError(t, registerWorkflow(c, mctx, WorkflowRegistration{ + WorkflowID: "wf", RunID: "run-1", + })) + *now = now.Add(time.Hour) + expired, err := c.RunIdleCheck(mctx, limits) + require.NoError(t, err) + require.False(t, expired) + require.False(t, c.State.GetIdleCheckPending()) + require.Len(t, tasksOf[*channelpb.ChannelIdleTask](mctx), 1) +} + +func registerWorkflow(c *Channel, mctx chasm.MutableContext, reg WorkflowRegistration) error { + _, err := c.RegisterWorkflowListener(mctx, reg) + return err +} + +// A run new to the channel is handed the latest notification, so a write that +// landed before it subscribed still wakes it; a repeat registration and an +// empty channel hand nothing. +func TestNewWorkflowListenerGetsLatest(t *testing.T) { + c, mctx, _ := newTestChannel(t) + latest, err := c.RegisterWorkflowListener(mctx, WorkflowRegistration{WorkflowID: "a", RunID: "r1"}) + require.NoError(t, err) + require.Nil(t, latest, "nothing to hand on an empty channel") + + _, err = c.Notify(mctx, note(3), Limits{}) + require.NoError(t, err) + latest, err = c.RegisterWorkflowListener(mctx, WorkflowRegistration{WorkflowID: "b", RunID: "r1"}) + require.NoError(t, err) + require.Equal(t, int64(3), latest.GetCounter()) + latest, err = c.RegisterWorkflowListener(mctx, WorkflowRegistration{WorkflowID: "b", RunID: "r1"}) + require.NoError(t, err) + require.Nil(t, latest, "a repeat is not new") + latest, err = c.RegisterWorkflowListener(mctx, WorkflowRegistration{WorkflowID: "b", RunID: "r2"}) + require.NoError(t, err) + require.Equal(t, int64(3), latest.GetCounter(), "a successor run is new") +} + +// A notification that does not advance the channel still reaches a callback +// listener that does not hold it, and changes nothing for one that does. +func TestRepeatHandsOnlyToCallbacksThatLackIt(t *testing.T) { + c, mctx, _ := newTestChannel(t) + id, err := c.RegisterCallbackListener(mctx, CallbackRegistration{ + RequestID: "req-1", Callback: testCallback(), + }) + require.NoError(t, err) + _, err = c.Notify(mctx, note(5), Limits{}) + require.NoError(t, err) + _, err = c.TakeFanOut(mctx, struct{}{}) + require.NoError(t, err) + require.Len(t, tasksOf[*channelpb.ChannelCallbackTask](mctx), 1) + + require.False(t, c.CallbacksNeed(mctx, 5), "in flight, so held") + started, folded, err := c.HandToCallbacks(mctx, note(5)) + require.NoError(t, err) + require.Zero(t, started) + require.Equal(t, 1, folded) + + task := tasksOf[*channelpb.ChannelCallbackTask](mctx)[0].Payload.(*channelpb.ChannelCallbackTask) + _, err = c.CompleteCallbackDelivery(mctx, CallbackOutcome{ListenerID: id, Sequence: task.GetSequence()}) + require.NoError(t, err) + require.True(t, c.CallbacksNeed(mctx, 5), "posted, so the same counter is news again") + started, _, err = c.HandToCallbacks(mctx, note(5)) + require.NoError(t, err) + require.Equal(t, 1, started) + require.Len(t, tasksOf[*channelpb.ChannelCallbackTask](mctx), 2) + require.Empty(t, c.WorkflowListenerList(mctx)) +} diff --git a/chasm/lib/channel/config.go b/chasm/lib/channel/config.go new file mode 100644 index 00000000000..7efff2424c0 --- /dev/null +++ b/chasm/lib/channel/config.go @@ -0,0 +1,138 @@ +package channel + +import ( + "time" + + "go.temporal.io/server/common/dynamicconfig" +) + +// MaxPositionBytes bounds the position a notification carries. It is a cursor +// for the listener to read from rather than data, and the channel retains it. +const MaxPositionBytes = 1024 + +// LongPollTimeout matches the other history long polls: on expiry the caller +// gets an empty response and polls again. +const LongPollTimeout = 20 * time.Second + +// LongPollBuffer leaves room to answer before the caller's own deadline. +const LongPollBuffer = 3 * time.Second + +// RoutedCallTimeout bounds one call to another shard, so a slow listener does +// not take the whole budget of the task that is telling the others. +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 +) + +var ( + MaxListenersSetting = dynamicconfig.NewNamespaceIntSetting( + "channel.maxListeners", + DefaultMaxListeners, + `Most listeners, workflow and callback together, one notification channel holds. A +registration past it is refused.`, + ) + RetainedNotificationsSetting = dynamicconfig.NewNamespaceIntSetting( + "channel.retainedNotifications", + DefaultRetainedNotifications, + `Most notifications a channel keeps for pollers. The oldest goes when a new one +arrives past it.`, + ) + MaxMetadataBytesSetting = dynamicconfig.NewNamespaceIntSetting( + "channel.maxMetadataBytes", + DefaultMaxMetadataBytes, + `Largest notification metadata accepted, summed over its payloads.`, + ) + RetentionSetting = dynamicconfig.NewNamespaceDurationSetting( + "channel.retention", + DefaultRetention, + `How long a channel with no listeners stays after its last notify, registration or +poll before it is deleted with what it retained.`, + ) + NotifyPerSecondSetting = dynamicconfig.NewNamespaceIntSetting( + "channel.notifyPerSecond", + DefaultNotifyPerSecond, + `Most NotifyChannel calls a namespace may make per second on one history host. Zero +means no limit.`, + ) + MaxSubscriptionsPerWorkflowSetting = dynamicconfig.NewNamespaceIntSetting( + "channel.maxSubscriptionsPerWorkflow", + DefaultMaxSubscriptionsPerWorkflow, + `Most notification channels one workflow run subscribes to. A subscribe command past +it fails the workflow task.`, + ) +) + +// Limits are the per-namespace bounds a channel transition applies, resolved +// by the caller so the component needs no config of its own. +type Limits struct { + MaxListeners int + RetainedNotifications int + MaxMetadataBytes int + Retention time.Duration +} + +// DefaultLimits is what applies when nothing is configured. +func DefaultLimits() Limits { + return Limits{ + MaxListeners: DefaultMaxListeners, + RetainedNotifications: DefaultRetainedNotifications, + MaxMetadataBytes: DefaultMaxMetadataBytes, + Retention: DefaultRetention, + } +} + +// withDefaults fills what a caller left at zero. +func (l Limits) withDefaults() Limits { + d := DefaultLimits() + if l.MaxListeners <= 0 { + l.MaxListeners = d.MaxListeners + } + if l.RetainedNotifications <= 0 { + l.RetainedNotifications = d.RetainedNotifications + } + if l.MaxMetadataBytes <= 0 { + l.MaxMetadataBytes = d.MaxMetadataBytes + } + if l.Retention <= 0 { + l.Retention = d.Retention + } + return l +} + +type Config struct { + MaxListeners dynamicconfig.IntPropertyFnWithNamespaceFilter + RetainedNotifications dynamicconfig.IntPropertyFnWithNamespaceFilter + MaxMetadataBytes dynamicconfig.IntPropertyFnWithNamespaceFilter + Retention dynamicconfig.DurationPropertyFnWithNamespaceFilter + NotifyPerSecond dynamicconfig.IntPropertyFnWithNamespaceFilter + MaxSubscriptionsPerWorkflow dynamicconfig.IntPropertyFnWithNamespaceFilter +} + +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), + } +} + +// 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), + } +} diff --git a/chasm/lib/channel/convert.go b/chasm/lib/channel/convert.go new file mode 100644 index 00000000000..2a25f6f2cd5 --- /dev/null +++ b/chasm/lib/channel/convert.go @@ -0,0 +1,71 @@ +package channel + +import ( + 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. + +func FromAPINotification(n *notificationpb.Notification) *channelpb.Notification { + if n == nil { + return nil + } + n = common.CloneProto(n) + return &channelpb.Notification{ + Channel: n.GetChannel(), + Position: n.GetPosition(), + Counter: n.GetCounter(), + Metadata: n.GetMetadata(), + } +} + +func ToAPINotification(n *channelpb.Notification) *notificationpb.Notification { + if n == nil { + return nil + } + n = common.CloneProto(n) + return ¬ificationpb.Notification{ + Channel: n.GetChannel(), + Position: n.GetPosition(), + Counter: n.GetCounter(), + Metadata: n.GetMetadata(), + } +} + +func ToAPINotifications(ns []*channelpb.Notification) []*notificationpb.Notification { + if len(ns) == 0 { + return nil + } + out := make([]*notificationpb.Notification, len(ns)) + for i, n := range ns { + out[i] = ToAPINotification(n) + } + return out +} + +// ToAPIListener converts a listener from describe. +func ToAPIListener(l *channelpb.ChannelListenerInfo) *notificationpb.ChannelListener { + out := ¬ificationpb.ChannelListener{ + ListenerId: l.GetListenerId(), + RegisteredTime: common.CloneProto(l.GetRegisteredTime()), + } + switch variant := l.GetVariant().(type) { + case *channelpb.ChannelListenerInfo_Workflow_: + out.Listener = ¬ificationpb.ChannelListener_Workflow{ + Workflow: ¬ificationpb.WorkflowListener{ + WorkflowId: variant.Workflow.GetWorkflowId(), + RunId: variant.Workflow.GetRunId(), + }, + } + case *channelpb.ChannelListenerInfo_Callback: + out.Listener = ¬ificationpb.ChannelListener_Callback{ + Callback: common.CloneProto(variant.Callback), + } + default: + // A listener with no variant has nothing more to say. + } + return out +} diff --git a/chasm/lib/channel/gen/channelpb/v1/channel_state.go-helpers.pb.go b/chasm/lib/channel/gen/channelpb/v1/channel_state.go-helpers.pb.go new file mode 100644 index 00000000000..19e0772f892 --- /dev/null +++ b/chasm/lib/channel/gen/channelpb/v1/channel_state.go-helpers.pb.go @@ -0,0 +1,43 @@ +// Code generated by protoc-gen-go-helpers. DO NOT EDIT. +package channelpb + +import ( + "google.golang.org/protobuf/proto" +) + +// Marshal an object of type ChannelState to the protobuf v3 wire format +func (val *ChannelState) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type ChannelState from the protobuf v3 wire format +func (val *ChannelState) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *ChannelState) Size() int { + return proto.Size(val) +} + +// Equal returns whether two ChannelState values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *ChannelState) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *ChannelState + switch t := that.(type) { + case *ChannelState: + that1 = t + case ChannelState: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} diff --git a/chasm/lib/channel/gen/channelpb/v1/channel_state.pb.go b/chasm/lib/channel/gen/channelpb/v1/channel_state.pb.go new file mode 100644 index 00000000000..2886f33b0ee --- /dev/null +++ b/chasm/lib/channel/gen/channelpb/v1/channel_state.pb.go @@ -0,0 +1,206 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// plugins: +// protoc-gen-go +// protoc +// source: temporal/server/chasm/lib/channel/proto/v1/channel_state.proto + +package channelpb + +import ( + reflect "reflect" + sync "sync" + unsafe "unsafe" + + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// ChannelState is the part of a channel written on every notify. The retained +// notifications and the listeners are nodes of their own, so a notify writes +// this, one retained notification, and nothing per listener. +type ChannelState struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The notification with the highest counter accepted so far. + Latest *Notification `protobuf:"bytes,1,opt,name=latest,proto3" json:"latest,omitempty"` + // Sequence of the oldest retained notification and the next one to be + // written. The retained notifications are keyed by sequence. + RetainedFirst int64 `protobuf:"varint,2,opt,name=retained_first,json=retainedFirst,proto3" json:"retained_first,omitempty"` + RetainedNext int64 `protobuf:"varint,3,opt,name=retained_next,json=retainedNext,proto3" json:"retained_next,omitempty"` + // A fan-out task is outstanding. Notifies that land while it is up schedule + // none of their own; the task reads the latest when it runs. + FanOutPending bool `protobuf:"varint,4,opt,name=fan_out_pending,json=fanOutPending,proto3" json:"fan_out_pending,omitempty"` + // Last notify, registration or poll. The idle task deletes a channel with no + // listeners once this is older than the retention. + LastActivityTime *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=last_activity_time,json=lastActivityTime,proto3" json:"last_activity_time,omitempty"` + IdleCheckPending bool `protobuf:"varint,6,opt,name=idle_check_pending,json=idleCheckPending,proto3" json:"idle_check_pending,omitempty"` + // Total notifications accepted, for describe. + AcceptedCount int64 `protobuf:"varint,7,opt,name=accepted_count,json=acceptedCount,proto3" json:"accepted_count,omitempty"` + // 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 +} + +func (x *ChannelState) Reset() { + *x = ChannelState{} + mi := &file_temporal_server_chasm_lib_channel_proto_v1_channel_state_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ChannelState) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ChannelState) ProtoMessage() {} + +func (x *ChannelState) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_channel_proto_v1_channel_state_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ChannelState.ProtoReflect.Descriptor instead. +func (*ChannelState) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_channel_proto_v1_channel_state_proto_rawDescGZIP(), []int{0} +} + +func (x *ChannelState) GetLatest() *Notification { + if x != nil { + return x.Latest + } + return nil +} + +func (x *ChannelState) GetRetainedFirst() int64 { + if x != nil { + return x.RetainedFirst + } + return 0 +} + +func (x *ChannelState) GetRetainedNext() int64 { + if x != nil { + return x.RetainedNext + } + return 0 +} + +func (x *ChannelState) GetFanOutPending() bool { + if x != nil { + return x.FanOutPending + } + return false +} + +func (x *ChannelState) GetLastActivityTime() *timestamppb.Timestamp { + if x != nil { + return x.LastActivityTime + } + return nil +} + +func (x *ChannelState) GetIdleCheckPending() bool { + if x != nil { + return x.IdleCheckPending + } + return false +} + +func (x *ChannelState) GetAcceptedCount() int64 { + if x != nil { + return x.AcceptedCount + } + return 0 +} + +func (x *ChannelState) GetClosed() bool { + if x != nil { + return x.Closed + } + return false +} + +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" + + "\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" + + "\rretained_next\x18\x03 \x01(\x03R\fretainedNext\x12&\n" + + "\x0ffan_out_pending\x18\x04 \x01(\bR\rfanOutPending\x12H\n" + + "\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" + +var ( + file_temporal_server_chasm_lib_channel_proto_v1_channel_state_proto_rawDescOnce sync.Once + file_temporal_server_chasm_lib_channel_proto_v1_channel_state_proto_rawDescData []byte +) + +func file_temporal_server_chasm_lib_channel_proto_v1_channel_state_proto_rawDescGZIP() []byte { + file_temporal_server_chasm_lib_channel_proto_v1_channel_state_proto_rawDescOnce.Do(func() { + file_temporal_server_chasm_lib_channel_proto_v1_channel_state_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_temporal_server_chasm_lib_channel_proto_v1_channel_state_proto_rawDesc), len(file_temporal_server_chasm_lib_channel_proto_v1_channel_state_proto_rawDesc))) + }) + return file_temporal_server_chasm_lib_channel_proto_v1_channel_state_proto_rawDescData +} + +var file_temporal_server_chasm_lib_channel_proto_v1_channel_state_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_temporal_server_chasm_lib_channel_proto_v1_channel_state_proto_goTypes = []any{ + (*ChannelState)(nil), // 0: temporal.server.chasm.lib.channel.proto.v1.ChannelState + (*Notification)(nil), // 1: temporal.server.chasm.lib.channel.proto.v1.Notification + (*timestamppb.Timestamp)(nil), // 2: google.protobuf.Timestamp +} +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 +} + +func init() { file_temporal_server_chasm_lib_channel_proto_v1_channel_state_proto_init() } +func file_temporal_server_chasm_lib_channel_proto_v1_channel_state_proto_init() { + if File_temporal_server_chasm_lib_channel_proto_v1_channel_state_proto != nil { + return + } + file_temporal_server_chasm_lib_channel_proto_v1_message_proto_init() + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_temporal_server_chasm_lib_channel_proto_v1_channel_state_proto_rawDesc), len(file_temporal_server_chasm_lib_channel_proto_v1_channel_state_proto_rawDesc)), + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_temporal_server_chasm_lib_channel_proto_v1_channel_state_proto_goTypes, + DependencyIndexes: file_temporal_server_chasm_lib_channel_proto_v1_channel_state_proto_depIdxs, + MessageInfos: file_temporal_server_chasm_lib_channel_proto_v1_channel_state_proto_msgTypes, + }.Build() + File_temporal_server_chasm_lib_channel_proto_v1_channel_state_proto = out.File + file_temporal_server_chasm_lib_channel_proto_v1_channel_state_proto_goTypes = nil + file_temporal_server_chasm_lib_channel_proto_v1_channel_state_proto_depIdxs = nil +} diff --git a/chasm/lib/channel/gen/channelpb/v1/message.go-helpers.pb.go b/chasm/lib/channel/gen/channelpb/v1/message.go-helpers.pb.go new file mode 100644 index 00000000000..f5089f2e084 --- /dev/null +++ b/chasm/lib/channel/gen/channelpb/v1/message.go-helpers.pb.go @@ -0,0 +1,117 @@ +// Code generated by protoc-gen-go-helpers. DO NOT EDIT. +package channelpb + +import ( + "google.golang.org/protobuf/proto" +) + +// Marshal an object of type Notification to the protobuf v3 wire format +func (val *Notification) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type Notification from the protobuf v3 wire format +func (val *Notification) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *Notification) Size() int { + return proto.Size(val) +} + +// Equal returns whether two Notification values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *Notification) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *Notification + switch t := that.(type) { + case *Notification: + that1 = t + case Notification: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type WorkflowListener to the protobuf v3 wire format +func (val *WorkflowListener) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type WorkflowListener from the protobuf v3 wire format +func (val *WorkflowListener) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *WorkflowListener) Size() int { + return proto.Size(val) +} + +// Equal returns whether two WorkflowListener values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *WorkflowListener) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *WorkflowListener + switch t := that.(type) { + case *WorkflowListener: + that1 = t + case WorkflowListener: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type CallbackListener to the protobuf v3 wire format +func (val *CallbackListener) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type CallbackListener from the protobuf v3 wire format +func (val *CallbackListener) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *CallbackListener) Size() int { + return proto.Size(val) +} + +// Equal returns whether two CallbackListener values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *CallbackListener) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *CallbackListener + switch t := that.(type) { + case *CallbackListener: + that1 = t + case CallbackListener: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} diff --git a/chasm/lib/channel/gen/channelpb/v1/message.pb.go b/chasm/lib/channel/gen/channelpb/v1/message.pb.go new file mode 100644 index 00000000000..dd8e8b7bff7 --- /dev/null +++ b/chasm/lib/channel/gen/channelpb/v1/message.pb.go @@ -0,0 +1,404 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// plugins: +// protoc-gen-go +// protoc +// source: temporal/server/chasm/lib/channel/proto/v1/message.proto + +package channelpb + +import ( + reflect "reflect" + sync "sync" + unsafe "unsafe" + + v1 "go.temporal.io/api/common/v1" + v11 "go.temporal.io/api/failure/v1" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// Notification has the fields of temporal.api.notification.v1.Notification +// under the same names, so the stored form and the JSON a callback listener +// receives read the same as the public message. +type Notification struct { + state protoimpl.MessageState `protogen:"open.v1"` + Channel string `protobuf:"bytes,1,opt,name=channel,proto3" json:"channel,omitempty"` + // 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"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Notification) Reset() { + *x = Notification{} + mi := &file_temporal_server_chasm_lib_channel_proto_v1_message_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Notification) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Notification) ProtoMessage() {} + +func (x *Notification) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_channel_proto_v1_message_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Notification.ProtoReflect.Descriptor instead. +func (*Notification) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_channel_proto_v1_message_proto_rawDescGZIP(), []int{0} +} + +func (x *Notification) GetChannel() string { + if x != nil { + return x.Channel + } + return "" +} + +func (x *Notification) GetPosition() []byte { + if x != nil { + return x.Position + } + return nil +} + +func (x *Notification) GetCounter() int64 { + if x != nil { + return x.Counter + } + return 0 +} + +func (x *Notification) GetMetadata() map[string]*v1.Payload { + if x != nil { + return x.Metadata + } + return nil +} + +// WorkflowListener is a workflow run that subscribed to the channel. +type WorkflowListener struct { + state protoimpl.MessageState `protogen:"open.v1"` + WorkflowId string `protobuf:"bytes,1,opt,name=workflow_id,json=workflowId,proto3" json:"workflow_id,omitempty"` + // The run that subscribed, or the one the subscription was re-keyed to. + RunId string `protobuf:"bytes,2,opt,name=run_id,json=runId,proto3" json:"run_id,omitempty"` + FirstExecutionRunId string `protobuf:"bytes,3,opt,name=first_execution_run_id,json=firstExecutionRunId,proto3" json:"first_execution_run_id,omitempty"` + RegisteredTime *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=registered_time,json=registeredTime,proto3" json:"registered_time,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WorkflowListener) Reset() { + *x = WorkflowListener{} + mi := &file_temporal_server_chasm_lib_channel_proto_v1_message_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WorkflowListener) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WorkflowListener) ProtoMessage() {} + +func (x *WorkflowListener) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_channel_proto_v1_message_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WorkflowListener.ProtoReflect.Descriptor instead. +func (*WorkflowListener) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_channel_proto_v1_message_proto_rawDescGZIP(), []int{1} +} + +func (x *WorkflowListener) GetWorkflowId() string { + if x != nil { + return x.WorkflowId + } + return "" +} + +func (x *WorkflowListener) GetRunId() string { + if x != nil { + return x.RunId + } + return "" +} + +func (x *WorkflowListener) GetFirstExecutionRunId() string { + if x != nil { + return x.FirstExecutionRunId + } + return "" +} + +func (x *WorkflowListener) GetRegisteredTime() *timestamppb.Timestamp { + if x != nil { + return x.RegisteredTime + } + return nil +} + +// CallbackListener is an HTTP endpoint the channel posts its notifications to. +type CallbackListener struct { + state protoimpl.MessageState `protogen:"open.v1"` + ListenerId string `protobuf:"bytes,1,opt,name=listener_id,json=listenerId,proto3" json:"listener_id,omitempty"` + Callback *v1.Callback `protobuf:"bytes,2,opt,name=callback,proto3" json:"callback,omitempty"` + // Request id of the registration, so a retried registration finds this + // listener rather than adding another. + RequestId string `protobuf:"bytes,3,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + RegisteredTime *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=registered_time,json=registeredTime,proto3" json:"registered_time,omitempty"` + // The notification being delivered, if any. One delivery is in flight at a + // time per listener. + InFlight *Notification `protobuf:"bytes,5,opt,name=in_flight,json=inFlight,proto3" json:"in_flight,omitempty"` + // The latest notification that arrived while one was in flight. A newer one + // replaces it, and it is sent when the in-flight delivery is done. + Pending *Notification `protobuf:"bytes,6,opt,name=pending,proto3" json:"pending,omitempty"` + // Highest counter handed to this listener, in flight or pending, so the + // fan-out hands each notification once. + HandedCounter int64 `protobuf:"varint,7,opt,name=handed_counter,json=handedCounter,proto3" json:"handed_counter,omitempty"` + // Attempts made at the in-flight delivery. + Attempt int32 `protobuf:"varint,8,opt,name=attempt,proto3" json:"attempt,omitempty"` + NextAttemptTime *timestamppb.Timestamp `protobuf:"bytes,9,opt,name=next_attempt_time,json=nextAttemptTime,proto3" json:"next_attempt_time,omitempty"` + LastAttemptFailure *v11.Failure `protobuf:"bytes,10,opt,name=last_attempt_failure,json=lastAttemptFailure,proto3" json:"last_attempt_failure,omitempty"` + // Bumped for every delivery task scheduled, so a task names one attempt of + // one delivery and a stale one finds nothing to do. + TaskSequence int64 `protobuf:"varint,11,opt,name=task_sequence,json=taskSequence,proto3" json:"task_sequence,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CallbackListener) Reset() { + *x = CallbackListener{} + mi := &file_temporal_server_chasm_lib_channel_proto_v1_message_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CallbackListener) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CallbackListener) ProtoMessage() {} + +func (x *CallbackListener) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_channel_proto_v1_message_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CallbackListener.ProtoReflect.Descriptor instead. +func (*CallbackListener) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_channel_proto_v1_message_proto_rawDescGZIP(), []int{2} +} + +func (x *CallbackListener) GetListenerId() string { + if x != nil { + return x.ListenerId + } + return "" +} + +func (x *CallbackListener) GetCallback() *v1.Callback { + if x != nil { + return x.Callback + } + return nil +} + +func (x *CallbackListener) GetRequestId() string { + if x != nil { + return x.RequestId + } + return "" +} + +func (x *CallbackListener) GetRegisteredTime() *timestamppb.Timestamp { + if x != nil { + return x.RegisteredTime + } + return nil +} + +func (x *CallbackListener) GetInFlight() *Notification { + if x != nil { + return x.InFlight + } + return nil +} + +func (x *CallbackListener) GetPending() *Notification { + if x != nil { + return x.Pending + } + return nil +} + +func (x *CallbackListener) GetHandedCounter() int64 { + if x != nil { + return x.HandedCounter + } + return 0 +} + +func (x *CallbackListener) GetAttempt() int32 { + if x != nil { + return x.Attempt + } + return 0 +} + +func (x *CallbackListener) GetNextAttemptTime() *timestamppb.Timestamp { + if x != nil { + return x.NextAttemptTime + } + return nil +} + +func (x *CallbackListener) GetLastAttemptFailure() *v11.Failure { + if x != nil { + return x.LastAttemptFailure + } + return nil +} + +func (x *CallbackListener) GetTaskSequence() int64 { + if x != nil { + return x.TaskSequence + } + return 0 +} + +var File_temporal_server_chasm_lib_channel_proto_v1_message_proto protoreflect.FileDescriptor + +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" + + "\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" + + "\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" + + "\x10WorkflowListener\x12\x1f\n" + + "\vworkflow_id\x18\x01 \x01(\tR\n" + + "workflowId\x12\x15\n" + + "\x06run_id\x18\x02 \x01(\tR\x05runId\x123\n" + + "\x16first_execution_run_id\x18\x03 \x01(\tR\x13firstExecutionRunId\x12C\n" + + "\x0fregistered_time\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\x0eregisteredTime\"\x82\x05\n" + + "\x10CallbackListener\x12\x1f\n" + + "\vlistener_id\x18\x01 \x01(\tR\n" + + "listenerId\x12<\n" + + "\bcallback\x18\x02 \x01(\v2 .temporal.api.common.v1.CallbackR\bcallback\x12\x1d\n" + + "\n" + + "request_id\x18\x03 \x01(\tR\trequestId\x12C\n" + + "\x0fregistered_time\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\x0eregisteredTime\x12U\n" + + "\tin_flight\x18\x05 \x01(\v28.temporal.server.chasm.lib.channel.proto.v1.NotificationR\binFlight\x12R\n" + + "\apending\x18\x06 \x01(\v28.temporal.server.chasm.lib.channel.proto.v1.NotificationR\apending\x12%\n" + + "\x0ehanded_counter\x18\a \x01(\x03R\rhandedCounter\x12\x18\n" + + "\aattempt\x18\b \x01(\x05R\aattempt\x12F\n" + + "\x11next_attempt_time\x18\t \x01(\v2\x1a.google.protobuf.TimestampR\x0fnextAttemptTime\x12R\n" + + "\x14last_attempt_failure\x18\n" + + " \x01(\v2 .temporal.api.failure.v1.FailureR\x12lastAttemptFailure\x12#\n" + + "\rtask_sequence\x18\v \x01(\x03R\ftaskSequenceBAZ?go.temporal.io/server/chasm/lib/channel/gen/channelpb;channelpbb\x06proto3" + +var ( + file_temporal_server_chasm_lib_channel_proto_v1_message_proto_rawDescOnce sync.Once + file_temporal_server_chasm_lib_channel_proto_v1_message_proto_rawDescData []byte +) + +func file_temporal_server_chasm_lib_channel_proto_v1_message_proto_rawDescGZIP() []byte { + file_temporal_server_chasm_lib_channel_proto_v1_message_proto_rawDescOnce.Do(func() { + file_temporal_server_chasm_lib_channel_proto_v1_message_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_temporal_server_chasm_lib_channel_proto_v1_message_proto_rawDesc), len(file_temporal_server_chasm_lib_channel_proto_v1_message_proto_rawDesc))) + }) + return file_temporal_server_chasm_lib_channel_proto_v1_message_proto_rawDescData +} + +var file_temporal_server_chasm_lib_channel_proto_v1_message_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_temporal_server_chasm_lib_channel_proto_v1_message_proto_goTypes = []any{ + (*Notification)(nil), // 0: temporal.server.chasm.lib.channel.proto.v1.Notification + (*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 +} +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 +} + +func init() { file_temporal_server_chasm_lib_channel_proto_v1_message_proto_init() } +func file_temporal_server_chasm_lib_channel_proto_v1_message_proto_init() { + if File_temporal_server_chasm_lib_channel_proto_v1_message_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_temporal_server_chasm_lib_channel_proto_v1_message_proto_rawDesc), len(file_temporal_server_chasm_lib_channel_proto_v1_message_proto_rawDesc)), + NumEnums: 0, + NumMessages: 4, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_temporal_server_chasm_lib_channel_proto_v1_message_proto_goTypes, + DependencyIndexes: file_temporal_server_chasm_lib_channel_proto_v1_message_proto_depIdxs, + MessageInfos: file_temporal_server_chasm_lib_channel_proto_v1_message_proto_msgTypes, + }.Build() + File_temporal_server_chasm_lib_channel_proto_v1_message_proto = out.File + file_temporal_server_chasm_lib_channel_proto_v1_message_proto_goTypes = nil + file_temporal_server_chasm_lib_channel_proto_v1_message_proto_depIdxs = nil +} diff --git a/chasm/lib/channel/gen/channelpb/v1/namespace.go b/chasm/lib/channel/gen/channelpb/v1/namespace.go new file mode 100644 index 00000000000..7609409c75f --- /dev/null +++ b/chasm/lib/channel/gen/channelpb/v1/namespace.go @@ -0,0 +1,35 @@ +package channelpb + +// Every RPC here carries its namespace inside `frontend_request`, because the +// top-level field is the namespace id the frontend resolves before routing. +// The interceptors find a request's namespace through `GetNamespace() string` +// on the request itself, and the generator cannot point that at a nested +// field, so the getters are written here. + +func (x *NotifyChannelRequest) GetNamespace() string { + return x.GetFrontendRequest().GetNamespace() +} + +func (x *RegisterChannelListenerRequest) GetNamespace() string { + return x.GetFrontendRequest().GetNamespace() +} + +func (x *UnregisterChannelListenerRequest) GetNamespace() string { + return x.GetFrontendRequest().GetNamespace() +} + +func (x *PollChannelRequest) GetNamespace() string { + return x.GetFrontendRequest().GetNamespace() +} + +func (x *DescribeChannelRequest) GetNamespace() string { + return x.GetFrontendRequest().GetNamespace() +} + +func (x *RegisterWorkflowListenerRequest) GetNamespace() string { + return x.GetFrontendRequest().GetNamespace() +} + +func (x *DeliverChannelNotificationRequest) GetNamespace() string { + return x.GetFrontendRequest().GetNamespace() +} diff --git a/chasm/lib/channel/gen/channelpb/v1/namespace_test.go b/chasm/lib/channel/gen/channelpb/v1/namespace_test.go new file mode 100644 index 00000000000..3b4af3fd53d --- /dev/null +++ b/chasm/lib/channel/gen/channelpb/v1/namespace_test.go @@ -0,0 +1,38 @@ +package channelpb + +import ( + "testing" + + "go.temporal.io/server/common/rpc/interceptor" + "google.golang.org/protobuf/reflect/protoregistry" +) + +// Every request carrying a frontend_request must expose its namespace to the +// interceptors. Driven off the descriptor rather than a hand-written list so a +// new RPC fails here instead of silently opting out of namespace rate limits, +// validation, authorization and redirection. +func TestEveryRoutedRequestExposesNamespace(t *testing.T) { + fd := File_temporal_server_chasm_lib_channel_proto_v1_request_response_proto + messages := fd.Messages() + + checked := 0 + for i := 0; i < messages.Len(); i++ { + md := messages.Get(i) + if md.Fields().ByName("frontend_request") == nil { + continue + } + mt, err := protoregistry.GlobalTypes.FindMessageByName(md.FullName()) + if err != nil { + t.Fatalf("%s is not registered: %v", md.FullName(), err) + } + msg := mt.New().Interface() + if _, ok := msg.(interceptor.NamespaceNameGetter); !ok { + t.Errorf("%s has a frontend_request but no GetNamespace; add it in namespace.go", md.FullName()) + } + checked++ + } + + if checked == 0 { + t.Fatal("found no routed requests, so this test is not checking anything") + } +} diff --git a/chasm/lib/channel/gen/channelpb/v1/request_response.go-helpers.pb.go b/chasm/lib/channel/gen/channelpb/v1/request_response.go-helpers.pb.go new file mode 100644 index 00000000000..da334dad83c --- /dev/null +++ b/chasm/lib/channel/gen/channelpb/v1/request_response.go-helpers.pb.go @@ -0,0 +1,1079 @@ +// Code generated by protoc-gen-go-helpers. DO NOT EDIT. +package channelpb + +import ( + "google.golang.org/protobuf/proto" +) + +// Marshal an object of type NotifyChannelInput to the protobuf v3 wire format +func (val *NotifyChannelInput) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type NotifyChannelInput from the protobuf v3 wire format +func (val *NotifyChannelInput) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *NotifyChannelInput) Size() int { + return proto.Size(val) +} + +// Equal returns whether two NotifyChannelInput values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *NotifyChannelInput) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *NotifyChannelInput + switch t := that.(type) { + case *NotifyChannelInput: + that1 = t + case NotifyChannelInput: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type NotifyChannelOutput to the protobuf v3 wire format +func (val *NotifyChannelOutput) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type NotifyChannelOutput from the protobuf v3 wire format +func (val *NotifyChannelOutput) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *NotifyChannelOutput) Size() int { + return proto.Size(val) +} + +// Equal returns whether two NotifyChannelOutput values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *NotifyChannelOutput) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *NotifyChannelOutput + switch t := that.(type) { + case *NotifyChannelOutput: + that1 = t + case NotifyChannelOutput: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type NotifyChannelRequest to the protobuf v3 wire format +func (val *NotifyChannelRequest) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type NotifyChannelRequest from the protobuf v3 wire format +func (val *NotifyChannelRequest) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *NotifyChannelRequest) Size() int { + return proto.Size(val) +} + +// Equal returns whether two NotifyChannelRequest values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *NotifyChannelRequest) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *NotifyChannelRequest + switch t := that.(type) { + case *NotifyChannelRequest: + that1 = t + case NotifyChannelRequest: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type NotifyChannelResponse to the protobuf v3 wire format +func (val *NotifyChannelResponse) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type NotifyChannelResponse from the protobuf v3 wire format +func (val *NotifyChannelResponse) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *NotifyChannelResponse) Size() int { + return proto.Size(val) +} + +// Equal returns whether two NotifyChannelResponse values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *NotifyChannelResponse) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *NotifyChannelResponse + switch t := that.(type) { + case *NotifyChannelResponse: + that1 = t + case NotifyChannelResponse: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type RegisterChannelListenerInput to the protobuf v3 wire format +func (val *RegisterChannelListenerInput) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type RegisterChannelListenerInput from the protobuf v3 wire format +func (val *RegisterChannelListenerInput) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *RegisterChannelListenerInput) Size() int { + return proto.Size(val) +} + +// Equal returns whether two RegisterChannelListenerInput values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *RegisterChannelListenerInput) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *RegisterChannelListenerInput + switch t := that.(type) { + case *RegisterChannelListenerInput: + that1 = t + case RegisterChannelListenerInput: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type RegisterChannelListenerOutput to the protobuf v3 wire format +func (val *RegisterChannelListenerOutput) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type RegisterChannelListenerOutput from the protobuf v3 wire format +func (val *RegisterChannelListenerOutput) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *RegisterChannelListenerOutput) Size() int { + return proto.Size(val) +} + +// Equal returns whether two RegisterChannelListenerOutput values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *RegisterChannelListenerOutput) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *RegisterChannelListenerOutput + switch t := that.(type) { + case *RegisterChannelListenerOutput: + that1 = t + case RegisterChannelListenerOutput: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type RegisterChannelListenerRequest to the protobuf v3 wire format +func (val *RegisterChannelListenerRequest) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type RegisterChannelListenerRequest from the protobuf v3 wire format +func (val *RegisterChannelListenerRequest) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *RegisterChannelListenerRequest) Size() int { + return proto.Size(val) +} + +// Equal returns whether two RegisterChannelListenerRequest values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *RegisterChannelListenerRequest) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *RegisterChannelListenerRequest + switch t := that.(type) { + case *RegisterChannelListenerRequest: + that1 = t + case RegisterChannelListenerRequest: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type RegisterChannelListenerResponse to the protobuf v3 wire format +func (val *RegisterChannelListenerResponse) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type RegisterChannelListenerResponse from the protobuf v3 wire format +func (val *RegisterChannelListenerResponse) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *RegisterChannelListenerResponse) Size() int { + return proto.Size(val) +} + +// Equal returns whether two RegisterChannelListenerResponse values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *RegisterChannelListenerResponse) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *RegisterChannelListenerResponse + switch t := that.(type) { + case *RegisterChannelListenerResponse: + that1 = t + case RegisterChannelListenerResponse: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type UnregisterChannelListenerInput to the protobuf v3 wire format +func (val *UnregisterChannelListenerInput) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type UnregisterChannelListenerInput from the protobuf v3 wire format +func (val *UnregisterChannelListenerInput) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *UnregisterChannelListenerInput) Size() int { + return proto.Size(val) +} + +// Equal returns whether two UnregisterChannelListenerInput values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *UnregisterChannelListenerInput) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *UnregisterChannelListenerInput + switch t := that.(type) { + case *UnregisterChannelListenerInput: + that1 = t + case UnregisterChannelListenerInput: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type UnregisterChannelListenerOutput to the protobuf v3 wire format +func (val *UnregisterChannelListenerOutput) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type UnregisterChannelListenerOutput from the protobuf v3 wire format +func (val *UnregisterChannelListenerOutput) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *UnregisterChannelListenerOutput) Size() int { + return proto.Size(val) +} + +// Equal returns whether two UnregisterChannelListenerOutput values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *UnregisterChannelListenerOutput) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *UnregisterChannelListenerOutput + switch t := that.(type) { + case *UnregisterChannelListenerOutput: + that1 = t + case UnregisterChannelListenerOutput: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type UnregisterChannelListenerRequest to the protobuf v3 wire format +func (val *UnregisterChannelListenerRequest) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type UnregisterChannelListenerRequest from the protobuf v3 wire format +func (val *UnregisterChannelListenerRequest) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *UnregisterChannelListenerRequest) Size() int { + return proto.Size(val) +} + +// Equal returns whether two UnregisterChannelListenerRequest values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *UnregisterChannelListenerRequest) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *UnregisterChannelListenerRequest + switch t := that.(type) { + case *UnregisterChannelListenerRequest: + that1 = t + case UnregisterChannelListenerRequest: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type UnregisterChannelListenerResponse to the protobuf v3 wire format +func (val *UnregisterChannelListenerResponse) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type UnregisterChannelListenerResponse from the protobuf v3 wire format +func (val *UnregisterChannelListenerResponse) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *UnregisterChannelListenerResponse) Size() int { + return proto.Size(val) +} + +// Equal returns whether two UnregisterChannelListenerResponse values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *UnregisterChannelListenerResponse) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *UnregisterChannelListenerResponse + switch t := that.(type) { + case *UnregisterChannelListenerResponse: + that1 = t + case UnregisterChannelListenerResponse: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type PollChannelInput to the protobuf v3 wire format +func (val *PollChannelInput) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type PollChannelInput from the protobuf v3 wire format +func (val *PollChannelInput) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *PollChannelInput) Size() int { + return proto.Size(val) +} + +// Equal returns whether two PollChannelInput values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *PollChannelInput) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *PollChannelInput + switch t := that.(type) { + case *PollChannelInput: + that1 = t + case PollChannelInput: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type PollChannelOutput to the protobuf v3 wire format +func (val *PollChannelOutput) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type PollChannelOutput from the protobuf v3 wire format +func (val *PollChannelOutput) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *PollChannelOutput) Size() int { + return proto.Size(val) +} + +// Equal returns whether two PollChannelOutput values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *PollChannelOutput) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *PollChannelOutput + switch t := that.(type) { + case *PollChannelOutput: + that1 = t + case PollChannelOutput: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type PollChannelRequest to the protobuf v3 wire format +func (val *PollChannelRequest) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type PollChannelRequest from the protobuf v3 wire format +func (val *PollChannelRequest) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *PollChannelRequest) Size() int { + return proto.Size(val) +} + +// Equal returns whether two PollChannelRequest values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *PollChannelRequest) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *PollChannelRequest + switch t := that.(type) { + case *PollChannelRequest: + that1 = t + case PollChannelRequest: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type PollChannelResponse to the protobuf v3 wire format +func (val *PollChannelResponse) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type PollChannelResponse from the protobuf v3 wire format +func (val *PollChannelResponse) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *PollChannelResponse) Size() int { + return proto.Size(val) +} + +// Equal returns whether two PollChannelResponse values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *PollChannelResponse) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *PollChannelResponse + switch t := that.(type) { + case *PollChannelResponse: + that1 = t + case PollChannelResponse: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type DescribeChannelInput to the protobuf v3 wire format +func (val *DescribeChannelInput) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type DescribeChannelInput from the protobuf v3 wire format +func (val *DescribeChannelInput) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *DescribeChannelInput) Size() int { + return proto.Size(val) +} + +// Equal returns whether two DescribeChannelInput values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *DescribeChannelInput) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *DescribeChannelInput + switch t := that.(type) { + case *DescribeChannelInput: + that1 = t + case DescribeChannelInput: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type ChannelListenerInfo to the protobuf v3 wire format +func (val *ChannelListenerInfo) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type ChannelListenerInfo from the protobuf v3 wire format +func (val *ChannelListenerInfo) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *ChannelListenerInfo) Size() int { + return proto.Size(val) +} + +// Equal returns whether two ChannelListenerInfo values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *ChannelListenerInfo) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *ChannelListenerInfo + switch t := that.(type) { + case *ChannelListenerInfo: + that1 = t + case ChannelListenerInfo: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type DescribeChannelOutput to the protobuf v3 wire format +func (val *DescribeChannelOutput) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type DescribeChannelOutput from the protobuf v3 wire format +func (val *DescribeChannelOutput) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *DescribeChannelOutput) Size() int { + return proto.Size(val) +} + +// Equal returns whether two DescribeChannelOutput values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *DescribeChannelOutput) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *DescribeChannelOutput + switch t := that.(type) { + case *DescribeChannelOutput: + that1 = t + case DescribeChannelOutput: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type DescribeChannelRequest to the protobuf v3 wire format +func (val *DescribeChannelRequest) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type DescribeChannelRequest from the protobuf v3 wire format +func (val *DescribeChannelRequest) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *DescribeChannelRequest) Size() int { + return proto.Size(val) +} + +// Equal returns whether two DescribeChannelRequest values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *DescribeChannelRequest) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *DescribeChannelRequest + switch t := that.(type) { + case *DescribeChannelRequest: + that1 = t + case DescribeChannelRequest: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type DescribeChannelResponse to the protobuf v3 wire format +func (val *DescribeChannelResponse) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type DescribeChannelResponse from the protobuf v3 wire format +func (val *DescribeChannelResponse) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *DescribeChannelResponse) Size() int { + return proto.Size(val) +} + +// Equal returns whether two DescribeChannelResponse values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *DescribeChannelResponse) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *DescribeChannelResponse + switch t := that.(type) { + case *DescribeChannelResponse: + that1 = t + case DescribeChannelResponse: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type RegisterWorkflowListenerInput to the protobuf v3 wire format +func (val *RegisterWorkflowListenerInput) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type RegisterWorkflowListenerInput from the protobuf v3 wire format +func (val *RegisterWorkflowListenerInput) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *RegisterWorkflowListenerInput) Size() int { + return proto.Size(val) +} + +// Equal returns whether two RegisterWorkflowListenerInput values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *RegisterWorkflowListenerInput) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *RegisterWorkflowListenerInput + switch t := that.(type) { + case *RegisterWorkflowListenerInput: + that1 = t + case RegisterWorkflowListenerInput: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type RegisterWorkflowListenerOutput to the protobuf v3 wire format +func (val *RegisterWorkflowListenerOutput) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type RegisterWorkflowListenerOutput from the protobuf v3 wire format +func (val *RegisterWorkflowListenerOutput) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *RegisterWorkflowListenerOutput) Size() int { + return proto.Size(val) +} + +// Equal returns whether two RegisterWorkflowListenerOutput values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *RegisterWorkflowListenerOutput) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *RegisterWorkflowListenerOutput + switch t := that.(type) { + case *RegisterWorkflowListenerOutput: + that1 = t + case RegisterWorkflowListenerOutput: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type RegisterWorkflowListenerRequest to the protobuf v3 wire format +func (val *RegisterWorkflowListenerRequest) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type RegisterWorkflowListenerRequest from the protobuf v3 wire format +func (val *RegisterWorkflowListenerRequest) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *RegisterWorkflowListenerRequest) Size() int { + return proto.Size(val) +} + +// Equal returns whether two RegisterWorkflowListenerRequest values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *RegisterWorkflowListenerRequest) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *RegisterWorkflowListenerRequest + switch t := that.(type) { + case *RegisterWorkflowListenerRequest: + that1 = t + case RegisterWorkflowListenerRequest: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type RegisterWorkflowListenerResponse to the protobuf v3 wire format +func (val *RegisterWorkflowListenerResponse) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type RegisterWorkflowListenerResponse from the protobuf v3 wire format +func (val *RegisterWorkflowListenerResponse) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *RegisterWorkflowListenerResponse) Size() int { + return proto.Size(val) +} + +// Equal returns whether two RegisterWorkflowListenerResponse values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *RegisterWorkflowListenerResponse) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *RegisterWorkflowListenerResponse + switch t := that.(type) { + case *RegisterWorkflowListenerResponse: + that1 = t + case RegisterWorkflowListenerResponse: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type DeliverChannelNotificationInput to the protobuf v3 wire format +func (val *DeliverChannelNotificationInput) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type DeliverChannelNotificationInput from the protobuf v3 wire format +func (val *DeliverChannelNotificationInput) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *DeliverChannelNotificationInput) Size() int { + return proto.Size(val) +} + +// Equal returns whether two DeliverChannelNotificationInput values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *DeliverChannelNotificationInput) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *DeliverChannelNotificationInput + switch t := that.(type) { + case *DeliverChannelNotificationInput: + that1 = t + case DeliverChannelNotificationInput: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type DeliverChannelNotificationOutput to the protobuf v3 wire format +func (val *DeliverChannelNotificationOutput) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type DeliverChannelNotificationOutput from the protobuf v3 wire format +func (val *DeliverChannelNotificationOutput) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *DeliverChannelNotificationOutput) Size() int { + return proto.Size(val) +} + +// Equal returns whether two DeliverChannelNotificationOutput values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *DeliverChannelNotificationOutput) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *DeliverChannelNotificationOutput + switch t := that.(type) { + case *DeliverChannelNotificationOutput: + that1 = t + case DeliverChannelNotificationOutput: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type DeliverChannelNotificationRequest to the protobuf v3 wire format +func (val *DeliverChannelNotificationRequest) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type DeliverChannelNotificationRequest from the protobuf v3 wire format +func (val *DeliverChannelNotificationRequest) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *DeliverChannelNotificationRequest) Size() int { + return proto.Size(val) +} + +// Equal returns whether two DeliverChannelNotificationRequest values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *DeliverChannelNotificationRequest) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *DeliverChannelNotificationRequest + switch t := that.(type) { + case *DeliverChannelNotificationRequest: + that1 = t + case DeliverChannelNotificationRequest: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type DeliverChannelNotificationResponse to the protobuf v3 wire format +func (val *DeliverChannelNotificationResponse) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type DeliverChannelNotificationResponse from the protobuf v3 wire format +func (val *DeliverChannelNotificationResponse) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *DeliverChannelNotificationResponse) Size() int { + return proto.Size(val) +} + +// Equal returns whether two DeliverChannelNotificationResponse values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *DeliverChannelNotificationResponse) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *DeliverChannelNotificationResponse + switch t := that.(type) { + case *DeliverChannelNotificationResponse: + that1 = t + case DeliverChannelNotificationResponse: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} diff --git a/chasm/lib/channel/gen/channelpb/v1/request_response.pb.go b/chasm/lib/channel/gen/channelpb/v1/request_response.pb.go new file mode 100644 index 00000000000..d907286eb39 --- /dev/null +++ b/chasm/lib/channel/gen/channelpb/v1/request_response.pb.go @@ -0,0 +1,1913 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// plugins: +// protoc-gen-go +// protoc +// source: temporal/server/chasm/lib/channel/proto/v1/request_response.proto + +package channelpb + +import ( + reflect "reflect" + sync "sync" + unsafe "unsafe" + + v1 "go.temporal.io/api/common/v1" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + durationpb "google.golang.org/protobuf/types/known/durationpb" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type 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 +} + +func (x *NotifyChannelInput) Reset() { + *x = NotifyChannelInput{} + mi := &file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *NotifyChannelInput) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NotifyChannelInput) ProtoMessage() {} + +func (x *NotifyChannelInput) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use NotifyChannelInput.ProtoReflect.Descriptor instead. +func (*NotifyChannelInput) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_rawDescGZIP(), []int{0} +} + +func (x *NotifyChannelInput) GetNamespace() string { + if x != nil { + return x.Namespace + } + return "" +} + +func (x *NotifyChannelInput) GetNotification() *Notification { + if x != nil { + return x.Notification + } + return nil +} + +func (x *NotifyChannelInput) GetIdentity() string { + if x != nil { + return x.Identity + } + return "" +} + +func (x *NotifyChannelInput) GetRequestId() string { + if x != nil { + return x.RequestId + } + return "" +} + +type NotifyChannelOutput struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Listeners registered when the notification was accepted. + ListenerCount int64 `protobuf:"varint,1,opt,name=listener_count,json=listenerCount,proto3" json:"listener_count,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *NotifyChannelOutput) Reset() { + *x = NotifyChannelOutput{} + mi := &file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *NotifyChannelOutput) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NotifyChannelOutput) ProtoMessage() {} + +func (x *NotifyChannelOutput) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use NotifyChannelOutput.ProtoReflect.Descriptor instead. +func (*NotifyChannelOutput) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_rawDescGZIP(), []int{1} +} + +func (x *NotifyChannelOutput) GetListenerCount() int64 { + if x != nil { + return x.ListenerCount + } + return 0 +} + +type NotifyChannelRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + NamespaceId string `protobuf:"bytes,1,opt,name=namespace_id,json=namespaceId,proto3" json:"namespace_id,omitempty"` + FrontendRequest *NotifyChannelInput `protobuf:"bytes,2,opt,name=frontend_request,json=frontendRequest,proto3" json:"frontend_request,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *NotifyChannelRequest) Reset() { + *x = NotifyChannelRequest{} + mi := &file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *NotifyChannelRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NotifyChannelRequest) ProtoMessage() {} + +func (x *NotifyChannelRequest) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use NotifyChannelRequest.ProtoReflect.Descriptor instead. +func (*NotifyChannelRequest) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_rawDescGZIP(), []int{2} +} + +func (x *NotifyChannelRequest) GetNamespaceId() string { + if x != nil { + return x.NamespaceId + } + return "" +} + +func (x *NotifyChannelRequest) GetFrontendRequest() *NotifyChannelInput { + if x != nil { + return x.FrontendRequest + } + return nil +} + +type NotifyChannelResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + FrontendResponse *NotifyChannelOutput `protobuf:"bytes,1,opt,name=frontend_response,json=frontendResponse,proto3" json:"frontend_response,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *NotifyChannelResponse) Reset() { + *x = NotifyChannelResponse{} + mi := &file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *NotifyChannelResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NotifyChannelResponse) ProtoMessage() {} + +func (x *NotifyChannelResponse) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use NotifyChannelResponse.ProtoReflect.Descriptor instead. +func (*NotifyChannelResponse) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_rawDescGZIP(), []int{3} +} + +func (x *NotifyChannelResponse) GetFrontendResponse() *NotifyChannelOutput { + if x != nil { + return x.FrontendResponse + } + return nil +} + +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 +} + +func (x *RegisterChannelListenerInput) Reset() { + *x = RegisterChannelListenerInput{} + mi := &file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RegisterChannelListenerInput) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RegisterChannelListenerInput) ProtoMessage() {} + +func (x *RegisterChannelListenerInput) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RegisterChannelListenerInput.ProtoReflect.Descriptor instead. +func (*RegisterChannelListenerInput) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_rawDescGZIP(), []int{4} +} + +func (x *RegisterChannelListenerInput) GetNamespace() string { + if x != nil { + return x.Namespace + } + return "" +} + +func (x *RegisterChannelListenerInput) GetChannel() string { + if x != nil { + return x.Channel + } + return "" +} + +func (x *RegisterChannelListenerInput) GetCallback() *v1.Callback { + if x != nil { + return x.Callback + } + return nil +} + +func (x *RegisterChannelListenerInput) GetRequestId() string { + if x != nil { + return x.RequestId + } + return "" +} + +func (x *RegisterChannelListenerInput) GetIdentity() string { + if x != nil { + return x.Identity + } + return "" +} + +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"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RegisterChannelListenerOutput) Reset() { + *x = RegisterChannelListenerOutput{} + mi := &file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RegisterChannelListenerOutput) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RegisterChannelListenerOutput) ProtoMessage() {} + +func (x *RegisterChannelListenerOutput) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RegisterChannelListenerOutput.ProtoReflect.Descriptor instead. +func (*RegisterChannelListenerOutput) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_rawDescGZIP(), []int{5} +} + +func (x *RegisterChannelListenerOutput) GetListenerId() string { + if x != nil { + return x.ListenerId + } + return "" +} + +type RegisterChannelListenerRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + NamespaceId string `protobuf:"bytes,1,opt,name=namespace_id,json=namespaceId,proto3" json:"namespace_id,omitempty"` + FrontendRequest *RegisterChannelListenerInput `protobuf:"bytes,2,opt,name=frontend_request,json=frontendRequest,proto3" json:"frontend_request,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RegisterChannelListenerRequest) Reset() { + *x = RegisterChannelListenerRequest{} + mi := &file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RegisterChannelListenerRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RegisterChannelListenerRequest) ProtoMessage() {} + +func (x *RegisterChannelListenerRequest) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RegisterChannelListenerRequest.ProtoReflect.Descriptor instead. +func (*RegisterChannelListenerRequest) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_rawDescGZIP(), []int{6} +} + +func (x *RegisterChannelListenerRequest) GetNamespaceId() string { + if x != nil { + return x.NamespaceId + } + return "" +} + +func (x *RegisterChannelListenerRequest) GetFrontendRequest() *RegisterChannelListenerInput { + if x != nil { + return x.FrontendRequest + } + return nil +} + +type RegisterChannelListenerResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + FrontendResponse *RegisterChannelListenerOutput `protobuf:"bytes,1,opt,name=frontend_response,json=frontendResponse,proto3" json:"frontend_response,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RegisterChannelListenerResponse) Reset() { + *x = RegisterChannelListenerResponse{} + mi := &file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RegisterChannelListenerResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RegisterChannelListenerResponse) ProtoMessage() {} + +func (x *RegisterChannelListenerResponse) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RegisterChannelListenerResponse.ProtoReflect.Descriptor instead. +func (*RegisterChannelListenerResponse) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_rawDescGZIP(), []int{7} +} + +func (x *RegisterChannelListenerResponse) GetFrontendResponse() *RegisterChannelListenerOutput { + if x != nil { + return x.FrontendResponse + } + return nil +} + +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 +} + +func (x *UnregisterChannelListenerInput) Reset() { + *x = UnregisterChannelListenerInput{} + mi := &file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UnregisterChannelListenerInput) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UnregisterChannelListenerInput) ProtoMessage() {} + +func (x *UnregisterChannelListenerInput) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UnregisterChannelListenerInput.ProtoReflect.Descriptor instead. +func (*UnregisterChannelListenerInput) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_rawDescGZIP(), []int{8} +} + +func (x *UnregisterChannelListenerInput) GetNamespace() string { + if x != nil { + return x.Namespace + } + return "" +} + +func (x *UnregisterChannelListenerInput) GetChannel() string { + if x != nil { + return x.Channel + } + return "" +} + +func (x *UnregisterChannelListenerInput) GetListenerId() string { + if x != nil { + return x.ListenerId + } + return "" +} + +func (x *UnregisterChannelListenerInput) GetIdentity() string { + if x != nil { + return x.Identity + } + return "" +} + +type UnregisterChannelListenerOutput struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UnregisterChannelListenerOutput) Reset() { + *x = UnregisterChannelListenerOutput{} + mi := &file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UnregisterChannelListenerOutput) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UnregisterChannelListenerOutput) ProtoMessage() {} + +func (x *UnregisterChannelListenerOutput) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UnregisterChannelListenerOutput.ProtoReflect.Descriptor instead. +func (*UnregisterChannelListenerOutput) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_rawDescGZIP(), []int{9} +} + +type UnregisterChannelListenerRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + NamespaceId string `protobuf:"bytes,1,opt,name=namespace_id,json=namespaceId,proto3" json:"namespace_id,omitempty"` + FrontendRequest *UnregisterChannelListenerInput `protobuf:"bytes,2,opt,name=frontend_request,json=frontendRequest,proto3" json:"frontend_request,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UnregisterChannelListenerRequest) Reset() { + *x = UnregisterChannelListenerRequest{} + mi := &file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UnregisterChannelListenerRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UnregisterChannelListenerRequest) ProtoMessage() {} + +func (x *UnregisterChannelListenerRequest) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UnregisterChannelListenerRequest.ProtoReflect.Descriptor instead. +func (*UnregisterChannelListenerRequest) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_rawDescGZIP(), []int{10} +} + +func (x *UnregisterChannelListenerRequest) GetNamespaceId() string { + if x != nil { + return x.NamespaceId + } + return "" +} + +func (x *UnregisterChannelListenerRequest) GetFrontendRequest() *UnregisterChannelListenerInput { + if x != nil { + return x.FrontendRequest + } + return nil +} + +type UnregisterChannelListenerResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + FrontendResponse *UnregisterChannelListenerOutput `protobuf:"bytes,1,opt,name=frontend_response,json=frontendResponse,proto3" json:"frontend_response,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UnregisterChannelListenerResponse) Reset() { + *x = UnregisterChannelListenerResponse{} + mi := &file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UnregisterChannelListenerResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UnregisterChannelListenerResponse) ProtoMessage() {} + +func (x *UnregisterChannelListenerResponse) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UnregisterChannelListenerResponse.ProtoReflect.Descriptor instead. +func (*UnregisterChannelListenerResponse) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_rawDescGZIP(), []int{11} +} + +func (x *UnregisterChannelListenerResponse) GetFrontendResponse() *UnregisterChannelListenerOutput { + if x != nil { + return x.FrontendResponse + } + return nil +} + +type PollChannelInput 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"` + // Return retained notifications with a counter above this one. + AfterCounter int64 `protobuf:"varint,3,opt,name=after_counter,json=afterCounter,proto3" json:"after_counter,omitempty"` + // How long to wait for one when none is retained above after_counter. + // Bounded by the server's long-poll timeout. Zero answers at once. + 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 +} + +func (x *PollChannelInput) Reset() { + *x = PollChannelInput{} + mi := &file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PollChannelInput) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PollChannelInput) ProtoMessage() {} + +func (x *PollChannelInput) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_msgTypes[12] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PollChannelInput.ProtoReflect.Descriptor instead. +func (*PollChannelInput) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_rawDescGZIP(), []int{12} +} + +func (x *PollChannelInput) GetNamespace() string { + if x != nil { + return x.Namespace + } + return "" +} + +func (x *PollChannelInput) GetChannel() string { + if x != nil { + return x.Channel + } + return "" +} + +func (x *PollChannelInput) GetAfterCounter() int64 { + if x != nil { + return x.AfterCounter + } + return 0 +} + +func (x *PollChannelInput) GetWait() *durationpb.Duration { + if x != nil { + return x.Wait + } + return nil +} + +func (x *PollChannelInput) GetMaxNotifications() int32 { + if x != nil { + return x.MaxNotifications + } + return 0 +} + +type PollChannelOutput struct { + state protoimpl.MessageState `protogen:"open.v1"` + Notifications []*Notification `protobuf:"bytes,1,rep,name=notifications,proto3" json:"notifications,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PollChannelOutput) Reset() { + *x = PollChannelOutput{} + mi := &file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PollChannelOutput) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PollChannelOutput) ProtoMessage() {} + +func (x *PollChannelOutput) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_msgTypes[13] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PollChannelOutput.ProtoReflect.Descriptor instead. +func (*PollChannelOutput) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_rawDescGZIP(), []int{13} +} + +func (x *PollChannelOutput) GetNotifications() []*Notification { + if x != nil { + return x.Notifications + } + return nil +} + +type PollChannelRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + NamespaceId string `protobuf:"bytes,1,opt,name=namespace_id,json=namespaceId,proto3" json:"namespace_id,omitempty"` + FrontendRequest *PollChannelInput `protobuf:"bytes,2,opt,name=frontend_request,json=frontendRequest,proto3" json:"frontend_request,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PollChannelRequest) Reset() { + *x = PollChannelRequest{} + mi := &file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PollChannelRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PollChannelRequest) ProtoMessage() {} + +func (x *PollChannelRequest) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_msgTypes[14] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PollChannelRequest.ProtoReflect.Descriptor instead. +func (*PollChannelRequest) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_rawDescGZIP(), []int{14} +} + +func (x *PollChannelRequest) GetNamespaceId() string { + if x != nil { + return x.NamespaceId + } + return "" +} + +func (x *PollChannelRequest) GetFrontendRequest() *PollChannelInput { + if x != nil { + return x.FrontendRequest + } + return nil +} + +type PollChannelResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + FrontendResponse *PollChannelOutput `protobuf:"bytes,1,opt,name=frontend_response,json=frontendResponse,proto3" json:"frontend_response,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PollChannelResponse) Reset() { + *x = PollChannelResponse{} + mi := &file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PollChannelResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PollChannelResponse) ProtoMessage() {} + +func (x *PollChannelResponse) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_msgTypes[15] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PollChannelResponse.ProtoReflect.Descriptor instead. +func (*PollChannelResponse) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_rawDescGZIP(), []int{15} +} + +func (x *PollChannelResponse) GetFrontendResponse() *PollChannelOutput { + if x != nil { + return x.FrontendResponse + } + return nil +} + +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 +} + +func (x *DescribeChannelInput) Reset() { + *x = DescribeChannelInput{} + mi := &file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DescribeChannelInput) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DescribeChannelInput) ProtoMessage() {} + +func (x *DescribeChannelInput) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_msgTypes[16] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DescribeChannelInput.ProtoReflect.Descriptor instead. +func (*DescribeChannelInput) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_rawDescGZIP(), []int{16} +} + +func (x *DescribeChannelInput) GetNamespace() string { + if x != nil { + return x.Namespace + } + return "" +} + +func (x *DescribeChannelInput) GetChannel() string { + if x != nil { + return x.Channel + } + return "" +} + +// ChannelListenerInfo has the fields of temporal.api.notification.v1.ChannelListener. +type ChannelListenerInfo struct { + state protoimpl.MessageState `protogen:"open.v1"` + ListenerId string `protobuf:"bytes,1,opt,name=listener_id,json=listenerId,proto3" json:"listener_id,omitempty"` + // Types that are valid to be assigned to Variant: + // + // *ChannelListenerInfo_Workflow_ + // *ChannelListenerInfo_Callback + Variant isChannelListenerInfo_Variant `protobuf_oneof:"variant"` + RegisteredTime *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=registered_time,json=registeredTime,proto3" json:"registered_time,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ChannelListenerInfo) Reset() { + *x = ChannelListenerInfo{} + mi := &file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ChannelListenerInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ChannelListenerInfo) ProtoMessage() {} + +func (x *ChannelListenerInfo) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_msgTypes[17] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ChannelListenerInfo.ProtoReflect.Descriptor instead. +func (*ChannelListenerInfo) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_rawDescGZIP(), []int{17} +} + +func (x *ChannelListenerInfo) GetListenerId() string { + if x != nil { + return x.ListenerId + } + return "" +} + +func (x *ChannelListenerInfo) GetVariant() isChannelListenerInfo_Variant { + if x != nil { + return x.Variant + } + return nil +} + +func (x *ChannelListenerInfo) GetWorkflow() *ChannelListenerInfo_Workflow { + if x != nil { + if x, ok := x.Variant.(*ChannelListenerInfo_Workflow_); ok { + return x.Workflow + } + } + return nil +} + +func (x *ChannelListenerInfo) GetCallback() *v1.Callback { + if x != nil { + if x, ok := x.Variant.(*ChannelListenerInfo_Callback); ok { + return x.Callback + } + } + return nil +} + +func (x *ChannelListenerInfo) GetRegisteredTime() *timestamppb.Timestamp { + if x != nil { + return x.RegisteredTime + } + return nil +} + +type isChannelListenerInfo_Variant interface { + isChannelListenerInfo_Variant() +} + +type ChannelListenerInfo_Workflow_ struct { + Workflow *ChannelListenerInfo_Workflow `protobuf:"bytes,2,opt,name=workflow,proto3,oneof"` +} + +type ChannelListenerInfo_Callback struct { + Callback *v1.Callback `protobuf:"bytes,3,opt,name=callback,proto3,oneof"` +} + +func (*ChannelListenerInfo_Workflow_) isChannelListenerInfo_Variant() {} + +func (*ChannelListenerInfo_Callback) isChannelListenerInfo_Variant() {} + +type DescribeChannelOutput struct { + state protoimpl.MessageState `protogen:"open.v1"` + 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"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DescribeChannelOutput) Reset() { + *x = DescribeChannelOutput{} + mi := &file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_msgTypes[18] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DescribeChannelOutput) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DescribeChannelOutput) ProtoMessage() {} + +func (x *DescribeChannelOutput) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_msgTypes[18] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DescribeChannelOutput.ProtoReflect.Descriptor instead. +func (*DescribeChannelOutput) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_rawDescGZIP(), []int{18} +} + +func (x *DescribeChannelOutput) GetListeners() []*ChannelListenerInfo { + if x != nil { + return x.Listeners + } + return nil +} + +func (x *DescribeChannelOutput) GetLatest() *Notification { + if x != nil { + return x.Latest + } + return nil +} + +func (x *DescribeChannelOutput) GetRetainedCount() int64 { + if x != nil { + return x.RetainedCount + } + return 0 +} + +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"` + FrontendRequest *DescribeChannelInput `protobuf:"bytes,2,opt,name=frontend_request,json=frontendRequest,proto3" json:"frontend_request,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DescribeChannelRequest) Reset() { + *x = DescribeChannelRequest{} + mi := &file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DescribeChannelRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DescribeChannelRequest) ProtoMessage() {} + +func (x *DescribeChannelRequest) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_msgTypes[19] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DescribeChannelRequest.ProtoReflect.Descriptor instead. +func (*DescribeChannelRequest) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_rawDescGZIP(), []int{19} +} + +func (x *DescribeChannelRequest) GetNamespaceId() string { + if x != nil { + return x.NamespaceId + } + return "" +} + +func (x *DescribeChannelRequest) GetFrontendRequest() *DescribeChannelInput { + if x != nil { + return x.FrontendRequest + } + return nil +} + +type DescribeChannelResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + FrontendResponse *DescribeChannelOutput `protobuf:"bytes,1,opt,name=frontend_response,json=frontendResponse,proto3" json:"frontend_response,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DescribeChannelResponse) Reset() { + *x = DescribeChannelResponse{} + mi := &file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_msgTypes[20] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DescribeChannelResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DescribeChannelResponse) ProtoMessage() {} + +func (x *DescribeChannelResponse) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_msgTypes[20] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DescribeChannelResponse.ProtoReflect.Descriptor instead. +func (*DescribeChannelResponse) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_rawDescGZIP(), []int{20} +} + +func (x *DescribeChannelResponse) GetFrontendResponse() *DescribeChannelOutput { + if x != nil { + return x.FrontendResponse + } + return nil +} + +type RegisterWorkflowListenerInput 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"` + WorkflowId string `protobuf:"bytes,3,opt,name=workflow_id,json=workflowId,proto3" json:"workflow_id,omitempty"` + RunId string `protobuf:"bytes,4,opt,name=run_id,json=runId,proto3" json:"run_id,omitempty"` + FirstExecutionRunId string `protobuf:"bytes,5,opt,name=first_execution_run_id,json=firstExecutionRunId,proto3" json:"first_execution_run_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RegisterWorkflowListenerInput) Reset() { + *x = RegisterWorkflowListenerInput{} + mi := &file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_msgTypes[21] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RegisterWorkflowListenerInput) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RegisterWorkflowListenerInput) ProtoMessage() {} + +func (x *RegisterWorkflowListenerInput) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_msgTypes[21] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RegisterWorkflowListenerInput.ProtoReflect.Descriptor instead. +func (*RegisterWorkflowListenerInput) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_rawDescGZIP(), []int{21} +} + +func (x *RegisterWorkflowListenerInput) GetNamespace() string { + if x != nil { + return x.Namespace + } + return "" +} + +func (x *RegisterWorkflowListenerInput) GetChannel() string { + if x != nil { + return x.Channel + } + return "" +} + +func (x *RegisterWorkflowListenerInput) GetWorkflowId() string { + if x != nil { + return x.WorkflowId + } + return "" +} + +func (x *RegisterWorkflowListenerInput) GetRunId() string { + if x != nil { + return x.RunId + } + return "" +} + +func (x *RegisterWorkflowListenerInput) GetFirstExecutionRunId() string { + if x != nil { + return x.FirstExecutionRunId + } + return "" +} + +type RegisterWorkflowListenerOutput struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The channel's latest notification, when the run is new to the channel. + // The run takes it as pending, so a write that landed before the run was + // registered still wakes it. + Latest *Notification `protobuf:"bytes,1,opt,name=latest,proto3" json:"latest,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RegisterWorkflowListenerOutput) Reset() { + *x = RegisterWorkflowListenerOutput{} + mi := &file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_msgTypes[22] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RegisterWorkflowListenerOutput) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RegisterWorkflowListenerOutput) ProtoMessage() {} + +func (x *RegisterWorkflowListenerOutput) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_msgTypes[22] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RegisterWorkflowListenerOutput.ProtoReflect.Descriptor instead. +func (*RegisterWorkflowListenerOutput) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_rawDescGZIP(), []int{22} +} + +func (x *RegisterWorkflowListenerOutput) GetLatest() *Notification { + if x != nil { + return x.Latest + } + return nil +} + +type RegisterWorkflowListenerRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + NamespaceId string `protobuf:"bytes,1,opt,name=namespace_id,json=namespaceId,proto3" json:"namespace_id,omitempty"` + FrontendRequest *RegisterWorkflowListenerInput `protobuf:"bytes,2,opt,name=frontend_request,json=frontendRequest,proto3" json:"frontend_request,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RegisterWorkflowListenerRequest) Reset() { + *x = RegisterWorkflowListenerRequest{} + mi := &file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_msgTypes[23] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RegisterWorkflowListenerRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RegisterWorkflowListenerRequest) ProtoMessage() {} + +func (x *RegisterWorkflowListenerRequest) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_msgTypes[23] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RegisterWorkflowListenerRequest.ProtoReflect.Descriptor instead. +func (*RegisterWorkflowListenerRequest) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_rawDescGZIP(), []int{23} +} + +func (x *RegisterWorkflowListenerRequest) GetNamespaceId() string { + if x != nil { + return x.NamespaceId + } + return "" +} + +func (x *RegisterWorkflowListenerRequest) GetFrontendRequest() *RegisterWorkflowListenerInput { + if x != nil { + return x.FrontendRequest + } + return nil +} + +type RegisterWorkflowListenerResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + FrontendResponse *RegisterWorkflowListenerOutput `protobuf:"bytes,1,opt,name=frontend_response,json=frontendResponse,proto3" json:"frontend_response,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RegisterWorkflowListenerResponse) Reset() { + *x = RegisterWorkflowListenerResponse{} + mi := &file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_msgTypes[24] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RegisterWorkflowListenerResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RegisterWorkflowListenerResponse) ProtoMessage() {} + +func (x *RegisterWorkflowListenerResponse) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_msgTypes[24] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RegisterWorkflowListenerResponse.ProtoReflect.Descriptor instead. +func (*RegisterWorkflowListenerResponse) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_rawDescGZIP(), []int{24} +} + +func (x *RegisterWorkflowListenerResponse) GetFrontendResponse() *RegisterWorkflowListenerOutput { + if x != nil { + return x.FrontendResponse + } + return nil +} + +type DeliverChannelNotificationInput struct { + state protoimpl.MessageState `protogen:"open.v1"` + Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"` + WorkflowId string `protobuf:"bytes,2,opt,name=workflow_id,json=workflowId,proto3" json:"workflow_id,omitempty"` + // The run the channel has on record. If it is closed, the current run is + // asked instead. + RunId string `protobuf:"bytes,3,opt,name=run_id,json=runId,proto3" json:"run_id,omitempty"` + Notification *Notification `protobuf:"bytes,4,opt,name=notification,proto3" json:"notification,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeliverChannelNotificationInput) Reset() { + *x = DeliverChannelNotificationInput{} + mi := &file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_msgTypes[25] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeliverChannelNotificationInput) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeliverChannelNotificationInput) ProtoMessage() {} + +func (x *DeliverChannelNotificationInput) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_msgTypes[25] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeliverChannelNotificationInput.ProtoReflect.Descriptor instead. +func (*DeliverChannelNotificationInput) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_rawDescGZIP(), []int{25} +} + +func (x *DeliverChannelNotificationInput) GetNamespace() string { + if x != nil { + return x.Namespace + } + return "" +} + +func (x *DeliverChannelNotificationInput) GetWorkflowId() string { + if x != nil { + return x.WorkflowId + } + return "" +} + +func (x *DeliverChannelNotificationInput) GetRunId() string { + if x != nil { + return x.RunId + } + return "" +} + +func (x *DeliverChannelNotificationInput) GetNotification() *Notification { + if x != nil { + return x.Notification + } + return nil +} + +type DeliverChannelNotificationOutput struct { + state protoimpl.MessageState `protogen:"open.v1"` + // No run of the workflow listens to the channel any more, so the channel + // drops the listener. + ListenerClosed bool `protobuf:"varint,1,opt,name=listener_closed,json=listenerClosed,proto3" json:"listener_closed,omitempty"` + // The run on record is closed and the current run listens, so the channel + // re-keys the listener to it. + SuccessorRunId string `protobuf:"bytes,2,opt,name=successor_run_id,json=successorRunId,proto3" json:"successor_run_id,omitempty"` + // The notification joined one the run had not seen yet. + Folded bool `protobuf:"varint,3,opt,name=folded,proto3" json:"folded,omitempty"` + // The run holds a pending notification at this counter or above, so + // nothing changed. + Duplicate bool `protobuf:"varint,4,opt,name=duplicate,proto3" json:"duplicate,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeliverChannelNotificationOutput) Reset() { + *x = DeliverChannelNotificationOutput{} + mi := &file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_msgTypes[26] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeliverChannelNotificationOutput) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeliverChannelNotificationOutput) ProtoMessage() {} + +func (x *DeliverChannelNotificationOutput) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_msgTypes[26] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeliverChannelNotificationOutput.ProtoReflect.Descriptor instead. +func (*DeliverChannelNotificationOutput) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_rawDescGZIP(), []int{26} +} + +func (x *DeliverChannelNotificationOutput) GetListenerClosed() bool { + if x != nil { + return x.ListenerClosed + } + return false +} + +func (x *DeliverChannelNotificationOutput) GetSuccessorRunId() string { + if x != nil { + return x.SuccessorRunId + } + return "" +} + +func (x *DeliverChannelNotificationOutput) GetFolded() bool { + if x != nil { + return x.Folded + } + return false +} + +func (x *DeliverChannelNotificationOutput) GetDuplicate() bool { + if x != nil { + return x.Duplicate + } + return false +} + +type DeliverChannelNotificationRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + NamespaceId string `protobuf:"bytes,1,opt,name=namespace_id,json=namespaceId,proto3" json:"namespace_id,omitempty"` + FrontendRequest *DeliverChannelNotificationInput `protobuf:"bytes,2,opt,name=frontend_request,json=frontendRequest,proto3" json:"frontend_request,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeliverChannelNotificationRequest) Reset() { + *x = DeliverChannelNotificationRequest{} + mi := &file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_msgTypes[27] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeliverChannelNotificationRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeliverChannelNotificationRequest) ProtoMessage() {} + +func (x *DeliverChannelNotificationRequest) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_msgTypes[27] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeliverChannelNotificationRequest.ProtoReflect.Descriptor instead. +func (*DeliverChannelNotificationRequest) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_rawDescGZIP(), []int{27} +} + +func (x *DeliverChannelNotificationRequest) GetNamespaceId() string { + if x != nil { + return x.NamespaceId + } + return "" +} + +func (x *DeliverChannelNotificationRequest) GetFrontendRequest() *DeliverChannelNotificationInput { + if x != nil { + return x.FrontendRequest + } + return nil +} + +type DeliverChannelNotificationResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + FrontendResponse *DeliverChannelNotificationOutput `protobuf:"bytes,1,opt,name=frontend_response,json=frontendResponse,proto3" json:"frontend_response,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeliverChannelNotificationResponse) Reset() { + *x = DeliverChannelNotificationResponse{} + mi := &file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_msgTypes[28] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeliverChannelNotificationResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeliverChannelNotificationResponse) ProtoMessage() {} + +func (x *DeliverChannelNotificationResponse) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_msgTypes[28] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeliverChannelNotificationResponse.ProtoReflect.Descriptor instead. +func (*DeliverChannelNotificationResponse) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_rawDescGZIP(), []int{28} +} + +func (x *DeliverChannelNotificationResponse) GetFrontendResponse() *DeliverChannelNotificationOutput { + if x != nil { + return x.FrontendResponse + } + return nil +} + +type ChannelListenerInfo_Workflow struct { + state protoimpl.MessageState `protogen:"open.v1"` + WorkflowId string `protobuf:"bytes,1,opt,name=workflow_id,json=workflowId,proto3" json:"workflow_id,omitempty"` + RunId string `protobuf:"bytes,2,opt,name=run_id,json=runId,proto3" json:"run_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ChannelListenerInfo_Workflow) Reset() { + *x = ChannelListenerInfo_Workflow{} + mi := &file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_msgTypes[29] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ChannelListenerInfo_Workflow) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ChannelListenerInfo_Workflow) ProtoMessage() {} + +func (x *ChannelListenerInfo_Workflow) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_msgTypes[29] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ChannelListenerInfo_Workflow.ProtoReflect.Descriptor instead. +func (*ChannelListenerInfo_Workflow) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_rawDescGZIP(), []int{17, 0} +} + +func (x *ChannelListenerInfo_Workflow) GetWorkflowId() string { + if x != nil { + return x.WorkflowId + } + return "" +} + +func (x *ChannelListenerInfo_Workflow) GetRunId() string { + if x != nil { + return x.RunId + } + return "" +} + +var File_temporal_server_chasm_lib_channel_proto_v1_request_response_proto protoreflect.FileDescriptor + +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" + + "\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" + + "\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" + + "\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" + + "\x1dRegisterChannelListenerOutput\x12\x1f\n" + + "\vlistener_id\x18\x01 \x01(\tR\n" + + "listenerId\"\xb8\x01\n" + + "\x1eRegisterChannelListenerRequest\x12!\n" + + "\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" + + "\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" + + "\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" + + "\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" + + "\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" + + "\x14DescribeChannelInput\x12\x1c\n" + + "\tnamespace\x18\x01 \x01(\tR\tnamespace\x12\x18\n" + + "\achannel\x18\x02 \x01(\tR\achannel\"\xf2\x02\n" + + "\x13ChannelListenerInfo\x12\x1f\n" + + "\vlistener_id\x18\x01 \x01(\tR\n" + + "listenerId\x12f\n" + + "\bworkflow\x18\x02 \x01(\v2H.temporal.server.chasm.lib.channel.proto.v1.ChannelListenerInfo.WorkflowH\x00R\bworkflow\x12>\n" + + "\bcallback\x18\x03 \x01(\v2 .temporal.api.common.v1.CallbackH\x00R\bcallback\x12C\n" + + "\x0fregistered_time\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\x0eregisteredTime\x1aB\n" + + "\bWorkflow\x12\x1f\n" + + "\vworkflow_id\x18\x01 \x01(\tR\n" + + "workflowId\x12\x15\n" + + "\x06run_id\x18\x02 \x01(\tR\x05runIdB\t\n" + + "\avariant\"\xef\x01\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" + + "\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" + + "\x17DescribeChannelResponse\x12n\n" + + "\x11frontend_response\x18\x01 \x01(\v2A.temporal.server.chasm.lib.channel.proto.v1.DescribeChannelOutputR\x10frontendResponse\"\xc4\x01\n" + + "\x1dRegisterWorkflowListenerInput\x12\x1c\n" + + "\tnamespace\x18\x01 \x01(\tR\tnamespace\x12\x18\n" + + "\achannel\x18\x02 \x01(\tR\achannel\x12\x1f\n" + + "\vworkflow_id\x18\x03 \x01(\tR\n" + + "workflowId\x12\x15\n" + + "\x06run_id\x18\x04 \x01(\tR\x05runId\x123\n" + + "\x16first_execution_run_id\x18\x05 \x01(\tR\x13firstExecutionRunId\"r\n" + + "\x1eRegisterWorkflowListenerOutput\x12P\n" + + "\x06latest\x18\x01 \x01(\v28.temporal.server.chasm.lib.channel.proto.v1.NotificationR\x06latest\"\xba\x01\n" + + "\x1fRegisterWorkflowListenerRequest\x12!\n" + + "\fnamespace_id\x18\x01 \x01(\tR\vnamespaceId\x12t\n" + + "\x10frontend_request\x18\x02 \x01(\v2I.temporal.server.chasm.lib.channel.proto.v1.RegisterWorkflowListenerInputR\x0ffrontendRequest\"\x9b\x01\n" + + " RegisterWorkflowListenerResponse\x12w\n" + + "\x11frontend_response\x18\x01 \x01(\v2J.temporal.server.chasm.lib.channel.proto.v1.RegisterWorkflowListenerOutputR\x10frontendResponse\"\xd5\x01\n" + + "\x1fDeliverChannelNotificationInput\x12\x1c\n" + + "\tnamespace\x18\x01 \x01(\tR\tnamespace\x12\x1f\n" + + "\vworkflow_id\x18\x02 \x01(\tR\n" + + "workflowId\x12\x15\n" + + "\x06run_id\x18\x03 \x01(\tR\x05runId\x12\\\n" + + "\fnotification\x18\x04 \x01(\v28.temporal.server.chasm.lib.channel.proto.v1.NotificationR\fnotification\"\xab\x01\n" + + " DeliverChannelNotificationOutput\x12'\n" + + "\x0flistener_closed\x18\x01 \x01(\bR\x0elistenerClosed\x12(\n" + + "\x10successor_run_id\x18\x02 \x01(\tR\x0esuccessorRunId\x12\x16\n" + + "\x06folded\x18\x03 \x01(\bR\x06folded\x12\x1c\n" + + "\tduplicate\x18\x04 \x01(\bR\tduplicate\"\xbe\x01\n" + + "!DeliverChannelNotificationRequest\x12!\n" + + "\fnamespace_id\x18\x01 \x01(\tR\vnamespaceId\x12v\n" + + "\x10frontend_request\x18\x02 \x01(\v2K.temporal.server.chasm.lib.channel.proto.v1.DeliverChannelNotificationInputR\x0ffrontendRequest\"\x9f\x01\n" + + "\"DeliverChannelNotificationResponse\x12y\n" + + "\x11frontend_response\x18\x01 \x01(\v2L.temporal.server.chasm.lib.channel.proto.v1.DeliverChannelNotificationOutputR\x10frontendResponseBAZ?go.temporal.io/server/chasm/lib/channel/gen/channelpb;channelpbb\x06proto3" + +var ( + file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_rawDescOnce sync.Once + file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_rawDescData []byte +) + +func file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_rawDescGZIP() []byte { + file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_rawDescOnce.Do(func() { + file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_rawDesc), len(file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_rawDesc))) + }) + return file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_rawDescData +} + +var file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_msgTypes = make([]protoimpl.MessageInfo, 30) +var file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_goTypes = []any{ + (*NotifyChannelInput)(nil), // 0: temporal.server.chasm.lib.channel.proto.v1.NotifyChannelInput + (*NotifyChannelOutput)(nil), // 1: temporal.server.chasm.lib.channel.proto.v1.NotifyChannelOutput + (*NotifyChannelRequest)(nil), // 2: temporal.server.chasm.lib.channel.proto.v1.NotifyChannelRequest + (*NotifyChannelResponse)(nil), // 3: temporal.server.chasm.lib.channel.proto.v1.NotifyChannelResponse + (*RegisterChannelListenerInput)(nil), // 4: temporal.server.chasm.lib.channel.proto.v1.RegisterChannelListenerInput + (*RegisterChannelListenerOutput)(nil), // 5: temporal.server.chasm.lib.channel.proto.v1.RegisterChannelListenerOutput + (*RegisterChannelListenerRequest)(nil), // 6: temporal.server.chasm.lib.channel.proto.v1.RegisterChannelListenerRequest + (*RegisterChannelListenerResponse)(nil), // 7: temporal.server.chasm.lib.channel.proto.v1.RegisterChannelListenerResponse + (*UnregisterChannelListenerInput)(nil), // 8: temporal.server.chasm.lib.channel.proto.v1.UnregisterChannelListenerInput + (*UnregisterChannelListenerOutput)(nil), // 9: temporal.server.chasm.lib.channel.proto.v1.UnregisterChannelListenerOutput + (*UnregisterChannelListenerRequest)(nil), // 10: temporal.server.chasm.lib.channel.proto.v1.UnregisterChannelListenerRequest + (*UnregisterChannelListenerResponse)(nil), // 11: temporal.server.chasm.lib.channel.proto.v1.UnregisterChannelListenerResponse + (*PollChannelInput)(nil), // 12: temporal.server.chasm.lib.channel.proto.v1.PollChannelInput + (*PollChannelOutput)(nil), // 13: temporal.server.chasm.lib.channel.proto.v1.PollChannelOutput + (*PollChannelRequest)(nil), // 14: temporal.server.chasm.lib.channel.proto.v1.PollChannelRequest + (*PollChannelResponse)(nil), // 15: temporal.server.chasm.lib.channel.proto.v1.PollChannelResponse + (*DescribeChannelInput)(nil), // 16: temporal.server.chasm.lib.channel.proto.v1.DescribeChannelInput + (*ChannelListenerInfo)(nil), // 17: temporal.server.chasm.lib.channel.proto.v1.ChannelListenerInfo + (*DescribeChannelOutput)(nil), // 18: temporal.server.chasm.lib.channel.proto.v1.DescribeChannelOutput + (*DescribeChannelRequest)(nil), // 19: temporal.server.chasm.lib.channel.proto.v1.DescribeChannelRequest + (*DescribeChannelResponse)(nil), // 20: temporal.server.chasm.lib.channel.proto.v1.DescribeChannelResponse + (*RegisterWorkflowListenerInput)(nil), // 21: temporal.server.chasm.lib.channel.proto.v1.RegisterWorkflowListenerInput + (*RegisterWorkflowListenerOutput)(nil), // 22: temporal.server.chasm.lib.channel.proto.v1.RegisterWorkflowListenerOutput + (*RegisterWorkflowListenerRequest)(nil), // 23: temporal.server.chasm.lib.channel.proto.v1.RegisterWorkflowListenerRequest + (*RegisterWorkflowListenerResponse)(nil), // 24: temporal.server.chasm.lib.channel.proto.v1.RegisterWorkflowListenerResponse + (*DeliverChannelNotificationInput)(nil), // 25: temporal.server.chasm.lib.channel.proto.v1.DeliverChannelNotificationInput + (*DeliverChannelNotificationOutput)(nil), // 26: temporal.server.chasm.lib.channel.proto.v1.DeliverChannelNotificationOutput + (*DeliverChannelNotificationRequest)(nil), // 27: temporal.server.chasm.lib.channel.proto.v1.DeliverChannelNotificationRequest + (*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 +} +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 +} + +func init() { file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_init() } +func file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_init() { + if File_temporal_server_chasm_lib_channel_proto_v1_request_response_proto != nil { + return + } + file_temporal_server_chasm_lib_channel_proto_v1_message_proto_init() + file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_msgTypes[17].OneofWrappers = []any{ + (*ChannelListenerInfo_Workflow_)(nil), + (*ChannelListenerInfo_Callback)(nil), + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_rawDesc), len(file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_rawDesc)), + NumEnums: 0, + NumMessages: 30, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_goTypes, + DependencyIndexes: file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_depIdxs, + MessageInfos: file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_msgTypes, + }.Build() + File_temporal_server_chasm_lib_channel_proto_v1_request_response_proto = out.File + file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_goTypes = nil + file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_depIdxs = nil +} diff --git a/chasm/lib/channel/gen/channelpb/v1/service.pb.go b/chasm/lib/channel/gen/channelpb/v1/service.pb.go new file mode 100644 index 00000000000..db157c165da --- /dev/null +++ b/chasm/lib/channel/gen/channelpb/v1/service.pb.go @@ -0,0 +1,100 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// plugins: +// protoc-gen-go +// protoc +// source: temporal/server/chasm/lib/channel/proto/v1/service.proto + +package channelpb + +import ( + reflect "reflect" + unsafe "unsafe" + + _ "go.temporal.io/server/api/common/v1" + _ "go.temporal.io/server/api/routing/v1" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +var File_temporal_server_chasm_lib_channel_proto_v1_service_proto protoreflect.FileDescriptor + +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" + + "\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" + + "\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" + +var file_temporal_server_chasm_lib_channel_proto_v1_service_proto_goTypes = []any{ + (*NotifyChannelRequest)(nil), // 0: temporal.server.chasm.lib.channel.proto.v1.NotifyChannelRequest + (*RegisterChannelListenerRequest)(nil), // 1: temporal.server.chasm.lib.channel.proto.v1.RegisterChannelListenerRequest + (*UnregisterChannelListenerRequest)(nil), // 2: temporal.server.chasm.lib.channel.proto.v1.UnregisterChannelListenerRequest + (*PollChannelRequest)(nil), // 3: temporal.server.chasm.lib.channel.proto.v1.PollChannelRequest + (*DescribeChannelRequest)(nil), // 4: temporal.server.chasm.lib.channel.proto.v1.DescribeChannelRequest + (*RegisterWorkflowListenerRequest)(nil), // 5: temporal.server.chasm.lib.channel.proto.v1.RegisterWorkflowListenerRequest + (*DeliverChannelNotificationRequest)(nil), // 6: temporal.server.chasm.lib.channel.proto.v1.DeliverChannelNotificationRequest + (*NotifyChannelResponse)(nil), // 7: temporal.server.chasm.lib.channel.proto.v1.NotifyChannelResponse + (*RegisterChannelListenerResponse)(nil), // 8: temporal.server.chasm.lib.channel.proto.v1.RegisterChannelListenerResponse + (*UnregisterChannelListenerResponse)(nil), // 9: temporal.server.chasm.lib.channel.proto.v1.UnregisterChannelListenerResponse + (*PollChannelResponse)(nil), // 10: temporal.server.chasm.lib.channel.proto.v1.PollChannelResponse + (*DescribeChannelResponse)(nil), // 11: temporal.server.chasm.lib.channel.proto.v1.DescribeChannelResponse + (*RegisterWorkflowListenerResponse)(nil), // 12: temporal.server.chasm.lib.channel.proto.v1.RegisterWorkflowListenerResponse + (*DeliverChannelNotificationResponse)(nil), // 13: temporal.server.chasm.lib.channel.proto.v1.DeliverChannelNotificationResponse +} +var file_temporal_server_chasm_lib_channel_proto_v1_service_proto_depIdxs = []int32{ + 0, // 0: temporal.server.chasm.lib.channel.proto.v1.ChannelService.NotifyChannel:input_type -> temporal.server.chasm.lib.channel.proto.v1.NotifyChannelRequest + 1, // 1: temporal.server.chasm.lib.channel.proto.v1.ChannelService.RegisterChannelListener:input_type -> temporal.server.chasm.lib.channel.proto.v1.RegisterChannelListenerRequest + 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, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_temporal_server_chasm_lib_channel_proto_v1_service_proto_init() } +func file_temporal_server_chasm_lib_channel_proto_v1_service_proto_init() { + if File_temporal_server_chasm_lib_channel_proto_v1_service_proto != nil { + return + } + file_temporal_server_chasm_lib_channel_proto_v1_request_response_proto_init() + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_temporal_server_chasm_lib_channel_proto_v1_service_proto_rawDesc), len(file_temporal_server_chasm_lib_channel_proto_v1_service_proto_rawDesc)), + NumEnums: 0, + NumMessages: 0, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_temporal_server_chasm_lib_channel_proto_v1_service_proto_goTypes, + DependencyIndexes: file_temporal_server_chasm_lib_channel_proto_v1_service_proto_depIdxs, + }.Build() + File_temporal_server_chasm_lib_channel_proto_v1_service_proto = out.File + file_temporal_server_chasm_lib_channel_proto_v1_service_proto_goTypes = nil + file_temporal_server_chasm_lib_channel_proto_v1_service_proto_depIdxs = nil +} diff --git a/chasm/lib/channel/gen/channelpb/v1/service_client.pb.go b/chasm/lib/channel/gen/channelpb/v1/service_client.pb.go new file mode 100644 index 00000000000..35982975c8d --- /dev/null +++ b/chasm/lib/channel/gen/channelpb/v1/service_client.pb.go @@ -0,0 +1,368 @@ +// Code generated by protoc-gen-go-chasm. DO NOT EDIT. +package channelpb + +import ( + "context" + "time" + + "go.temporal.io/server/client/history" + "go.temporal.io/server/common" + "go.temporal.io/server/common/backoff" + "go.temporal.io/server/common/config" + "go.temporal.io/server/common/dynamicconfig" + "go.temporal.io/server/common/headers" + "go.temporal.io/server/common/log" + "go.temporal.io/server/common/membership" + "go.temporal.io/server/common/metrics" + "go.temporal.io/server/common/primitives" + "go.uber.org/fx" + "google.golang.org/grpc" +) + +// ChannelServiceLayeredClient is a client for ChannelService. +type ChannelServiceLayeredClient struct { + metricsHandler metrics.Handler + numShards int32 + redirector history.Redirector[ChannelServiceClient] + retryPolicy backoff.RetryPolicy +} + +// NewChannelServiceLayeredClient initializes a new ChannelServiceLayeredClient. +func NewChannelServiceLayeredClient( + lc fx.Lifecycle, + dc *dynamicconfig.Collection, + rpcFactory common.RPCFactory, + monitor membership.Monitor, + config *config.Persistence, + logger log.Logger, + metricsHandler metrics.Handler, +) (ChannelServiceClient, error) { + resolver, err := monitor.GetResolver(primitives.HistoryService) + if err != nil { + return nil, err + } + connections := history.NewConnectionPool(resolver, rpcFactory, NewChannelServiceClient, logger, dynamicconfig.HistoryConnectionCloseDelay.Get(dc)) + var redirector history.Redirector[ChannelServiceClient] + if dynamicconfig.HistoryClientOwnershipCachingEnabled.Get(dc)() { + redirector = history.NewCachingRedirector( + connections, + resolver, + logger, + dynamicconfig.HistoryClientOwnershipCachingStaleTTL.Get(dc), + ) + } else { + redirector = history.NewBasicRedirector(connections, resolver) + } + client := &ChannelServiceLayeredClient{ + metricsHandler: metricsHandler, + redirector: redirector, + numShards: config.NumHistoryShards, + retryPolicy: common.CreateHistoryClientRetryPolicy(dynamicconfig.RetryUnboundedOnSystemResourceExhausted.Get(dc)), + } + lc.Append(fx.StopHook(client.Stop)) + return client, nil +} +func (c *ChannelServiceLayeredClient) Stop() { + c.redirector.Close() +} +func (c *ChannelServiceLayeredClient) callNotifyChannelNoRetry( + 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.NotifyChannel"), + 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().GetNotification().GetChannel(), 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.NotifyChannel(ctx, request, opts...) + return err + } + err = c.redirector.Execute(ctx, shardID, op) + return response, err +} +func (c *ChannelServiceLayeredClient) NotifyChannel( + ctx context.Context, + request *NotifyChannelRequest, + opts ...grpc.CallOption, +) (*NotifyChannelResponse, error) { + call := func(ctx context.Context) (*NotifyChannelResponse, error) { + return c.callNotifyChannelNoRetry(ctx, request, opts...) + } + return backoff.ThrottleRetryContextWithReturn(ctx, call, c.retryPolicy, common.IsServiceClientTransientError) +} +func (c *ChannelServiceLayeredClient) callRegisterChannelListenerNoRetry( + 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.RegisterChannelListener"), + 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().GetChannel(), 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.RegisterChannelListener(ctx, request, opts...) + return err + } + err = c.redirector.Execute(ctx, shardID, op) + return response, err +} +func (c *ChannelServiceLayeredClient) RegisterChannelListener( + ctx context.Context, + request *RegisterChannelListenerRequest, + opts ...grpc.CallOption, +) (*RegisterChannelListenerResponse, error) { + call := func(ctx context.Context) (*RegisterChannelListenerResponse, error) { + return c.callRegisterChannelListenerNoRetry(ctx, request, opts...) + } + return backoff.ThrottleRetryContextWithReturn(ctx, call, c.retryPolicy, common.IsServiceClientTransientError) +} +func (c *ChannelServiceLayeredClient) callUnregisterChannelListenerNoRetry( + 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.UnregisterChannelListener"), + 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().GetChannel(), 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.UnregisterChannelListener(ctx, request, opts...) + return err + } + err = c.redirector.Execute(ctx, shardID, op) + return response, err +} +func (c *ChannelServiceLayeredClient) UnregisterChannelListener( + ctx context.Context, + request *UnregisterChannelListenerRequest, + opts ...grpc.CallOption, +) (*UnregisterChannelListenerResponse, error) { + call := func(ctx context.Context) (*UnregisterChannelListenerResponse, error) { + return c.callUnregisterChannelListenerNoRetry(ctx, request, opts...) + } + return backoff.ThrottleRetryContextWithReturn(ctx, call, c.retryPolicy, common.IsServiceClientTransientError) +} +func (c *ChannelServiceLayeredClient) callPollChannelNoRetry( + 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.PollChannel"), + 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().GetChannel(), 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.PollChannel(ctx, request, opts...) + return err + } + err = c.redirector.Execute(ctx, shardID, op) + return response, err +} +func (c *ChannelServiceLayeredClient) PollChannel( + ctx context.Context, + request *PollChannelRequest, + opts ...grpc.CallOption, +) (*PollChannelResponse, error) { + call := func(ctx context.Context) (*PollChannelResponse, error) { + return c.callPollChannelNoRetry(ctx, request, opts...) + } + return backoff.ThrottleRetryContextWithReturn(ctx, call, c.retryPolicy, common.IsServiceClientTransientError) +} +func (c *ChannelServiceLayeredClient) callDescribeChannelNoRetry( + 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.DescribeChannel"), + 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().GetChannel(), 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.DescribeChannel(ctx, request, opts...) + return err + } + err = c.redirector.Execute(ctx, shardID, op) + return response, err +} +func (c *ChannelServiceLayeredClient) DescribeChannel( + ctx context.Context, + request *DescribeChannelRequest, + opts ...grpc.CallOption, +) (*DescribeChannelResponse, error) { + call := func(ctx context.Context) (*DescribeChannelResponse, error) { + return c.callDescribeChannelNoRetry(ctx, request, opts...) + } + return backoff.ThrottleRetryContextWithReturn(ctx, call, c.retryPolicy, common.IsServiceClientTransientError) +} +func (c *ChannelServiceLayeredClient) callRegisterWorkflowListenerNoRetry( + ctx context.Context, + request *RegisterWorkflowListenerRequest, + opts ...grpc.CallOption, +) (*RegisterWorkflowListenerResponse, error) { + var response *RegisterWorkflowListenerResponse + 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.RegisterWorkflowListener"), + 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().GetChannel(), 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.RegisterWorkflowListener(ctx, request, opts...) + return err + } + err = c.redirector.Execute(ctx, shardID, op) + return response, err +} +func (c *ChannelServiceLayeredClient) RegisterWorkflowListener( + ctx context.Context, + request *RegisterWorkflowListenerRequest, + opts ...grpc.CallOption, +) (*RegisterWorkflowListenerResponse, error) { + call := func(ctx context.Context) (*RegisterWorkflowListenerResponse, error) { + return c.callRegisterWorkflowListenerNoRetry(ctx, request, opts...) + } + return backoff.ThrottleRetryContextWithReturn(ctx, call, c.retryPolicy, common.IsServiceClientTransientError) +} +func (c *ChannelServiceLayeredClient) callDeliverChannelNotificationNoRetry( + ctx context.Context, + request *DeliverChannelNotificationRequest, + opts ...grpc.CallOption, +) (*DeliverChannelNotificationResponse, error) { + var response *DeliverChannelNotificationResponse + 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.DeliverChannelNotification"), + metrics.NamespaceTag(caller), + metrics.ServiceRoleTag(metrics.HistoryRoleTagValue), + ) + metrics.ClientRequests.With(metricsHandler).Record(1) + defer func() { + if err != nil { + metrics.ClientFailures.With(metricsHandler).Record(1, metrics.ServiceErrorTypeTag(err)) + } + metrics.ClientLatency.With(metricsHandler).Record(time.Since(startTime)) + }() + shardID := common.WorkflowIDToHistoryShard(request.GetNamespaceId(), request.GetFrontendRequest().GetWorkflowId(), c.numShards) + op := func(ctx context.Context, client ChannelServiceClient) error { + var err error + ctx, cancel := context.WithTimeout(ctx, history.DefaultTimeout) + defer cancel() + response, err = client.DeliverChannelNotification(ctx, request, opts...) + return err + } + err = c.redirector.Execute(ctx, shardID, op) + return response, err +} +func (c *ChannelServiceLayeredClient) DeliverChannelNotification( + ctx context.Context, + request *DeliverChannelNotificationRequest, + opts ...grpc.CallOption, +) (*DeliverChannelNotificationResponse, error) { + call := func(ctx context.Context) (*DeliverChannelNotificationResponse, error) { + return c.callDeliverChannelNotificationNoRetry(ctx, request, opts...) + } + return backoff.ThrottleRetryContextWithReturn(ctx, call, c.retryPolicy, common.IsServiceClientTransientError) +} diff --git a/chasm/lib/channel/gen/channelpb/v1/service_grpc.pb.go b/chasm/lib/channel/gen/channelpb/v1/service_grpc.pb.go new file mode 100644 index 00000000000..e37f6203c59 --- /dev/null +++ b/chasm/lib/channel/gen/channelpb/v1/service_grpc.pb.go @@ -0,0 +1,338 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// plugins: +// - protoc-gen-go-grpc +// - protoc +// source: temporal/server/chasm/lib/channel/proto/v1/service.proto + +package channelpb + +import ( + context "context" + + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +const ( + 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" +) + +// ChannelServiceClient is the client API for ChannelService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type ChannelServiceClient interface { + NotifyChannel(ctx context.Context, in *NotifyChannelRequest, opts ...grpc.CallOption) (*NotifyChannelResponse, error) + RegisterChannelListener(ctx context.Context, in *RegisterChannelListenerRequest, opts ...grpc.CallOption) (*RegisterChannelListenerResponse, error) + 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) + // 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) + // Internal. The channel's fan-out calls this on the listener's shard. + DeliverChannelNotification(ctx context.Context, in *DeliverChannelNotificationRequest, opts ...grpc.CallOption) (*DeliverChannelNotificationResponse, error) +} + +type channelServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewChannelServiceClient(cc grpc.ClientConnInterface) ChannelServiceClient { + return &channelServiceClient{cc} +} + +func (c *channelServiceClient) NotifyChannel(ctx context.Context, in *NotifyChannelRequest, opts ...grpc.CallOption) (*NotifyChannelResponse, error) { + out := new(NotifyChannelResponse) + err := c.cc.Invoke(ctx, ChannelService_NotifyChannel_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *channelServiceClient) RegisterChannelListener(ctx context.Context, in *RegisterChannelListenerRequest, opts ...grpc.CallOption) (*RegisterChannelListenerResponse, error) { + out := new(RegisterChannelListenerResponse) + err := c.cc.Invoke(ctx, ChannelService_RegisterChannelListener_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *channelServiceClient) UnregisterChannelListener(ctx context.Context, in *UnregisterChannelListenerRequest, opts ...grpc.CallOption) (*UnregisterChannelListenerResponse, error) { + out := new(UnregisterChannelListenerResponse) + err := c.cc.Invoke(ctx, ChannelService_UnregisterChannelListener_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *channelServiceClient) PollChannel(ctx context.Context, in *PollChannelRequest, opts ...grpc.CallOption) (*PollChannelResponse, error) { + out := new(PollChannelResponse) + err := c.cc.Invoke(ctx, ChannelService_PollChannel_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *channelServiceClient) DescribeChannel(ctx context.Context, in *DescribeChannelRequest, opts ...grpc.CallOption) (*DescribeChannelResponse, error) { + out := new(DescribeChannelResponse) + err := c.cc.Invoke(ctx, ChannelService_DescribeChannel_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...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *channelServiceClient) DeliverChannelNotification(ctx context.Context, in *DeliverChannelNotificationRequest, opts ...grpc.CallOption) (*DeliverChannelNotificationResponse, error) { + out := new(DeliverChannelNotificationResponse) + err := c.cc.Invoke(ctx, ChannelService_DeliverChannelNotification_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// ChannelServiceServer is the server API for ChannelService service. +// All implementations must embed UnimplementedChannelServiceServer +// for forward compatibility +type ChannelServiceServer interface { + NotifyChannel(context.Context, *NotifyChannelRequest) (*NotifyChannelResponse, error) + RegisterChannelListener(context.Context, *RegisterChannelListenerRequest) (*RegisterChannelListenerResponse, error) + UnregisterChannelListener(context.Context, *UnregisterChannelListenerRequest) (*UnregisterChannelListenerResponse, error) + PollChannel(context.Context, *PollChannelRequest) (*PollChannelResponse, error) + DescribeChannel(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) + // Internal. The channel's fan-out calls this on the listener's shard. + DeliverChannelNotification(context.Context, *DeliverChannelNotificationRequest) (*DeliverChannelNotificationResponse, error) + mustEmbedUnimplementedChannelServiceServer() +} + +// UnimplementedChannelServiceServer must be embedded to have forward compatible implementations. +type UnimplementedChannelServiceServer struct { +} + +func (UnimplementedChannelServiceServer) NotifyChannel(context.Context, *NotifyChannelRequest) (*NotifyChannelResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method NotifyChannel not implemented") +} +func (UnimplementedChannelServiceServer) RegisterChannelListener(context.Context, *RegisterChannelListenerRequest) (*RegisterChannelListenerResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method RegisterChannelListener not implemented") +} +func (UnimplementedChannelServiceServer) UnregisterChannelListener(context.Context, *UnregisterChannelListenerRequest) (*UnregisterChannelListenerResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method UnregisterChannelListener not implemented") +} +func (UnimplementedChannelServiceServer) PollChannel(context.Context, *PollChannelRequest) (*PollChannelResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method PollChannel not implemented") +} +func (UnimplementedChannelServiceServer) DescribeChannel(context.Context, *DescribeChannelRequest) (*DescribeChannelResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method DescribeChannel not implemented") +} +func (UnimplementedChannelServiceServer) RegisterWorkflowListener(context.Context, *RegisterWorkflowListenerRequest) (*RegisterWorkflowListenerResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method RegisterWorkflowListener not implemented") +} +func (UnimplementedChannelServiceServer) DeliverChannelNotification(context.Context, *DeliverChannelNotificationRequest) (*DeliverChannelNotificationResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method DeliverChannelNotification not implemented") +} +func (UnimplementedChannelServiceServer) mustEmbedUnimplementedChannelServiceServer() {} + +// UnsafeChannelServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to ChannelServiceServer will +// result in compilation errors. +type UnsafeChannelServiceServer interface { + mustEmbedUnimplementedChannelServiceServer() +} + +func RegisterChannelServiceServer(s grpc.ServiceRegistrar, srv ChannelServiceServer) { + s.RegisterService(&ChannelService_ServiceDesc, srv) +} + +func _ChannelService_NotifyChannel_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).NotifyChannel(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ChannelService_NotifyChannel_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ChannelServiceServer).NotifyChannel(ctx, req.(*NotifyChannelRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ChannelService_RegisterChannelListener_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).RegisterChannelListener(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ChannelService_RegisterChannelListener_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ChannelServiceServer).RegisterChannelListener(ctx, req.(*RegisterChannelListenerRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ChannelService_UnregisterChannelListener_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).UnregisterChannelListener(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ChannelService_UnregisterChannelListener_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ChannelServiceServer).UnregisterChannelListener(ctx, req.(*UnregisterChannelListenerRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ChannelService_PollChannel_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).PollChannel(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ChannelService_PollChannel_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ChannelServiceServer).PollChannel(ctx, req.(*PollChannelRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ChannelService_DescribeChannel_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).DescribeChannel(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ChannelService_DescribeChannel_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ChannelServiceServer).DescribeChannel(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 { + return nil, err + } + if interceptor == nil { + return srv.(ChannelServiceServer).RegisterWorkflowListener(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ChannelService_RegisterWorkflowListener_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ChannelServiceServer).RegisterWorkflowListener(ctx, req.(*RegisterWorkflowListenerRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ChannelService_DeliverChannelNotification_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DeliverChannelNotificationRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ChannelServiceServer).DeliverChannelNotification(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ChannelService_DeliverChannelNotification_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ChannelServiceServer).DeliverChannelNotification(ctx, req.(*DeliverChannelNotificationRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// ChannelService_ServiceDesc is the grpc.ServiceDesc for ChannelService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var ChannelService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "temporal.server.chasm.lib.channel.proto.v1.ChannelService", + HandlerType: (*ChannelServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "NotifyChannel", + Handler: _ChannelService_NotifyChannel_Handler, + }, + { + MethodName: "RegisterChannelListener", + Handler: _ChannelService_RegisterChannelListener_Handler, + }, + { + MethodName: "UnregisterChannelListener", + Handler: _ChannelService_UnregisterChannelListener_Handler, + }, + { + MethodName: "PollChannel", + Handler: _ChannelService_PollChannel_Handler, + }, + { + MethodName: "DescribeChannel", + Handler: _ChannelService_DescribeChannel_Handler, + }, + { + MethodName: "RegisterWorkflowListener", + Handler: _ChannelService_RegisterWorkflowListener_Handler, + }, + { + MethodName: "DeliverChannelNotification", + Handler: _ChannelService_DeliverChannelNotification_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "temporal/server/chasm/lib/channel/proto/v1/service.proto", +} diff --git a/chasm/lib/channel/gen/channelpb/v1/tasks.go-helpers.pb.go b/chasm/lib/channel/gen/channelpb/v1/tasks.go-helpers.pb.go new file mode 100644 index 00000000000..44fd230c300 --- /dev/null +++ b/chasm/lib/channel/gen/channelpb/v1/tasks.go-helpers.pb.go @@ -0,0 +1,154 @@ +// Code generated by protoc-gen-go-helpers. DO NOT EDIT. +package channelpb + +import ( + "google.golang.org/protobuf/proto" +) + +// Marshal an object of type ChannelFanOutTask to the protobuf v3 wire format +func (val *ChannelFanOutTask) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type ChannelFanOutTask from the protobuf v3 wire format +func (val *ChannelFanOutTask) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *ChannelFanOutTask) Size() int { + return proto.Size(val) +} + +// Equal returns whether two ChannelFanOutTask values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *ChannelFanOutTask) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *ChannelFanOutTask + switch t := that.(type) { + case *ChannelFanOutTask: + that1 = t + case ChannelFanOutTask: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type ChannelCallbackTask to the protobuf v3 wire format +func (val *ChannelCallbackTask) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type ChannelCallbackTask from the protobuf v3 wire format +func (val *ChannelCallbackTask) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *ChannelCallbackTask) Size() int { + return proto.Size(val) +} + +// Equal returns whether two ChannelCallbackTask values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *ChannelCallbackTask) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *ChannelCallbackTask + switch t := that.(type) { + case *ChannelCallbackTask: + that1 = t + case ChannelCallbackTask: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type ChannelCallbackBackoffTask to the protobuf v3 wire format +func (val *ChannelCallbackBackoffTask) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type ChannelCallbackBackoffTask from the protobuf v3 wire format +func (val *ChannelCallbackBackoffTask) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *ChannelCallbackBackoffTask) Size() int { + return proto.Size(val) +} + +// Equal returns whether two ChannelCallbackBackoffTask values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *ChannelCallbackBackoffTask) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *ChannelCallbackBackoffTask + switch t := that.(type) { + case *ChannelCallbackBackoffTask: + that1 = t + case ChannelCallbackBackoffTask: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type ChannelIdleTask to the protobuf v3 wire format +func (val *ChannelIdleTask) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type ChannelIdleTask from the protobuf v3 wire format +func (val *ChannelIdleTask) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *ChannelIdleTask) Size() int { + return proto.Size(val) +} + +// Equal returns whether two ChannelIdleTask values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *ChannelIdleTask) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *ChannelIdleTask + switch t := that.(type) { + case *ChannelIdleTask: + that1 = t + case ChannelIdleTask: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} diff --git a/chasm/lib/channel/gen/channelpb/v1/tasks.pb.go b/chasm/lib/channel/gen/channelpb/v1/tasks.pb.go new file mode 100644 index 00000000000..db2a9105d9a --- /dev/null +++ b/chasm/lib/channel/gen/channelpb/v1/tasks.pb.go @@ -0,0 +1,277 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// plugins: +// protoc-gen-go +// protoc +// source: temporal/server/chasm/lib/channel/proto/v1/tasks.proto + +package channelpb + +import ( + reflect "reflect" + sync "sync" + unsafe "unsafe" + + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// Fires after a notify on a channel with listeners. One is outstanding at a +// time, and it delivers the latest notification to every listener that has +// not been handed it yet. +type ChannelFanOutTask struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ChannelFanOutTask) Reset() { + *x = ChannelFanOutTask{} + mi := &file_temporal_server_chasm_lib_channel_proto_v1_tasks_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ChannelFanOutTask) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ChannelFanOutTask) ProtoMessage() {} + +func (x *ChannelFanOutTask) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_channel_proto_v1_tasks_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ChannelFanOutTask.ProtoReflect.Descriptor instead. +func (*ChannelFanOutTask) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_channel_proto_v1_tasks_proto_rawDescGZIP(), []int{0} +} + +// Posts a callback listener's in-flight notification. Runs on the outbound +// queue, keyed by the callback's destination. +type ChannelCallbackTask struct { + state protoimpl.MessageState `protogen:"open.v1"` + ListenerId string `protobuf:"bytes,1,opt,name=listener_id,json=listenerId,proto3" json:"listener_id,omitempty"` + // The listener's task sequence when this was scheduled. + Sequence int64 `protobuf:"varint,2,opt,name=sequence,proto3" json:"sequence,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ChannelCallbackTask) Reset() { + *x = ChannelCallbackTask{} + mi := &file_temporal_server_chasm_lib_channel_proto_v1_tasks_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ChannelCallbackTask) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ChannelCallbackTask) ProtoMessage() {} + +func (x *ChannelCallbackTask) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_channel_proto_v1_tasks_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ChannelCallbackTask.ProtoReflect.Descriptor instead. +func (*ChannelCallbackTask) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_channel_proto_v1_tasks_proto_rawDescGZIP(), []int{1} +} + +func (x *ChannelCallbackTask) GetListenerId() string { + if x != nil { + return x.ListenerId + } + return "" +} + +func (x *ChannelCallbackTask) GetSequence() int64 { + if x != nil { + return x.Sequence + } + return 0 +} + +// Fires when a failed callback delivery has backed off long enough to try +// again. +type ChannelCallbackBackoffTask struct { + state protoimpl.MessageState `protogen:"open.v1"` + ListenerId string `protobuf:"bytes,1,opt,name=listener_id,json=listenerId,proto3" json:"listener_id,omitempty"` + // The listener's task sequence when the failed attempt ran. + Sequence int64 `protobuf:"varint,2,opt,name=sequence,proto3" json:"sequence,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ChannelCallbackBackoffTask) Reset() { + *x = ChannelCallbackBackoffTask{} + mi := &file_temporal_server_chasm_lib_channel_proto_v1_tasks_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ChannelCallbackBackoffTask) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ChannelCallbackBackoffTask) ProtoMessage() {} + +func (x *ChannelCallbackBackoffTask) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_channel_proto_v1_tasks_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ChannelCallbackBackoffTask.ProtoReflect.Descriptor instead. +func (*ChannelCallbackBackoffTask) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_channel_proto_v1_tasks_proto_rawDescGZIP(), []int{2} +} + +func (x *ChannelCallbackBackoffTask) GetListenerId() string { + if x != nil { + return x.ListenerId + } + return "" +} + +func (x *ChannelCallbackBackoffTask) GetSequence() int64 { + if x != nil { + return x.Sequence + } + return 0 +} + +// Fires a retention after the channel's last activity. A channel with no +// listeners that nobody touched in that time is deleted. +type ChannelIdleTask struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ChannelIdleTask) Reset() { + *x = ChannelIdleTask{} + mi := &file_temporal_server_chasm_lib_channel_proto_v1_tasks_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ChannelIdleTask) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ChannelIdleTask) ProtoMessage() {} + +func (x *ChannelIdleTask) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_channel_proto_v1_tasks_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ChannelIdleTask.ProtoReflect.Descriptor instead. +func (*ChannelIdleTask) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_channel_proto_v1_tasks_proto_rawDescGZIP(), []int{3} +} + +var File_temporal_server_chasm_lib_channel_proto_v1_tasks_proto protoreflect.FileDescriptor + +const file_temporal_server_chasm_lib_channel_proto_v1_tasks_proto_rawDesc = "" + + "\n" + + "6temporal/server/chasm/lib/channel/proto/v1/tasks.proto\x12*temporal.server.chasm.lib.channel.proto.v1\"\x13\n" + + "\x11ChannelFanOutTask\"R\n" + + "\x13ChannelCallbackTask\x12\x1f\n" + + "\vlistener_id\x18\x01 \x01(\tR\n" + + "listenerId\x12\x1a\n" + + "\bsequence\x18\x02 \x01(\x03R\bsequence\"Y\n" + + "\x1aChannelCallbackBackoffTask\x12\x1f\n" + + "\vlistener_id\x18\x01 \x01(\tR\n" + + "listenerId\x12\x1a\n" + + "\bsequence\x18\x02 \x01(\x03R\bsequence\"\x11\n" + + "\x0fChannelIdleTaskBAZ?go.temporal.io/server/chasm/lib/channel/gen/channelpb;channelpbb\x06proto3" + +var ( + file_temporal_server_chasm_lib_channel_proto_v1_tasks_proto_rawDescOnce sync.Once + file_temporal_server_chasm_lib_channel_proto_v1_tasks_proto_rawDescData []byte +) + +func file_temporal_server_chasm_lib_channel_proto_v1_tasks_proto_rawDescGZIP() []byte { + file_temporal_server_chasm_lib_channel_proto_v1_tasks_proto_rawDescOnce.Do(func() { + file_temporal_server_chasm_lib_channel_proto_v1_tasks_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_temporal_server_chasm_lib_channel_proto_v1_tasks_proto_rawDesc), len(file_temporal_server_chasm_lib_channel_proto_v1_tasks_proto_rawDesc))) + }) + return file_temporal_server_chasm_lib_channel_proto_v1_tasks_proto_rawDescData +} + +var file_temporal_server_chasm_lib_channel_proto_v1_tasks_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_temporal_server_chasm_lib_channel_proto_v1_tasks_proto_goTypes = []any{ + (*ChannelFanOutTask)(nil), // 0: temporal.server.chasm.lib.channel.proto.v1.ChannelFanOutTask + (*ChannelCallbackTask)(nil), // 1: temporal.server.chasm.lib.channel.proto.v1.ChannelCallbackTask + (*ChannelCallbackBackoffTask)(nil), // 2: temporal.server.chasm.lib.channel.proto.v1.ChannelCallbackBackoffTask + (*ChannelIdleTask)(nil), // 3: temporal.server.chasm.lib.channel.proto.v1.ChannelIdleTask +} +var file_temporal_server_chasm_lib_channel_proto_v1_tasks_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_temporal_server_chasm_lib_channel_proto_v1_tasks_proto_init() } +func file_temporal_server_chasm_lib_channel_proto_v1_tasks_proto_init() { + if File_temporal_server_chasm_lib_channel_proto_v1_tasks_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_temporal_server_chasm_lib_channel_proto_v1_tasks_proto_rawDesc), len(file_temporal_server_chasm_lib_channel_proto_v1_tasks_proto_rawDesc)), + NumEnums: 0, + NumMessages: 4, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_temporal_server_chasm_lib_channel_proto_v1_tasks_proto_goTypes, + DependencyIndexes: file_temporal_server_chasm_lib_channel_proto_v1_tasks_proto_depIdxs, + MessageInfos: file_temporal_server_chasm_lib_channel_proto_v1_tasks_proto_msgTypes, + }.Build() + File_temporal_server_chasm_lib_channel_proto_v1_tasks_proto = out.File + file_temporal_server_chasm_lib_channel_proto_v1_tasks_proto_goTypes = nil + file_temporal_server_chasm_lib_channel_proto_v1_tasks_proto_depIdxs = nil +} diff --git a/chasm/lib/channel/proto/v1/channel_state.proto b/chasm/lib/channel/proto/v1/channel_state.proto new file mode 100644 index 00000000000..273205f52b5 --- /dev/null +++ b/chasm/lib/channel/proto/v1/channel_state.proto @@ -0,0 +1,37 @@ +syntax = "proto3"; + +package temporal.server.chasm.lib.channel.proto.v1; + +import "chasm/lib/channel/proto/v1/message.proto"; +import "google/protobuf/timestamp.proto"; + +option go_package = "go.temporal.io/server/chasm/lib/channel/gen/channelpb;channelpb"; + +// ChannelState is the part of a channel written on every notify. The retained +// notifications and the listeners are nodes of their own, so a notify writes +// this, one retained notification, and nothing per listener. +message ChannelState { + // The notification with the highest counter accepted so far. + Notification latest = 1; + // Sequence of the oldest retained notification and the next one to be + // written. The retained notifications are keyed by sequence. + int64 retained_first = 2; + int64 retained_next = 3; + + // A fan-out task is outstanding. Notifies that land while it is up schedule + // none of their own; the task reads the latest when it runs. + bool fan_out_pending = 4; + + // Last notify, registration or poll. The idle task deletes a channel with no + // listeners once this is older than the retention. + google.protobuf.Timestamp last_activity_time = 5; + bool idle_check_pending = 6; + + // Total notifications accepted, for describe. + int64 accepted_count = 7; + + // 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. + bool closed = 8; +} diff --git a/chasm/lib/channel/proto/v1/message.proto b/chasm/lib/channel/proto/v1/message.proto new file mode 100644 index 00000000000..8b95dc1b8e3 --- /dev/null +++ b/chasm/lib/channel/proto/v1/message.proto @@ -0,0 +1,57 @@ +syntax = "proto3"; + +package temporal.server.chasm.lib.channel.proto.v1; + +import "google/protobuf/timestamp.proto"; +import "temporal/api/common/v1/message.proto"; +import "temporal/api/failure/v1/message.proto"; + +option go_package = "go.temporal.io/server/chasm/lib/channel/gen/channelpb;channelpb"; + +// Notification has the fields of temporal.api.notification.v1.Notification +// under the same names, so the stored form and the JSON a callback listener +// receives read the same as the public message. +message Notification { + string channel = 1; + // Opaque to the server: where the writer stood when it notified. + bytes position = 2; + // Set by the writer and expected to grow. Folding keeps the highest. + int64 counter = 3; + map metadata = 4; +} + +// WorkflowListener is a workflow run that subscribed to the channel. +message WorkflowListener { + string workflow_id = 1; + // The run that subscribed, or the one the subscription was re-keyed to. + string run_id = 2; + string first_execution_run_id = 3; + google.protobuf.Timestamp registered_time = 4; +} + +// CallbackListener is an HTTP endpoint the channel posts its notifications to. +message CallbackListener { + string listener_id = 1; + temporal.api.common.v1.Callback callback = 2; + // Request id of the registration, so a retried registration finds this + // listener rather than adding another. + string request_id = 3; + google.protobuf.Timestamp registered_time = 4; + + // The notification being delivered, if any. One delivery is in flight at a + // time per listener. + Notification in_flight = 5; + // The latest notification that arrived while one was in flight. A newer one + // replaces it, and it is sent when the in-flight delivery is done. + Notification pending = 6; + // Highest counter handed to this listener, in flight or pending, so the + // fan-out hands each notification once. + int64 handed_counter = 7; + // Attempts made at the in-flight delivery. + int32 attempt = 8; + google.protobuf.Timestamp next_attempt_time = 9; + temporal.api.failure.v1.Failure last_attempt_failure = 10; + // Bumped for every delivery task scheduled, so a task names one attempt of + // one delivery and a stale one finds nothing to do. + int64 task_sequence = 11; +} diff --git a/chasm/lib/channel/proto/v1/request_response.proto b/chasm/lib/channel/proto/v1/request_response.proto new file mode 100644 index 00000000000..445446283ed --- /dev/null +++ b/chasm/lib/channel/proto/v1/request_response.proto @@ -0,0 +1,190 @@ +syntax = "proto3"; + +package temporal.server.chasm.lib.channel.proto.v1; + +import "chasm/lib/channel/proto/v1/message.proto"; +import "google/protobuf/duration.proto"; +import "google/protobuf/timestamp.proto"; +import "temporal/api/common/v1/message.proto"; + +option go_package = "go.temporal.io/server/chasm/lib/channel/gen/channelpb;channelpb"; + +// The frontend serves the public channel calls on WorkflowService and forwards +// them here, to the shard that owns the channel. Each request keeps the +// caller's fields in frontend_request, next to the namespace id the frontend +// resolved, as the other CHASM libraries do. + +message NotifyChannelInput { + string namespace = 1; + Notification notification = 2; + string identity = 3; + string request_id = 4; +} + +message NotifyChannelOutput { + // Listeners registered when the notification was accepted. + int64 listener_count = 1; +} + +message NotifyChannelRequest { + string namespace_id = 1; + NotifyChannelInput frontend_request = 2; +} + +message NotifyChannelResponse { + NotifyChannelOutput frontend_response = 1; +} + +message RegisterChannelListenerInput { + string namespace = 1; + string channel = 2; + temporal.api.common.v1.Callback callback = 3; + string request_id = 4; + string identity = 5; +} + +message RegisterChannelListenerOutput { + string listener_id = 1; +} + +message RegisterChannelListenerRequest { + string namespace_id = 1; + RegisterChannelListenerInput frontend_request = 2; +} + +message RegisterChannelListenerResponse { + RegisterChannelListenerOutput frontend_response = 1; +} + +message UnregisterChannelListenerInput { + string namespace = 1; + string channel = 2; + string listener_id = 3; + string identity = 4; +} + +message UnregisterChannelListenerOutput {} + +message UnregisterChannelListenerRequest { + string namespace_id = 1; + UnregisterChannelListenerInput frontend_request = 2; +} + +message UnregisterChannelListenerResponse { + UnregisterChannelListenerOutput frontend_response = 1; +} + +message PollChannelInput { + string namespace = 1; + string channel = 2; + // Return retained notifications with a counter above this one. + int64 after_counter = 3; + // How long to wait for one when none is retained above after_counter. + // Bounded by the server's long-poll timeout. Zero answers at once. + google.protobuf.Duration wait = 4; + // Zero means the server's page size. + int32 max_notifications = 5; +} + +message PollChannelOutput { + repeated Notification notifications = 1; +} + +message PollChannelRequest { + string namespace_id = 1; + PollChannelInput frontend_request = 2; +} + +message PollChannelResponse { + PollChannelOutput frontend_response = 1; +} + +message DescribeChannelInput { + string namespace = 1; + string channel = 2; +} + +// ChannelListenerInfo has the fields of temporal.api.notification.v1.ChannelListener. +message ChannelListenerInfo { + message Workflow { + string workflow_id = 1; + string run_id = 2; + } + string listener_id = 1; + oneof variant { + Workflow workflow = 2; + temporal.api.common.v1.Callback callback = 3; + } + google.protobuf.Timestamp registered_time = 4; +} + +message DescribeChannelOutput { + repeated ChannelListenerInfo listeners = 1; + Notification latest = 2; + int64 retained_count = 3; +} + +message DescribeChannelRequest { + string namespace_id = 1; + DescribeChannelInput frontend_request = 2; +} + +message DescribeChannelResponse { + DescribeChannelOutput frontend_response = 1; +} + +message RegisterWorkflowListenerInput { + string namespace = 1; + string channel = 2; + string workflow_id = 3; + string run_id = 4; + string first_execution_run_id = 5; +} + +message RegisterWorkflowListenerOutput { + // The channel's latest notification, when the run is new to the channel. + // The run takes it as pending, so a write that landed before the run was + // registered still wakes it. + Notification latest = 1; +} + +message RegisterWorkflowListenerRequest { + string namespace_id = 1; + RegisterWorkflowListenerInput frontend_request = 2; +} + +message RegisterWorkflowListenerResponse { + RegisterWorkflowListenerOutput frontend_response = 1; +} + +message DeliverChannelNotificationInput { + string namespace = 1; + string workflow_id = 2; + // The run the channel has on record. If it is closed, the current run is + // asked instead. + string run_id = 3; + Notification notification = 4; +} + +message DeliverChannelNotificationOutput { + // No run of the workflow listens to the channel any more, so the channel + // drops the listener. + bool listener_closed = 1; + // The run on record is closed and the current run listens, so the channel + // re-keys the listener to it. + string successor_run_id = 2; + // The notification joined one the run had not seen yet. + bool folded = 3; + // The run holds a pending notification at this counter or above, so + // nothing changed. + bool duplicate = 4; +} + +message DeliverChannelNotificationRequest { + string namespace_id = 1; + DeliverChannelNotificationInput frontend_request = 2; +} + +message DeliverChannelNotificationResponse { + DeliverChannelNotificationOutput frontend_response = 1; +} diff --git a/chasm/lib/channel/proto/v1/service.proto b/chasm/lib/channel/proto/v1/service.proto new file mode 100644 index 00000000000..22c1a4d21bc --- /dev/null +++ b/chasm/lib/channel/proto/v1/service.proto @@ -0,0 +1,52 @@ +syntax = "proto3"; + +package temporal.server.chasm.lib.channel.proto.v1; + +import "chasm/lib/channel/proto/v1/request_response.proto"; +import "temporal/server/api/common/v1/api_category.proto"; +import "temporal/server/api/routing/v1/extension.proto"; + +option go_package = "go.temporal.io/server/chasm/lib/channel/gen/channelpb;channelpb"; + +// ChannelService is served by History. The frontend reaches it for the public +// channel calls, and History reaches it to cross from a channel's shard to a +// listener's shard and back. +service ChannelService { + rpc NotifyChannel(NotifyChannelRequest) returns (NotifyChannelResponse) { + option (temporal.server.api.routing.v1.routing).business_id = "frontend_request.notification.channel"; + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + rpc RegisterChannelListener(RegisterChannelListenerRequest) returns (RegisterChannelListenerResponse) { + option (temporal.server.api.routing.v1.routing).business_id = "frontend_request.channel"; + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + rpc UnregisterChannelListener(UnregisterChannelListenerRequest) returns (UnregisterChannelListenerResponse) { + option (temporal.server.api.routing.v1.routing).business_id = "frontend_request.channel"; + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + rpc PollChannel(PollChannelRequest) returns (PollChannelResponse) { + option (temporal.server.api.routing.v1.routing).business_id = "frontend_request.channel"; + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_LONG_POLL; + } + + rpc DescribeChannel(DescribeChannelRequest) returns (DescribeChannelResponse) { + option (temporal.server.api.routing.v1.routing).business_id = "frontend_request.channel"; + 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) { + option (temporal.server.api.routing.v1.routing).business_id = "frontend_request.channel"; + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + // Internal. The channel's fan-out calls this on the listener's shard. + rpc DeliverChannelNotification(DeliverChannelNotificationRequest) returns (DeliverChannelNotificationResponse) { + option (temporal.server.api.routing.v1.routing).business_id = "frontend_request.workflow_id"; + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } +} diff --git a/chasm/lib/channel/proto/v1/tasks.proto b/chasm/lib/channel/proto/v1/tasks.proto new file mode 100644 index 00000000000..ff2064c2877 --- /dev/null +++ b/chasm/lib/channel/proto/v1/tasks.proto @@ -0,0 +1,30 @@ +syntax = "proto3"; + +package temporal.server.chasm.lib.channel.proto.v1; + +option go_package = "go.temporal.io/server/chasm/lib/channel/gen/channelpb;channelpb"; + +// Fires after a notify on a channel with listeners. One is outstanding at a +// time, and it delivers the latest notification to every listener that has +// not been handed it yet. +message ChannelFanOutTask {} + +// Posts a callback listener's in-flight notification. Runs on the outbound +// queue, keyed by the callback's destination. +message ChannelCallbackTask { + string listener_id = 1; + // The listener's task sequence when this was scheduled. + int64 sequence = 2; +} + +// Fires when a failed callback delivery has backed off long enough to try +// again. +message ChannelCallbackBackoffTask { + string listener_id = 1; + // The listener's task sequence when the failed attempt ran. + int64 sequence = 2; +} + +// Fires a retention after the channel's last activity. A channel with no +// listeners that nobody touched in that time is deleted. +message ChannelIdleTask {} diff --git a/chasm/lib/channel/routing.go b/chasm/lib/channel/routing.go new file mode 100644 index 00000000000..f5a373944ff --- /dev/null +++ b/chasm/lib/channel/routing.go @@ -0,0 +1,24 @@ +package channel + +import ( + "context" + + channelpb "go.temporal.io/server/chasm/lib/channel/gen/channelpb/v1" +) + +type clientContextKey struct{} + +// WithClient supplies shard routing for a call that reaches a channel from +// inside a workflow task's transaction, which runs on the workflow's shard. +func WithClient(ctx context.Context, client channelpb.ChannelServiceClient) context.Context { + if client == nil { + return ctx + } + return context.WithValue(ctx, clientContextKey{}, client) +} + +// ClientFromContext returns the routed client installed by the engine. +func ClientFromContext(ctx context.Context) (channelpb.ChannelServiceClient, bool) { + client, ok := ctx.Value(clientContextKey{}).(channelpb.ChannelServiceClient) + return client, ok +} diff --git a/chasm/lib/channel/service/deliver.go b/chasm/lib/channel/service/deliver.go new file mode 100644 index 00000000000..bff2e3b2868 --- /dev/null +++ b/chasm/lib/channel/service/deliver.go @@ -0,0 +1,104 @@ +package service + +import ( + "context" + + "go.temporal.io/server/chasm" + "go.temporal.io/server/chasm/lib/channel" + channelpb "go.temporal.io/server/chasm/lib/channel/gen/channelpb/v1" + "go.temporal.io/server/common/log" + "go.temporal.io/server/common/log/tag" + "golang.org/x/sync/errgroup" +) + +// workflowDeliverer hands a notification to workflow listeners, each on its +// own shard through the routed service, and keeps the listener table honest +// about runs that have ended. The fan-out task uses it for an advancing +// notify, and NotifyChannel uses it directly for one that does not advance +// the channel, which writes nothing on the channel itself. +type workflowDeliverer struct { + logger log.Logger + config *channel.Config + routed channelpb.ChannelServiceClient +} + +func newWorkflowDeliverer( + logger log.Logger, + config *channel.Config, + routed channelpb.ChannelServiceClient, +) *workflowDeliverer { + return &workflowDeliverer{logger: logger, config: config, routed: routed} +} + +// deliverAll tells every listener. No cancelling context on purpose: one +// unreachable listener must not stop the others being told. The first error +// comes back, so the caller retries the set; a run that already holds the +// notification pending takes the retry as a fold and changes nothing. +func (h *workflowDeliverer) deliverAll( + ctx context.Context, + ref chasm.ComponentRef, + ns string, + listeners []*channelpb.WorkflowListener, + n *channelpb.Notification, +) error { + var group errgroup.Group + group.SetLimit(fanOutConcurrency) + for _, listener := range listeners { + group.Go(func() error { + return h.deliverOne(ctx, ref, ns, listener, n) + }) + } + return group.Wait() +} + +// deliverOne tells one workflow listener and acts on its answer. The call +// carries its own deadline, so a listener on a slow host does not take the +// whole task's. +func (h *workflowDeliverer) deliverOne( + ctx context.Context, + ref chasm.ComponentRef, + ns string, + listener *channelpb.WorkflowListener, + n *channelpb.Notification, +) error { + callCtx, cancel := context.WithTimeout(ctx, channel.RoutedCallTimeout) + defer cancel() + response, err := h.routed.DeliverChannelNotification(callCtx, &channelpb.DeliverChannelNotificationRequest{ + NamespaceId: ref.NamespaceID, + FrontendRequest: &channelpb.DeliverChannelNotificationInput{ + Namespace: ns, + WorkflowId: listener.GetWorkflowId(), + RunId: listener.GetRunId(), + Notification: n, + }, + }) + if err != nil { + // Not taken as proof the listener is gone. The handler already reads a + // missing run as closed and says so in its answer, so an error here is + // the transport's, and dropping a listener on that would be a guess. + h.logger.Warn("failed to deliver a channel notification to a workflow", + tag.NewStringTag("channel", ref.BusinessID), + tag.WorkflowID(listener.GetWorkflowId()), + tag.Error(err)) + return err + } + out := response.GetFrontendResponse() + limits := h.config.LimitsFor(ns) + switch { + case out.GetListenerClosed(): + _, _, err = chasm.UpdateComponent(ctx, ref, + func(c *channel.Channel, mctx chasm.MutableContext, l *channelpb.WorkflowListener) (struct{}, error) { + c.ForgetWorkflowListener(mctx, l.GetWorkflowId(), l.GetRunId(), limits) + return struct{}{}, nil + }, listener) + case out.GetSuccessorRunId() != "": + _, _, err = chasm.UpdateComponent(ctx, ref, + func(c *channel.Channel, mctx chasm.MutableContext, l *channelpb.WorkflowListener) (struct{}, error) { + c.RekeyWorkflowListener(mctx, l.GetWorkflowId(), l.GetRunId(), out.GetSuccessorRunId()) + return struct{}{}, nil + }, listener) + default: + // Delivered, or a duplicate. The listener stays as it is. + } + return err +} diff --git a/chasm/lib/channel/service/events.go b/chasm/lib/channel/service/events.go new file mode 100644 index 00000000000..b2f860255f4 --- /dev/null +++ b/chasm/lib/channel/service/events.go @@ -0,0 +1,39 @@ +package service + +import ( + enumspb "go.temporal.io/api/enums/v1" + historypb "go.temporal.io/api/history/v1" + "go.temporal.io/server/service/history/hsm" +) + +// channelSubscribedEventDefinition tells the history service how to treat the +// event a subscribe command writes. The subscription itself is CHASM state on +// the Workflow component, so there is nothing here to apply. +type channelSubscribedEventDefinition struct{} + +func (channelSubscribedEventDefinition) Type() enumspb.EventType { + return enumspb.EVENT_TYPE_WORKFLOW_NOTIFICATION_CHANNEL_SUBSCRIBED +} + +// Subscribing gives the workflow nothing to decide on. A notification does, +// and it schedules its own Workflow Task. +func (channelSubscribedEventDefinition) IsWorkflowTaskTrigger() bool { return false } + +func (channelSubscribedEventDefinition) Apply(*hsm.Node, *historypb.HistoryEvent) error { + return nil +} + +// A command event, so never reapplied onto another branch. +func (channelSubscribedEventDefinition) CherryPick( + *hsm.Node, + *historypb.HistoryEvent, + map[enumspb.ResetReapplyExcludeType]struct{}, +) error { + return hsm.ErrNotCherryPickable +} + +// RegisterEventDefinitions makes the channel event known to the history +// service. +func RegisterEventDefinitions(reg *hsm.Registry) error { + return reg.RegisterEventDefinition(channelSubscribedEventDefinition{}) +} diff --git a/chasm/lib/channel/service/fx.go b/chasm/lib/channel/service/fx.go new file mode 100644 index 00000000000..628809d2431 --- /dev/null +++ b/chasm/lib/channel/service/fx.go @@ -0,0 +1,40 @@ +package service + +import ( + "go.temporal.io/server/chasm" + channelpb "go.temporal.io/server/chasm/lib/channel/gen/channelpb/v1" + "go.uber.org/fx" +) + +// HistoryModule serves ChannelService on History. The routed client is here +// too: the fan-out runs on the channel's shard and has to reach each +// listener's, which this host may not own. The channel config comes from the +// workflow library module, which every service running this one has. +var HistoryModule = fx.Module( + "channel-history", + fx.Provide( + channelpb.NewChannelServiceLayeredClient, + newWorkflowDeliverer, + newHandler, + newFanOutTaskHandler, + newCallbackTaskHandler, + newCallbackBackoffTaskHandler, + newIdleTaskHandler, + newLibrary, + ), + fx.Invoke(func(l *library, registry *chasm.Registry) error { + return registry.Register(l) + }), + fx.Invoke(RegisterEventDefinitions), +) + +// FrontendModule gives the frontend the routed client its channel handlers +// forward to, and registers the component type. +var FrontendModule = fx.Module( + "channel-frontend", + fx.Provide(channelpb.NewChannelServiceLayeredClient), + fx.Provide(newComponentOnlyLibrary), + fx.Invoke(func(l *componentOnlyLibrary, registry *chasm.Registry) error { + return registry.Register(l) + }), +) diff --git a/chasm/lib/channel/service/handler.go b/chasm/lib/channel/service/handler.go new file mode 100644 index 00000000000..cf6f7c0e634 --- /dev/null +++ b/chasm/lib/channel/service/handler.go @@ -0,0 +1,644 @@ +package service + +import ( + "context" + "errors" + "time" + + "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/clock" + "go.temporal.io/server/common/contextutil" + "go.temporal.io/server/common/headers" + "go.temporal.io/server/common/log" + "go.temporal.io/server/common/metrics" + "go.temporal.io/server/common/namespace" +) + +type handler struct { + channelpb.UnimplementedChannelServiceServer + + namespaceRegistry namespace.Registry + logger log.Logger + metricsHandler metrics.Handler + timeSource clock.TimeSource + config *channel.Config + limiters *notifyLimiters + deliverer *workflowDeliverer +} + +func newHandler( + namespaceRegistry namespace.Registry, + logger log.Logger, + metricsHandler metrics.Handler, + timeSource clock.TimeSource, + config *channel.Config, + deliverer *workflowDeliverer, +) *handler { + return &handler{ + namespaceRegistry: namespaceRegistry, + logger: logger, + metricsHandler: metricsHandler, + timeSource: timeSource, + config: config, + limiters: newNotifyLimiters(config), + deliverer: deliverer, + } +} + +// namespaceName resolves an id for the limits and the metrics tag. The +// interceptors have already refused a namespace that does not exist, so an id +// the registry cannot name resolves to the empty string. +func (h *handler) namespaceName(namespaceID string) string { + name, err := h.namespaceRegistry.GetNamespaceName(namespace.ID(namespaceID)) + if err != nil { + return "" + } + return name.String() +} + +func (h *handler) limitsFor(namespaceName string) channel.Limits { + if namespaceName == "" { + return channel.DefaultLimits() + } + return h.config.LimitsFor(namespaceName) +} + +// withCallerInfo attributes the channel's persistence calls to the namespace +// that caused them, so they are rate limited, prioritized and metered as that +// namespace's. +func withCallerInfo(ctx context.Context, namespaceName string) context.Context { + if namespaceName == "" { + return ctx + } + return headers.SetCallerInfo(ctx, headers.NewCallerInfo(namespaceName, headers.CallerTypeAPI, "")) +} + +func channelKey(namespaceID, name string) chasm.ExecutionKey { + return chasm.ExecutionKey{NamespaceID: namespaceID, BusinessID: name} +} + +func channelRef(namespaceID, name string) chasm.ComponentRef { + return chasm.NewComponentRef[*channel.Channel](channelKey(namespaceID, name)) +} + +func workflowRef(namespaceID, workflowID, runID string) chasm.ComponentRef { + return chasm.NewComponentRef[*chasmworkflow.Workflow](chasm.ExecutionKey{ + NamespaceID: namespaceID, + BusinessID: workflowID, + RunID: runID, + }) +} + +func startChannel(mctx chasm.MutableContext, _ struct{}) (*channel.Channel, error) { + return channel.NewChannel(mctx), nil +} + +// errChannelClosed refuses an update to a channel the idle task has closed on +// its way to deleting it, so the caller starts a new one instead. +var errChannelClosed = serviceerror.NewNotFound("channel is closed") + +// upsert applies an update to the channel, creating the channel first when +// there is none or the one there is closed. +// +// Creation is a transition of its own rather than an update-with-start: the +// engine applies the update of an update-with-start after it has synced the +// new execution's structure, so listeners and retained notifications added by +// that update would not be persisted. +func upsert[O any]( + ctx context.Context, + namespaceID, name string, + update func(*channel.Channel, chasm.MutableContext) (O, error), + opts ...chasm.TransitionOption, +) (O, error) { + ref := channelRef(namespaceID, name) + apply := func(c *channel.Channel, mctx chasm.MutableContext, _ struct{}) (O, error) { + if c.State.GetClosed() { + var zero O + return zero, errChannelClosed + } + return update(c, mctx) + } + out, _, err := chasm.UpdateComponent(ctx, ref, apply, struct{}{}, opts...) + if !executionAbsent(err) { + return out, err + } + _, err = chasm.StartExecution(ctx, channelKey(namespaceID, name), startChannel, struct{}{}) + if _, started := errors.AsType[*chasm.ExecutionAlreadyStartedError](err); err != nil && !started { + var zero O + return zero, err + } + out, _, err = chasm.UpdateComponent(ctx, ref, apply, struct{}{}, opts...) + return out, err +} + +func executionAbsent(err error) bool { + var notFound *serviceerror.NotFound + return errors.As(err, ¬Found) +} + +// NotifyChannel accepts a notification, creating the channel if this is the +// first anyone has heard of it. A channel with no listeners still retains the +// notification, so a poller that arrives later can catch up. +func (h *handler) NotifyChannel( + ctx context.Context, + req *channelpb.NotifyChannelRequest, +) (*channelpb.NotifyChannelResponse, error) { + in := req.GetFrontendRequest() + ns := h.namespaceName(req.GetNamespaceId()) + ctx = withCallerInfo(ctx, ns) + limits := h.limitsFor(ns) + if err := channel.CheckNotification(in.GetNotification(), limits.MaxMetadataBytes); err != nil { + return nil, err + } + if err := h.limiters.allowNotify(ns, h.timeSource.Now()); err != nil { + return nil, err + } + + // Read first. A notify that would not raise the channel's latest counter + // leaves the ring alone, but it still reaches every listener: one that + // holds it pending folds it, and one that does not, because its last + // task already ran, is woken again. + repeat, err := h.readRepeat(ctx, req.GetNamespaceId(), in.GetNotification()) + if err != nil { + return nil, err + } + if repeat.repeat { + if err := h.redeliver(ctx, req.GetNamespaceId(), ns, in.GetNotification(), repeat); err != nil { + return nil, err + } + return &channelpb.NotifyChannelResponse{ + FrontendResponse: &channelpb.NotifyChannelOutput{ListenerCount: int64(repeat.listeners)}, + }, nil + } + count, err := h.notify(ctx, req.GetNamespaceId(), in, limits) + if errors.Is(err, chasm.ErrRequestIDAlreadyUsed) { + // A retry of a notify that was accepted. Answered with the listeners + // the channel holds now, without accepting it a second time. + snapshot, readErr := chasm.ReadComponent(ctx, + channelRef(req.GetNamespaceId(), in.GetNotification().GetChannel()), + (*channel.Channel).Describe, struct{}{}) + if readErr != nil { + return nil, readErr + } + return &channelpb.NotifyChannelResponse{ + FrontendResponse: &channelpb.NotifyChannelOutput{ListenerCount: int64(len(snapshot.Listeners))}, + }, nil + } + if err != nil { + return nil, err + } + metrics.ChannelNotificationsAccepted.With(h.metricsHandler).Record(1, metrics.NamespaceTag(ns)) + return &channelpb.NotifyChannelResponse{ + FrontendResponse: &channelpb.NotifyChannelOutput{ + ListenerCount: int64(count), + }, + }, nil +} + +// repeatRead is what a notify learns from reading the channel first. +type repeatRead struct { + // The counter is not above the channel's latest, so the ring is left as + // it is. + repeat bool + listeners int + workflows []*channelpb.WorkflowListener + // Some callback listener does not hold the notification. + callbacksNeed bool +} + +// readRepeat reads whether the notification would advance the channel. A +// channel that does not exist, or is closed, is not a repeat: the notify +// creates a channel. +func (h *handler) readRepeat( + ctx context.Context, + namespaceID string, + n *channelpb.Notification, +) (repeatRead, error) { + got, err := chasm.ReadComponent(ctx, channelRef(namespaceID, n.GetChannel()), + func(c *channel.Channel, cctx chasm.Context, counter int64) (repeatRead, error) { + if c.State.GetClosed() || c.Advances(counter) { + return repeatRead{}, nil + } + return repeatRead{ + repeat: true, + listeners: c.ListenerCount(), + workflows: c.WorkflowListenerList(cctx), + callbacksNeed: c.CallbacksNeed(cctx, counter), + }, nil + }, n.GetCounter()) + if executionAbsent(err) { + return repeatRead{}, nil + } + return got, err +} + +// redeliver hands a notification that does not advance the channel to every +// listener. The channel is written only when a callback listener needs it; +// each workflow listener decides on its own shard, and one that holds it +// pending writes nothing. +func (h *handler) redeliver( + ctx context.Context, + namespaceID, ns string, + n *channelpb.Notification, + read repeatRead, +) error { + ref := channelRef(namespaceID, n.GetChannel()) + if read.callbacksNeed { + folded, _, err := chasm.UpdateComponent(ctx, ref, + func(c *channel.Channel, mctx chasm.MutableContext, n *channelpb.Notification) (int, error) { + _, folded, err := c.HandToCallbacks(mctx, n) + return folded, err + }, n) + if err != nil && !executionAbsent(err) { + return err + } + if folded > 0 { + metrics.ChannelNotificationsFolded.With(h.metricsHandler).Record( + int64(folded), metrics.NamespaceTag(ns), callbackKindTag) + } + } + if holders := read.listeners - len(read.workflows); !read.callbacksNeed && holders > 0 { + metrics.ChannelNotificationsFolded.With(h.metricsHandler).Record( + int64(holders), metrics.NamespaceTag(ns), callbackKindTag) + } + return h.deliverer.deliverAll(ctx, ref, ns, read.workflows, n) +} + +// notify accepts the notification on the channel, creating it if absent. A +// request id makes a retry of an accepted notify a no-op: the engine records +// it with the update and refuses its reuse. +func (h *handler) notify( + ctx context.Context, + namespaceID string, + in *channelpb.NotifyChannelInput, + limits channel.Limits, +) (int, error) { + n := in.GetNotification() + var opts []chasm.TransitionOption + if in.GetRequestId() != "" { + opts = append(opts, chasm.WithRequestID(in.GetRequestId())) + } + result, err := upsert(ctx, namespaceID, n.GetChannel(), + func(c *channel.Channel, mctx chasm.MutableContext) (channel.NotifyResult, error) { + return c.Notify(mctx, n, limits) + }, opts...) + return result.ListenerCount, err +} + +// RegisterChannelListener adds a callback listener, creating the channel if it +// is absent. A retry with the same request id gets the same listener back. +func (h *handler) RegisterChannelListener( + ctx context.Context, + req *channelpb.RegisterChannelListenerRequest, +) (*channelpb.RegisterChannelListenerResponse, error) { + in := req.GetFrontendRequest() + ns := h.namespaceName(req.GetNamespaceId()) + ctx = withCallerInfo(ctx, ns) + if err := channel.CheckChannelName(in.GetChannel(), 0); err != nil { + return nil, err + } + reg := channel.CallbackRegistration{ + RequestID: in.GetRequestId(), + Callback: in.GetCallback(), + Limits: h.limitsFor(ns), + } + listenerID, err := upsert(ctx, req.GetNamespaceId(), in.GetChannel(), + func(c *channel.Channel, mctx chasm.MutableContext) (string, error) { + return c.RegisterCallbackListener(mctx, reg) + }) + if err != nil { + return nil, err + } + return &channelpb.RegisterChannelListenerResponse{ + FrontendResponse: &channelpb.RegisterChannelListenerOutput{ListenerId: listenerID}, + }, nil +} + +// UnregisterChannelListener drops a callback listener. A channel or listener +// that is already gone is not an error, so a retry succeeds. +func (h *handler) UnregisterChannelListener( + ctx context.Context, + req *channelpb.UnregisterChannelListenerRequest, +) (*channelpb.UnregisterChannelListenerResponse, error) { + in := req.GetFrontendRequest() + ns := h.namespaceName(req.GetNamespaceId()) + ctx = withCallerInfo(ctx, ns) + limits := h.limitsFor(ns) + _, _, err := chasm.UpdateComponent(ctx, channelRef(req.GetNamespaceId(), in.GetChannel()), + func(c *channel.Channel, mctx chasm.MutableContext, id string) (struct{}, error) { + return struct{}{}, c.UnregisterListener(mctx, id, limits) + }, in.GetListenerId()) + if err != nil && !executionAbsent(err) { + return nil, err + } + return &channelpb.UnregisterChannelListenerResponse{ + FrontendResponse: &channelpb.UnregisterChannelListenerOutput{}, + }, nil +} + +// pollBudget is how long one blocking poll may hold its caller: what it asked +// for, capped at the server's long-poll timeout. It ends before the caller's +// own deadline, so expiry is answered with an empty response. +func pollBudget(ctx context.Context, wait time.Duration) (context.Context, context.CancelFunc) { + return contextutil.WithDeadlineBuffer(ctx, min(wait, channel.LongPollTimeout), channel.LongPollBuffer) +} + +// pollRecheckInterval is how often a blocking poll on a channel nobody has +// created yet asks again whether it exists. +const pollRecheckInterval = time.Second + +// PollChannel returns the retained notifications above the caller's counter. +// With wait set and none to return, it blocks until a notify brings one or the +// long-poll budget runs out, and then answers with what there is. +func (h *handler) PollChannel( + ctx context.Context, + req *channelpb.PollChannelRequest, +) (*channelpb.PollChannelResponse, error) { + in := req.GetFrontendRequest() + ns := h.namespaceName(req.GetNamespaceId()) + ctx = withCallerInfo(ctx, ns) + metrics.ChannelPollers.With(h.metricsHandler).Record(1, metrics.NamespaceTag(ns)) + ref := channelRef(req.GetNamespaceId(), in.GetChannel()) + limits := h.limitsFor(ns) + + wait := in.GetWait().AsDuration() + latest, lastActivity, err := h.readLatest(ctx, ref) + if executionAbsent(err) { + if wait <= 0 { + return &channelpb.PollChannelResponse{FrontendResponse: &channelpb.PollChannelOutput{}}, nil + } + pollCtx, cancel := pollBudget(ctx, wait) + defer cancel() + latest, lastActivity, err = h.waitForChannel(pollCtx, ctx, ref) + if err != nil || pollCtx.Err() != nil { + if ctx.Err() != nil { + return nil, ctx.Err() + } + return &channelpb.PollChannelResponse{FrontendResponse: &channelpb.PollChannelOutput{}}, nil + } + } else if err != nil { + return nil, err + } + + if wait > 0 && latest <= in.GetAfterCounter() { + pollCtx, cancel := pollBudget(ctx, wait) + defer cancel() + _, _, err := chasm.PollComponent(pollCtx, ref, + func(c *channel.Channel, _ chasm.Context, after int64) (struct{}, bool, error) { + // Monotonic, as PollComponent requires: the latest counter + // only grows. + return struct{}{}, c.LatestCounter() > after, nil + }, in.GetAfterCounter()) + if err != nil && (pollCtx.Err() == nil || ctx.Err() != nil) { + return nil, err + } + } + + notifications, err := chasm.ReadComponent(ctx, ref, (*channel.Channel).Poll, channel.PollRequest{ + AfterCounter: in.GetAfterCounter(), + Max: int(in.GetMaxNotifications()), + }) + if err != nil { + return nil, err + } + h.touchForPoll(ctx, ref, lastActivity, limits) + return &channelpb.PollChannelResponse{ + FrontendResponse: &channelpb.PollChannelOutput{Notifications: notifications}, + }, nil +} + +func (h *handler) readLatest(ctx context.Context, ref chasm.ComponentRef) (int64, time.Time, error) { + snapshot, err := chasm.ReadComponent(ctx, ref, + func(c *channel.Channel, _ chasm.Context, _ struct{}) (channel.Snapshot, error) { + return channel.Snapshot{ + Latest: c.State.GetLatest(), + LastActivityTime: c.State.GetLastActivityTime().AsTime(), + }, nil + }, struct{}{}) + if err != nil { + return 0, time.Time{}, err + } + return snapshot.Latest.GetCounter(), snapshot.LastActivityTime, nil +} + +// waitForChannel parks a blocking poll on a channel nobody has created yet, +// asking again on an interval, so a client can start polling before the first +// notify. +func (h *handler) waitForChannel( + pollCtx, callerCtx context.Context, + ref chasm.ComponentRef, +) (int64, time.Time, error) { + ticker := time.NewTicker(pollRecheckInterval) + defer ticker.Stop() + for { + select { + case <-pollCtx.Done(): + if callerCtx.Err() != nil { + return 0, time.Time{}, callerCtx.Err() + } + return 0, time.Time{}, pollCtx.Err() + case <-ticker.C: + } + latest, lastActivity, err := h.readLatest(pollCtx, ref) + switch { + case err == nil: + return latest, lastActivity, nil + case executionAbsent(err), pollCtx.Err() != nil: + continue + default: + return 0, time.Time{}, err + } + } +} + +// touchForPoll records the poll as activity when the last one is old enough to +// matter to the idle check. A failure here costs nothing the caller needs, so +// it is logged rather than returned. +func (h *handler) touchForPoll( + ctx context.Context, + ref chasm.ComponentRef, + lastActivity time.Time, + limits channel.Limits, +) { + retention := limits.Retention + if retention <= 0 { + retention = channel.DefaultRetention + } + if h.timeSource.Now().Sub(lastActivity) < retention/2 { + return + } + _, _, err := chasm.UpdateComponent(ctx, ref, (*channel.Channel).TouchForPoll, limits) + if err != nil && !executionAbsent(err) { + h.logger.Warn("failed to record a channel poll as activity") + } +} + +// DescribeChannel lists the listeners and reports the latest notification and +// how many are retained. +func (h *handler) DescribeChannel( + ctx context.Context, + req *channelpb.DescribeChannelRequest, +) (*channelpb.DescribeChannelResponse, error) { + in := req.GetFrontendRequest() + ctx = withCallerInfo(ctx, h.namespaceName(req.GetNamespaceId())) + snapshot, err := chasm.ReadComponent(ctx, channelRef(req.GetNamespaceId(), in.GetChannel()), + (*channel.Channel).Describe, struct{}{}) + if err != nil { + return nil, err + } + return &channelpb.DescribeChannelResponse{ + FrontendResponse: &channelpb.DescribeChannelOutput{ + Listeners: snapshot.Listeners, + Latest: snapshot.Latest, + RetainedCount: snapshot.RetainedCount, + }, + }, nil +} + +// RegisterWorkflowListener records a workflow run as a listener on the +// channel's shard, creating the channel if it is absent. +// +// Internal. Called by the completion of the Workflow Task that subscribed, +// which runs on the workflow's shard and cannot reach the channel itself. +func (h *handler) RegisterWorkflowListener( + ctx context.Context, + req *channelpb.RegisterWorkflowListenerRequest, +) (*channelpb.RegisterWorkflowListenerResponse, error) { + in := req.GetFrontendRequest() + ns := h.namespaceName(req.GetNamespaceId()) + ctx = withCallerInfo(ctx, ns) + if err := channel.CheckChannelName(in.GetChannel(), 0); err != nil { + return nil, err + } + reg := channel.WorkflowRegistration{ + WorkflowID: in.GetWorkflowId(), + RunID: in.GetRunId(), + FirstExecutionRunID: in.GetFirstExecutionRunId(), + Limits: h.limitsFor(ns), + } + latest, err := upsert(ctx, req.GetNamespaceId(), in.GetChannel(), + func(c *channel.Channel, mctx chasm.MutableContext) (*channelpb.Notification, error) { + return c.RegisterWorkflowListener(mctx, reg) + }) + if err != nil { + return nil, err + } + return &channelpb.RegisterWorkflowListenerResponse{ + FrontendResponse: &channelpb.RegisterWorkflowListenerOutput{Latest: latest}, + }, nil +} + +// listenerProbe is what one run says about a channel: whether it is still +// open, whether it subscribed, and whether it already has this counter. +type listenerProbe struct { + runID string + closed bool + subscribed bool + duplicate bool +} + +// probeListener reads a run without touching it. A run that is gone, or that +// never had a Workflow component and so cannot have subscribed, reads as +// closed: the fan-out only needs to know whether delivering can achieve +// anything. +func (h *handler) probeListener( + ctx context.Context, + namespaceID, workflowID, runID string, + n *channelpb.Notification, +) (listenerProbe, error) { + probe, err := chasm.ReadComponent(ctx, workflowRef(namespaceID, workflowID, runID), + func(wf *chasmworkflow.Workflow, cctx chasm.Context, _ struct{}) (listenerProbe, error) { + p := listenerProbe{ + runID: cctx.ExecutionKey().RunID, + closed: !cctx.ExecutionInfo().CloseTime.IsZero(), + subscribed: wf.SubscribedToChannel(n.GetChannel()), + } + if p.subscribed { + p.duplicate = wf.ChannelNotificationIsDuplicate(cctx, n.GetChannel(), n.GetCounter()) + } + return p, nil + }, struct{}{}) + if executionAbsent(err) { + return listenerProbe{runID: runID, closed: true}, nil + } + return probe, err +} + +func (h *handler) accept( + ctx context.Context, + namespaceID, workflowID, runID string, + n *channelpb.Notification, +) (bool, error) { + folded, _, err := chasm.UpdateComponent(ctx, workflowRef(namespaceID, workflowID, runID), + (*chasmworkflow.Workflow).AcceptChannelNotification, n) + return folded, err +} + +// DeliverChannelNotification hands a notification to one workflow listener, +// on the shard that owns it. +// +// Internal. Called by the channel's fan-out, which runs on the channel's shard. +// The channel has a run on record, and a run ends: if it has, the current run +// is asked instead, and the answer tells the channel to re-key the listener to +// it or, when no run listens, to drop it. +func (h *handler) DeliverChannelNotification( + ctx context.Context, + req *channelpb.DeliverChannelNotificationRequest, +) (*channelpb.DeliverChannelNotificationResponse, error) { + in := req.GetFrontendRequest() + ns := h.namespaceName(req.GetNamespaceId()) + ctx = withCallerInfo(ctx, ns) + namespaceID, workflowID, n := req.GetNamespaceId(), in.GetWorkflowId(), in.GetNotification() + if n.GetChannel() == "" || n.GetCounter() <= 0 { + return nil, serviceerror.NewInvalidArgument("notification needs a channel and a counter") + } + + out := &channelpb.DeliverChannelNotificationOutput{} + target, err := h.probeListener(ctx, namespaceID, workflowID, in.GetRunId(), n) + if err != nil { + return nil, err + } + if target.closed { + pinned := target.runID + if target, err = h.probeListener(ctx, namespaceID, workflowID, "", n); err != nil { + return nil, err + } + if target.closed || target.runID == pinned || !target.subscribed { + out.ListenerClosed = true + return &channelpb.DeliverChannelNotificationResponse{FrontendResponse: out}, nil + } + out.SuccessorRunId = target.runID + } else if !target.subscribed { + out.ListenerClosed = true + return &channelpb.DeliverChannelNotificationResponse{FrontendResponse: out}, nil + } + + // A duplicate leaves the run exactly as it was: reaching the component + // mutably is already a write. It is a fold that changed nothing. + if target.duplicate { + out.Duplicate = true + metrics.ChannelNotificationsFolded.With(h.metricsHandler).Record( + 1, metrics.NamespaceTag(ns), workflowKindTag) + return &channelpb.DeliverChannelNotificationResponse{FrontendResponse: out}, nil + } + folded, err := h.accept(ctx, namespaceID, workflowID, target.runID, n) + if err != nil { + return nil, err + } + out.Folded = folded + tag := metrics.NamespaceTag(ns) + metrics.ChannelNotificationsDelivered.With(h.metricsHandler).Record(1, tag, workflowKindTag) + if folded { + metrics.ChannelNotificationsFolded.With(h.metricsHandler).Record(1, tag, workflowKindTag) + } + return &channelpb.DeliverChannelNotificationResponse{FrontendResponse: out}, nil +} + +var ( + workflowKindTag = metrics.StringTag("listener_kind", "workflow") + callbackKindTag = metrics.StringTag("listener_kind", "callback") +) diff --git a/chasm/lib/channel/service/library.go b/chasm/lib/channel/service/library.go new file mode 100644 index 00000000000..ca6efadb69d --- /dev/null +++ b/chasm/lib/channel/service/library.go @@ -0,0 +1,86 @@ +package service + +import ( + "go.temporal.io/server/chasm" + "go.temporal.io/server/chasm/lib/channel" + channelpb "go.temporal.io/server/chasm/lib/channel/gen/channelpb/v1" + "google.golang.org/grpc" +) + +const ( + libraryName = "channel" + componentName = "channel" +) + +var ( + Archetype = chasm.FullyQualifiedName(libraryName, componentName) + ArchetypeID = chasm.GenerateTypeID(Archetype) +) + +type library struct { + chasm.UnimplementedLibrary + + handler *handler + fanOut *fanOutTaskHandler + callback *callbackTaskHandler + backoff *callbackBackoffTaskHandler + idle *idleTaskHandler +} + +func newLibrary( + h *handler, + fanOut *fanOutTaskHandler, + callback *callbackTaskHandler, + backoff *callbackBackoffTaskHandler, + idle *idleTaskHandler, +) *library { + return &library{handler: h, fanOut: fanOut, callback: callback, backoff: backoff, idle: idle} +} + +// componentOnlyLibrary registers the component without the service, for a +// process that has to know the type but serves nothing. +type componentOnlyLibrary struct { + chasm.UnimplementedLibrary +} + +func newComponentOnlyLibrary() *componentOnlyLibrary { + return &componentOnlyLibrary{} +} + +func (l *componentOnlyLibrary) Name() string { + return libraryName +} + +func (l *componentOnlyLibrary) Components() []*chasm.RegistrableComponent { + return components() +} + +func components() []*chasm.RegistrableComponent { + return []*chasm.RegistrableComponent{ + chasm.NewRegistrableComponent[*channel.Channel]( + componentName, + chasm.WithBusinessIDAlias("Channel"), + ), + } +} + +func (l *library) Name() string { + return libraryName +} + +func (l *library) Components() []*chasm.RegistrableComponent { + return components() +} + +func (l *library) Tasks() []*chasm.RegistrableTask { + return []*chasm.RegistrableTask{ + chasm.NewRegistrableSideEffectTask("fanOut", l.fanOut), + chasm.NewRegistrableSideEffectTask("callback", l.callback), + chasm.NewRegistrablePureTask("callbackBackoff", l.backoff), + chasm.NewRegistrableSideEffectTask("idle", l.idle), + } +} + +func (l *library) RegisterServices(server *grpc.Server) { + channelpb.RegisterChannelServiceServer(server, l.handler) +} diff --git a/chasm/lib/channel/service/ratelimit.go b/chasm/lib/channel/service/ratelimit.go new file mode 100644 index 00000000000..a868d7194f4 --- /dev/null +++ b/chasm/lib/channel/service/ratelimit.go @@ -0,0 +1,58 @@ +package service + +import ( + "fmt" + "sync" + "time" + + enumspb "go.temporal.io/api/enums/v1" + "go.temporal.io/api/serviceerror" + "go.temporal.io/server/chasm/lib/channel" + "go.temporal.io/server/common/quotas" +) + +// limiterRefreshInterval is how long a changed rate takes to reach a namespace +// that already has a limiter. +const limiterRefreshInterval = 10 * time.Second + +// notifyLimiters enforces the per-namespace notify rate on this host. One +// limiter per namespace, made on first use and refreshed from config, so a +// change lands without a restart. A rate of zero is not enforced. +type notifyLimiters struct { + config *channel.Config + + mu sync.Mutex + byNamespace map[string]quotas.RateLimiter +} + +func newNotifyLimiters(config *channel.Config) *notifyLimiters { + return ¬ifyLimiters{config: config, byNamespace: make(map[string]quotas.RateLimiter)} +} + +func (l *notifyLimiters) forNamespace(ns string) quotas.RateLimiter { + l.mu.Lock() + defer l.mu.Unlock() + if limiter, ok := l.byNamespace[ns]; ok { + return limiter + } + rate := func() int { return max(l.config.NotifyPerSecond(ns), 0) } + limiter := quotas.NewDynamicRateLimiter(quotas.NewRateBurst( + func() float64 { return float64(rate()) }, + func() int { return max(rate(), 1) }, + ), limiterRefreshInterval) + l.byNamespace[ns] = limiter + return limiter +} + +func (l *notifyLimiters) allowNotify(ns string, now time.Time) error { + rate := l.config.NotifyPerSecond(ns) + if rate <= 0 || l.forNamespace(ns).AllowN(now, 1) { + return nil + } + return &serviceerror.ResourceExhausted{ + Cause: enumspb.RESOURCE_EXHAUSTED_CAUSE_RPS_LIMIT, + Scope: enumspb.RESOURCE_EXHAUSTED_SCOPE_NAMESPACE, + Message: fmt.Sprintf( + "namespace %q is over its limit of %d channel notifications per second", ns, rate), + } +} diff --git a/chasm/lib/channel/service/tasks.go b/chasm/lib/channel/service/tasks.go new file mode 100644 index 00000000000..828fa430ede --- /dev/null +++ b/chasm/lib/channel/service/tasks.go @@ -0,0 +1,385 @@ +package service + +import ( + "bytes" + "context" + "errors" + "fmt" + "io" + "net/http" + + "go.temporal.io/api/serviceerror" + "go.temporal.io/server/chasm" + "go.temporal.io/server/chasm/lib/callback" + "go.temporal.io/server/chasm/lib/channel" + channelpb "go.temporal.io/server/chasm/lib/channel/gen/channelpb/v1" + "go.temporal.io/server/common/headers" + "go.temporal.io/server/common/log" + "go.temporal.io/server/common/log/tag" + "go.temporal.io/server/common/metrics" + "go.temporal.io/server/common/namespace" + queuescommon "go.temporal.io/server/service/history/queues/common" + queueserrors "go.temporal.io/server/service/history/queues/errors" + "google.golang.org/protobuf/encoding/protojson" +) + +// ChannelHeader names the channel on a callback post, so an endpoint serving +// several channels can route without parsing the body. +const ChannelHeader = "Temporal-Notification-Channel" + +// fanOutConcurrency bounds how many workflow listeners one fan-out tells at +// once. A channel holds up to a thousand, and telling them one at a time makes +// the task's runtime grow with that count. +const fanOutConcurrency = 16 + +// backgroundCallerContext tags a task's context with the namespace, since the +// task runs outside any request. The name comes back too: a routed call the +// task makes carries it on the request, which is what the interceptors resolve +// the call against. +func backgroundCallerContext( + ctx context.Context, + registry namespace.Registry, + namespaceID string, +) (context.Context, string) { + name, err := registry.GetNamespaceName(namespace.ID(namespaceID)) + if err != nil { + return ctx, "" + } + return headers.SetCallerInfo(ctx, headers.NewBackgroundLowCallerInfo(name.String())), name.String() +} + +// fanOutTaskHandler hands the channel's latest notification to its listeners. +// Callback listeners are handed it inside the channel's own transition; each +// workflow listener is reached on its own shard through the routed service. +type fanOutTaskHandler struct { + chasm.SideEffectTaskHandlerBase[*channelpb.ChannelFanOutTask] + + namespaceRegistry namespace.Registry + metricsHandler metrics.Handler + deliverer *workflowDeliverer +} + +func newFanOutTaskHandler( + namespaceRegistry namespace.Registry, + metricsHandler metrics.Handler, + deliverer *workflowDeliverer, +) *fanOutTaskHandler { + return &fanOutTaskHandler{ + namespaceRegistry: namespaceRegistry, + metricsHandler: metricsHandler, + deliverer: deliverer, + } +} + +// Validate accepts the task unconditionally. The notify that scheduled it +// raised a flag so later notifies schedule none of their own, and turning the +// task down would leave the flag up with nothing to lower it. +func (h *fanOutTaskHandler) Validate( + _ chasm.Context, + _ *channel.Channel, + _ chasm.TaskInvocation, + _ *channelpb.ChannelFanOutTask, +) (bool, error) { + return true, nil +} + +// Execute delivers the latest notification. The first error comes back so the +// task retries and tells the whole set again; a run that already has the +// counter takes the retry as a duplicate and changes nothing. +func (h *fanOutTaskHandler) Execute( + ctx context.Context, + ref chasm.ComponentRef, + _ chasm.TaskAttributes, + _ *channelpb.ChannelFanOutTask, +) error { + ctx, ns := backgroundCallerContext(ctx, h.namespaceRegistry, ref.NamespaceID) + fanOut, _, err := chasm.UpdateComponent(ctx, ref, (*channel.Channel).TakeFanOut, struct{}{}) + if err != nil { + return err + } + if fanOut.CallbackFolded > 0 { + metrics.ChannelNotificationsFolded.With(h.metricsHandler).Record( + int64(fanOut.CallbackFolded), metrics.NamespaceTag(ns), callbackKindTag) + } + if fanOut.Latest == nil { + return nil + } + + return h.deliverer.deliverAll(ctx, ref, ns, fanOut.WorkflowListeners, fanOut.Latest) +} + +// Discard lowers the coalescing flag the scheduling notify raised. Left up, no +// notify would ever schedule another fan-out. A channel that is gone has no +// flag to lower. +func (h *fanOutTaskHandler) Discard( + ctx context.Context, + ref chasm.ComponentRef, + _ chasm.TaskAttributes, + _ *channelpb.ChannelFanOutTask, +) error { + ctx, _ = backgroundCallerContext(ctx, h.namespaceRegistry, ref.NamespaceID) + _, _, err := chasm.UpdateComponent(ctx, ref, + func(c *channel.Channel, _ chasm.MutableContext, _ struct{}) (struct{}, error) { + c.State.FanOutPending = false + return struct{}{}, nil + }, struct{}{}) + if executionAbsent(err) { + return nil + } + return err +} + +// callbackTaskHandler posts one callback listener's in-flight notification. +// It runs on the outbound queue, so a destination that keeps failing trips the +// same per-destination circuit breaker completion callbacks use, and it goes +// out through the callback library's HTTP caller. +type callbackTaskHandler struct { + chasm.SideEffectTaskHandlerBase[*channelpb.ChannelCallbackTask] + + namespaceRegistry namespace.Registry + logger log.Logger + metricsHandler metrics.Handler + callbackConfig *callback.Config + httpCallerProvider callback.HTTPCallerProvider +} + +func newCallbackTaskHandler( + namespaceRegistry namespace.Registry, + logger log.Logger, + metricsHandler metrics.Handler, + callbackConfig *callback.Config, + httpCallerProvider callback.HTTPCallerProvider, +) *callbackTaskHandler { + return &callbackTaskHandler{ + namespaceRegistry: namespaceRegistry, + logger: logger, + metricsHandler: metricsHandler, + callbackConfig: callbackConfig, + httpCallerProvider: httpCallerProvider, + } +} + +func (h *callbackTaskHandler) Validate( + ctx chasm.Context, + c *channel.Channel, + _ chasm.TaskInvocation, + task *channelpb.ChannelCallbackTask, +) (bool, error) { + _, ok := c.CallbackDeliveryFor(ctx, task) + return ok, nil +} + +func (h *callbackTaskHandler) Execute( + ctx context.Context, + ref chasm.ComponentRef, + attrs chasm.TaskAttributes, + task *channelpb.ChannelCallbackTask, +) error { + ctx, ns := backgroundCallerContext(ctx, h.namespaceRegistry, ref.NamespaceID) + delivery, err := chasm.ReadComponent(ctx, ref, + func(c *channel.Channel, cctx chasm.Context, t *channelpb.ChannelCallbackTask) (channel.CallbackDelivery, error) { + d, ok := c.CallbackDeliveryFor(cctx, t) + if !ok { + return channel.CallbackDelivery{}, errStaleDelivery + } + return d, nil + }, task) + if errors.Is(err, errStaleDelivery) { + return nil + } + if err != nil { + return err + } + + callCtx, cancel := context.WithTimeout(ctx, h.callbackConfig.RequestTimeout(ns, attrs.Destination)) + defer cancel() + retryable, postErr := h.post(callCtx, ref, attrs.Destination, delivery) + if postErr != nil { + h.logger.Warn("channel callback delivery failed", + tag.NewStringTag("channel", ref.BusinessID), + tag.NewStringTag("listener-id", task.GetListenerId()), + tag.Attempt(delivery.Attempt), + tag.Bool("retryable", retryable), + tag.Error(postErr)) + } + + delivered, _, saveErr := chasm.UpdateComponent(ctx, ref, (*channel.Channel).CompleteCallbackDelivery, + channel.CallbackOutcome{ + ListenerID: task.GetListenerId(), + Sequence: task.GetSequence(), + Err: postErr, + Retryable: retryable, + RetryPolicy: h.callbackConfig.RetryPolicy(), + }) + if saveErr == nil && delivered { + metrics.ChannelNotificationsDelivered.With(h.metricsHandler).Record( + 1, metrics.NamespaceTag(ns), callbackKindTag) + } + if postErr != nil && retryable { + // Reported to the queue as the destination being down, which feeds its + // circuit breaker. The retry itself is the backoff task the outcome + // scheduled, so a rerun of this task finds its attempt over. + return queueserrors.NewDestinationDownError(postErr.Error(), saveErr) + } + return saveErr +} + +var errStaleDelivery = errors.New("channel callback delivery is no longer in flight") + +// post sends the notification as JSON, with the callback's own headers and the +// channel named in a header of its own. A 2xx is a delivery. A 4xx other than +// a timeout or a throttle will not succeed on retry, so it is not retried. +func (h *callbackTaskHandler) post( + ctx context.Context, + ref chasm.ComponentRef, + destination string, + delivery channel.CallbackDelivery, +) (bool, error) { + body, err := protojson.Marshal(delivery.Notification) + if err != nil { + return false, err + } + nexus := delivery.Callback.GetNexus() + request, err := http.NewRequestWithContext(ctx, http.MethodPost, nexus.GetUrl(), bytes.NewReader(body)) + if err != nil { + return false, err + } + for key, value := range nexus.GetHeader() { + request.Header.Set(key, value) + } + request.Header.Set("Content-Type", "application/json") + request.Header.Set(ChannelHeader, delivery.Notification.GetChannel()) + + caller := h.httpCallerProvider(queuescommon.NamespaceIDAndDestination{ + NamespaceID: ref.NamespaceID, + Destination: destination, + }) + response, err := caller(request) + if err != nil { + return true, err + } + defer func() { + _, _ = io.Copy(io.Discard, response.Body) + _ = response.Body.Close() + }() + if response.StatusCode >= 200 && response.StatusCode < 300 { + return false, nil + } + statusErr := fmt.Errorf("callback endpoint answered %s", response.Status) + switch { + case response.StatusCode == http.StatusRequestTimeout, + response.StatusCode == http.StatusTooManyRequests, + response.StatusCode >= 500: + return true, statusErr + default: + return false, statusErr + } +} + +func (h *callbackTaskHandler) Discard( + _ context.Context, + _ chasm.ComponentRef, + _ chasm.TaskAttributes, + _ *channelpb.ChannelCallbackTask, +) error { + // Nothing to undo: the task has no side effect until it executes. + return nil +} + +// callbackBackoffTaskHandler resumes a callback delivery once its backoff has +// run out. +type callbackBackoffTaskHandler struct { + chasm.PureTaskHandlerBase +} + +func newCallbackBackoffTaskHandler() *callbackBackoffTaskHandler { + return &callbackBackoffTaskHandler{} +} + +func (h *callbackBackoffTaskHandler) Validate( + ctx chasm.Context, + c *channel.Channel, + _ chasm.TaskInvocation, + task *channelpb.ChannelCallbackBackoffTask, +) (bool, error) { + return c.BackoffDone(ctx, task), nil +} + +func (h *callbackBackoffTaskHandler) Execute( + mctx chasm.MutableContext, + c *channel.Channel, + _ chasm.TaskAttributes, + task *channelpb.ChannelCallbackBackoffTask, +) error { + return c.ResumeCallback(mctx, task) +} + +// idleTaskHandler deletes a channel that has had no listeners and no activity +// for a full retention. +type idleTaskHandler struct { + chasm.SideEffectTaskHandlerBase[*channelpb.ChannelIdleTask] + + namespaceRegistry namespace.Registry + config *channel.Config +} + +func newIdleTaskHandler(namespaceRegistry namespace.Registry, config *channel.Config) *idleTaskHandler { + return &idleTaskHandler{namespaceRegistry: namespaceRegistry, config: config} +} + +func (h *idleTaskHandler) Validate( + _ chasm.Context, + c *channel.Channel, + _ chasm.TaskInvocation, + _ *channelpb.ChannelIdleTask, +) (bool, error) { + return c.State.GetIdleCheckPending() && !c.State.GetClosed(), nil +} + +// Execute closes the channel in one transition and deletes it in the next. +// Closing first means a notify or registration that lands in between starts a +// new channel instead of joining the one about to be deleted, and the delete +// names this run so it cannot take that new one. +func (h *idleTaskHandler) Execute( + ctx context.Context, + ref chasm.ComponentRef, + _ chasm.TaskAttributes, + _ *channelpb.ChannelIdleTask, +) error { + ctx, ns := backgroundCallerContext(ctx, h.namespaceRegistry, ref.NamespaceID) + limits := channel.DefaultLimits() + if ns != "" { + limits = h.config.LimitsFor(ns) + } + expired, _, err := chasm.UpdateComponent(ctx, ref, (*channel.Channel).RunIdleCheck, limits) + if err != nil || !expired { + return err + } + key := ref.ExecutionKey + if key.RunID == "" { + return serviceerror.NewInternal("idle channel task has no run id to delete") + } + err = chasm.DeleteExecution[*channel.Channel](ctx, key, chasm.DeleteExecutionRequest{}) + if executionAbsent(err) { + return nil + } + return err +} + +func (h *idleTaskHandler) Discard( + ctx context.Context, + ref chasm.ComponentRef, + _ chasm.TaskAttributes, + _ *channelpb.ChannelIdleTask, +) error { + ctx, _ = backgroundCallerContext(ctx, h.namespaceRegistry, ref.NamespaceID) + _, _, err := chasm.UpdateComponent(ctx, ref, + func(c *channel.Channel, _ chasm.MutableContext, _ struct{}) (struct{}, error) { + c.State.IdleCheckPending = false + return struct{}{}, nil + }, struct{}{}) + if executionAbsent(err) { + return nil + } + return err +} diff --git a/chasm/lib/workflow/fx.go b/chasm/lib/workflow/fx.go index 8e69ef2f8b8..8f6111a5d1b 100644 --- a/chasm/lib/workflow/fx.go +++ b/chasm/lib/workflow/fx.go @@ -3,6 +3,7 @@ package workflow import ( "go.temporal.io/server/api/historyservice/v1" "go.temporal.io/server/chasm" + "go.temporal.io/server/chasm/lib/channel" "go.temporal.io/server/chasm/lib/nexusoperation" "go.temporal.io/server/chasm/lib/stream" "go.uber.org/fx" @@ -16,11 +17,14 @@ var Module = fx.Module( // Provided here rather than by the stream service module, because the // command handlers need it in every service that runs this library. fx.Provide(stream.NewConfig), + fx.Provide(channel.NewConfig), fx.Invoke(func( chasmRegistry *chasm.Registry, library *library, config *nexusoperation.Config, streamConfig *stream.Config, + workflowConfig Config, + channelConfig *channel.Config, ) error { if err := library.registry.Register( newNexusLibrary(config, chasmRegistry.NexusEndpointProcessor), @@ -30,6 +34,11 @@ var Module = fx.Module( if err := library.registry.Register(newStreamLibrary(streamConfig)); err != nil { return err } + if err := library.registry.Register( + newNotificationLibrary(workflowConfig, channelConfig), + ); err != nil { + return err + } return chasmRegistry.Register(library) }), ) diff --git a/chasm/lib/workflow/gen/workflowpb/v1/state.go-helpers.pb.go b/chasm/lib/workflow/gen/workflowpb/v1/state.go-helpers.pb.go index 50980db658e..d2234bfd0d4 100644 --- a/chasm/lib/workflow/gen/workflowpb/v1/state.go-helpers.pb.go +++ b/chasm/lib/workflow/gen/workflowpb/v1/state.go-helpers.pb.go @@ -115,3 +115,77 @@ func (this *IncomingSignalData) Equal(that interface{}) bool { return proto.Equal(this, that1) } + +// Marshal an object of type ChannelSubscription to the protobuf v3 wire format +func (val *ChannelSubscription) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type ChannelSubscription from the protobuf v3 wire format +func (val *ChannelSubscription) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *ChannelSubscription) Size() int { + return proto.Size(val) +} + +// Equal returns whether two ChannelSubscription values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *ChannelSubscription) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *ChannelSubscription + switch t := that.(type) { + case *ChannelSubscription: + that1 = t + case ChannelSubscription: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type ChannelNotificationEntry to the protobuf v3 wire format +func (val *ChannelNotificationEntry) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type ChannelNotificationEntry from the protobuf v3 wire format +func (val *ChannelNotificationEntry) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *ChannelNotificationEntry) Size() int { + return proto.Size(val) +} + +// Equal returns whether two ChannelNotificationEntry values are equivalent by recursively +// comparing the message's fields. +// For more information see the documentation for +// https://pkg.go.dev/google.golang.org/protobuf/proto#Equal +func (this *ChannelNotificationEntry) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *ChannelNotificationEntry + switch t := that.(type) { + case *ChannelNotificationEntry: + that1 = t + case ChannelNotificationEntry: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} diff --git a/chasm/lib/workflow/gen/workflowpb/v1/state.pb.go b/chasm/lib/workflow/gen/workflowpb/v1/state.pb.go index 304853e6afd..28c6c518e33 100644 --- a/chasm/lib/workflow/gen/workflowpb/v1/state.pb.go +++ b/chasm/lib/workflow/gen/workflowpb/v1/state.pb.go @@ -11,6 +11,7 @@ import ( sync "sync" unsafe "unsafe" + v1 "go.temporal.io/api/common/v1" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" ) @@ -171,18 +172,159 @@ func (x *IncomingSignalData) GetEventId() int64 { return 0 } +// ChannelSubscription is a notification channel this run subscribed to with a +// command. It lives for the run. +type ChannelSubscription struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The WorkflowNotificationChannelSubscribed event that recorded it. + EventId int64 `protobuf:"varint,1,opt,name=event_id,json=eventId,proto3" json:"event_id,omitempty"` + // Highest counter this run accepted from the channel. Kept for describing + // the subscription; folding compares against the pending entry instead. + LastCounter int64 `protobuf:"varint,2,opt,name=last_counter,json=lastCounter,proto3" json:"last_counter,omitempty"` + // Counter the scheduled event of the run's current task carried, while that + // task has not started. A repeat at or below it folds into that task, which + // has not read anything yet. Zero once the task starts, fails or times out. + ScheduledCounter int64 `protobuf:"varint,3,opt,name=scheduled_counter,json=scheduledCounter,proto3" json:"scheduled_counter,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ChannelSubscription) Reset() { + *x = ChannelSubscription{} + mi := &file_temporal_server_chasm_lib_workflow_proto_v1_state_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ChannelSubscription) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ChannelSubscription) ProtoMessage() {} + +func (x *ChannelSubscription) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_workflow_proto_v1_state_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ChannelSubscription.ProtoReflect.Descriptor instead. +func (*ChannelSubscription) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_workflow_proto_v1_state_proto_rawDescGZIP(), []int{3} +} + +func (x *ChannelSubscription) GetEventId() int64 { + if x != nil { + return x.EventId + } + return 0 +} + +func (x *ChannelSubscription) GetLastCounter() int64 { + if x != nil { + return x.LastCounter + } + return 0 +} + +func (x *ChannelSubscription) GetScheduledCounter() int64 { + if x != nil { + return x.ScheduledCounter + } + return 0 +} + +// ChannelNotificationEntry is the latest notification from one channel that +// no WorkflowTaskScheduled event has carried yet. Deleted when one does. +type ChannelNotificationEntry struct { + state protoimpl.MessageState `protogen:"open.v1"` + Position []byte `protobuf:"bytes,1,opt,name=position,proto3" json:"position,omitempty"` + Counter int64 `protobuf:"varint,2,opt,name=counter,proto3" json:"counter,omitempty"` + Metadata map[string]*v1.Payload `protobuf:"bytes,3,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ChannelNotificationEntry) Reset() { + *x = ChannelNotificationEntry{} + mi := &file_temporal_server_chasm_lib_workflow_proto_v1_state_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ChannelNotificationEntry) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ChannelNotificationEntry) ProtoMessage() {} + +func (x *ChannelNotificationEntry) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_workflow_proto_v1_state_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ChannelNotificationEntry.ProtoReflect.Descriptor instead. +func (*ChannelNotificationEntry) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_workflow_proto_v1_state_proto_rawDescGZIP(), []int{4} +} + +func (x *ChannelNotificationEntry) GetPosition() []byte { + if x != nil { + return x.Position + } + return nil +} + +func (x *ChannelNotificationEntry) GetCounter() int64 { + if x != nil { + return x.Counter + } + return 0 +} + +func (x *ChannelNotificationEntry) GetMetadata() map[string]*v1.Payload { + if x != nil { + return x.Metadata + } + return nil +} + var File_temporal_server_chasm_lib_workflow_proto_v1_state_proto protoreflect.FileDescriptor const file_temporal_server_chasm_lib_workflow_proto_v1_state_proto_rawDesc = "" + "\n" + - "7temporal/server/chasm/lib/workflow/proto/v1/state.proto\x12+temporal.server.chasm.lib.workflow.proto.v1\"|\n" + + "7temporal/server/chasm/lib/workflow/proto/v1/state.proto\x12+temporal.server.chasm.lib.workflow.proto.v1\x1a$temporal/api/common/v1/message.proto\"|\n" + "\x18NexusOperationParentData\x12,\n" + "\x12scheduled_event_id\x18\x01 \x01(\x03R\x10scheduledEventId\x122\n" + "\x15scheduled_event_token\x18\x02 \x01(\fR\x13scheduledEventToken\"K\n" + "\x1bNexusCancellationParentData\x12,\n" + "\x12requested_event_id\x18\x01 \x01(\x03R\x10requestedEventId\"/\n" + "\x12IncomingSignalData\x12\x19\n" + - "\bevent_id\x18\x01 \x01(\x03R\aeventIdBDZBgo.temporal.io/server/chasm/lib/workflow/gen/workflowpb;workflowpbb\x06proto3" + "\bevent_id\x18\x01 \x01(\x03R\aeventId\"\x80\x01\n" + + "\x13ChannelSubscription\x12\x19\n" + + "\bevent_id\x18\x01 \x01(\x03R\aeventId\x12!\n" + + "\flast_counter\x18\x02 \x01(\x03R\vlastCounter\x12+\n" + + "\x11scheduled_counter\x18\x03 \x01(\x03R\x10scheduledCounter\"\x9f\x02\n" + + "\x18ChannelNotificationEntry\x12\x1a\n" + + "\bposition\x18\x01 \x01(\fR\bposition\x12\x18\n" + + "\acounter\x18\x02 \x01(\x03R\acounter\x12o\n" + + "\bmetadata\x18\x03 \x03(\v2S.temporal.server.chasm.lib.workflow.proto.v1.ChannelNotificationEntry.MetadataEntryR\bmetadata\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\x01BDZBgo.temporal.io/server/chasm/lib/workflow/gen/workflowpb;workflowpbb\x06proto3" var ( file_temporal_server_chasm_lib_workflow_proto_v1_state_proto_rawDescOnce sync.Once @@ -196,18 +338,24 @@ func file_temporal_server_chasm_lib_workflow_proto_v1_state_proto_rawDescGZIP() return file_temporal_server_chasm_lib_workflow_proto_v1_state_proto_rawDescData } -var file_temporal_server_chasm_lib_workflow_proto_v1_state_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_temporal_server_chasm_lib_workflow_proto_v1_state_proto_msgTypes = make([]protoimpl.MessageInfo, 6) var file_temporal_server_chasm_lib_workflow_proto_v1_state_proto_goTypes = []any{ (*NexusOperationParentData)(nil), // 0: temporal.server.chasm.lib.workflow.proto.v1.NexusOperationParentData (*NexusCancellationParentData)(nil), // 1: temporal.server.chasm.lib.workflow.proto.v1.NexusCancellationParentData (*IncomingSignalData)(nil), // 2: temporal.server.chasm.lib.workflow.proto.v1.IncomingSignalData + (*ChannelSubscription)(nil), // 3: temporal.server.chasm.lib.workflow.proto.v1.ChannelSubscription + (*ChannelNotificationEntry)(nil), // 4: temporal.server.chasm.lib.workflow.proto.v1.ChannelNotificationEntry + nil, // 5: temporal.server.chasm.lib.workflow.proto.v1.ChannelNotificationEntry.MetadataEntry + (*v1.Payload)(nil), // 6: temporal.api.common.v1.Payload } var file_temporal_server_chasm_lib_workflow_proto_v1_state_proto_depIdxs = []int32{ - 0, // [0:0] is the sub-list for method output_type - 0, // [0:0] 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 + 5, // 0: temporal.server.chasm.lib.workflow.proto.v1.ChannelNotificationEntry.metadata:type_name -> temporal.server.chasm.lib.workflow.proto.v1.ChannelNotificationEntry.MetadataEntry + 6, // 1: temporal.server.chasm.lib.workflow.proto.v1.ChannelNotificationEntry.MetadataEntry.value:type_name -> temporal.api.common.v1.Payload + 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 } func init() { file_temporal_server_chasm_lib_workflow_proto_v1_state_proto_init() } @@ -221,7 +369,7 @@ func file_temporal_server_chasm_lib_workflow_proto_v1_state_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_temporal_server_chasm_lib_workflow_proto_v1_state_proto_rawDesc), len(file_temporal_server_chasm_lib_workflow_proto_v1_state_proto_rawDesc)), NumEnums: 0, - NumMessages: 3, + NumMessages: 6, NumExtensions: 0, NumServices: 0, }, diff --git a/chasm/lib/workflow/notification.go b/chasm/lib/workflow/notification.go new file mode 100644 index 00000000000..6a514ddee3d --- /dev/null +++ b/chasm/lib/workflow/notification.go @@ -0,0 +1,204 @@ +package workflow + +import ( + "maps" + "slices" + + enumspb "go.temporal.io/api/enums/v1" + "go.temporal.io/api/serviceerror" + "go.temporal.io/server/chasm" + channelpb "go.temporal.io/server/chasm/lib/channel/gen/channelpb/v1" + chasmworkflowpb "go.temporal.io/server/chasm/lib/workflow/gen/workflowpb/v1" + "go.temporal.io/server/common" +) + +// readOnly hides the mutable half of a context. Reaching a data field through +// a mutable context marks it for persistence, and the subscription and +// notification tables are read on paths that must leave an execution +// untouched when nothing changes. +func readOnly(ctx chasm.Context) chasm.Context { + return struct{ chasm.Context }{ctx} +} + +// SubscribedToChannel reports whether this run subscribed to the channel. +// The channel's fan-out asks, through a probe of the run, before it delivers, +// and a run that does not answer yes is dropped from the channel. +func (w *Workflow) SubscribedToChannel(channel string) bool { + _, ok := w.ChannelSubscriptions[channel] + return ok +} + +// ChannelSubscriptionCount is how many channels this run subscribed to, +// counting those its open Workflow Task is about to register. +func (w *Workflow) ChannelSubscriptionCount() int { + return len(w.ChannelSubscriptions) +} + +// RecordChannelSubscription records that this run subscribed to a channel, +// at the event that says so. A second subscription to the same channel keeps +// the first record. The limit bounds what one run may hold; zero means none. +// +// It reports whether the subscription is new, which is when the channel has +// to be told. +func (w *Workflow) RecordChannelSubscription( + mctx chasm.MutableContext, + channel string, + eventID int64, + limit int, +) (bool, error) { + if _, ok := w.ChannelSubscriptions[channel]; ok { + return false, nil + } + if limit > 0 && len(w.ChannelSubscriptions) >= limit { + return false, serviceerror.NewResourceExhaustedf( + enumspb.RESOURCE_EXHAUSTED_CAUSE_CONCURRENT_LIMIT, + "workflow already subscribes to %d notification channels, which is the limit", + len(w.ChannelSubscriptions)) + } + if w.ChannelSubscriptions == nil { + w.ChannelSubscriptions = make(chasm.Map[string, *chasmworkflowpb.ChannelSubscription]) + } + w.ChannelSubscriptions[channel] = chasm.NewDataField(mctx, + &chasmworkflowpb.ChannelSubscription{EventId: eventID}) + return true, nil +} + +// StageChannelRegistration queues a channel for the completion path to +// register this run on. A command handler holds the state lock and cannot +// reach the channel's shard, so the registration happens after the commands +// and before the commit. +func (w *Workflow) StageChannelRegistration(channel string) { + w.pendingChannelRegistrations = append(w.pendingChannelRegistrations, channel) +} + +// DrainChannelRegistrations returns and clears the staged registrations. +func (w *Workflow) DrainChannelRegistrations() []string { + out := w.pendingChannelRegistrations + w.pendingChannelRegistrations = nil + return out +} + +// ChannelNotificationIsDuplicate reports whether accepting the notification +// would change nothing: the run holds a pending notification from the channel +// at this counter or a higher one, its scheduled and not yet started task +// carries one, or it does not listen to the channel at all. +// +// A started task does not count. It may have read its source before the +// write the repeat stands for, so the same counter arriving then is a new +// reason to run: a watcher that finds a record with no task open sends the +// counter it already sent, and folding that away would leave its reader +// waiting. +func (w *Workflow) ChannelNotificationIsDuplicate( + ctx chasm.Context, + channel string, + counter int64, +) bool { + subscription, ok := w.ChannelSubscriptions[channel] + if !ok { + return true + } + view := readOnly(ctx) + if counter <= subscription.Get(view).GetScheduledCounter() { + return true + } + field, ok := w.ChannelNotifications[channel] + return ok && counter <= field.Get(view).GetCounter() +} + +// AcceptChannelNotification records a notification from a channel this run +// listens to, for the next WorkflowTaskScheduled event to carry. Nothing is +// written to History here; the transaction close schedules the Workflow Task +// whose scheduled event carries it. +// +// One entry per channel. A notification at or below the pending entry's +// counter is folded into it and changes nothing; a higher one replaces it; +// with no pending entry it starts one, whatever its counter. It reports +// whether the notification folded into a pending entry. +func (w *Workflow) AcceptChannelNotification( + mctx chasm.MutableContext, + n *channelpb.Notification, +) (bool, error) { + channel := n.GetChannel() + subscription, ok := w.ChannelSubscriptions[channel] + if !ok { + return false, serviceerror.NewFailedPreconditionf( + "workflow does not listen to notification channel %q", channel) + } + if w.ChannelNotificationIsDuplicate(mctx, channel, n.GetCounter()) { + return true, nil + } + if n.GetCounter() > subscription.Get(readOnly(mctx)).GetLastCounter() { + subscription.Get(mctx).LastCounter = n.GetCounter() + } + + entry := &chasmworkflowpb.ChannelNotificationEntry{ + Position: n.GetPosition(), + Counter: n.GetCounter(), + Metadata: common.CloneProto(n).GetMetadata(), + } + if w.ChannelNotifications == nil { + w.ChannelNotifications = make(chasm.Map[string, *chasmworkflowpb.ChannelNotificationEntry]) + } + _, folded := w.ChannelNotifications[channel] + w.ChannelNotifications[channel] = chasm.NewDataField(mctx, entry) + return folded, nil +} + +// 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 +} + +// TakeChannelNotifications returns the pending notifications, one per +// channel and sorted by channel, and clears them. Called for a +// WorkflowTaskScheduled event that is being written to History, which is +// what acknowledges them: nothing is redelivered once an event carries it. +// Each channel's subscription remembers the counter the event carries until +// 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 + } + view := readOnly(mctx) + channels := slices.Sorted(maps.Keys(w.ChannelNotifications)) + out := make([]*channelpb.Notification, 0, len(channels)) + for _, channel := range channels { + entry := w.ChannelNotifications[channel].Get(view) + out = append(out, &channelpb.Notification{ + Channel: channel, + Position: entry.GetPosition(), + Counter: entry.GetCounter(), + Metadata: entry.GetMetadata(), + }) + delete(w.ChannelNotifications, channel) + if sub, ok := w.ChannelSubscriptions[channel]; ok { + sub.Get(mctx).ScheduledCounter = entry.GetCounter() + } + } + return out +} + +// HasScheduledChannelCounters reports whether any subscription remembers what +// a scheduled task carries. +func (w *Workflow) HasScheduledChannelCounters(ctx chasm.Context) bool { + view := readOnly(ctx) + for _, field := range w.ChannelSubscriptions { + if field.Get(view).GetScheduledCounter() > 0 { + return true + } + } + return false +} + +// ClearScheduledChannelCounters forgets what the scheduled task carries. +// Called when the task starts, fails or times out: from then on a repeat +// cannot be assumed to reach a task that has not read yet. +func (w *Workflow) ClearScheduledChannelCounters(mctx chasm.MutableContext) { + view := readOnly(mctx) + for _, field := range w.ChannelSubscriptions { + if field.Get(view).GetScheduledCounter() > 0 { + field.Get(mctx).ScheduledCounter = 0 + } + } +} diff --git a/chasm/lib/workflow/notification_commands.go b/chasm/lib/workflow/notification_commands.go new file mode 100644 index 00000000000..d9e7306f135 --- /dev/null +++ b/chasm/lib/workflow/notification_commands.go @@ -0,0 +1,143 @@ +package workflow + +import ( + "fmt" + + commandpb "go.temporal.io/api/command/v1" + enumspb "go.temporal.io/api/enums/v1" + historypb "go.temporal.io/api/history/v1" + "go.temporal.io/server/chasm" + "go.temporal.io/server/chasm/lib/channel" +) + +// handleSubscribeNotificationChannelCommand makes this run a listener of a +// notification channel. +// +// The event is written and the subscription recorded here, with the command, +// so the event sits in command order. The channel lives on its own shard, +// which a command handler cannot reach while it holds the state lock, so the +// registration there is staged for the completion path, which makes it before +// the commit and fails the task if the channel refuses. +func handleSubscribeNotificationChannelCommand( + chasmCtx chasm.MutableContext, + wf *Workflow, + command *commandpb.Command, + opts CommandHandlerOptions, + maxIDLength int, + maxSubscriptions int, +) error { + badAttributes := enumspb.WORKFLOW_TASK_FAILED_CAUSE_BAD_SUBSCRIBE_NOTIFICATION_CHANNEL_ATTRIBUTES + attrs := command.GetSubscribeNotificationChannelCommandAttributes() + if attrs == nil { + return FailWorkflowTaskError{ + Cause: badAttributes, + Message: "SubscribeNotificationChannelCommandAttributes is not set", + } + } + name := attrs.GetChannel() + if err := channel.CheckChannelName(name, maxIDLength); err != nil { + return FailWorkflowTaskError{Cause: badAttributes, Message: err.Error()} + } + + // A run already subscribed still gets the event, since every SDK matches + // the commands it issued against the events they produced, in order. It + // changes nothing else: no new listener and no latest notification. + already := wf.SubscribedToChannel(name) + + // Refused before the event is written, so a refusal leaves nothing behind + // even before the failed task is rolled back. + if !already && maxSubscriptions > 0 && wf.ChannelSubscriptionCount() >= maxSubscriptions { + return FailWorkflowTaskError{ + Cause: badAttributes, + Message: fmt.Sprintf( + "workflow already subscribes to %d notification channels, the limit", maxSubscriptions), + } + } + + event := wf.AddHistoryEvent(enumspb.EVENT_TYPE_WORKFLOW_NOTIFICATION_CHANNEL_SUBSCRIBED, + func(e *historypb.HistoryEvent) { + e.Attributes = &historypb.HistoryEvent_WorkflowNotificationChannelSubscribedEventAttributes{ + WorkflowNotificationChannelSubscribedEventAttributes: &historypb. + WorkflowNotificationChannelSubscribedEventAttributes{ + WorkflowTaskCompletedEventId: opts.WorkflowTaskCompletedEventID, + Channel: name, + }, + } + }) + if already { + return nil + } + if _, err := wf.RecordChannelSubscription(chasmCtx, name, event.GetEventId(), 0); err != nil { + return err + } + wf.StageChannelRegistration(name) + return nil +} + +// channelSubscribedEvent is the event a subscribe command writes. It is also +// what puts the subscription back on a run rebuilt from History, which is how +// a reset run and a run replicated by events come to listen. +type channelSubscribedEvent struct{} + +func (channelSubscribedEvent) Type() enumspb.EventType { + return enumspb.EVENT_TYPE_WORKFLOW_NOTIFICATION_CHANNEL_SUBSCRIBED +} + +func (channelSubscribedEvent) IsWorkflowTaskTrigger() bool { return false } + +// Apply records the subscription. A second event for the same channel keeps +// the first record, so the run's first subscription is the one on record. +func (channelSubscribedEvent) Apply( + mctx chasm.MutableContext, + wf *Workflow, + event *historypb.HistoryEvent, +) error { + attrs := event.GetWorkflowNotificationChannelSubscribedEventAttributes() + _, err := wf.RecordChannelSubscription(mctx, attrs.GetChannel(), event.GetEventId(), 0) + return err +} + +// A command event, so it is never cherry-picked: the workflow reissues the +// subscribe command on the new branch if it still wants one. +func (channelSubscribedEvent) CherryPick( + chasm.MutableContext, + *Workflow, + *historypb.HistoryEvent, + map[enumspb.ResetReapplyExcludeType]struct{}, +) error { + return ErrEventNotCherryPickable +} + +// notificationLibrary registers the subscribe command with the workflow +// registry. +type notificationLibrary struct { + workflowConfig Config + channelConfig *channel.Config +} + +func newNotificationLibrary(workflowConfig Config, channelConfig *channel.Config) *notificationLibrary { + return ¬ificationLibrary{workflowConfig: workflowConfig, channelConfig: channelConfig} +} + +func (l *notificationLibrary) CommandHandlers() map[enumspb.CommandType]CommandHandler { + return map[enumspb.CommandType]CommandHandler{ + enumspb.COMMAND_TYPE_SUBSCRIBE_NOTIFICATION_CHANNEL: func( + chasmCtx chasm.MutableContext, + wf *Workflow, + _ Validator, + command *commandpb.Command, + opts CommandHandlerOptions, + ) error { + namespaceName := chasmCtx.NamespaceEntry().Name().String() + return handleSubscribeNotificationChannelCommand( + chasmCtx, wf, command, opts, + l.workflowConfig.maxIDLengthLimit(), + l.channelConfig.MaxSubscriptionsPerWorkflow(namespaceName), + ) + }, + } +} + +func (l *notificationLibrary) EventDefinitions() []EventDefinition { + return []EventDefinition{channelSubscribedEvent{}} +} diff --git a/chasm/lib/workflow/notification_test.go b/chasm/lib/workflow/notification_test.go new file mode 100644 index 00000000000..30133bbf4bd --- /dev/null +++ b/chasm/lib/workflow/notification_test.go @@ -0,0 +1,137 @@ +package workflow + +import ( + "testing" + + "github.com/stretchr/testify/require" + commonpb "go.temporal.io/api/common/v1" + "go.temporal.io/api/serviceerror" + "go.temporal.io/server/chasm" + channelpb "go.temporal.io/server/chasm/lib/channel/gen/channelpb/v1" +) + +func channelNote(channel string, counter int64) *channelpb.Notification { + return &channelpb.Notification{ + Channel: channel, + Counter: counter, + Position: []byte{byte(counter)}, + Metadata: map[string]*commonpb.Payload{"k": {Data: []byte{byte(counter)}}}, + } +} + +func subscribe(t *testing.T, w *Workflow, ctx chasm.MutableContext, channel string) { + t.Helper() + added, err := w.RecordChannelSubscription(ctx, channel, 5, 0) + require.NoError(t, err) + require.True(t, added) +} + +// Notifications fold to the highest counter per channel, and a scheduled +// event takes one per channel, sorted, and clears them. +func TestChannelNotificationsFoldAndSnapshot(t *testing.T) { + ctx := &chasm.MockMutableContext{} + w := &Workflow{} + subscribe(t, w, ctx, "orders") + subscribe(t, w, ctx, "billing") + + folded, err := w.AcceptChannelNotification(ctx, channelNote("orders", 1)) + require.NoError(t, err) + require.False(t, folded) + folded, err = w.AcceptChannelNotification(ctx, channelNote("orders", 3)) + require.NoError(t, err) + require.True(t, folded) + folded, err = w.AcceptChannelNotification(ctx, channelNote("orders", 2)) + require.NoError(t, err) + 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()) + + taken := w.TakeChannelNotifications(ctx) + require.Len(t, taken, 2) + require.Equal(t, "billing", taken[0].GetChannel()) + require.Equal(t, "orders", taken[1].GetChannel()) + 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.Empty(t, w.TakeChannelNotifications(ctx)) +} + +// Folding is against the pending entry and the unstarted task. While one is +// pending, the same or a lower counter changes nothing; once a task carrying +// it has started, the same counter is a new reason to run, since a watcher +// that finds a record with no task open sends the counter it already sent. +func TestChannelNotificationFoldsOnlyWhilePending(t *testing.T) { + ctx := &chasm.MockMutableContext{} + w := &Workflow{} + subscribe(t, w, ctx, "orders") + _, err := w.AcceptChannelNotification(ctx, channelNote("orders", 4)) + require.NoError(t, err) + + require.True(t, w.ChannelNotificationIsDuplicate(ctx, "orders", 4)) + require.True(t, w.ChannelNotificationIsDuplicate(ctx, "orders", 3)) + require.False(t, w.ChannelNotificationIsDuplicate(ctx, "orders", 5)) + + w.TakeChannelNotifications(ctx) + w.ClearScheduledChannelCounters(ctx) + require.False(t, w.ChannelNotificationIsDuplicate(ctx, "orders", 4), "carried and started, so news again") + folded, err := w.AcceptChannelNotification(ctx, channelNote("orders", 4)) + require.NoError(t, err) + require.False(t, folded) + require.True(t, w.HasPendingChannelNotifications()) + require.Equal(t, int64(4), w.ChannelSubscriptions["orders"].Get(ctx).GetLastCounter()) +} + +func TestChannelSubscriptionRules(t *testing.T) { + ctx := &chasm.MockMutableContext{} + w := &Workflow{} + + _, err := w.AcceptChannelNotification(ctx, channelNote("orders", 1)) + var precondition *serviceerror.FailedPrecondition + require.ErrorAs(t, err, &precondition, "a run that did not subscribe takes nothing") + require.True(t, w.ChannelNotificationIsDuplicate(ctx, "orders", 1)) + + added, err := w.RecordChannelSubscription(ctx, "orders", 5, 1) + require.NoError(t, err) + require.True(t, added) + added, err = w.RecordChannelSubscription(ctx, "orders", 9, 1) + require.NoError(t, err) + require.False(t, added, "a repeat keeps the first record") + require.Equal(t, int64(5), w.ChannelSubscriptions["orders"].Get(ctx).GetEventId()) + _, err = w.RecordChannelSubscription(ctx, "billing", 10, 1) + var exhausted *serviceerror.ResourceExhausted + require.ErrorAs(t, err, &exhausted) + require.True(t, w.SubscribedToChannel("orders")) + require.False(t, w.SubscribedToChannel("billing")) + + w.StageChannelRegistration("orders") + require.Equal(t, []string{"orders"}, w.DrainChannelRegistrations()) + require.Empty(t, w.DrainChannelRegistrations()) +} + +// The scheduled event of a task that has not started remembers what it +// carries, so a repeat in that window folds into the task; once the task +// starts, fails or times out, the memory is cleared and a repeat is news. +func TestChannelNotificationFoldsIntoUnstartedTask(t *testing.T) { + ctx := &chasm.MockMutableContext{} + w := &Workflow{} + subscribe(t, w, ctx, "orders") + _, err := w.AcceptChannelNotification(ctx, channelNote("orders", 4)) + require.NoError(t, err) + w.TakeChannelNotifications(ctx) + require.True(t, w.HasScheduledChannelCounters(ctx)) + + 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.ChannelNotificationIsDuplicate(ctx, "orders", 5)) + + w.ClearScheduledChannelCounters(ctx) + require.False(t, w.HasScheduledChannelCounters(ctx)) + 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()) +} diff --git a/chasm/lib/workflow/proto/v1/state.proto b/chasm/lib/workflow/proto/v1/state.proto index 0ff01d116b5..850526067df 100644 --- a/chasm/lib/workflow/proto/v1/state.proto +++ b/chasm/lib/workflow/proto/v1/state.proto @@ -2,6 +2,8 @@ syntax = "proto3"; package temporal.server.chasm.lib.workflow.proto.v1; +import "temporal/api/common/v1/message.proto"; + option go_package = "go.temporal.io/server/chasm/lib/workflow/gen/workflowpb;workflowpb"; // NexusOperationParentData contains workflow-specific data stored in a nexus operation's @@ -25,3 +27,25 @@ message NexusCancellationParentData { message IncomingSignalData { int64 event_id = 1; } + +// ChannelSubscription is a notification channel this run subscribed to with a +// command. It lives for the run. +message ChannelSubscription { + // The WorkflowNotificationChannelSubscribed event that recorded it. + int64 event_id = 1; + // Highest counter this run accepted from the channel. Kept for describing + // the subscription; folding compares against the pending entry instead. + int64 last_counter = 2; + // Counter the scheduled event of the run's current task carried, while that + // task has not started. A repeat at or below it folds into that task, which + // has not read anything yet. Zero once the task starts, fails or times out. + int64 scheduled_counter = 3; +} + +// ChannelNotificationEntry is the latest notification from one channel that +// no WorkflowTaskScheduled event has carried yet. Deleted when one does. +message ChannelNotificationEntry { + bytes position = 1; + int64 counter = 2; + map metadata = 3; +} diff --git a/chasm/lib/workflow/workflow.go b/chasm/lib/workflow/workflow.go index 14f6345fb74..937b18b5bba 100644 --- a/chasm/lib/workflow/workflow.go +++ b/chasm/lib/workflow/workflow.go @@ -59,6 +59,18 @@ type Workflow struct { // than silently handed the wrong cursor. StreamCursors chasm.Map[string, *stream.Cursor] + // Notification channels this run subscribed to, keyed by channel name. + // Not carried to a successor run, which subscribes again if it listens. + ChannelSubscriptions chasm.Map[string, *chasmworkflowpb.ChannelSubscription] + + // The latest notification per channel that no WorkflowTaskScheduled event + // has carried. The event that carries one is its acknowledgment. + ChannelNotifications chasm.Map[string, *chasmworkflowpb.ChannelNotificationEntry] + + // Channels a subscribe command in the open Workflow Task has to register + // this run on. In memory only, drained before commit. + pendingChannelRegistrations []string + // 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/common/metrics/metric_defs.go b/common/metrics/metric_defs.go index fce10f82415..914b5387a97 100644 --- a/common/metrics/metric_defs.go +++ b/common/metrics/metric_defs.go @@ -783,6 +783,25 @@ var ( "stream_polls", WithDescription("The number of stream polls admitted by the stream service."), ) + // Notification channel meters, per namespace. + ChannelNotificationsAccepted = NewCounterDef( + "channel_notifications_accepted", + WithDescription("The number of notifications a notification channel accepted."), + ) + ChannelNotificationsDelivered = NewCounterDef( + "channel_notifications_delivered", + WithDescription("The number of notifications handed to a channel listener, tagged by the "+ + "listener kind."), + ) + ChannelNotificationsFolded = NewCounterDef( + "channel_notifications_folded", + WithDescription("The number of notifications that replaced one a listener had not "+ + "been handed yet."), + ) + ChannelPollers = NewCounterDef( + "channel_pollers", + WithDescription("The number of PollChannel calls admitted."), + ) NexusRequests = NewCounterDef( "nexus_requests", WithDescription("The number of Nexus requests received by the service."), diff --git a/service/frontend/errors.go b/service/frontend/errors.go index dccc89db6c8..063f1ed40e2 100644 --- a/service/frontend/errors.go +++ b/service/frontend/errors.go @@ -28,6 +28,16 @@ var ( errWorkflowIDTooLong = serviceerror.NewInvalidArgument("WorkflowId length exceeds limit.") errWorkflowRuleIDTooLong = serviceerror.NewInvalidArgument("Workflow Rule Id length exceeds limit.") errSignalNameTooLong = serviceerror.NewInvalidArgument("SignalName length exceeds limit.") + errNotificationNotSet = serviceerror.NewInvalidArgument("Notification is not set on request.") + errChannelNotSet = serviceerror.NewInvalidArgument("Channel is not set on request.") + errChannelTooLong = serviceerror.NewInvalidArgument("Channel length exceeds limit.") + errNotificationPositionTooLarge = serviceerror.NewInvalidArgument("Notification.Position exceeds 1024 bytes.") + errNotificationCounterNotPositive = serviceerror.NewInvalidArgument("Notification.Counter must be greater than zero.") + errChannelCallbackNotSet = serviceerror.NewInvalidArgument("Callback is not set on request.") + errListenerIDNotSet = serviceerror.NewInvalidArgument("ListenerId is not set on request.") + errListenerIDTooLong = serviceerror.NewInvalidArgument("ListenerId length exceeds limit.") + errMaxNotificationsNegative = serviceerror.NewInvalidArgument("MaxNotifications must not be negative.") + errPollWaitNegative = serviceerror.NewInvalidArgument("Wait must not be negative.") errRequestIDTooLong = serviceerror.NewInvalidArgument("RequestId length exceeds limit.") errIdentityTooLong = serviceerror.NewInvalidArgument("Identity length exceeds limit.") errNotesTooLong = serviceerror.NewInvalidArgument("Schedule notes exceeds limit.") diff --git a/service/frontend/fx.go b/service/frontend/fx.go index 6dbf6b651c5..b51a66c51e5 100644 --- a/service/frontend/fx.go +++ b/service/frontend/fx.go @@ -10,6 +10,8 @@ import ( "go.temporal.io/server/chasm" "go.temporal.io/server/chasm/lib/activity" chasmcallback "go.temporal.io/server/chasm/lib/callback" + channelpb "go.temporal.io/server/chasm/lib/channel/gen/channelpb/v1" + chasmchannel "go.temporal.io/server/chasm/lib/channel/service" chasmnexus "go.temporal.io/server/chasm/lib/nexusoperation" nexusoperationpb "go.temporal.io/server/chasm/lib/nexusoperation/gen/nexusoperationpb/v1" chasmscheduler "go.temporal.io/server/chasm/lib/scheduler" @@ -159,6 +161,7 @@ var Module = fx.Options( chasmcallback.Module, activity.FrontendModule, chasmstream.FrontendModule, + chasmchannel.FrontendModule, fx.Provide(visibility.ChasmVisibilityManagerProvider), fx.Provide(chasm.ChasmVisibilityInterceptorProvider), ) @@ -994,6 +997,7 @@ func HandlerProvider( matchingClient resource.MatchingClient, workerDeploymentStoreClient workerdeployment.Client, schedulerClient schedulerpb.SchedulerServiceClient, + channelClient channelpb.ChannelServiceClient, archiverProvider provider.ArchiverProvider, metricsHandler metrics.Handler, payloadSerializer serialization.Serializer, @@ -1036,6 +1040,7 @@ func HandlerProvider( matchingClient, workerDeploymentStoreClient, schedulerClient, + channelClient, archiverProvider, payloadSerializer, namespaceRegistry, diff --git a/service/frontend/workflow_handler.go b/service/frontend/workflow_handler.go index 4f9c0043ecb..5de6ad1e8cd 100644 --- a/service/frontend/workflow_handler.go +++ b/service/frontend/workflow_handler.go @@ -22,6 +22,7 @@ import ( failurepb "go.temporal.io/api/failure/v1" filterpb "go.temporal.io/api/filter/v1" historypb "go.temporal.io/api/history/v1" + notificationpb "go.temporal.io/api/notification/v1" querypb "go.temporal.io/api/query/v1" schedulepb "go.temporal.io/api/schedule/v1" "go.temporal.io/api/serviceerror" @@ -38,6 +39,8 @@ import ( taskqueuespb "go.temporal.io/server/api/taskqueue/v1" "go.temporal.io/server/chasm" "go.temporal.io/server/chasm/lib/activity" + "go.temporal.io/server/chasm/lib/channel" + channelpb "go.temporal.io/server/chasm/lib/channel/gen/channelpb/v1" chasmnexus "go.temporal.io/server/chasm/lib/nexusoperation" chasmscheduler "go.temporal.io/server/chasm/lib/scheduler" schedulerpb "go.temporal.io/server/chasm/lib/scheduler/gen/schedulerpb/v1" @@ -147,6 +150,7 @@ type ( matchingClient matchingservice.MatchingServiceClient workerDeploymentClient workerdeployment.Client schedulerClient schedulerpb.SchedulerServiceClient + channelClient channelpb.ChannelServiceClient archiverProvider provider.ArchiverProvider payloadSerializer serialization.Serializer namespaceRegistry namespace.Registry @@ -324,6 +328,7 @@ func NewWorkflowHandler( matchingClient matchingservice.MatchingServiceClient, workerDeploymentClient workerdeployment.Client, schedulerClient schedulerpb.SchedulerServiceClient, + channelClient channelpb.ChannelServiceClient, archiverProvider provider.ArchiverProvider, payloadSerializer serialization.Serializer, namespaceRegistry namespace.Registry, @@ -381,6 +386,7 @@ func NewWorkflowHandler( matchingClient: matchingClient, workerDeploymentClient: workerDeploymentClient, schedulerClient: schedulerClient, + channelClient: channelClient, archiverProvider: archiverProvider, payloadSerializer: payloadSerializer, namespaceRegistry: namespaceRegistry, @@ -2360,49 +2366,271 @@ func (wh *WorkflowHandler) SignalWorkflowExecution(ctx context.Context, request }, nil } -// NotifyChannel is declared by the pinned API and served by a later layer of -// this series. +// channelNamespaceID resolves the namespace of a channel call. +func (wh *WorkflowHandler) channelNamespaceID(name string) (string, error) { + if name == "" { + return "", errNamespaceNotSet + } + id, err := wh.namespaceRegistry.GetNamespaceID(namespace.Name(name)) + if err != nil { + return "", err + } + return id.String(), nil +} + +// validateChannelName refuses a channel name that cannot key an execution. +func (wh *WorkflowHandler) validateChannelName(name string) error { + if name == "" { + return errChannelNotSet + } + if len(name) > wh.config.MaxIDLengthLimit() { + return errChannelTooLong + } + return nil +} + +// validateCallerFields checks the identity and request id a channel call +// carries, the same way Signal checks them. +func (wh *WorkflowHandler) validateCallerFields(identity, requestID string) error { + if len(identity) > wh.config.MaxIDLengthLimit() { + return errIdentityTooLong + } + if len(requestID) > wh.config.MaxIDLengthLimit() { + return errRequestIDTooLong + } + return nil +} + +// 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. func (wh *WorkflowHandler) NotifyChannel( - _ context.Context, - _ *workflowservice.NotifyChannelRequest, -) (*workflowservice.NotifyChannelResponse, error) { - return nil, serviceerror.NewUnimplemented("NotifyChannel is not implemented") + ctx context.Context, + request *workflowservice.NotifyChannelRequest, +) (_ *workflowservice.NotifyChannelResponse, retError error) { + defer log.CapturePanic(wh.logger, &retError) + + if request == nil { + return nil, errRequestNotSet + } + n := request.GetNotification() + if n == nil { + return nil, errNotificationNotSet + } + if err := wh.validateChannelName(n.GetChannel()); err != nil { + return nil, err + } + if len(n.GetPosition()) > channel.MaxPositionBytes { + return nil, errNotificationPositionTooLarge + } + if n.GetCounter() <= 0 { + return nil, errNotificationCounterNotPositive + } + if err := wh.validateCallerFields(request.GetIdentity(), request.GetRequestId()); err != nil { + return nil, err + } + namespaceID, err := wh.channelNamespaceID(request.GetNamespace()) + if err != nil { + return nil, err + } + + resp, err := wh.channelClient.NotifyChannel(ctx, &channelpb.NotifyChannelRequest{ + NamespaceId: namespaceID, + FrontendRequest: &channelpb.NotifyChannelInput{ + Namespace: request.GetNamespace(), + Notification: channel.FromAPINotification(n), + Identity: request.GetIdentity(), + RequestId: request.GetRequestId(), + }, + }) + if err != nil { + return nil, err + } + return &workflowservice.NotifyChannelResponse{ + ListenerCount: int32(resp.GetFrontendResponse().GetListenerCount()), + }, nil } -// RegisterChannelListener is declared by the pinned API and served by a later -// layer of this series. +// RegisterChannelListener registers a callback as a listener of a channel. +// The server posts each notification to it. A retry with the same request id +// returns the listener the first call made. func (wh *WorkflowHandler) RegisterChannelListener( - _ context.Context, - _ *workflowservice.RegisterChannelListenerRequest, -) (*workflowservice.RegisterChannelListenerResponse, error) { - return nil, serviceerror.NewUnimplemented("RegisterChannelListener is not implemented") + ctx context.Context, + request *workflowservice.RegisterChannelListenerRequest, +) (_ *workflowservice.RegisterChannelListenerResponse, retError error) { + defer log.CapturePanic(wh.logger, &retError) + + if request == nil { + return nil, errRequestNotSet + } + if err := wh.validateChannelName(request.GetChannel()); err != nil { + return nil, err + } + if err := wh.validateCallerFields(request.GetIdentity(), request.GetRequestId()); err != nil { + return nil, err + } + if request.GetCallback() == nil { + return nil, errChannelCallbackNotSet + } + // Only a URL callback is accepted: the server posts the notification to + // it, which a Nexus handler callback has no way to receive. + opts := callbacks.ValidatorOptions{EnabledKinds: []callbacks.Kind{callbacks.KindNexus}} + cbs := []*commonpb.Callback{request.GetCallback()} + if err := wh.callbackValidator.Validate(ctx, request.GetNamespace(), cbs, opts); err != nil { + return nil, err + } + namespaceID, err := wh.channelNamespaceID(request.GetNamespace()) + if err != nil { + return nil, err + } + requestID := request.GetRequestId() + if requestID == "" { + requestID = uuid.NewString() + } + + resp, err := wh.channelClient.RegisterChannelListener(ctx, &channelpb.RegisterChannelListenerRequest{ + NamespaceId: namespaceID, + FrontendRequest: &channelpb.RegisterChannelListenerInput{ + Namespace: request.GetNamespace(), + Channel: request.GetChannel(), + Callback: request.GetCallback(), + RequestId: requestID, + Identity: request.GetIdentity(), + }, + }) + if err != nil { + return nil, err + } + return &workflowservice.RegisterChannelListenerResponse{ + ListenerId: resp.GetFrontendResponse().GetListenerId(), + }, nil } -// UnregisterChannelListener is declared by the pinned API and served by a -// later layer of this series. +// UnregisterChannelListener removes a callback listener. A listener that is +// already gone is not an error, so a retry succeeds. func (wh *WorkflowHandler) UnregisterChannelListener( - _ context.Context, - _ *workflowservice.UnregisterChannelListenerRequest, -) (*workflowservice.UnregisterChannelListenerResponse, error) { - return nil, serviceerror.NewUnimplemented("UnregisterChannelListener is not implemented") + ctx context.Context, + request *workflowservice.UnregisterChannelListenerRequest, +) (_ *workflowservice.UnregisterChannelListenerResponse, retError error) { + defer log.CapturePanic(wh.logger, &retError) + + if request == nil { + return nil, errRequestNotSet + } + if err := wh.validateChannelName(request.GetChannel()); err != nil { + return nil, err + } + if request.GetListenerId() == "" { + return nil, errListenerIDNotSet + } + if len(request.GetListenerId()) > wh.config.MaxIDLengthLimit() { + return nil, errListenerIDTooLong + } + if err := wh.validateCallerFields(request.GetIdentity(), ""); err != nil { + return nil, err + } + namespaceID, err := wh.channelNamespaceID(request.GetNamespace()) + if err != nil { + return nil, err + } + + _, err = wh.channelClient.UnregisterChannelListener(ctx, &channelpb.UnregisterChannelListenerRequest{ + NamespaceId: namespaceID, + FrontendRequest: &channelpb.UnregisterChannelListenerInput{ + Namespace: request.GetNamespace(), + Channel: request.GetChannel(), + ListenerId: request.GetListenerId(), + Identity: request.GetIdentity(), + }, + }) + if err != nil { + return nil, err + } + return &workflowservice.UnregisterChannelListenerResponse{}, nil } -// PollChannel is declared by the pinned API and served by a later layer of -// this series. +// PollChannel returns the retained notifications above the caller's counter, +// waiting up to the requested time for one when none is retained yet. func (wh *WorkflowHandler) PollChannel( - _ context.Context, - _ *workflowservice.PollChannelRequest, -) (*workflowservice.PollChannelResponse, error) { - return nil, serviceerror.NewUnimplemented("PollChannel is not implemented") + ctx context.Context, + request *workflowservice.PollChannelRequest, +) (_ *workflowservice.PollChannelResponse, retError error) { + defer log.CapturePanic(wh.logger, &retError) + + if request == nil { + return nil, errRequestNotSet + } + if err := wh.validateChannelName(request.GetChannel()); err != nil { + return nil, err + } + if request.GetMaxNotifications() < 0 { + return nil, errMaxNotificationsNegative + } + if request.GetWait().AsDuration() < 0 { + return nil, errPollWaitNegative + } + namespaceID, err := wh.channelNamespaceID(request.GetNamespace()) + if err != nil { + return nil, err + } + + resp, err := wh.channelClient.PollChannel(ctx, &channelpb.PollChannelRequest{ + NamespaceId: namespaceID, + FrontendRequest: &channelpb.PollChannelInput{ + Namespace: request.GetNamespace(), + Channel: request.GetChannel(), + AfterCounter: request.GetAfterCounter(), + Wait: request.GetWait(), + MaxNotifications: request.GetMaxNotifications(), + }, + }) + if err != nil { + return nil, err + } + return &workflowservice.PollChannelResponse{ + Notifications: channel.ToAPINotifications(resp.GetFrontendResponse().GetNotifications()), + }, nil } -// DescribeChannel is declared by the pinned API and served by a later layer of -// this series. +// DescribeChannel returns a channel's listeners, its latest notification and +// how many notifications it retains. func (wh *WorkflowHandler) DescribeChannel( - _ context.Context, - _ *workflowservice.DescribeChannelRequest, -) (*workflowservice.DescribeChannelResponse, error) { - return nil, serviceerror.NewUnimplemented("DescribeChannel is not implemented") + ctx context.Context, + request *workflowservice.DescribeChannelRequest, +) (_ *workflowservice.DescribeChannelResponse, retError error) { + defer log.CapturePanic(wh.logger, &retError) + + if request == nil { + return nil, errRequestNotSet + } + if err := wh.validateChannelName(request.GetChannel()); err != nil { + return nil, err + } + namespaceID, err := wh.channelNamespaceID(request.GetNamespace()) + if err != nil { + return nil, err + } + + resp, err := wh.channelClient.DescribeChannel(ctx, &channelpb.DescribeChannelRequest{ + NamespaceId: namespaceID, + FrontendRequest: &channelpb.DescribeChannelInput{ + Namespace: request.GetNamespace(), + Channel: request.GetChannel(), + }, + }) + if err != nil { + return nil, err + } + out := resp.GetFrontendResponse() + listeners := make([]*notificationpb.ChannelListener, 0, len(out.GetListeners())) + for _, l := range out.GetListeners() { + listeners = append(listeners, channel.ToAPIListener(l)) + } + return &workflowservice.DescribeChannelResponse{ + Listeners: listeners, + Latest: channel.ToAPINotification(out.GetLatest()), + RetainedCount: int32(out.GetRetainedCount()), + }, nil } // SignalWithStartWorkflowExecution is used to ensure sending signal to a workflow. diff --git a/service/frontend/workflow_handler_test.go b/service/frontend/workflow_handler_test.go index 613676e9117..4064ff7bef8 100644 --- a/service/frontend/workflow_handler_test.go +++ b/service/frontend/workflow_handler_test.go @@ -211,6 +211,7 @@ func (s *WorkflowHandlerSuite) getWorkflowHandler(config *Config) *WorkflowHandl s.mockResource.GetMatchingClient(), nil, nil, // Not initializing the scheduler client here. + nil, // Nor the channel client. s.mockResource.GetArchiverProvider(), s.mockResource.GetPayloadSerializer(), s.mockResource.GetNamespaceRegistry(), diff --git a/service/history/api/command_attr_validator.go b/service/history/api/command_attr_validator.go index 257135405d9..b8dabbe323f 100644 --- a/service/history/api/command_attr_validator.go +++ b/service/history/api/command_attr_validator.go @@ -667,7 +667,10 @@ func (v *CommandAttrValidator) ValidateCommandSequence( enumspb.COMMAND_TYPE_APPEND_STREAM_RECORDS, // Subscribing to a stream. Also not closing: it records a cursor // and the workflow carries on. - enumspb.COMMAND_TYPE_SUBSCRIBE_STREAM: + enumspb.COMMAND_TYPE_SUBSCRIBE_STREAM, + // Listening on a notification channel. Not closing either: it + // records a subscription for the run. + enumspb.COMMAND_TYPE_SUBSCRIBE_NOTIFICATION_CHANNEL: // noop case enumspb.COMMAND_TYPE_CONTINUE_AS_NEW_WORKFLOW_EXECUTION, enumspb.COMMAND_TYPE_COMPLETE_WORKFLOW_EXECUTION, diff --git a/service/history/api/respondworkflowtaskcompleted/api.go b/service/history/api/respondworkflowtaskcompleted/api.go index b460e874cf1..17beed0c769 100644 --- a/service/history/api/respondworkflowtaskcompleted/api.go +++ b/service/history/api/respondworkflowtaskcompleted/api.go @@ -481,6 +481,24 @@ func (handler *WorkflowTaskCompletedHandler) Invoke( } } + // Notification channel subscriptions, registered on each channel's + // shard before the commit. Skipped once the task has failed, since the + // subscription and its event are about to be rolled back. + if workflowTaskHandler.workflowTaskFailedCause == nil && !workflowTaskHandler.stopProcessing { + err = registerStagedChannelListeners( + ctx, + ms, + enumspb.WORKFLOW_TASK_FAILED_CAUSE_BAD_SUBSCRIBE_NOTIFICATION_CHANNEL_ATTRIBUTES, + workflowTaskHandler.stagedChannelRegistrations, + ) + if failWFTErr, ok := errors.AsType[chasmworkflow.FailWorkflowTaskError](err); ok { + err = workflowTaskHandler.failWorkflowTask(failWFTErr.Cause, failWFTErr) + } + if err != nil { + return nil, err + } + } + // Worker must respond with Update Accepted or Update Rejected message on every Update Requested // message that were delivered on specific WT, when completing this WT. // If worker ignored the update request (old SDK or SDK bug), then server rejects this update. diff --git a/service/history/api/respondworkflowtaskcompleted/channel_subscriptions.go b/service/history/api/respondworkflowtaskcompleted/channel_subscriptions.go new file mode 100644 index 00000000000..6010a6860e1 --- /dev/null +++ b/service/history/api/respondworkflowtaskcompleted/channel_subscriptions.go @@ -0,0 +1,77 @@ +package respondworkflowtaskcompleted + +import ( + "context" + + enumspb "go.temporal.io/api/enums/v1" + "go.temporal.io/api/serviceerror" + "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/service/history/api/recordworkflowtaskstarted" + historyi "go.temporal.io/server/service/history/interfaces" +) + +// registerStagedChannelListeners registers this run on every channel a +// subscribe command in the task named, on each channel's own shard. +// +// The command handler recorded the subscription and wrote its event, but it +// holds the state lock with nowhere to do I/O from, so the registration +// happens here, between the commands and the commit. The channel hands back +// its latest notification, which the run takes as pending so the task this +// completion schedules carries it. A refusal fails the task +// with the given cause, which rolls the subscription and its event back with +// it. A registration that succeeded before a later one was refused stays on +// its channel; the channel's next fan-out finds the run does not listen and +// drops it. +func registerStagedChannelListeners( + ctx context.Context, + ms historyi.MutableState, + cause enumspb.WorkflowTaskFailedCause, + staged []string, +) error { + if len(staged) == 0 { + return nil + } + client, ok := channel.ClientFromContext(ctx) + if !ok { + return serviceerror.NewInternal( + "channel service client is not available on the workflow task completion path") + } + + // One budget over the whole set, since the execution's lock is held for + // all of them. + ctx, cancelBudget := recordworkflowtaskstarted.WithRoutedBudget(ctx) + defer cancelBudget() + + wf, chasmCtx, err := ms.ChasmWorkflowComponent(ctx) + if err != nil { + return err + } + key := ms.GetWorkflowKey() + namespaceName := ms.GetNamespaceEntry().Name().String() + firstRunID := ms.GetExecutionInfo().GetFirstExecutionRunId() + for _, name := range staged { + callCtx, cancel := context.WithTimeout(ctx, channel.RoutedCallTimeout) + resp, err := client.RegisterWorkflowListener(callCtx, &channelpb.RegisterWorkflowListenerRequest{ + NamespaceId: key.NamespaceID, + FrontendRequest: &channelpb.RegisterWorkflowListenerInput{ + Namespace: namespaceName, + Channel: name, + WorkflowId: key.WorkflowID, + RunId: key.RunID, + FirstExecutionRunId: firstRunID, + }, + }) + cancel() + if err != nil { + return chasmworkflow.StreamAdmissionFailure(cause, err) + } + if latest := resp.GetFrontendResponse().GetLatest(); latest != nil { + if _, err := wf.AcceptChannelNotification(chasmCtx, latest); err != nil { + return err + } + } + } + return nil +} diff --git a/service/history/api/respondworkflowtaskcompleted/workflow_task_completed_handler.go b/service/history/api/respondworkflowtaskcompleted/workflow_task_completed_handler.go index 8baf1728e6d..b4609967d48 100644 --- a/service/history/api/respondworkflowtaskcompleted/workflow_task_completed_handler.go +++ b/service/history/api/respondworkflowtaskcompleted/workflow_task_completed_handler.go @@ -64,6 +64,7 @@ type ( // Subscribe commands for streams in other executions, resolved before // this workflow task commits. stagedStreamSubscriptions []chasmworkflow.PendingStreamSubscription + stagedChannelRegistrations []string hasBufferedEventsOrMessages bool workflowTaskFailedCause *workflowTaskFailedCause activityNotStartedCancelled bool @@ -363,6 +364,10 @@ func (handler *workflowTaskCompletedHandler) handleCommand( // resolution that has to precede this task's commit. handler.stagedStreamSubscriptions = append( handler.stagedStreamSubscriptions, chasmWorkflow.DrainStreamSubscriptions()...) + // A notification channel subscription stages its registration + // for the same reason. + handler.stagedChannelRegistrations = append( + handler.stagedChannelRegistrations, chasmWorkflow.DrainChannelRegistrations()...) // Fall back to the HSM handler either when the command type is not supported by CHASM (disabled // feature flag) or when the targeted entity is not owned by the CHASM tree (e.g. an operation // scheduled in HSM before the flag was flipped on). diff --git a/service/history/fx.go b/service/history/fx.go index 595bce956c2..4a3271ac068 100644 --- a/service/history/fx.go +++ b/service/history/fx.go @@ -10,6 +10,7 @@ import ( "go.temporal.io/server/chasm" "go.temporal.io/server/chasm/lib/activity" "go.temporal.io/server/chasm/lib/callback" + chasmchannel "go.temporal.io/server/chasm/lib/channel/service" chasmnexus "go.temporal.io/server/chasm/lib/nexusoperation" "go.temporal.io/server/chasm/lib/scheduler" chasmstream "go.temporal.io/server/chasm/lib/stream/service" @@ -129,6 +130,7 @@ var Module = fx.Options( fx.Invoke(hsmnexusworkflow.RegisterCommandHandlers), activity.HistoryModule, chasmstream.HistoryModule, + chasmchannel.HistoryModule, scheduler.Module, callback.Module, chasmnexus.Module, diff --git a/service/history/history_engine.go b/service/history/history_engine.go index 82291b49c5a..b32973873ce 100644 --- a/service/history/history_engine.go +++ b/service/history/history_engine.go @@ -14,6 +14,8 @@ import ( replicationspb "go.temporal.io/server/api/replication/v1" workflowspb "go.temporal.io/server/api/workflow/v1" "go.temporal.io/server/chasm" + "go.temporal.io/server/chasm/lib/channel" + channelpb "go.temporal.io/server/chasm/lib/channel/gen/channelpb/v1" streampb "go.temporal.io/server/chasm/lib/stream/gen/streampb/v1" chasmworkflow "go.temporal.io/server/chasm/lib/workflow" "go.temporal.io/server/client" @@ -109,6 +111,7 @@ type ( engineOptions struct { workflowResendScheduler workflowresend.Scheduler streamClient streampb.StreamServiceClient + channelClient channelpb.ChannelServiceClient } // EngineOption adds optional history engine dependencies without adding required parameters. @@ -151,6 +154,7 @@ type ( workflowResendScheduler workflowresend.Scheduler chasmEngine chasm.Engine streamClient streampb.StreamServiceClient + channelClient channelpb.ChannelServiceClient versionChecker headers.VersionChecker versionCache worker_versioning.VersionMembershipAndReactivationStatusCache workerDeploymentClient workerdeployment.Client @@ -181,6 +185,14 @@ func WithStreamClient(streamClient streampb.StreamServiceClient) EngineOption { } } +// WithChannelClient routes notification channel calls to the shard owning the +// channel. +func WithChannelClient(channelClient channelpb.ChannelServiceClient) EngineOption { + return func(options *engineOptions) { + options.channelClient = channelClient + } +} + func applyEngineOptions(options []EngineOption) engineOptions { var result engineOptions for _, option := range options { @@ -276,6 +288,7 @@ func NewEngineWithShardContext( testHooks: testHooks, chasmEngine: chasmEngine, streamClient: engineOptions.streamClient, + channelClient: engineOptions.channelClient, versionCache: versionCache, workerDeploymentClient: workerDeploymentClient, routingInfoCache: routingInfoCache, @@ -678,6 +691,7 @@ func (e *historyEngineImpl) RespondWorkflowTaskCompleted( e.matchingClient, e.versionCache, ) + ctx = channel.WithClient(ctx, e.channelClient) return h.Invoke(recordworkflowtaskstarted.WithStreamClient(ctx, e.streamClient), req) } diff --git a/service/history/history_engine_factory.go b/service/history/history_engine_factory.go index fb75ae56779..f2abbf9e137 100644 --- a/service/history/history_engine_factory.go +++ b/service/history/history_engine_factory.go @@ -3,6 +3,7 @@ package history import ( "go.opentelemetry.io/otel/trace" "go.temporal.io/server/chasm" + channelpb "go.temporal.io/server/chasm/lib/channel/gen/channelpb/v1" streampb "go.temporal.io/server/chasm/lib/stream/gen/streampb/v1" chasmworkflow "go.temporal.io/server/chasm/lib/workflow" "go.temporal.io/server/client" @@ -55,6 +56,7 @@ type ( TestHooks testhooks.TestHooks ChasmEngine chasm.Engine StreamClient streampb.StreamServiceClient + ChannelClient channelpb.ChannelServiceClient VersionMembershipCache worker_versioning.VersionMembershipAndReactivationStatusCache WorkerDeploymentClient workerdeployment.Client RoutingInfoCache worker_versioning.RoutingInfoCache @@ -100,5 +102,6 @@ func (f *historyEngineFactory) CreateEngine( f.ChasmEngine, WithWorkflowResendScheduler(f.WorkflowResendScheduler), WithStreamClient(f.StreamClient), + WithChannelClient(f.ChannelClient), ) } diff --git a/service/history/hsm/hsmtest/sm.go b/service/history/hsm/hsmtest/sm.go index d01aec19a01..0a5c72308cc 100644 --- a/service/history/hsm/hsmtest/sm.go +++ b/service/history/hsm/hsmtest/sm.go @@ -3,6 +3,7 @@ package hsmtest import ( "fmt" "strings" + "sync/atomic" "time" "go.temporal.io/server/service/history/hsm" @@ -41,10 +42,17 @@ func (d *Data) SetState(s State) { d.state = s } +// regenerated counts the timer tasks handed out. Timers with equal deadlines +// are grouped into one state machine timer, and the wall clock does not tick +// between two nodes of one walk on every platform, so each deadline is pushed +// out by one more nanosecond than the last. +var regenerated atomic.Int64 + func (d *Data) RegenerateTasks(node *hsm.Node) ([]hsm.Task, error) { + deadline := time.Now().Add(time.Hour + time.Duration(regenerated.Add(1))) return []hsm.Task{ NewTask( - hsm.TaskAttributes{Deadline: time.Now().Add(time.Hour)}, + hsm.TaskAttributes{Deadline: deadline}, false, ), NewTask( diff --git a/service/history/workflow/mutable_state_impl.go b/service/history/workflow/mutable_state_impl.go index fa74f30ed05..d758b5525cf 100644 --- a/service/history/workflow/mutable_state_impl.go +++ b/service/history/workflow/mutable_state_impl.go @@ -37,6 +37,8 @@ import ( tokenspb "go.temporal.io/server/api/token/v1" workflowspb "go.temporal.io/server/api/workflow/v1" "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" "go.temporal.io/server/common/backoff" @@ -731,12 +733,16 @@ func (ms *MutableStateImpl) carryStreamSubscriptionsTo(newMutableState *MutableS } // HasPendingStreamData reports whether a subscription of this workflow has -// offsets left to deliver, which is the one condition under which stream -// traffic schedules a workflow task. +// offsets left to deliver, or a channel notification is waiting for a +// scheduled event. These are the conditions under which stream traffic and +// notifications schedule a workflow task; neither writes an event that would. +// +// A notification does not cut short a first workflow task backoff: the task +// that ends the backoff carries it. // // Called on every transaction close for every workflow, almost none of which -// have a subscription, so it resolves the root component once rather than -// asking whether it exists and then asking for it. +// have a subscription or a notification, so it resolves the root component +// once rather than asking whether it exists and then asking for it. func (ms *MutableStateImpl) HasPendingStreamData() bool { node, ok := ms.chasmTree.(*chasm.Node) if !ok { @@ -751,7 +757,78 @@ func (ms *MutableStateImpl) HasPendingStreamData() bool { if !ok { return false } - return wf.StreamCursorsBehind(chasmCtx) + if wf.StreamCursorsBehind(chasmCtx) { + return true + } + if !wf.HasPendingChannelNotifications() { + return false + } + return !ms.IsWorkflowPendingOnWorkflowTaskBackoff() +} + +// attachChannelNotifications puts the pending channel notifications on a +// WorkflowTaskScheduled event being written for a task no worker has seen. +// Scheduled events written after the fact, for a transient or speculative +// task a worker already ran, carry none: History has to match what that +// worker saw, and the notifications wait for the next scheduled event. +func (ms *MutableStateImpl) attachChannelNotifications(event *historypb.HistoryEvent) { + notifications := ms.takeChannelNotifications() + if len(notifications) == 0 { + return + } + event.GetWorkflowTaskScheduledEventAttributes().Notifications = + channel.ToAPINotifications(notifications) +} + +// clearScheduledChannelCounters forgets the counters the scheduled task +// carries, once that task starts, fails or times out. Checked through a +// read-only view first, since almost no workflow has any. +func (ms *MutableStateImpl) clearScheduledChannelCounters() { + node, ok := ms.chasmTree.(*chasm.Node) + if !ok { + return + } + view := chasm.NewContext(context.Background(), node) + rootComponent, err := node.ComponentByPath(view, nil) + if err != nil { + return + } + if wf, ok := rootComponent.(*chasmworkflow.Workflow); !ok || !wf.HasScheduledChannelCounters(view) { + return + } + wf, chasmCtx, err := ms.ChasmWorkflowComponent(context.Background()) + if err != nil { + return + } + wf.ClearScheduledChannelCounters(chasmCtx) +} + +// takeChannelNotifications hands the pending channel notifications to a +// WorkflowTaskScheduled event that is being written, and clears them. The +// event is the acknowledgment: once History carries a notification nothing +// redelivers it. +// +// Called for every scheduled event, almost none of which have anything to +// carry, so the keys are checked through a read-only view first and the +// component is only taken for writing when there is something to take. +func (ms *MutableStateImpl) takeChannelNotifications() []*channelpb.Notification { + node, ok := ms.chasmTree.(*chasm.Node) + if !ok { + return nil + } + view := chasm.NewContext(context.Background(), node) + rootComponent, err := node.ComponentByPath(view, nil) + if err != nil { + return nil + } + if wf, ok := rootComponent.(*chasmworkflow.Workflow); !ok || !wf.HasPendingChannelNotifications() { + return nil + } + wf, chasmCtx, err := ms.ChasmWorkflowComponent(context.Background()) + if err != nil { + return nil + } + return wf.TakeChannelNotifications(chasmCtx) } // ConsumesStreams reports whether this workflow holds a cursor into any diff --git a/service/history/workflow/workflow_task_state_machine.go b/service/history/workflow/workflow_task_state_machine.go index 9ecc63b75ae..08b5e36fe94 100644 --- a/service/history/workflow/workflow_task_state_machine.go +++ b/service/history/workflow/workflow_task_state_machine.go @@ -267,6 +267,7 @@ func (m *workflowTaskStateMachine) ApplyWorkflowTaskTimedOutEvent(timeoutType en func (m *workflowTaskStateMachine) AddWorkflowTaskScheduleToStartTimeoutEvent( workflowTask *historyi.WorkflowTaskInfo, ) (*historypb.HistoryEvent, error) { + m.ms.clearScheduledChannelCounters() opTag := tag.WorkflowActionWorkflowTaskTimedOut if m.ms.executionInfo.WorkflowTaskScheduledEventId != workflowTask.ScheduledEventID || m.ms.executionInfo.WorkflowTaskStartedEventId > 0 { m.ms.logger.Warn(mutableStateInvalidHistoryActionMsg, opTag, @@ -367,6 +368,7 @@ func (m *workflowTaskStateMachine) AddWorkflowTaskScheduledEventAsHeartbeat( attempt, scheduleTime, ) + m.ms.attachChannelNotifications(scheduledEvent) scheduledEventID = scheduledEvent.GetEventId() } else { // WorkflowTaskScheduledEvent will be created later. @@ -584,6 +586,9 @@ func (m *workflowTaskStateMachine) AddWorkflowTaskStartedEvent( workflowTask.Attempt, startTime, ) + // The worker has not seen this task yet, so its scheduled event can + // carry what is pending. + m.ms.attachChannelNotifications(scheduledEvent) scheduledEventID = scheduledEvent.GetEventId() } @@ -626,6 +631,10 @@ func (m *workflowTaskStateMachine) AddWorkflowTaskStartedEvent( m.emitWorkflowTaskAttemptStats(workflowTask.Attempt) + // The task is reading from here on, so a repeated notification can no + // longer be assumed to reach it and goes to the next task instead. + m.ms.clearScheduledChannelCounters() + // TODO merge active & passive task generation if err = m.ms.taskGenerator.GenerateStartWorkflowTaskTasks( scheduledEventID, @@ -672,6 +681,7 @@ func (m *workflowTaskStateMachine) processBuildIdRedirectInfo( workflowTask.Attempt, workflowTask.ScheduledTime, ) + m.ms.attachChannelNotifications(scheduledEvent) newWorkflowTask = m.getWorkflowTaskInfo() newWorkflowTask.ScheduledEventID = scheduledEvent.GetEventId() // Using 1 as the attempt in MS. it's needed so that the new WFT is not considered transient. @@ -912,6 +922,7 @@ func (m *workflowTaskStateMachine) AddWorkflowTaskFailedEvent( newRunID string, forkEventVersion int64, ) (*historypb.HistoryEvent, error) { + m.ms.clearScheduledChannelCounters() // IMPORTANT: returned event can be nil under some circumstances. Specifically, if WT is transient. @@ -987,6 +998,7 @@ func (m *workflowTaskStateMachine) AddWorkflowTaskFailedEvent( func (m *workflowTaskStateMachine) AddWorkflowTaskTimedOutEvent( workflowTask *historyi.WorkflowTaskInfo, ) (*historypb.HistoryEvent, error) { + m.ms.clearScheduledChannelCounters() if workflowTask.Type == enumsspb.WORKFLOW_TASK_TYPE_SPECULATIVE { m.ms.RemoveSpeculativeWorkflowTaskTimeoutTask() @@ -1567,6 +1579,12 @@ func (m *workflowTaskStateMachine) convertSpeculativeWorkflowTaskToNormal() erro if scheduledEvent.EventId != wt.ScheduledEventID { return serviceerror.NewInternalf("it could be a bug, scheduled event Id: %d for normal workflow task doesn't match the one from speculative workflow task: %d", scheduledEvent.EventId, wt.ScheduledEventID) } + // A started speculative task already went out without notifications, and + // its History has to say what the worker saw. One not started yet carries + // what is pending. + if wt.StartedEventID == common.EmptyEventID { + m.ms.attachChannelNotifications(scheduledEvent) + } if wtAlreadyStarted := wt.StartedEventID != common.EmptyEventID; wtAlreadyStarted { // If WT was already started then started event is written to the history and diff --git a/tests/notification_channel_test.go b/tests/notification_channel_test.go new file mode 100644 index 00000000000..a0736b90e11 --- /dev/null +++ b/tests/notification_channel_test.go @@ -0,0 +1,952 @@ +package tests + +import ( + "context" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "strconv" + "sync" + "testing" + "time" + + "github.com/google/uuid" + "github.com/stretchr/testify/require" + commandpb "go.temporal.io/api/command/v1" + commonpb "go.temporal.io/api/common/v1" + enumspb "go.temporal.io/api/enums/v1" + historypb "go.temporal.io/api/history/v1" + notificationpb "go.temporal.io/api/notification/v1" + "go.temporal.io/api/serviceerror" + taskqueuepb "go.temporal.io/api/taskqueue/v1" + "go.temporal.io/api/workflowservice/v1" + "go.temporal.io/server/api/adminservice/v1" + "go.temporal.io/server/chasm" + "go.temporal.io/server/chasm/lib/callback" + "go.temporal.io/server/chasm/lib/channel" + channelpb "go.temporal.io/server/chasm/lib/channel/gen/channelpb/v1" + channelservice "go.temporal.io/server/chasm/lib/channel/service" + chasmworkflowpb "go.temporal.io/server/chasm/lib/workflow/gen/workflowpb/v1" + "go.temporal.io/server/common/testing/await" + "go.temporal.io/server/tests/testcore" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/durationpb" +) + +// channelTestEnv drives the notification channel with the raw client, playing +// the worker by hand so each scheduled event can be inspected. +type channelTestEnv struct { + t *testing.T + env *testcore.TestEnv + ns string +} + +func newChannelTestEnv(t *testing.T, opts ...testcore.TestOption) *channelTestEnv { + opts = append([]testcore.TestOption{ + testcore.WithDynamicConfig(callback.AllowedAddresses, + []any{map[string]any{"Pattern": "*", "AllowInsecure": true}}), + testcore.WithDynamicConfig(callback.RetryPolicyInitialInterval, 10*time.Millisecond), + testcore.WithDynamicConfig(callback.RetryPolicyMaximumInterval, 50*time.Millisecond), + }, opts...) + env := testcore.NewEnv(t, opts...) + return &channelTestEnv{t: t, env: env, ns: env.Namespace().String()} +} + +func (c *channelTestEnv) ctx() context.Context { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + c.t.Cleanup(cancel) + return ctx +} + +func (c *channelTestEnv) taskQueue(id string) *taskqueuepb.TaskQueue { + return &taskqueuepb.TaskQueue{Name: id + "-tq", Kind: enumspb.TASK_QUEUE_KIND_NORMAL} +} + +func (c *channelTestEnv) start(id string) string { + c.t.Helper() + resp, err := c.env.FrontendClient().StartWorkflowExecution(c.ctx(), + &workflowservice.StartWorkflowExecutionRequest{ + RequestId: uuid.NewString(), + Namespace: c.ns, + WorkflowId: id, + WorkflowType: &commonpb.WorkflowType{Name: "channel-listener"}, + TaskQueue: c.taskQueue(id), + WorkflowRunTimeout: durationpb.New(100 * time.Second), + WorkflowTaskTimeout: durationpb.New(10 * time.Second), + Identity: "tester", + }) + require.NoError(c.t, err) + return resp.GetRunId() +} + +func (c *channelTestEnv) poll(id string) *workflowservice.PollWorkflowTaskQueueResponse { + c.t.Helper() + resp, err := c.env.FrontendClient().PollWorkflowTaskQueue(c.ctx(), + &workflowservice.PollWorkflowTaskQueueRequest{ + Namespace: c.ns, + TaskQueue: c.taskQueue(id), + Identity: "tester", + }) + require.NoError(c.t, err) + require.NotEmpty(c.t, resp.GetTaskToken(), "expected a workflow task") + return resp +} + +func (c *channelTestEnv) complete( + task *workflowservice.PollWorkflowTaskQueueResponse, + returnNewTask bool, + commands ...*commandpb.Command, +) *workflowservice.RespondWorkflowTaskCompletedResponse { + c.t.Helper() + resp, err := c.env.FrontendClient().RespondWorkflowTaskCompleted(c.ctx(), + &workflowservice.RespondWorkflowTaskCompletedRequest{ + Namespace: c.ns, + TaskToken: task.GetTaskToken(), + Commands: commands, + Identity: "tester", + ReturnNewWorkflowTask: returnNewTask, + }) + require.NoError(c.t, err) + return resp +} + +func (c *channelTestEnv) continueAsNewCommand(id string) *commandpb.Command { + attrs := &commandpb.ContinueAsNewWorkflowExecutionCommandAttributes{ + WorkflowType: &commonpb.WorkflowType{Name: "channel-listener"}, + TaskQueue: c.taskQueue(id), + WorkflowRunTimeout: durationpb.New(100 * time.Second), + WorkflowTaskTimeout: durationpb.New(10 * time.Second), + } + return &commandpb.Command{ + CommandType: enumspb.COMMAND_TYPE_CONTINUE_AS_NEW_WORKFLOW_EXECUTION, + Attributes: &commandpb.Command_ContinueAsNewWorkflowExecutionCommandAttributes{ + ContinueAsNewWorkflowExecutionCommandAttributes: attrs, + }, + } +} + +func (c *channelTestEnv) hasPendingTask(id string) bool { + c.t.Helper() + resp, err := c.env.FrontendClient().DescribeWorkflowExecution(c.ctx(), + &workflowservice.DescribeWorkflowExecutionRequest{ + Namespace: c.ns, + Execution: &commonpb.WorkflowExecution{WorkflowId: id}, + }) + require.NoError(c.t, err) + return resp.GetPendingWorkflowTask() != nil +} + +func (c *channelTestEnv) persisted(id string) *adminservice.DescribeMutableStateResponse { + c.t.Helper() + resp, err := c.env.AdminClient().DescribeMutableState(c.ctx(), + &adminservice.DescribeMutableStateRequest{ + Namespace: c.ns, + Execution: &commonpb.WorkflowExecution{WorkflowId: id}, + Archetype: chasm.WorkflowArchetype, + }) + require.NoError(c.t, err) + return resp +} + +func (c *channelTestEnv) stateTransitions(id string) int64 { + c.t.Helper() + return c.persisted(id).GetDatabaseMutableState().GetExecutionInfo().GetStateTransitionCount() +} + +func requireNotFound(t *testing.T, err error) { + t.Helper() + var notFound *serviceerror.NotFound + require.ErrorAs(t, err, ¬Found) +} + +func subscribeChannelCommand(name string) *commandpb.Command { + return &commandpb.Command{ + CommandType: enumspb.COMMAND_TYPE_SUBSCRIBE_NOTIFICATION_CHANNEL, + Attributes: &commandpb.Command_SubscribeNotificationChannelCommandAttributes{ + SubscribeNotificationChannelCommandAttributes: &commandpb.SubscribeNotificationChannelCommandAttributes{ + Channel: name, + }, + }, + } +} + +func channelNotification(name string, counter int64) *notificationpb.Notification { + return ¬ificationpb.Notification{ + Channel: name, + Position: []byte(name + "@" + strconv.FormatInt(counter, 10)), + Counter: counter, + Metadata: map[string]*commonpb.Payload{ + "topic": {Data: []byte("t" + strconv.FormatInt(counter, 10))}, + }, + } +} + +func (c *channelTestEnv) notify(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(), + }) +} + +func (c *channelTestEnv) mustNotify(name string, counter int64) int32 { + c.t.Helper() + resp, err := c.notify(name, counter) + require.NoError(c.t, err) + return resp.GetListenerCount() +} + +func (c *channelTestEnv) describe(name string) *workflowservice.DescribeChannelResponse { + c.t.Helper() + resp, err := c.env.FrontendClient().DescribeChannel(c.ctx(), &workflowservice.DescribeChannelRequest{ + Namespace: c.ns, + Channel: name, + }) + require.NoError(c.t, err) + return resp +} + +// workflowListenerRun is the run the channel has on record for a workflow id, +// or the empty string when it has none. +func (c *channelTestEnv) workflowListenerRun(name, workflowID string) string { + c.t.Helper() + for _, l := range c.describe(name).GetListeners() { + if l.GetWorkflow().GetWorkflowId() == workflowID { + return l.GetWorkflow().GetRunId() + } + } + return "" +} + +// subscribe starts a workflow and completes its first task with a subscribe +// command, returning the run id. +func (c *channelTestEnv) subscribe(id string, channels ...string) string { + c.t.Helper() + runID := c.start(id) + var commands []*commandpb.Command + for _, name := range channels { + commands = append(commands, subscribeChannelCommand(name)) + } + c.complete(c.poll(id), false, commands...) + for _, name := range channels { + require.Equal(c.t, runID, c.workflowListenerRun(name, id)) + } + return runID +} + +// scheduledNotifications reads the notifications on the scheduled event of +// the task, from History as a replaying worker would. +func (c *channelTestEnv) scheduledNotifications( + id string, + task *workflowservice.PollWorkflowTaskQueueResponse, +) []*notificationpb.Notification { + c.t.Helper() + events := c.env.GetHistory(c.ns, task.GetWorkflowExecution()) + var scheduled *historypb.HistoryEvent + for _, e := range events { + if e.GetEventType() == enumspb.EVENT_TYPE_WORKFLOW_TASK_SCHEDULED && + e.GetEventId() <= task.GetStartedEventId() { + scheduled = e + } + } + require.NotNil(c.t, scheduled, "no scheduled event for the task of %s", id) + return scheduled.GetWorkflowTaskScheduledEventAttributes().GetNotifications() +} + +// awaitPending waits until the run holds a pending notification for the +// channel at the counter, which is how a test knows a fan-out reached it +// while it had no way to schedule a task. +func (c *channelTestEnv) awaitPending(id, name string, counter int64) { + c.t.Helper() + await.Require(c.ctx(), c.t, func(t *await.T) { + nodes := c.persisted(id).GetDatabaseMutableState().GetChasmNodes() + node, ok := nodes["ChannelNotifications#"+name] + require.True(t, ok, "no pending notification for %s yet", name) + var entry chasmworkflowpb.ChannelNotificationEntry + require.NoError(t, proto.Unmarshal(node.GetData().GetData(), &entry)) + require.Equal(t, counter, entry.GetCounter()) + }, 20*time.Second, 50*time.Millisecond) +} + +// awaitCallbackPending waits until the callback listener holds a pending +// notification at the counter behind the one in flight. +func (c *channelTestEnv) awaitCallbackPending(name, listenerID string, counter int64) { + c.t.Helper() + await.Require(c.ctx(), c.t, func(t *await.T) { + resp, err := c.env.AdminClient().DescribeMutableState(c.ctx(), + &adminservice.DescribeMutableStateRequest{ + Namespace: c.ns, + Execution: &commonpb.WorkflowExecution{WorkflowId: name}, + Archetype: channelservice.Archetype, + }) + require.NoError(t, err) + node, ok := resp.GetDatabaseMutableState().GetChasmNodes()["CallbackListeners#"+listenerID] + require.True(t, ok) + var listener channelpb.CallbackListener + require.NoError(t, proto.Unmarshal(node.GetData().GetData(), &listener)) + require.Equal(t, counter, listener.GetPending().GetCounter()) + }, 20*time.Second, 50*time.Millisecond) +} + +func requireNotifications(t *testing.T, got []*notificationpb.Notification, want map[string]int64) { + t.Helper() + require.Len(t, got, len(want), "notifications: %v", got) + for _, n := range got { + counter, ok := want[n.GetChannel()] + require.True(t, ok, "unexpected notification from %q", n.GetChannel()) + require.Equal(t, counter, n.GetCounter(), "counter of %q", n.GetChannel()) + require.Equal(t, []byte(n.GetChannel()+"@"+strconv.FormatInt(counter, 10)), n.GetPosition()) + require.Equal(t, []byte("t"+strconv.FormatInt(counter, 10)), n.GetMetadata()["topic"].GetData()) + } +} + +func requireInvalidArgument(t *testing.T, err error) { + t.Helper() + var invalid *serviceerror.InvalidArgument + require.ErrorAs(t, err, &invalid) +} + +// A workflow subscribes by completing a task with the command, the event says +// so, and a notify then schedules one task whose scheduled event carries the +// notification. History is where it lives: the poll response carries nothing +// of its own, and a replayed history shows the same event. +func TestNotificationChannelSubscribeAndNotify(t *testing.T) { + c := newChannelTestEnv(t) + id := "channel-basic-" + uuid.NewString() + name := "orders-" + uuid.NewString() + runID := c.subscribe(id, name) + + events := c.env.GetHistory(c.ns, &commonpb.WorkflowExecution{WorkflowId: id, RunId: runID}) + var subscribed *historypb.WorkflowNotificationChannelSubscribedEventAttributes + for _, e := range events { + if e.GetEventType() == enumspb.EVENT_TYPE_WORKFLOW_NOTIFICATION_CHANNEL_SUBSCRIBED { + subscribed = e.GetWorkflowNotificationChannelSubscribedEventAttributes() + } + } + require.NotNil(t, subscribed) + require.Equal(t, name, subscribed.GetChannel()) + require.Positive(t, subscribed.GetWorkflowTaskCompletedEventId()) + require.False(t, c.hasPendingTask(id)) + + require.Equal(t, int32(1), c.mustNotify(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), "History acknowledged it, so nothing is redelivered") + + // A replayed history reads the same scheduled event. + reread, err := c.env.FrontendClient().GetWorkflowExecutionHistory(c.ctx(), + &workflowservice.GetWorkflowExecutionHistoryRequest{ + Namespace: c.ns, + Execution: &commonpb.WorkflowExecution{WorkflowId: id, RunId: runID}, + }) + require.NoError(t, err) + var carried int + for _, e := range reread.GetHistory().GetEvents() { + if n := e.GetWorkflowTaskScheduledEventAttributes().GetNotifications(); len(n) > 0 { + requireNotifications(t, n, map[string]int64{name: 1}) + carried++ + } + } + require.Equal(t, 1, carried) + + desc := c.describe(name) + require.Len(t, desc.GetListeners(), 1) + require.Equal(t, id, desc.GetListeners()[0].GetWorkflow().GetWorkflowId()) + require.Equal(t, int64(1), desc.GetLatest().GetCounter()) + require.Equal(t, int32(1), desc.GetRetainedCount()) +} + +// Notifications that reach a workflow while it has a task open wait for the +// next scheduled event and fold there into the highest counter. +func TestNotificationChannelFoldsWhileTaskOpen(t *testing.T) { + c := newChannelTestEnv(t) + id := "channel-fold-" + uuid.NewString() + name := "orders-" + uuid.NewString() + c.subscribe(id, name) + + c.mustNotify(name, 1) + open := c.poll(id) + requireNotifications(t, c.scheduledNotifications(id, open), map[string]int64{name: 1}) + + for counter := int64(2); counter <= 4; counter++ { + c.mustNotify(name, counter) + } + c.awaitPending(id, name, 4) + + c.complete(open, false) + next := c.poll(id) + got := c.scheduledNotifications(id, next) + require.Len(t, got, 1, "one per channel") + require.Equal(t, int64(4), got[0].GetCounter()) + c.complete(next, false) + require.False(t, c.hasPendingTask(id)) +} + +// Every listener is woken, and the writer learns how many there were. +func TestNotificationChannelWakesEveryListener(t *testing.T) { + c := newChannelTestEnv(t) + name := "orders-" + uuid.NewString() + first := "channel-many-a-" + uuid.NewString() + second := "channel-many-b-" + uuid.NewString() + c.subscribe(first, name) + c.subscribe(second, name) + + require.Equal(t, int32(2), c.mustNotify(name, 7)) + for _, id := range []string{first, second} { + task := c.poll(id) + requireNotifications(t, c.scheduledNotifications(id, task), map[string]int64{name: 7}) + c.complete(task, false) + } +} + +// A subscription ends with its run. After a continue-as-new the predecessor's +// listener is dropped on the next notify unless the successor subscribes, +// and a successor that subscribes takes the entry and gets the next one. +func TestNotificationChannelContinueAsNew(t *testing.T) { + c := newChannelTestEnv(t) + name := "orders-" + uuid.NewString() + + id := "channel-can-" + uuid.NewString() + predecessor := c.subscribe(id, name) + c.mustNotify(name, 1) + task := c.poll(id) + c.complete(task, false, c.continueAsNewCommand(id)) + successorTask := c.poll(id) + require.NotEqual(t, predecessor, successorTask.GetWorkflowExecution().GetRunId()) + c.complete(successorTask, false) + + c.mustNotify(name, 2) + await.Require(c.ctx(), t, func(t *await.T) { + require.Empty(t, c.describe(name).GetListeners(), "the closed run's listener goes") + }, 20*time.Second, 50*time.Millisecond) + require.False(t, c.hasPendingTask(id), "a successor that did not subscribe is not woken") + + // A new subscriber is handed the latest the channel holds, and a + // successor that subscribes again is handed it too, then the next one. + id2 := "channel-can2-" + uuid.NewString() + c.subscribe(id2, name) + task = c.poll(id2) + requireNotifications(t, c.scheduledNotifications(id2, task), map[string]int64{name: 2}) + c.complete(task, false) + c.mustNotify(name, 3) + task = c.poll(id2) + requireNotifications(t, c.scheduledNotifications(id2, task), map[string]int64{name: 3}) + c.complete(task, false, c.continueAsNewCommand(id2)) + successorTask = c.poll(id2) + successor := successorTask.GetWorkflowExecution().GetRunId() + c.complete(successorTask, false, subscribeChannelCommand(name)) + require.Equal(t, successor, c.workflowListenerRun(name, id2), "the successor takes the entry") + c.mustNotify(name, 4) + task = c.poll(id2) + require.Equal(t, successor, task.GetWorkflowExecution().GetRunId()) + requireNotifications(t, c.scheduledNotifications(id2, task), map[string]int64{name: 3}) + c.complete(task, false) + task = c.poll(id2) + requireNotifications(t, c.scheduledNotifications(id2, task), map[string]int64{name: 4}) +} + +// A reset run is rebuilt from History, subscription event included, so it +// listens. The channel still has the reset-away run on record, and the next +// notify follows the chain to the reset run and re-keys the listener. +func TestNotificationChannelReKeysToResetRun(t *testing.T) { + c := newChannelTestEnv(t) + id := "channel-reset-" + uuid.NewString() + name := "orders-" + uuid.NewString() + base := c.subscribe(id, name) + + c.mustNotify(name, 1) + task := c.poll(id) + c.complete(task, false) + + resp, err := c.env.FrontendClient().ResetWorkflowExecution(c.ctx(), + &workflowservice.ResetWorkflowExecutionRequest{ + Namespace: c.ns, + WorkflowExecution: &commonpb.WorkflowExecution{WorkflowId: id, RunId: base}, + Reason: "test", + WorkflowTaskFinishEventId: task.GetStartedEventId(), + RequestId: uuid.NewString(), + }) + require.NoError(t, err) + resetRun := resp.GetRunId() + c.complete(c.poll(id), false) + require.Equal(t, base, c.workflowListenerRun(name, id)) + + c.mustNotify(name, 2) + task = c.poll(id) + require.Equal(t, resetRun, task.GetWorkflowExecution().GetRunId()) + requireNotifications(t, c.scheduledNotifications(id, task), map[string]int64{name: 2}) + await.Require(c.ctx(), t, func(t *await.T) { + require.Equal(t, resetRun, c.workflowListenerRun(name, id)) + }, 20*time.Second, 50*time.Millisecond) +} + +// callbackRecorder is an HTTP endpoint that records what the channel posts, +// and can hold a request open to keep a delivery in flight. +type callbackRecorder struct { + mu sync.Mutex + bodies []map[string]any + channels []string + hold chan struct{} + arrived chan struct{} +} + +func newCallbackRecorder(t *testing.T) (*callbackRecorder, string) { + r := &callbackRecorder{arrived: make(chan struct{}, 100)} + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + data, _ := io.ReadAll(req.Body) + var body map[string]any + _ = json.Unmarshal(data, &body) + r.mu.Lock() + r.bodies = append(r.bodies, body) + r.channels = append(r.channels, req.Header.Get(channelservice.ChannelHeader)) + hold := r.hold + r.mu.Unlock() + r.arrived <- struct{}{} + if hold != nil { + <-hold + } + w.WriteHeader(http.StatusOK) + })) + t.Cleanup(server.Close) + return r, server.URL + "/notify" +} + +func (r *callbackRecorder) counters() []string { + r.mu.Lock() + defer r.mu.Unlock() + out := make([]string, len(r.bodies)) + for i, b := range r.bodies { + out[i], _ = b["counter"].(string) + } + return out +} + +// A callback listener is posted each notification as JSON with the channel +// in a header, and while one post is in flight what arrives folds into one, +// sent when the post completes. +func TestNotificationChannelCallbackListener(t *testing.T) { + c := newChannelTestEnv(t) + name := "orders-" + uuid.NewString() + 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", + }) + 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.Equal(t, int32(1), c.mustNotify(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]) + require.Equal(t, name, recorder.bodies[0]["channel"]) + recorder.hold = make(chan struct{}) + hold := recorder.hold + recorder.mu.Unlock() + + c.mustNotify(name, 2) + select { + case <-recorder.arrived: + case <-time.After(20 * time.Second): + t.Fatal("the second notification was not posted") + } + c.mustNotify(name, 3) + c.mustNotify(name, 4) + // The fan-outs for 3 and 4 hand their notifications over while 2 is still + // in flight, and 4 replaces 3. + c.awaitCallbackPending(name, listenerID, 4) + recorder.mu.Lock() + recorder.hold = nil + recorder.mu.Unlock() + close(hold) + + await.Require(c.ctx(), t, func(t *await.T) { + require.Equal(t, []string{"1", "2", "4"}, recorder.counters()) + }, 20*time.Second, 50*time.Millisecond) + + _, err := c.env.FrontendClient().UnregisterChannelListener(c.ctx(), + &workflowservice.UnregisterChannelListenerRequest{ + Namespace: c.ns, Channel: name, ListenerId: listenerID, Identity: "tester", + }) + require.NoError(t, err) + require.Empty(t, c.describe(name).GetListeners()) + require.Equal(t, int32(0), c.mustNotify(name, 5)) +} + +// A long poll returns a notification published while it waits. +func TestNotificationChannelPollWaits(t *testing.T) { + c := newChannelTestEnv(t) + name := "orders-" + uuid.NewString() + require.Equal(t, int32(0), c.mustNotify(name, 1), "nobody listens yet") + + done := make(chan *workflowservice.PollChannelResponse, 1) + errs := make(chan error, 1) + go func() { + resp, err := c.env.FrontendClient().PollChannel(c.ctx(), &workflowservice.PollChannelRequest{ + Namespace: c.ns, + Channel: name, + AfterCounter: 1, + Wait: durationpb.New(15 * time.Second), + }) + 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.mustNotify(name, 2) + select { + case resp := <-done: + require.Len(t, resp.GetNotifications(), 1) + require.Equal(t, int64(2), resp.GetNotifications()[0].GetCounter()) + case err := <-errs: + t.Fatal(err) + case <-time.After(20 * time.Second): + t.Fatal("the poll did not return") + } +} + +// A notify with no listeners is retained, so a poll that arrives later +// catches up on it; describe reports what the channel holds. +func TestNotificationChannelRetainsWithoutListeners(t *testing.T) { + c := newChannelTestEnv(t) + name := "orders-" + uuid.NewString() + require.Equal(t, int32(0), c.mustNotify(name, 1)) + require.Equal(t, int32(0), c.mustNotify(name, 2)) + + resp, err := c.env.FrontendClient().PollChannel(c.ctx(), &workflowservice.PollChannelRequest{ + Namespace: c.ns, + Channel: name, + }) + require.NoError(t, err) + requireNotifications(t, resp.GetNotifications()[:1], map[string]int64{name: 1}) + require.Len(t, resp.GetNotifications(), 2) + + resp, err = c.env.FrontendClient().PollChannel(c.ctx(), &workflowservice.PollChannelRequest{ + Namespace: c.ns, Channel: name, AfterCounter: 1, MaxNotifications: 1, + }) + require.NoError(t, err) + require.Len(t, resp.GetNotifications(), 1) + require.Equal(t, int64(2), resp.GetNotifications()[0].GetCounter()) + + desc := c.describe(name) + require.Empty(t, desc.GetListeners()) + require.Equal(t, int64(2), desc.GetLatest().GetCounter()) + require.Equal(t, int32(2), desc.GetRetainedCount()) + + _, err = c.env.FrontendClient().DescribeChannel(c.ctx(), &workflowservice.DescribeChannelRequest{ + Namespace: c.ns, Channel: "absent-" + uuid.NewString(), + }) + requireNotFound(t, err) +} + +// The frontend refuses what the channel could not accept, and the limits hold. +func TestNotificationChannelValidationAndLimits(t *testing.T) { + c := newChannelTestEnv(t, + testcore.WithDynamicConfig(channel.MaxListenersSetting, 1), + testcore.WithDynamicConfig(channel.MaxMetadataBytesSetting, 16), + testcore.WithDynamicConfig(channel.MaxSubscriptionsPerWorkflowSetting, 1), + testcore.WithDynamicConfig(channel.RetainedNotificationsSetting, 2), + ) + name := "orders-" + uuid.NewString() + notify := func(n *notificationpb.Notification) error { + _, err := c.env.FrontendClient().NotifyChannel(c.ctx(), &workflowservice.NotifyChannelRequest{ + Namespace: c.ns, Notification: n, + }) + return err + } + requireInvalidArgument(t, notify(nil)) + requireInvalidArgument(t, notify(¬ificationpb.Notification{Counter: 1})) + requireInvalidArgument(t, notify(¬ificationpb.Notification{Channel: name})) + requireInvalidArgument(t, notify(¬ificationpb.Notification{ + Channel: name, Counter: 1, Position: make([]byte, channel.MaxPositionBytes+1), + })) + requireInvalidArgument(t, notify(¬ificationpb.Notification{ + Channel: name, Counter: 1, + Metadata: map[string]*commonpb.Payload{"k": {Data: make([]byte, 64)}}, + })) + requireInvalidArgument(t, notify(¬ificationpb.Notification{ + Channel: string(make([]byte, 2000)), Counter: 1, + })) + + // The ring keeps the newest two. + for counter := int64(1); counter <= 3; counter++ { + c.mustNotify(name, counter) + } + require.Equal(t, int32(2), c.describe(name).GetRetainedCount()) + + // One listener is the limit. + id := "channel-limit-" + uuid.NewString() + c.subscribe(id, name) + _, 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: "http://localhost:1/notify"}, + }}, + RequestId: uuid.NewString(), + }) + var exhausted *serviceerror.ResourceExhausted + require.ErrorAs(t, err, &exhausted) + + // One subscription per run is the limit, and the refusal fails the task + // with the channel's cause. + c.mustNotify(name, 4) + task := c.poll(id) + _, err = c.env.FrontendClient().RespondWorkflowTaskCompleted(c.ctx(), + &workflowservice.RespondWorkflowTaskCompletedRequest{ + Namespace: c.ns, + TaskToken: task.GetTaskToken(), + Commands: []*commandpb.Command{subscribeChannelCommand("other-" + uuid.NewString())}, + Identity: "tester", + }) + requireInvalidArgument(t, err) + var failed *historypb.WorkflowTaskFailedEventAttributes + for _, e := range c.env.GetHistory(c.ns, task.GetWorkflowExecution()) { + if e.GetEventType() == enumspb.EVENT_TYPE_WORKFLOW_TASK_FAILED { + failed = e.GetWorkflowTaskFailedEventAttributes() + } + } + require.NotNil(t, failed) + require.Equal(t, + enumspb.WORKFLOW_TASK_FAILED_CAUSE_BAD_SUBSCRIBE_NOTIFICATION_CHANNEL_ATTRIBUTES, + failed.GetCause()) +} + +// The notify rate is per namespace; past it a notify is refused with the rate +// limit cause. +func TestNotificationChannelNotifyRate(t *testing.T) { + c := newChannelTestEnv(t, testcore.WithDynamicConfig(channel.NotifyPerSecondSetting, 1)) + name := "orders-" + uuid.NewString() + var refused error + for counter := int64(1); counter <= 20 && refused == nil; counter++ { + _, refused = c.notify(name, counter) + } + var exhausted *serviceerror.ResourceExhausted + require.ErrorAs(t, refused, &exhausted) + require.Equal(t, enumspb.RESOURCE_EXHAUSTED_CAUSE_RPS_LIMIT, exhausted.Cause) +} + +// eventsOfType returns the run's events of one type, in order. +func (c *channelTestEnv) eventsOfType( + execution *commonpb.WorkflowExecution, + eventType enumspb.EventType, +) []*historypb.HistoryEvent { + var out []*historypb.HistoryEvent + for _, e := range c.env.GetHistory(c.ns, execution) { + if e.GetEventType() == eventType { + out = append(out, e) + } + } + return out +} + +// retryScheduled is the scheduled event History records for a retried task, +// written when the retry completes: the last one at attempt two. +func (c *channelTestEnv) retryScheduled( + retry *workflowservice.PollWorkflowTaskQueueResponse, +) *historypb.WorkflowTaskScheduledEventAttributes { + c.t.Helper() + var out *historypb.WorkflowTaskScheduledEventAttributes + for _, e := range c.eventsOfType(retry.GetWorkflowExecution(), enumspb.EVENT_TYPE_WORKFLOW_TASK_SCHEDULED) { + if attrs := e.GetWorkflowTaskScheduledEventAttributes(); attrs.GetAttempt() == 2 { + out = attrs + } + } + require.NotNil(c.t, out, "no scheduled event for the retry") + return out +} + +// A retry after a failed task does not carry again what the failed attempt's +// scheduled event carried: an SDK reads every scheduled event since its last +// completed task, so carrying it twice would deliver it twice. A notification +// that arrives after the failure rides the scheduled event after the retry. +func TestNotificationChannelRetryDoesNotRecarry(t *testing.T) { + c := newChannelTestEnv(t) + id := "channel-retry-" + uuid.NewString() + name := "orders-" + uuid.NewString() + c.subscribe(id, name) + + c.mustNotify(name, 1) + task := c.poll(id) + requireNotifications(t, c.scheduledNotifications(id, task), map[string]int64{name: 1}) + _, err := c.env.FrontendClient().RespondWorkflowTaskFailed(c.ctx(), + &workflowservice.RespondWorkflowTaskFailedRequest{ + Namespace: c.ns, + TaskToken: task.GetTaskToken(), + Cause: enumspb.WORKFLOW_TASK_FAILED_CAUSE_WORKFLOW_WORKER_UNHANDLED_FAILURE, + Identity: "tester", + }) + require.NoError(t, err) + + retry := c.poll(id) + require.Equal(t, int32(2), retry.GetAttempt()) + c.complete(retry, false) + require.Empty(t, c.retryScheduled(retry).GetNotifications(), "the retry does not carry it again") + require.False(t, c.hasPendingTask(id)) + + // A notification arriving between a failure and the retry is not carried + // by the retry, whose scheduled event the worker never saw with it; it + // rides the next scheduled event, alone. + c.mustNotify(name, 2) + task = c.poll(id) + requireNotifications(t, c.scheduledNotifications(id, task), map[string]int64{name: 2}) + _, err = c.env.FrontendClient().RespondWorkflowTaskFailed(c.ctx(), + &workflowservice.RespondWorkflowTaskFailedRequest{ + Namespace: c.ns, + TaskToken: task.GetTaskToken(), + Cause: enumspb.WORKFLOW_TASK_FAILED_CAUSE_WORKFLOW_WORKER_UNHANDLED_FAILURE, + Identity: "tester", + }) + require.NoError(t, err) + c.mustNotify(name, 3) + c.awaitPending(id, name, 3) + retry = c.poll(id) + c.complete(retry, false) + require.Empty(t, c.retryScheduled(retry).GetNotifications()) + next := c.poll(id) + requireNotifications(t, c.scheduledNotifications(id, next), map[string]int64{name: 3}) +} + +// A notify that found no listeners is handed to a workflow that subscribes +// afterwards, so a reader that checked its source, found nothing and +// subscribed is not left waiting on a write that landed in between. +func TestNotificationChannelSubscribeGetsLatest(t *testing.T) { + c := newChannelTestEnv(t) + id := "channel-late-" + uuid.NewString() + name := "orders-" + uuid.NewString() + require.Equal(t, int32(0), c.mustNotify(name, 1)) + require.Equal(t, int32(0), c.mustNotify(name, 2)) + + c.subscribe(id, name) + task := c.poll(id) + requireNotifications(t, c.scheduledNotifications(id, task), map[string]int64{name: 2}) + c.complete(task, false) + require.False(t, c.hasPendingTask(id)) +} + +// Subscribing to a channel that holds nothing schedules nothing. A second +// subscribe to the same channel records its own event, since every command +// needs one, and changes nothing else: no new listener and no delivery. +func TestNotificationChannelSubscribeOnEmptyChannel(t *testing.T) { + c := newChannelTestEnv(t) + id := "channel-empty-" + uuid.NewString() + name := "orders-" + uuid.NewString() + runID := c.subscribe(id, name) + require.False(t, c.hasPendingTask(id)) + + c.mustNotify(name, 1) + task := c.poll(id) + c.complete(task, false, subscribeChannelCommand(name)) + execution := &commonpb.WorkflowExecution{WorkflowId: id, RunId: runID} + subscribed := c.eventsOfType(execution, enumspb.EVENT_TYPE_WORKFLOW_NOTIFICATION_CHANNEL_SUBSCRIBED) + require.Len(t, subscribed, 2) + require.Equal(t, name, + subscribed[1].GetWorkflowNotificationChannelSubscribedEventAttributes().GetChannel()) + require.Len(t, c.describe(name).GetListeners(), 1) + require.False(t, c.hasPendingTask(id), "the latest is not handed out again") + +} + +// channelTransitions reads the channel execution's state transition count, +// which moves on every write. +func (c *channelTestEnv) channelTransitions(name string) int64 { + c.t.Helper() + resp, err := c.env.AdminClient().DescribeMutableState(c.ctx(), + &adminservice.DescribeMutableStateRequest{ + Namespace: c.ns, + Execution: &commonpb.WorkflowExecution{WorkflowId: name}, + Archetype: channelservice.Archetype, + }) + require.NoError(c.t, err) + return resp.GetDatabaseMutableState().GetExecutionInfo().GetStateTransitionCount() +} + +// A listener's task runs to completion, and a watcher that later finds the +// same record with no task open notifies the same counter again. That is a new reason to run, so it schedules a task +// whose scheduled event carries the counter. It does not join the ring again. +func TestNotificationChannelRepeatAfterTaskWakesAgain(t *testing.T) { + c := newChannelTestEnv(t) + id := "channel-repeat-" + uuid.NewString() + name := "orders-" + uuid.NewString() + c.subscribe(id, name) + + c.mustNotify(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)) + + require.Equal(t, int32(1), c.mustNotify(name, 1)) + task = c.poll(id) + requireNotifications(t, c.scheduledNotifications(id, task), map[string]int64{name: 1}) + require.Equal(t, int32(1), c.describe(name).GetRetainedCount()) + + // The task has started, so the same counter again may stand for a write + // its read missed: it goes pending for the next task. + c.mustNotify(name, 1) + c.awaitPending(id, name, 1) + + // While the open task keeps the repeat pending, the same counter again + // writes neither the run nor the channel. + c.mustNotify(name, 2) + c.awaitPending(id, name, 2) + runWrites, channelWrites := c.stateTransitions(id), c.channelTransitions(name) + require.Equal(t, int32(1), c.mustNotify(name, 2)) + require.Equal(t, runWrites, c.stateTransitions(id), "folded into the pending entry") + require.Equal(t, channelWrites, c.channelTransitions(name), "the ring and the table are unchanged") + c.complete(task, false) + next := c.poll(id) + requireNotifications(t, c.scheduledNotifications(id, next), map[string]int64{name: 2}) +} + +// A repeat that arrives while the task carrying the counter is scheduled and +// not yet started folds into that task: it has not read anything, so it will +// see the write the repeat stands for. One task, and neither the run nor the +// channel is written. +func TestNotificationChannelFoldsIntoUnstartedTask(t *testing.T) { + c := newChannelTestEnv(t) + id := "channel-unstarted-" + uuid.NewString() + name := "orders-" + uuid.NewString() + c.subscribe(id, name) + + c.mustNotify(name, 1) + await.Require(c.ctx(), t, func(t *await.T) { + require.True(t, c.hasPendingTask(id)) + }, 20*time.Second, 50*time.Millisecond) + runWrites, channelWrites := c.stateTransitions(id), c.channelTransitions(name) + require.Equal(t, int32(1), c.mustNotify(name, 1)) + require.Equal(t, runWrites, c.stateTransitions(id), "folded into the unstarted task") + require.Equal(t, channelWrites, c.channelTransitions(name)) + + 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), "one task") +}