diff --git a/chasm/lib/activity/activity.go b/chasm/lib/activity/activity.go index df45a9ac490..a8d08c282fc 100644 --- a/chasm/lib/activity/activity.go +++ b/chasm/lib/activity/activity.go @@ -335,9 +335,9 @@ func (a *Activity) addCompletionCallbacks( return serviceerror.NewInvalidArgumentf("unsupported callback variant: %T", variant) } - // requestID (unique per API call) + idx (position within the request) ensures unique,idempotent callback IDs. + // requestID (unique per API call) + idx (position within the request) ensures unique, idempotent callback IDs. id := fmt.Sprintf("%s-%d", requestID, idx) - callbackObj := callback.NewCallback(requestID, registrationTime, &callbackspb.CallbackState{}, chasmCB) + callbackObj := callback.NewEmbeddedCallback(ctx, requestID, registrationTime, chasmCB) a.Callbacks[id] = chasm.NewComponentField(ctx, callbackObj) } return nil diff --git a/chasm/lib/callback/component.go b/chasm/lib/callback/component.go index c017a7d2f30..02a04e8f18e 100644 --- a/chasm/lib/callback/component.go +++ b/chasm/lib/callback/component.go @@ -4,13 +4,17 @@ import ( "fmt" "time" + callbackpb "go.temporal.io/api/callback/v1" commonpb "go.temporal.io/api/common/v1" + failurepb "go.temporal.io/api/failure/v1" "go.temporal.io/api/serviceerror" "go.temporal.io/server/chasm" callbackspb "go.temporal.io/server/chasm/lib/callback/gen/callbackpb/v1" "go.temporal.io/server/common/backoff" "go.temporal.io/server/common/nexus/nexusrpc" queueserrors "go.temporal.io/server/service/history/queues/errors" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/durationpb" "google.golang.org/protobuf/types/known/timestamppb" ) @@ -18,8 +22,27 @@ type CompletionSource interface { GetNexusCompletion(ctx chasm.Context, requestID string) (nexusrpc.CompleteOperationOptions, error) } -var _ chasm.Component = (*Callback)(nil) -var _ chasm.StateMachine[callbackspb.CallbackStatus] = (*Callback)(nil) +// CompletionSourceFn allows a function value to be used as a CompletionSource instance. +type CompletionSourceFn func(chasm.Context, string) (nexusrpc.CompleteOperationOptions, error) + +func (csFunc CompletionSourceFn) GetNexusCompletion(ctx chasm.Context, requestID string) (nexusrpc.CompleteOperationOptions, error) { + return csFunc(ctx, requestID) +} + +var ( + _ chasm.Component = (*Callback)(nil) + _ chasm.StateMachine[callbackspb.CallbackStatus] = (*Callback)(nil) + + // Capabilities only supported/used for standalone callbacks. + _ chasm.RootComponent = (*Callback)(nil) + _ chasm.VisibilityMemoProvider = (*Callback)(nil) + _ chasm.VisibilitySearchAttributesProvider = (*Callback)(nil) +) + +var executionStatusSearchAttribute = chasm.NewSearchAttributeKeyword( + "ExecutionStatus", + chasm.SearchAttributeFieldLowCardinalityKeyword01, +) // Callback represents a callback component in CHASM. type Callback struct { @@ -27,15 +50,26 @@ type Callback struct { // Persisted internal state *callbackspb.CallbackState + // Failure from an external termination (timeout or terminate), stored separately because + // of its potential size, and to not overload CallbackState::LastAttemptFailure. + TerminalFailure chasm.Field[*failurepb.Failure] + + // For most callbacks, the completion result is obtained from the parent component. + // e.g. the Workflow result to be delivered. However, for "standalone" callbacks, there + // is no parent and the user-supplied SuppliedCompletion will be used instead. + ParentCompletionSource chasm.ParentPtr[CompletionSource] + SuppliedCompletion chasm.Field[*callbackpb.CallbackExecutionCompletion] - // Interface to retrieve Nexus operation completion data - CompletionSource chasm.ParentPtr[CompletionSource] + // Visibility sub-component for search attributes and memo indexing. + Visibility chasm.Field[*chasm.Visibility] } -func NewCallback( +// NewEmbeddedCallback returns a Callback component, which will deliver the completion from +// its parent CHASM component. The parent must implement CompletionSource. +func NewEmbeddedCallback( + ctx chasm.MutableContext, requestID string, registrationTime *timestamppb.Timestamp, - state *callbackspb.CallbackState, cb *callbackspb.Callback, ) *Callback { return &Callback{ @@ -45,14 +79,49 @@ func NewCallback( Callback: cb, Status: callbackspb.CALLBACK_STATUS_STANDBY, }, + TerminalFailure: chasm.NewDataField[*failurepb.Failure](ctx, nil), } } +type newStandaloneCallbackOpts struct { + RequestID string + RegistrationTime *timestamppb.Timestamp + Callback *callbackspb.Callback + + CallbackID string + CompletionScheduleToCloseTimeout *durationpb.Duration + Completion *callbackpb.CallbackExecutionCompletion + SearchAttributes map[string]*commonpb.Payload +} + +// newStandaloneCallback returns a new Callback component which will deliver the supplied +// completion result. +func newStandaloneCallback( + ctx chasm.MutableContext, + opts newStandaloneCallbackOpts, +) *Callback { + cb := NewEmbeddedCallback(ctx, opts.RequestID, opts.RegistrationTime, opts.Callback) + + // Add standalone-specific fields. + cb.CallbackId = opts.CallbackID + cb.CompletionScheduleToCloseTimeout = opts.CompletionScheduleToCloseTimeout + cb.SuppliedCompletion = chasm.NewDataField(ctx, opts.Completion) + + visibility := chasm.NewVisibilityWithData(ctx, opts.SearchAttributes, nil) + cb.Visibility = chasm.NewComponentField(ctx, visibility) + + return cb +} + func (c *Callback) LifecycleState(_ chasm.Context) chasm.LifecycleState { switch c.Status { case callbackspb.CALLBACK_STATUS_SUCCEEDED: return chasm.LifecycleStateCompleted - case callbackspb.CALLBACK_STATUS_FAILED: + case callbackspb.CALLBACK_STATUS_FAILED, + callbackspb.CALLBACK_STATUS_TERMINATED: + // TODO: Use chasm.LifecycleStateTerminated when it's available (currently commented out + // in chasm/component.go:70). For now, LifecycleStateFailed is functionally correct + // as IsClosed() returns true for all states >= LifecycleStateCompleted. return chasm.LifecycleStateFailed default: return chasm.LifecycleStateRunning @@ -67,6 +136,62 @@ func (c *Callback) SetStateMachineState(status callbackspb.CallbackStatus) { c.Status = status } +func (c *Callback) ContextMetadata(_ chasm.Context) map[string]string { + return map[string]string{ + "RequestID": c.RequestId, + // Only set for standalone callbacks. + "CallbackID": c.CallbackId, + } +} + +// SearchAttributes implements chasm.VisibilitySearchAttributesProvider. +func (c *Callback) SearchAttributes(ctx chasm.Context) []chasm.SearchAttributeKeyValue { + apiStatus := callbackStatusToAPIExecutionStatus(c.Status) + return []chasm.SearchAttributeKeyValue{ + executionStatusSearchAttribute.Value(apiStatus.String()), + } +} + +// Memo implements chasm.VisibilityMemoProvider. Returns the CallbackExecutionListInfo +// as the memo for visibility queries. +func (c *Callback) Memo(ctx chasm.Context) proto.Message { + return &callbackpb.CallbackExecutionListInfo{ + CallbackId: c.CallbackId, + Status: callbackStatusToAPIExecutionStatus(c.Status), + CreateTime: c.RegistrationTime, + CloseTime: c.CloseTime, + } +} + +// Terminate forcefully terminates the callback execution. +// +// If already terminated with the same request ID, this is a no-op. +// If already terminated with a different request ID, returns FailedPrecondition. +func (c *Callback) Terminate( + ctx chasm.MutableContext, + req chasm.TerminateComponentRequest, +) (chasm.TerminateComponentResponse, error) { + if c.LifecycleState(ctx).IsClosed() { + if c.TerminateRequestId == "" { + // Completed organically (succeeded/failed/timed out), not via Terminate. + err := serviceerror.NewFailedPreconditionf("callback execution already in terminal state %v", c.Status) + return chasm.TerminateComponentResponse{}, err + } + if c.TerminateRequestId != req.RequestID { + err := serviceerror.NewFailedPreconditionf("already terminated with request ID %s", c.TerminateRequestId) + return chasm.TerminateComponentResponse{}, err + } + return chasm.TerminateComponentResponse{}, nil + } + if err := TransitionTerminated.Apply(c, ctx, EventTerminated{Reason: req.Reason}); err != nil { + return chasm.TerminateComponentResponse{}, fmt.Errorf("failed to terminate callback: %w", err) + } + + c.TerminateRequestId = req.RequestID + // c.TerminalFailure is set in the transition handler. + return chasm.TerminateComponentResponse{}, nil +} + func (c *Callback) recordAttempt(ts time.Time) { c.Attempt++ c.LastAttemptCompleteTime = timestamppb.New(ts) @@ -77,9 +202,9 @@ func (c *Callback) loadInvocationArgs( ctx chasm.Context, _ chasm.NoValue, ) (invocable, error) { - target := c.CompletionSource.Get(ctx) - - completion, err := target.GetNexusCompletion(ctx, c.RequestId) + // Get the completion result to be delivered. + completionSource := c.CompletionSource(ctx) + completion, err := completionSource.GetNexusCompletion(ctx, c.RequestId) if err != nil { return nil, err } @@ -117,6 +242,16 @@ func (c *Callback) saveResult( ctx chasm.MutableContext, input saveResultInput, ) (chasm.NoValue, error) { + // If the callback was terminated while the invocation was in-flight, + // the result is no longer relevant. We'll just drop it silently. + // + // This shouldn't happen outside of tests, since the Nexus machinary + // would prevent an invalid transition anyways. (e.g. terminating + // an already terminated Callback.) + if c.LifecycleState(ctx).IsClosed() { + return nil, nil + } + switch r := input.result.(type) { case invocationResultOK: err := TransitionSucceeded.Apply(c, ctx, EventSucceeded{Time: ctx.Now(c)}) diff --git a/chasm/lib/callback/component_properties.go b/chasm/lib/callback/component_properties.go new file mode 100644 index 00000000000..497e15f876f --- /dev/null +++ b/chasm/lib/callback/component_properties.go @@ -0,0 +1,165 @@ +package callback + +import ( + "fmt" + + "github.com/nexus-rpc/sdk-go/nexus" + callbackpb "go.temporal.io/api/callback/v1" + enumspb "go.temporal.io/api/enums/v1" + "go.temporal.io/api/serviceerror" + "go.temporal.io/server/chasm" + callbackspb "go.temporal.io/server/chasm/lib/callback/gen/callbackpb/v1" + commonnexus "go.temporal.io/server/common/nexus" + "go.temporal.io/server/common/nexus/nexusrpc" +) + +func callbackCompletionToNexusCompleteOperationOpts( + cb *Callback, + completion *callbackpb.CallbackExecutionCompletion) (nexusrpc.CompleteOperationOptions, error) { + + nexusCompletion := nexusrpc.CompleteOperationOptions{ + StartTime: cb.GetRegistrationTime().AsTime(), + CloseTime: cb.CloseTime.AsTime(), + } + + switch completion.Result.(type) { + case *callbackpb.CallbackExecutionCompletion_Success: + nexusCompletion.Result = completion.GetSuccess() + return nexusCompletion, nil + + case *callbackpb.CallbackExecutionCompletion_Failure: + f, err := commonnexus.TemporalFailureToNexusFailure(completion.GetFailure()) + if err != nil { + wrappedErr := fmt.Errorf("failed to convert failure: %w", err) + return nexusrpc.CompleteOperationOptions{}, wrappedErr + } + opErr := &nexus.OperationError{ + State: nexus.OperationStateFailed, + Message: "operation failed", + Cause: &nexus.FailureError{Failure: f}, + } + if err := nexusrpc.MarkAsWrapperError(nexusrpc.DefaultFailureConverter(), opErr); err != nil { + wrappedErr := fmt.Errorf("failed to mark wrapper error: %w", err) + return nexusrpc.CompleteOperationOptions{}, wrappedErr + } + nexusCompletion.Error = opErr + return nexusCompletion, nil + + default: + return nexusrpc.CompleteOperationOptions{}, serviceerror.NewInvalidArgument("no completion result provided") + } +} + +// CompletionSource returns the CompletionSource from the callback, which depends on whether it +// is embedded or is running in standalone mode. +func (c *Callback) CompletionSource(ctx chasm.Context) CompletionSource { + // Embedded callbacks use their parent component as a CompletionSource. + source, ok := c.ParentCompletionSource.TryGet(ctx) + if ok { + return source + } + + // For standalone completions, get the user-supplied value and convert it + // into the Nexus API type. + suppliedCompletion, ok := c.SuppliedCompletion.TryGet(ctx) + if !ok { + return CompletionSourceFn(func(_ chasm.Context, _ string) (nexusrpc.CompleteOperationOptions, error) { + return nexusrpc.CompleteOperationOptions{}, serviceerror.NewInternal("no completion available") + }) + } + + convertOutcomeProtoFn := func(_ chasm.Context, _ string) (nexusrpc.CompleteOperationOptions, error) { + return callbackCompletionToNexusCompleteOperationOpts(c, suppliedCompletion) + } + return CompletionSourceFn(convertOutcomeProtoFn) +} + +// callbackStatusToAPIExecutionStatus maps internal CallbackStatus to public API CallbackExecutionStatus. +func callbackStatusToAPIExecutionStatus(status callbackspb.CallbackStatus) enumspb.CallbackExecutionStatus { + switch status { + case callbackspb.CALLBACK_STATUS_STANDBY, + callbackspb.CALLBACK_STATUS_SCHEDULED, + callbackspb.CALLBACK_STATUS_BACKING_OFF: + return enumspb.CALLBACK_EXECUTION_STATUS_RUNNING + case callbackspb.CALLBACK_STATUS_FAILED: + return enumspb.CALLBACK_EXECUTION_STATUS_FAILED + case callbackspb.CALLBACK_STATUS_SUCCEEDED: + return enumspb.CALLBACK_EXECUTION_STATUS_SUCCEEDED + case callbackspb.CALLBACK_STATUS_TERMINATED: + return enumspb.CALLBACK_EXECUTION_STATUS_TERMINATED + default: + return enumspb.CALLBACK_EXECUTION_STATUS_UNSPECIFIED + } +} + +// callbackStatusToAPIState maps internal CallbackStatus to public API CallbackState. +func callbackStatusToAPIState(status callbackspb.CallbackStatus) enumspb.CallbackState { + switch status { + case callbackspb.CALLBACK_STATUS_STANDBY: + return enumspb.CALLBACK_STATE_STANDBY + case callbackspb.CALLBACK_STATUS_SCHEDULED: + return enumspb.CALLBACK_STATE_SCHEDULED + case callbackspb.CALLBACK_STATUS_BACKING_OFF: + return enumspb.CALLBACK_STATE_BACKING_OFF + case callbackspb.CALLBACK_STATUS_FAILED: + return enumspb.CALLBACK_STATE_FAILED + case callbackspb.CALLBACK_STATUS_SUCCEEDED: + return enumspb.CALLBACK_STATE_SUCCEEDED + case callbackspb.CALLBACK_STATUS_TERMINATED: + return enumspb.CALLBACK_STATE_TERMINATED + default: + return enumspb.CALLBACK_STATE_UNSPECIFIED + } +} + +// Describe returns the CallbackExecutionInfo for the describe RPC. Only applies to standalone callbacks. +func (c *Callback) Describe(ctx chasm.Context) (*callbackpb.CallbackExecutionInfo, error) { + apiCb, err := c.ToAPICallback() + if err != nil { + return nil, err + } + + exInfo := ctx.ExecutionInfo() + info := &callbackpb.CallbackExecutionInfo{ + CallbackId: c.CallbackId, + RunId: ctx.ExecutionKey().RunID, + Callback: apiCb, + Status: callbackStatusToAPIExecutionStatus(c.Status), + State: callbackStatusToAPIState(c.Status), + Attempt: c.Attempt, + CreateTime: c.RegistrationTime, + LastAttemptCompleteTime: c.LastAttemptCompleteTime, + LastAttemptFailure: c.LastAttemptFailure, + NextAttemptScheduleTime: c.NextAttemptScheduleTime, + CloseTime: c.CloseTime, + ScheduleToCloseTimeout: c.CompletionScheduleToCloseTimeout, + StateTransitionCount: exInfo.StateTransitionCount, + } + return info, nil +} + +// Outcome returns the callback execution outcome if the execution is in a terminal state. (Otherwise, nil.) +// +// IMPORTANT: This is specific to the callback delivery, and not the actual completion. The outcome will be +// a success even if it was to deliver a failed completion result. +func (c *Callback) Outcome(ctx chasm.Context) *callbackpb.CallbackExecutionOutcome { + switch c.Status { + case callbackspb.CALLBACK_STATUS_SUCCEEDED: + val := &callbackpb.CallbackExecutionOutcome_Success{} + return &callbackpb.CallbackExecutionOutcome{ + Value: val, + } + + case callbackspb.CALLBACK_STATUS_FAILED, + callbackspb.CALLBACK_STATUS_TERMINATED: + val := &callbackpb.CallbackExecutionOutcome_Failure{ + Failure: c.TerminalFailure.Get(ctx), + } + return &callbackpb.CallbackExecutionOutcome{ + Value: val, + } + + default: + return nil + } +} diff --git a/chasm/lib/callback/component_test.go b/chasm/lib/callback/component_test.go new file mode 100644 index 00000000000..7cf38f19042 --- /dev/null +++ b/chasm/lib/callback/component_test.go @@ -0,0 +1,52 @@ +package callback + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + "go.temporal.io/server/chasm" + callbackspb "go.temporal.io/server/chasm/lib/callback/gen/callbackpb/v1" + "go.temporal.io/server/common/backoff" + commonnexus "go.temporal.io/server/common/nexus" + "go.temporal.io/server/components/nexusoperations" +) + +// Confirm that callback delivery failures due to the Nexus operation not having +// started will be retried and not trigger circuit breakers. +func TestCallbacksToUnstartedNexusOperations(t *testing.T) { + cb := &Callback{ + CallbackState: &callbackspb.CallbackState{ + Callback: &callbackspb.Callback{ + Variant: &callbackspb.Callback_Nexus_{ + Nexus: &callbackspb.Callback_Nexus{ + Url: commonnexus.SystemCallbackURL, + }, + }, + }, + Status: callbackspb.CALLBACK_STATUS_SCHEDULED, + }, + } + + // Simulate the InvocationTask being executed, which ends with the invocation's + // result being saved on the Callback. + mctx := &chasm.MockMutableContext{} + _, err := cb.saveResult(mctx, saveResultInput{ + result: invocationResultRetry{err: nexusoperations.ErrOperationNotStarted}, + retryPolicy: backoff.NewExponentialRetryPolicy(time.Second), + }) + + require.NoError(t, err) + require.Equal(t, callbackspb.CALLBACK_STATUS_BACKING_OFF, cb.StateMachineState()) + require.Equal(t, int32(1), cb.Attempt) + require.Equal(t, "nexus operation not started", cb.LastAttemptFailure.Message) + require.False(t, cb.LastAttemptFailure.GetApplicationFailureInfo().NonRetryable) + require.NotNil(t, cb.NextAttemptScheduleTime) + + _, ok := cb.TerminalFailure.TryGet(mctx) + require.False(t, ok) + + // Confirm backoff task was generated. + require.Len(t, mctx.Tasks, 1) + require.IsType(t, &callbackspb.BackoffTask{}, mctx.Tasks[0].Payload) +} diff --git a/chasm/lib/callback/config.go b/chasm/lib/callback/config.go index 844add8d671..63a1ffa58ee 100644 --- a/chasm/lib/callback/config.go +++ b/chasm/lib/callback/config.go @@ -14,6 +14,25 @@ import ( "google.golang.org/grpc/status" ) +var EnableStandaloneExecutions = dynamicconfig.NewNamespaceBoolSetting( + "callback.enableStandaloneExecutions", + false, + `Toggles standalone callback execution functionality on the server.`, +) + +var LongPollBuffer = dynamicconfig.NewNamespaceDurationSetting( + "callback.longPollBuffer", + time.Second, + `A buffer used to adjust the callback execution long-poll timeouts. +The long-poll response is sent before the caller's deadline by this amount of time.`, +) + +var LongPollTimeout = dynamicconfig.NewNamespaceDurationSetting( + "callback.longPollTimeout", + 20*time.Second, + `Timeout for callback execution long-poll requests.`, +) + var MaxPerExecution = dynamicconfig.NewNamespaceIntSetting( "callback.maxPerExecution", 2000, @@ -39,13 +58,37 @@ var RetryPolicyMaximumInterval = dynamicconfig.NewGlobalDurationSetting( ) type Config struct { - RequestTimeout dynamicconfig.DurationPropertyFnWithDestinationFilter - RetryPolicy func() backoff.RetryPolicy + // callback.* settings. + EnableStandaloneExecutions dynamicconfig.BoolPropertyFnWithNamespaceFilter + LongPollBuffer dynamicconfig.DurationPropertyFnWithNamespaceFilter + LongPollTimeout dynamicconfig.DurationPropertyFnWithNamespaceFilter + RequestTimeout dynamicconfig.DurationPropertyFnWithDestinationFilter + RetryPolicy func() backoff.RetryPolicy + + // Settings defined elsewhere. + CHASMEnabled dynamicconfig.BoolPropertyFnWithNamespaceFilter + CHASMCallbacksEnabled dynamicconfig.BoolPropertyFnWithNamespaceFilter + + // Validation related config. + BlobSizeLimitError dynamicconfig.IntPropertyFnWithNamespaceFilter + BlobSizeLimitWarn dynamicconfig.IntPropertyFnWithNamespaceFilter + MaxIDLength dynamicconfig.IntPropertyFn // Used to check CallbackID, RequestID, etc. + + // NOTE: The configuration setting defining the allowlist of supported callback + // addresses is defined in components/callbacks/config.go, via AllowedAddresses. + // + // Similarly, MaxPerExecution is missing. It is used by `Validator` and is loaded there. + // Once HSM callbacks (components/callbacks) are removed, the callbackValidatorProvider in + // frontend/fx.go can be moved into this package. And at that time, we can simply have the + // callback.Validator inject callback.Config. (And have a single location for all config.) } -func configProvider(dc *dynamicconfig.Collection) *Config { +func ConfigProvider(dc *dynamicconfig.Collection) *Config { return &Config{ - RequestTimeout: RequestTimeout.Get(dc), + EnableStandaloneExecutions: EnableStandaloneExecutions.Get(dc), + LongPollBuffer: LongPollBuffer.Get(dc), + LongPollTimeout: LongPollTimeout.Get(dc), + RequestTimeout: RequestTimeout.Get(dc), RetryPolicy: func() backoff.RetryPolicy { return backoff.NewExponentialRetryPolicy( RetryPolicyInitialInterval.Get(dc)(), @@ -55,21 +98,16 @@ func configProvider(dc *dynamicconfig.Collection) *Config { backoff.NoInterval, ) }, + + CHASMEnabled: dynamicconfig.EnableChasm.Get(dc), + CHASMCallbacksEnabled: dynamicconfig.EnableCHASMCallbacks.Get(dc), + + MaxIDLength: dynamicconfig.MaxIDLengthLimit.Get(dc), + BlobSizeLimitError: dynamicconfig.BlobSizeLimitError.Get(dc), + BlobSizeLimitWarn: dynamicconfig.BlobSizeLimitWarn.Get(dc), } } -var AllowedAddresses = dynamicconfig.NewNamespaceTypedSettingWithConverter( - "chasm.callback.allowedAddresses", - allowedAddressConverter, - AddressMatchRules{}, - `The per-namespace list of addresses that are allowed for callbacks and whether secure connections (https) are required. -URL: "temporal://system" is always allowed for worker callbacks. The default is no address rules. -URLs are checked against each in order when starting a workflow with attached callbacks and only need to match one to pass validation. -This configuration is required for external endpoint targets; any invalid entries are ignored. Each entry is a map with possible values: - - "Pattern":string (required) the host:port pattern to which this config applies. - Wildcards, '*', are supported and can match any number of characters (e.g. '*' matches everything, 'prefix.*.domain' matches 'prefix.a.domain' as well as 'prefix.a.b.domain'). - - "AllowInsecure":bool (optional, default=false) indicates whether https is required`) - type AddressMatchRules struct { Rules []AddressMatchRule } diff --git a/chasm/lib/callback/frontend.go b/chasm/lib/callback/frontend.go new file mode 100644 index 00000000000..b2d79d6942f --- /dev/null +++ b/chasm/lib/callback/frontend.go @@ -0,0 +1,335 @@ +package callback + +import ( + "context" + + callbackpb "go.temporal.io/api/callback/v1" + commonpb "go.temporal.io/api/common/v1" + enumspb "go.temporal.io/api/enums/v1" + "go.temporal.io/api/serviceerror" + "go.temporal.io/api/workflowservice/v1" + "go.temporal.io/server/chasm" + callbackspb "go.temporal.io/server/chasm/lib/callback/gen/callbackpb/v1" + "go.temporal.io/server/common/log" + "go.temporal.io/server/common/namespace" + "go.temporal.io/server/common/searchattribute" + "google.golang.org/protobuf/types/known/timestamppb" +) + +var ErrStandaloneCallbacksDisabled = serviceerror.NewUnimplemented("standalone callback executions are not enabled") + +// FrontendHandler defines the frontend interface for standalone callback execution RPCs, +// in which the Frontend microservice receives requests from the Temporal SDK and proxies +// them to the implementation of the CHASM component running in the History service. +type FrontendHandler interface { + StartCallbackExecution(context.Context, *workflowservice.StartCallbackExecutionRequest) (*workflowservice.StartCallbackExecutionResponse, error) + DescribeCallbackExecution(context.Context, *workflowservice.DescribeCallbackExecutionRequest) (*workflowservice.DescribeCallbackExecutionResponse, error) + PollCallbackExecution(context.Context, *workflowservice.PollCallbackExecutionRequest) (*workflowservice.PollCallbackExecutionResponse, error) + ListCallbackExecutions(context.Context, *workflowservice.ListCallbackExecutionsRequest) (*workflowservice.ListCallbackExecutionsResponse, error) + CountCallbackExecutions(context.Context, *workflowservice.CountCallbackExecutionsRequest) (*workflowservice.CountCallbackExecutionsResponse, error) + TerminateCallbackExecution(context.Context, *workflowservice.TerminateCallbackExecutionRequest) (*workflowservice.TerminateCallbackExecutionResponse, error) + DeleteCallbackExecution(context.Context, *workflowservice.DeleteCallbackExecutionRequest) (*workflowservice.DeleteCallbackExecutionResponse, error) +} + +type frontendHandler struct { + logger log.Logger + namespaceRegistry namespace.Registry + + client callbackspb.CallbackServiceClient + config *Config + reqValidator *frontendRequestValidator +} + +func NewFrontendHandler( + logger log.Logger, + namespaceRegistry namespace.Registry, + client callbackspb.CallbackServiceClient, + config *Config, + callbackValidator Validator, + saMapperProvider searchattribute.MapperProvider, + saValidator *searchattribute.Validator, +) FrontendHandler { + return &frontendHandler{ + logger: logger, + namespaceRegistry: namespaceRegistry, + client: client, + config: config, + reqValidator: &frontendRequestValidator{ + config: config, + cbValidator: callbackValidator, + logger: logger, + saMapperProvider: saMapperProvider, + saValidator: saValidator, + }, + } +} + +type Namespacer interface{ GetNamespace() string } + +// Looks up the namespace ID from the user-supplied namespace name in the request proto. +func (h *frontendHandler) getTargetNamespace(requestProto Namespacer) (namespace.ID, error) { + targetNamespaceName := namespace.Name(requestProto.GetNamespace()) + namespaceID, err := h.namespaceRegistry.GetNamespaceID(targetNamespaceName) + if err != nil { + return "", err + } + return namespaceID, nil +} + +// Checks if standalone callback executions are supported in the target namespace. +func (h *frontendHandler) checkFeatureEnabled(requestProto Namespacer) error { + // Confirm CHASM is enabled. + targetNamespaceName := requestProto.GetNamespace() + if !h.config.CHASMEnabled(targetNamespaceName) || !h.config.CHASMCallbacksEnabled(targetNamespaceName) { + return ErrStandaloneCallbacksDisabled + } + if !h.config.EnableStandaloneExecutions(targetNamespaceName) { + return ErrStandaloneCallbacksDisabled + } + return nil +} + +// StartCallbackExecution creates a new standalone callback execution that will deliver the +// provided Nexus completion payload to the target callback URL with retries. +func (h *frontendHandler) StartCallbackExecution( + ctx context.Context, + request *workflowservice.StartCallbackExecutionRequest, +) (*workflowservice.StartCallbackExecutionResponse, error) { + // Validate + if err := h.checkFeatureEnabled(request); err != nil { + return nil, err + } + if err := h.reqValidator.ValidateStartCallbackExecution(request); err != nil { + return nil, err + } + + // Execute + namespaceID, err := h.getTargetNamespace(request) + if err != nil { + return nil, err + } + resp, err := h.client.StartCallbackExecution(ctx, &callbackspb.StartCallbackExecutionRequest{ + NamespaceId: namespaceID.String(), + FrontendRequest: request, + }) + if err != nil { + return nil, err + } + return resp.GetFrontendResponse(), nil +} + +// DescribeCallbackExecution returns detailed information about a callback execution +// including its current state, delivery attempt history, and timing information. +// Optionally takes a long-poll token and waits for any change. +func (h *frontendHandler) DescribeCallbackExecution( + ctx context.Context, + request *workflowservice.DescribeCallbackExecutionRequest, +) (*workflowservice.DescribeCallbackExecutionResponse, error) { + // Validate + if err := h.checkFeatureEnabled(request); err != nil { + return nil, err + } + if err := h.reqValidator.ValidateDescribeCallbackExecution(request); err != nil { + return nil, err + } + + // Execute + namespaceID, err := h.getTargetNamespace(request) + if err != nil { + return nil, err + } + resp, err := h.client.DescribeCallbackExecution(ctx, &callbackspb.DescribeCallbackExecutionRequest{ + NamespaceId: namespaceID.String(), + FrontendRequest: request, + }) + if err != nil { + return nil, err + } + return resp.GetFrontendResponse(), nil +} + +// PollCallbackExecution blocks until the callback execution completes and returns its outcome. +func (h *frontendHandler) PollCallbackExecution( + ctx context.Context, + request *workflowservice.PollCallbackExecutionRequest, +) (*workflowservice.PollCallbackExecutionResponse, error) { + // Validate + if err := h.checkFeatureEnabled(request); err != nil { + return nil, err + } + if err := h.reqValidator.ValidatePollCallbackExecution(request); err != nil { + return nil, err + } + + // Execute + namespaceID, err := h.getTargetNamespace(request) + if err != nil { + return nil, err + } + resp, err := h.client.PollCallbackExecution(ctx, &callbackspb.PollCallbackExecutionRequest{ + NamespaceId: namespaceID.String(), + FrontendRequest: request, + }) + if err != nil { + return nil, err + } + return resp.GetFrontendResponse(), nil +} + +// TerminateCallbackExecution forcefully stops a running callback execution. +// No-op if already in a terminal state. +func (h *frontendHandler) TerminateCallbackExecution( + ctx context.Context, + request *workflowservice.TerminateCallbackExecutionRequest, +) (*workflowservice.TerminateCallbackExecutionResponse, error) { + // Validate + if err := h.checkFeatureEnabled(request); err != nil { + return nil, err + } + if err := h.reqValidator.ValidateTerminateCallbackExecution(request); err != nil { + return nil, err + } + + // Execute + namespaceID, err := h.getTargetNamespace(request) + if err != nil { + return nil, err + } + resp, err := h.client.TerminateCallbackExecution(ctx, &callbackspb.TerminateCallbackExecutionRequest{ + NamespaceId: namespaceID.String(), + FrontendRequest: request, + }) + if err != nil { + return nil, err + } + return resp.GetFrontendResponse(), nil +} + +// DeleteCallbackExecution terminates the callback if still running and marks it for cleanup. +func (h *frontendHandler) DeleteCallbackExecution( + ctx context.Context, + request *workflowservice.DeleteCallbackExecutionRequest, +) (*workflowservice.DeleteCallbackExecutionResponse, error) { + // Validate + if err := h.checkFeatureEnabled(request); err != nil { + return nil, err + } + if err := h.reqValidator.ValidateDeleteCallbackExecution(request); err != nil { + return nil, err + } + + // Execute + namespaceID, err := h.getTargetNamespace(request) + if err != nil { + return nil, err + } + resp, err := h.client.DeleteCallbackExecution(ctx, &callbackspb.DeleteCallbackExecutionRequest{ + NamespaceId: namespaceID.String(), + FrontendRequest: request, + }) + if err != nil { + return nil, err + } + return resp.GetFrontendResponse(), nil +} + +// ListCallbackExecutions queries the visibility store for callback executions matching +// the provided filter. Supports the same query syntax as workflow list filters. +func (h *frontendHandler) ListCallbackExecutions( + ctx context.Context, + request *workflowservice.ListCallbackExecutionsRequest, +) (*workflowservice.ListCallbackExecutionsResponse, error) { + // Validate + if err := h.checkFeatureEnabled(request); err != nil { + return nil, err + } + if err := h.reqValidator.ValidateListCallbackExecutions(request); err != nil { + return nil, err + } + + // Lookup the namespace by its name, to confirm it actually exists. + namespaceName := namespace.Name(request.GetNamespace()) + if _, err := h.namespaceRegistry.GetNamespaceID(namespaceName); err != nil { + return nil, err + } + + resp, err := chasm.ListExecutions[*Callback, *callbackpb.CallbackExecutionListInfo]( + ctx, + &chasm.ListExecutionsRequest{ + NamespaceName: namespaceName.String(), + PageSize: int(request.GetPageSize()), + NextPageToken: request.GetNextPageToken(), + Query: request.GetQuery(), + }, + ) + if err != nil { + return nil, err + } + + // Build the response object. + executions := make([]*callbackpb.CallbackExecutionListInfo, 0, len(resp.Executions)) + for _, exec := range resp.Executions { + + statusStr, _ := chasm.SearchAttributeValue(exec.ChasmSearchAttributes, executionStatusSearchAttribute) + status, _ := enumspb.CallbackExecutionStatusFromString(statusStr) + + info := callbackpb.CallbackExecutionListInfo{ + CallbackId: exec.BusinessID, + RunId: exec.RunID, + Status: status, + CreateTime: timestamppb.New(exec.StartTime), + CloseTime: timestamppb.New(exec.CloseTime), + SearchAttributes: &commonpb.SearchAttributes{IndexedFields: exec.CustomSearchAttributes}, + StateTransitionCount: exec.StateTransitionCount, + } + executions = append(executions, &info) + } + return &workflowservice.ListCallbackExecutionsResponse{ + Executions: executions, + NextPageToken: resp.NextPageToken, + }, nil +} + +// CountCallbackExecutions returns the number of callback executions matching the query, +// with optional grouping by search attribute values. +func (h *frontendHandler) CountCallbackExecutions( + ctx context.Context, + request *workflowservice.CountCallbackExecutionsRequest, +) (*workflowservice.CountCallbackExecutionsResponse, error) { + // Validate + if err := h.checkFeatureEnabled(request); err != nil { + return nil, err + } + if err := h.reqValidator.ValidateCountCallbackExecutions(request); err != nil { + return nil, err + } + + // Lookup the namespace by its name, to confirm it actually exists. + namespaceName := namespace.Name(request.GetNamespace()) + if _, err := h.namespaceRegistry.GetNamespaceID(namespaceName); err != nil { + return nil, err + } + resp, err := chasm.CountExecutions[*Callback]( + ctx, + &chasm.CountExecutionsRequest{ + NamespaceName: namespaceName.String(), + Query: request.GetQuery(), + }, + ) + if err != nil { + return nil, err + } + + // Build the response object. + groups := make([]*workflowservice.CountCallbackExecutionsResponse_AggregationGroup, 0, len(resp.Groups)) + for _, g := range resp.Groups { + groups = append(groups, &workflowservice.CountCallbackExecutionsResponse_AggregationGroup{ + GroupValues: g.Values, + Count: g.Count, + }) + } + return &workflowservice.CountCallbackExecutionsResponse{ + Count: resp.Count, + Groups: groups, + }, nil +} diff --git a/chasm/lib/callback/frontend_validation.go b/chasm/lib/callback/frontend_validation.go new file mode 100644 index 00000000000..69ad72df517 --- /dev/null +++ b/chasm/lib/callback/frontend_validation.go @@ -0,0 +1,261 @@ +package callback + +import ( + "fmt" + + "github.com/google/uuid" + callbackpb "go.temporal.io/api/callback/v1" + commonpb "go.temporal.io/api/common/v1" + "go.temporal.io/api/serviceerror" + "go.temporal.io/api/workflowservice/v1" + "go.temporal.io/server/common" + "go.temporal.io/server/common/log" + "go.temporal.io/server/common/log/tag" + "go.temporal.io/server/common/searchattribute" + "google.golang.org/protobuf/proto" +) + +// Returns a serviceerror.InvalidArgument error for a missing required field. +func missingRequiredFieldError(fieldName string) error { + msg := fmt.Sprintf("%s is not set on request.", fieldName) + return serviceerror.NewInvalidArgument(msg) +} + +type RequestIDer interface { + GetRequestId() string +} + +func verifyRequestIDLength(reqProto RequestIDer, config *Config) error { + l := len(reqProto.GetRequestId()) + maxLen := config.MaxIDLength() + if l > maxLen { + return serviceerror.NewInvalidArgumentf("callback ID exceeds length limit. Length=%d Limit=%d", l, maxLen) + } + return nil +} + +type CallbackIDer interface { + GetCallbackId() string +} + +func verifyCallbackIDLength(reqProto CallbackIDer, config *Config) error { + l := len(reqProto.GetCallbackId()) + maxLen := config.MaxIDLength() + if l > maxLen { + return serviceerror.NewInvalidArgumentf("callback ID exceeds length limit. Length=%d Limit=%d", l, maxLen) + } + return nil +} + +// frontendRequestValidator bundles the configuration data for validating an incomming request. +// +// IMPORTANT: Validation methods MAY mutate the incomming request, in order to ensure they all have +// a valid RunID (if one was not specified already). +type frontendRequestValidator struct { + config *Config + cbValidator Validator + logger log.Logger + saMapperProvider searchattribute.MapperProvider + saValidator *searchattribute.Validator +} + +func (rv *frontendRequestValidator) ValidateStartCallbackExecution(req *workflowservice.StartCallbackExecutionRequest) error { + // Set RequestID if missing. + if req.GetRequestId() == "" { + req.RequestId = uuid.NewString() + } + + // Required fields. + requiredFields := map[string]string{ + "Namespace": req.GetNamespace(), + "Identity": req.GetIdentity(), + "RequestId": req.GetRequestId(), + "CallbackId": req.GetCallbackId(), + } + for k, v := range requiredFields { + if v == "" { + return missingRequiredFieldError(k) + } + } + + // Field lengths + if err := verifyRequestIDLength(req, rv.config); err != nil { + return err + } + if err := verifyCallbackIDLength(req, rv.config); err != nil { + return err + } + + // Validate the callback to be invoked and its parameters. + if err := rv.cbValidator.Validate(req.GetNamespace(), []*commonpb.Callback{req.Callback}); err != nil { + return err + } + + // ScheduleToCloseTimeout + if req.GetScheduleToCloseTimeout() == nil || req.GetScheduleToCloseTimeout().AsDuration() <= 0 { + return serviceerror.NewInvalidArgument("ScheduleToCloseTimeout must be set and positive.") + } + + // Validate the input data to deliver to the callback URL, currently only one kind is supported (Completion). + completion := req.GetCompletion() + if completion == nil { + return serviceerror.NewInvalidArgument("Completion is not set on request.") + } + if completion.GetSuccess() == nil && completion.GetFailure() == nil { + return serviceerror.NewInvalidArgument("Completion must have either success or failure set.") + } + if completion.GetSuccess() != nil && completion.GetFailure() != nil { + return serviceerror.NewInvalidArgument("Completion must have exactly one of success or failure set, not both.") + } + // Validate the size of the completion is reasonable. + if err := rv.validateCompletionSize(req, completion); err != nil { + return err + } + + // Search Attributes + if searchAttrib := req.GetSearchAttributes(); searchAttrib != nil { + if err := rv.validateSearchAttributes(req, searchAttrib); err != nil { + return err + } + } + + return nil +} + +func (rv *frontendRequestValidator) validateCompletionSize(req Namespacer, completion *callbackpb.CallbackExecutionCompletion) error { + namespace := req.GetNamespace() + + sizeWarnLimit := rv.config.BlobSizeLimitWarn(namespace) + sizeErrorLimit := rv.config.BlobSizeLimitError(namespace) + + blobSize := proto.Size(completion) + if blobSize > sizeWarnLimit { + rv.logger.Warn("Completion blob size exceeds the warning limit.", + tag.WorkflowNamespace(namespace), + tag.BlobSize(int64(blobSize))) + } + + if blobSize > sizeErrorLimit { + return common.ErrBlobSizeExceedsLimit + } + + return nil +} + +func (rv *frontendRequestValidator) validateSearchAttributes(req Namespacer, saToValidate *commonpb.SearchAttributes) error { + namespaceName := req.GetNamespace() + + // Unalias search attributes for validation. + if rv.saMapperProvider != nil && saToValidate != nil { + var err error + saToValidate, err = searchattribute.UnaliasFields(rv.saMapperProvider, saToValidate, namespaceName) + if err != nil { + return err + } + } + + if err := rv.saValidator.Validate(saToValidate, namespaceName); err != nil { + return err + } + + return rv.saValidator.ValidateSize(saToValidate, namespaceName) +} + +func (rv *frontendRequestValidator) ValidateDescribeCallbackExecution(req *workflowservice.DescribeCallbackExecutionRequest) error { + // Required fields. + requiredFields := map[string]string{ + "Namespace": req.GetNamespace(), + "CallbackId": req.GetCallbackId(), + } + for k, v := range requiredFields { + if v == "" { + return missingRequiredFieldError(k) + } + } + + // Field lengths + if err := verifyCallbackIDLength(req, rv.config); err != nil { + return err + } + + // A long-poll token requires the RunID be set. + if len(req.GetLongPollToken()) > 0 && req.GetRunId() == "" { + return serviceerror.NewInvalidArgument("RunID is required when LongPollToken is provided") + } + + return nil +} + +func (rv *frontendRequestValidator) ValidatePollCallbackExecution(req *workflowservice.PollCallbackExecutionRequest) error { + // Required fields. + requiredFields := map[string]string{ + "Namespace": req.GetNamespace(), + "CallbackId": req.GetCallbackId(), + } + for k, v := range requiredFields { + if v == "" { + return missingRequiredFieldError(k) + } + } + + // Field lengths + return verifyCallbackIDLength(req, rv.config) +} + +func (rv *frontendRequestValidator) ValidateTerminateCallbackExecution(req *workflowservice.TerminateCallbackExecutionRequest) error { + // Set RequestID if missing. + if req.GetRequestId() == "" { + req.RequestId = uuid.NewString() + } + + // Required fields. + requiredFields := map[string]string{ + "RequestId": req.GetRequestId(), + "Namespace": req.GetNamespace(), + "CallbackId": req.GetCallbackId(), + + // NOTE: We don't require the Identity or Reason fields to be set, + // and just set reasonable defaults. + } + for k, v := range requiredFields { + if v == "" { + return missingRequiredFieldError(k) + } + } + + // Field lengths + if err := verifyRequestIDLength(req, rv.config); err != nil { + return err + } + return verifyCallbackIDLength(req, rv.config) +} + +func (rv *frontendRequestValidator) ValidateDeleteCallbackExecution(req *workflowservice.DeleteCallbackExecutionRequest) error { + // Required fields. + requiredFields := map[string]string{ + "Namespace": req.GetNamespace(), + "CallbackId": req.GetCallbackId(), + } + for k, v := range requiredFields { + if v == "" { + return missingRequiredFieldError(k) + } + } + + // Field lengths + return verifyCallbackIDLength(req, rv.config) +} + +func (rv *frontendRequestValidator) ValidateListCallbackExecutions(req *workflowservice.ListCallbackExecutionsRequest) error { + if req.GetNamespace() == "" { + return missingRequiredFieldError("Namespace") + } + return nil +} + +func (rv *frontendRequestValidator) ValidateCountCallbackExecutions(req *workflowservice.CountCallbackExecutionsRequest) error { + if req.GetNamespace() == "" { + return missingRequiredFieldError("Namespace") + } + return nil +} diff --git a/chasm/lib/callback/fx.go b/chasm/lib/callback/fx.go index 1da518d8368..3d4401db7f3 100644 --- a/chasm/lib/callback/fx.go +++ b/chasm/lib/callback/fx.go @@ -5,6 +5,7 @@ import ( "net/http" "go.temporal.io/server/chasm" + callbackspb "go.temporal.io/server/chasm/lib/callback/gen/callbackpb/v1" "go.temporal.io/server/common" "go.temporal.io/server/common/cluster" "go.temporal.io/server/common/collection" @@ -15,13 +16,6 @@ import ( "go.uber.org/fx" ) -func register( - registry *chasm.Registry, - library *Library, -) error { - return registry.Register(library) -} - // httpCallerProviderProvider provides an HTTPCallerProvider for CHASM callbacks. func httpCallerProviderProvider( clusterMetadata cluster.Metadata, @@ -53,12 +47,30 @@ func httpCallerProviderProvider( return m.Get, nil } -var Module = fx.Module( +// FrontendModule just contains the CHASM components, but not their implementation. +var FrontendModule = fx.Module( + "callback-frontend", + fx.Provide(callbackspb.NewCallbackServiceLayeredClient), + fx.Provide(ConfigProvider), + fx.Provide(NewFrontendHandler), + + fx.Provide(newComponentOnlyLibrary), + fx.Invoke(func(registry *chasm.Registry, coLibrary *componentOnlyLibrary) error { + return registry.Register(coLibrary) + }), +) + +var HistoryModule = fx.Module( "chasm.lib.callback", - fx.Provide(configProvider), + fx.Provide(ConfigProvider), fx.Provide(httpCallerProviderProvider), fx.Provide(newInvocationTaskHandler), fx.Provide(newBackoffTaskHandler), + fx.Provide(newCallbackHandler), + fx.Provide(NewCompletionScheduleToCloseTimeoutTaskHandler), + fx.Provide(newLibrary), - fx.Invoke(register), + fx.Invoke(func(registry *chasm.Registry, library *library) error { + return registry.Register(library) + }), ) diff --git a/chasm/lib/callback/gen/callbackpb/v1/message.go-helpers.pb.go b/chasm/lib/callback/gen/callbackpb/v1/message.go-helpers.pb.go index 4e8000266ae..c9900bb64b0 100644 --- a/chasm/lib/callback/gen/callbackpb/v1/message.go-helpers.pb.go +++ b/chasm/lib/callback/gen/callbackpb/v1/message.go-helpers.pb.go @@ -89,6 +89,7 @@ var ( "BackingOff": 3, "Failed": 4, "Succeeded": 5, + "Terminated": 6, } ) diff --git a/chasm/lib/callback/gen/callbackpb/v1/message.pb.go b/chasm/lib/callback/gen/callbackpb/v1/message.pb.go index d998ef3fc8f..6ae4eed6fd5 100644 --- a/chasm/lib/callback/gen/callbackpb/v1/message.pb.go +++ b/chasm/lib/callback/gen/callbackpb/v1/message.pb.go @@ -16,6 +16,7 @@ import ( v1 "go.temporal.io/api/failure/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" ) @@ -42,6 +43,8 @@ const ( CALLBACK_STATUS_FAILED CallbackStatus = 4 // Callback has succeeded. CALLBACK_STATUS_SUCCEEDED CallbackStatus = 5 + // Callback was terminated by request. + CALLBACK_STATUS_TERMINATED CallbackStatus = 6 ) // Enum value maps for CallbackStatus. @@ -53,6 +56,7 @@ var ( 3: "CALLBACK_STATUS_BACKING_OFF", 4: "CALLBACK_STATUS_FAILED", 5: "CALLBACK_STATUS_SUCCEEDED", + 6: "CALLBACK_STATUS_TERMINATED", } CallbackStatus_value = map[string]int32{ "CALLBACK_STATUS_UNSPECIFIED": 0, @@ -61,6 +65,7 @@ var ( "CALLBACK_STATUS_BACKING_OFF": 3, "CALLBACK_STATUS_FAILED": 4, "CALLBACK_STATUS_SUCCEEDED": 5, + "CALLBACK_STATUS_TERMINATED": 6, } ) @@ -84,6 +89,8 @@ func (x CallbackStatus) String() string { return "Failed" case CALLBACK_STATUS_SUCCEEDED: return "Succeeded" + case CALLBACK_STATUS_TERMINATED: + return "Terminated" default: return strconv.Itoa(int(x)) } @@ -126,9 +133,19 @@ type CallbackState struct { // https://github.com/temporalio/temporal/pull/8473#discussion_r2427348436 NextAttemptScheduleTime *timestamppb.Timestamp `protobuf:"bytes,8,opt,name=next_attempt_schedule_time,json=nextAttemptScheduleTime,proto3" json:"next_attempt_schedule_time,omitempty"` // Request ID that added the callback. - RequestId string `protobuf:"bytes,9,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + RequestId string `protobuf:"bytes,9,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + // Request ID that terminated the callback, if applicable. Used for idempotency. + TerminateRequestId string `protobuf:"bytes,10,opt,name=terminate_request_id,json=terminateRequestId,proto3" json:"terminate_request_id,omitempty"` + // The time when the callback reached a terminal state. + CloseTime *timestamppb.Timestamp `protobuf:"bytes,11,opt,name=close_time,json=closeTime,proto3" json:"close_time,omitempty"` + // (standalone only) User-supplied business ID set when StartCallbackExecution() is + // called. Used to identify the callback for operations like Describe- or Terminate-. + CallbackId string `protobuf:"bytes,12,opt,name=callback_id,json=callbackId,proto3" json:"callback_id,omitempty"` + // (standalone only) Schedule-to-close timeout from when StartCallbackExecution() + // is called to when the result gets delivered. + CompletionScheduleToCloseTimeout *durationpb.Duration `protobuf:"bytes,13,opt,name=completion_schedule_to_close_timeout,json=completionScheduleToCloseTimeout,proto3" json:"completion_schedule_to_close_timeout,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *CallbackState) Reset() { @@ -217,6 +234,34 @@ func (x *CallbackState) GetRequestId() string { return "" } +func (x *CallbackState) GetTerminateRequestId() string { + if x != nil { + return x.TerminateRequestId + } + return "" +} + +func (x *CallbackState) GetCloseTime() *timestamppb.Timestamp { + if x != nil { + return x.CloseTime + } + return nil +} + +func (x *CallbackState) GetCallbackId() string { + if x != nil { + return x.CallbackId + } + return "" +} + +func (x *CallbackState) GetCompletionScheduleToCloseTimeout() *durationpb.Duration { + if x != nil { + return x.CompletionScheduleToCloseTimeout + } + return nil +} + type Callback struct { state protoimpl.MessageState `protogen:"open.v1"` // Types that are valid to be assigned to Variant: @@ -336,7 +381,9 @@ type Callback_Nexus struct { // aip.dev/not-precedent: Not respecting aip here. --) Url string `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"` // Header to attach to callback request. - Header map[string]string `protobuf:"bytes,2,rep,name=header,proto3" json:"header,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + Header map[string]string `protobuf:"bytes,2,rep,name=header,proto3" json:"header,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + // Token identifying the target callback to resolve. + Token string `protobuf:"bytes,3,opt,name=token,proto3" json:"token,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -385,11 +432,18 @@ func (x *Callback_Nexus) GetHeader() map[string]string { return nil } +func (x *Callback_Nexus) GetToken() string { + if x != nil { + return x.Token + } + return "" +} + var File_temporal_server_chasm_lib_callback_proto_v1_message_proto protoreflect.FileDescriptor const file_temporal_server_chasm_lib_callback_proto_v1_message_proto_rawDesc = "" + "\n" + - "9temporal/server/chasm/lib/callback/proto/v1/message.proto\x12,temporal.server.chasm.lib.callbacks.proto.v1\x1a\x1fgoogle/protobuf/timestamp.proto\x1a$temporal/api/common/v1/message.proto\x1a%temporal/api/failure/v1/message.proto\"\xd3\x04\n" + + "9temporal/server/chasm/lib/callback/proto/v1/message.proto\x12,temporal.server.chasm.lib.callbacks.proto.v1\x1a\x1egoogle/protobuf/duration.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a$temporal/api/common/v1/message.proto\x1a%temporal/api/failure/v1/message.proto\"\xcc\x06\n" + "\rCallbackState\x12R\n" + "\bcallback\x18\x01 \x01(\v26.temporal.server.chasm.lib.callbacks.proto.v1.CallbackR\bcallback\x12G\n" + "\x11registration_time\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampR\x10registrationTime\x12T\n" + @@ -399,25 +453,34 @@ const file_temporal_server_chasm_lib_callback_proto_v1_message_proto_rawDesc = " "\x14last_attempt_failure\x18\a \x01(\v2 .temporal.api.failure.v1.FailureR\x12lastAttemptFailure\x12W\n" + "\x1anext_attempt_schedule_time\x18\b \x01(\v2\x1a.google.protobuf.TimestampR\x17nextAttemptScheduleTime\x12\x1d\n" + "\n" + - "request_id\x18\t \x01(\tR\trequestId\x1a\x10\n" + - "\x0eWorkflowClosed\"\xde\x02\n" + + "request_id\x18\t \x01(\tR\trequestId\x120\n" + + "\x14terminate_request_id\x18\n" + + " \x01(\tR\x12terminateRequestId\x129\n" + + "\n" + + "close_time\x18\v \x01(\v2\x1a.google.protobuf.TimestampR\tcloseTime\x12\x1f\n" + + "\vcallback_id\x18\f \x01(\tR\n" + + "callbackId\x12i\n" + + "$completion_schedule_to_close_timeout\x18\r \x01(\v2\x19.google.protobuf.DurationR completionScheduleToCloseTimeout\x1a\x10\n" + + "\x0eWorkflowClosed\"\xf4\x02\n" + "\bCallback\x12T\n" + "\x05nexus\x18\x02 \x01(\v2<.temporal.server.chasm.lib.callbacks.proto.v1.Callback.NexusH\x00R\x05nexus\x122\n" + - "\x05links\x18d \x03(\v2\x1c.temporal.api.common.v1.LinkR\x05links\x1a\xb6\x01\n" + + "\x05links\x18d \x03(\v2\x1c.temporal.api.common.v1.LinkR\x05links\x1a\xcc\x01\n" + "\x05Nexus\x12\x10\n" + "\x03url\x18\x01 \x01(\tR\x03url\x12`\n" + - "\x06header\x18\x02 \x03(\v2H.temporal.server.chasm.lib.callbacks.proto.v1.Callback.Nexus.HeaderEntryR\x06header\x1a9\n" + + "\x06header\x18\x02 \x03(\v2H.temporal.server.chasm.lib.callbacks.proto.v1.Callback.Nexus.HeaderEntryR\x06header\x12\x14\n" + + "\x05token\x18\x03 \x01(\tR\x05token\x1a9\n" + "\vHeaderEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\t\n" + - "\avariantJ\x04\b\x01\x10\x02*\xc9\x01\n" + + "\avariantJ\x04\b\x01\x10\x02*\xe9\x01\n" + "\x0eCallbackStatus\x12\x1f\n" + "\x1bCALLBACK_STATUS_UNSPECIFIED\x10\x00\x12\x1b\n" + "\x17CALLBACK_STATUS_STANDBY\x10\x01\x12\x1d\n" + "\x19CALLBACK_STATUS_SCHEDULED\x10\x02\x12\x1f\n" + "\x1bCALLBACK_STATUS_BACKING_OFF\x10\x03\x12\x1a\n" + "\x16CALLBACK_STATUS_FAILED\x10\x04\x12\x1d\n" + - "\x19CALLBACK_STATUS_SUCCEEDED\x10\x05BGZEgo.temporal.io/server/chasm/lib/callbacks/gen/callbackspb;callbackspbb\x06proto3" + "\x19CALLBACK_STATUS_SUCCEEDED\x10\x05\x12\x1e\n" + + "\x1aCALLBACK_STATUS_TERMINATED\x10\x06BGZEgo.temporal.io/server/chasm/lib/callbacks/gen/callbackspb;callbackspbb\x06proto3" var ( file_temporal_server_chasm_lib_callback_proto_v1_message_proto_rawDescOnce sync.Once @@ -442,23 +505,26 @@ var file_temporal_server_chasm_lib_callback_proto_v1_message_proto_goTypes = []a nil, // 5: temporal.server.chasm.lib.callbacks.proto.v1.Callback.Nexus.HeaderEntry (*timestamppb.Timestamp)(nil), // 6: google.protobuf.Timestamp (*v1.Failure)(nil), // 7: temporal.api.failure.v1.Failure - (*v11.Link)(nil), // 8: temporal.api.common.v1.Link + (*durationpb.Duration)(nil), // 8: google.protobuf.Duration + (*v11.Link)(nil), // 9: temporal.api.common.v1.Link } var file_temporal_server_chasm_lib_callback_proto_v1_message_proto_depIdxs = []int32{ - 2, // 0: temporal.server.chasm.lib.callbacks.proto.v1.CallbackState.callback:type_name -> temporal.server.chasm.lib.callbacks.proto.v1.Callback - 6, // 1: temporal.server.chasm.lib.callbacks.proto.v1.CallbackState.registration_time:type_name -> google.protobuf.Timestamp - 0, // 2: temporal.server.chasm.lib.callbacks.proto.v1.CallbackState.status:type_name -> temporal.server.chasm.lib.callbacks.proto.v1.CallbackStatus - 6, // 3: temporal.server.chasm.lib.callbacks.proto.v1.CallbackState.last_attempt_complete_time:type_name -> google.protobuf.Timestamp - 7, // 4: temporal.server.chasm.lib.callbacks.proto.v1.CallbackState.last_attempt_failure:type_name -> temporal.api.failure.v1.Failure - 6, // 5: temporal.server.chasm.lib.callbacks.proto.v1.CallbackState.next_attempt_schedule_time:type_name -> google.protobuf.Timestamp - 4, // 6: temporal.server.chasm.lib.callbacks.proto.v1.Callback.nexus:type_name -> temporal.server.chasm.lib.callbacks.proto.v1.Callback.Nexus - 8, // 7: temporal.server.chasm.lib.callbacks.proto.v1.Callback.links:type_name -> temporal.api.common.v1.Link - 5, // 8: temporal.server.chasm.lib.callbacks.proto.v1.Callback.Nexus.header:type_name -> temporal.server.chasm.lib.callbacks.proto.v1.Callback.Nexus.HeaderEntry - 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 + 2, // 0: temporal.server.chasm.lib.callbacks.proto.v1.CallbackState.callback:type_name -> temporal.server.chasm.lib.callbacks.proto.v1.Callback + 6, // 1: temporal.server.chasm.lib.callbacks.proto.v1.CallbackState.registration_time:type_name -> google.protobuf.Timestamp + 0, // 2: temporal.server.chasm.lib.callbacks.proto.v1.CallbackState.status:type_name -> temporal.server.chasm.lib.callbacks.proto.v1.CallbackStatus + 6, // 3: temporal.server.chasm.lib.callbacks.proto.v1.CallbackState.last_attempt_complete_time:type_name -> google.protobuf.Timestamp + 7, // 4: temporal.server.chasm.lib.callbacks.proto.v1.CallbackState.last_attempt_failure:type_name -> temporal.api.failure.v1.Failure + 6, // 5: temporal.server.chasm.lib.callbacks.proto.v1.CallbackState.next_attempt_schedule_time:type_name -> google.protobuf.Timestamp + 6, // 6: temporal.server.chasm.lib.callbacks.proto.v1.CallbackState.close_time:type_name -> google.protobuf.Timestamp + 8, // 7: temporal.server.chasm.lib.callbacks.proto.v1.CallbackState.completion_schedule_to_close_timeout:type_name -> google.protobuf.Duration + 4, // 8: temporal.server.chasm.lib.callbacks.proto.v1.Callback.nexus:type_name -> temporal.server.chasm.lib.callbacks.proto.v1.Callback.Nexus + 9, // 9: temporal.server.chasm.lib.callbacks.proto.v1.Callback.links:type_name -> temporal.api.common.v1.Link + 5, // 10: temporal.server.chasm.lib.callbacks.proto.v1.Callback.Nexus.header:type_name -> temporal.server.chasm.lib.callbacks.proto.v1.Callback.Nexus.HeaderEntry + 11, // [11:11] is the sub-list for method output_type + 11, // [11:11] is the sub-list for method input_type + 11, // [11:11] is the sub-list for extension type_name + 11, // [11:11] is the sub-list for extension extendee + 0, // [0:11] is the sub-list for field type_name } func init() { file_temporal_server_chasm_lib_callback_proto_v1_message_proto_init() } diff --git a/chasm/lib/callback/gen/callbackpb/v1/request_response.go-helpers.pb.go b/chasm/lib/callback/gen/callbackpb/v1/request_response.go-helpers.pb.go new file mode 100644 index 00000000000..aae605a5967 --- /dev/null +++ b/chasm/lib/callback/gen/callbackpb/v1/request_response.go-helpers.pb.go @@ -0,0 +1,376 @@ +// Code generated by protoc-gen-go-helpers. DO NOT EDIT. +package callbackspb + +import ( + "google.golang.org/protobuf/proto" +) + +// Marshal an object of type StartCallbackExecutionRequest to the protobuf v3 wire format +func (val *StartCallbackExecutionRequest) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type StartCallbackExecutionRequest from the protobuf v3 wire format +func (val *StartCallbackExecutionRequest) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *StartCallbackExecutionRequest) Size() int { + return proto.Size(val) +} + +// Equal returns whether two StartCallbackExecutionRequest 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 *StartCallbackExecutionRequest) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *StartCallbackExecutionRequest + switch t := that.(type) { + case *StartCallbackExecutionRequest: + that1 = t + case StartCallbackExecutionRequest: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type StartCallbackExecutionResponse to the protobuf v3 wire format +func (val *StartCallbackExecutionResponse) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type StartCallbackExecutionResponse from the protobuf v3 wire format +func (val *StartCallbackExecutionResponse) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *StartCallbackExecutionResponse) Size() int { + return proto.Size(val) +} + +// Equal returns whether two StartCallbackExecutionResponse 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 *StartCallbackExecutionResponse) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *StartCallbackExecutionResponse + switch t := that.(type) { + case *StartCallbackExecutionResponse: + that1 = t + case StartCallbackExecutionResponse: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type DescribeCallbackExecutionRequest to the protobuf v3 wire format +func (val *DescribeCallbackExecutionRequest) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type DescribeCallbackExecutionRequest from the protobuf v3 wire format +func (val *DescribeCallbackExecutionRequest) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *DescribeCallbackExecutionRequest) Size() int { + return proto.Size(val) +} + +// Equal returns whether two DescribeCallbackExecutionRequest 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 *DescribeCallbackExecutionRequest) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *DescribeCallbackExecutionRequest + switch t := that.(type) { + case *DescribeCallbackExecutionRequest: + that1 = t + case DescribeCallbackExecutionRequest: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type DescribeCallbackExecutionResponse to the protobuf v3 wire format +func (val *DescribeCallbackExecutionResponse) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type DescribeCallbackExecutionResponse from the protobuf v3 wire format +func (val *DescribeCallbackExecutionResponse) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *DescribeCallbackExecutionResponse) Size() int { + return proto.Size(val) +} + +// Equal returns whether two DescribeCallbackExecutionResponse 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 *DescribeCallbackExecutionResponse) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *DescribeCallbackExecutionResponse + switch t := that.(type) { + case *DescribeCallbackExecutionResponse: + that1 = t + case DescribeCallbackExecutionResponse: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type PollCallbackExecutionRequest to the protobuf v3 wire format +func (val *PollCallbackExecutionRequest) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type PollCallbackExecutionRequest from the protobuf v3 wire format +func (val *PollCallbackExecutionRequest) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *PollCallbackExecutionRequest) Size() int { + return proto.Size(val) +} + +// Equal returns whether two PollCallbackExecutionRequest 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 *PollCallbackExecutionRequest) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *PollCallbackExecutionRequest + switch t := that.(type) { + case *PollCallbackExecutionRequest: + that1 = t + case PollCallbackExecutionRequest: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type PollCallbackExecutionResponse to the protobuf v3 wire format +func (val *PollCallbackExecutionResponse) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type PollCallbackExecutionResponse from the protobuf v3 wire format +func (val *PollCallbackExecutionResponse) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *PollCallbackExecutionResponse) Size() int { + return proto.Size(val) +} + +// Equal returns whether two PollCallbackExecutionResponse 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 *PollCallbackExecutionResponse) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *PollCallbackExecutionResponse + switch t := that.(type) { + case *PollCallbackExecutionResponse: + that1 = t + case PollCallbackExecutionResponse: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type TerminateCallbackExecutionRequest to the protobuf v3 wire format +func (val *TerminateCallbackExecutionRequest) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type TerminateCallbackExecutionRequest from the protobuf v3 wire format +func (val *TerminateCallbackExecutionRequest) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *TerminateCallbackExecutionRequest) Size() int { + return proto.Size(val) +} + +// Equal returns whether two TerminateCallbackExecutionRequest 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 *TerminateCallbackExecutionRequest) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *TerminateCallbackExecutionRequest + switch t := that.(type) { + case *TerminateCallbackExecutionRequest: + that1 = t + case TerminateCallbackExecutionRequest: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type TerminateCallbackExecutionResponse to the protobuf v3 wire format +func (val *TerminateCallbackExecutionResponse) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type TerminateCallbackExecutionResponse from the protobuf v3 wire format +func (val *TerminateCallbackExecutionResponse) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *TerminateCallbackExecutionResponse) Size() int { + return proto.Size(val) +} + +// Equal returns whether two TerminateCallbackExecutionResponse 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 *TerminateCallbackExecutionResponse) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *TerminateCallbackExecutionResponse + switch t := that.(type) { + case *TerminateCallbackExecutionResponse: + that1 = t + case TerminateCallbackExecutionResponse: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type DeleteCallbackExecutionRequest to the protobuf v3 wire format +func (val *DeleteCallbackExecutionRequest) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type DeleteCallbackExecutionRequest from the protobuf v3 wire format +func (val *DeleteCallbackExecutionRequest) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *DeleteCallbackExecutionRequest) Size() int { + return proto.Size(val) +} + +// Equal returns whether two DeleteCallbackExecutionRequest 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 *DeleteCallbackExecutionRequest) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *DeleteCallbackExecutionRequest + switch t := that.(type) { + case *DeleteCallbackExecutionRequest: + that1 = t + case DeleteCallbackExecutionRequest: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type DeleteCallbackExecutionResponse to the protobuf v3 wire format +func (val *DeleteCallbackExecutionResponse) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type DeleteCallbackExecutionResponse from the protobuf v3 wire format +func (val *DeleteCallbackExecutionResponse) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *DeleteCallbackExecutionResponse) Size() int { + return proto.Size(val) +} + +// Equal returns whether two DeleteCallbackExecutionResponse 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 *DeleteCallbackExecutionResponse) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *DeleteCallbackExecutionResponse + switch t := that.(type) { + case *DeleteCallbackExecutionResponse: + that1 = t + case DeleteCallbackExecutionResponse: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} diff --git a/chasm/lib/callback/gen/callbackpb/v1/request_response.pb.go b/chasm/lib/callback/gen/callbackpb/v1/request_response.pb.go new file mode 100644 index 00000000000..3af5bf8dc16 --- /dev/null +++ b/chasm/lib/callback/gen/callbackpb/v1/request_response.pb.go @@ -0,0 +1,617 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// plugins: +// protoc-gen-go +// protoc +// source: temporal/server/chasm/lib/callback/proto/v1/request_response.proto + +package callbackspb + +import ( + reflect "reflect" + sync "sync" + unsafe "unsafe" + + v1 "go.temporal.io/api/workflowservice/v1" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type StartCallbackExecutionRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Internal namespace ID (UUID). + NamespaceId string `protobuf:"bytes,1,opt,name=namespace_id,json=namespaceId,proto3" json:"namespace_id,omitempty"` + FrontendRequest *v1.StartCallbackExecutionRequest `protobuf:"bytes,2,opt,name=frontend_request,json=frontendRequest,proto3" json:"frontend_request,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StartCallbackExecutionRequest) Reset() { + *x = StartCallbackExecutionRequest{} + mi := &file_temporal_server_chasm_lib_callback_proto_v1_request_response_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StartCallbackExecutionRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StartCallbackExecutionRequest) ProtoMessage() {} + +func (x *StartCallbackExecutionRequest) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_callback_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 StartCallbackExecutionRequest.ProtoReflect.Descriptor instead. +func (*StartCallbackExecutionRequest) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_callback_proto_v1_request_response_proto_rawDescGZIP(), []int{0} +} + +func (x *StartCallbackExecutionRequest) GetNamespaceId() string { + if x != nil { + return x.NamespaceId + } + return "" +} + +func (x *StartCallbackExecutionRequest) GetFrontendRequest() *v1.StartCallbackExecutionRequest { + if x != nil { + return x.FrontendRequest + } + return nil +} + +type StartCallbackExecutionResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + FrontendResponse *v1.StartCallbackExecutionResponse `protobuf:"bytes,1,opt,name=frontend_response,json=frontendResponse,proto3" json:"frontend_response,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StartCallbackExecutionResponse) Reset() { + *x = StartCallbackExecutionResponse{} + mi := &file_temporal_server_chasm_lib_callback_proto_v1_request_response_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StartCallbackExecutionResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StartCallbackExecutionResponse) ProtoMessage() {} + +func (x *StartCallbackExecutionResponse) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_callback_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 StartCallbackExecutionResponse.ProtoReflect.Descriptor instead. +func (*StartCallbackExecutionResponse) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_callback_proto_v1_request_response_proto_rawDescGZIP(), []int{1} +} + +func (x *StartCallbackExecutionResponse) GetFrontendResponse() *v1.StartCallbackExecutionResponse { + if x != nil { + return x.FrontendResponse + } + return nil +} + +type DescribeCallbackExecutionRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Internal namespace ID (UUID). + NamespaceId string `protobuf:"bytes,1,opt,name=namespace_id,json=namespaceId,proto3" json:"namespace_id,omitempty"` + FrontendRequest *v1.DescribeCallbackExecutionRequest `protobuf:"bytes,2,opt,name=frontend_request,json=frontendRequest,proto3" json:"frontend_request,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DescribeCallbackExecutionRequest) Reset() { + *x = DescribeCallbackExecutionRequest{} + mi := &file_temporal_server_chasm_lib_callback_proto_v1_request_response_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DescribeCallbackExecutionRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DescribeCallbackExecutionRequest) ProtoMessage() {} + +func (x *DescribeCallbackExecutionRequest) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_callback_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 DescribeCallbackExecutionRequest.ProtoReflect.Descriptor instead. +func (*DescribeCallbackExecutionRequest) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_callback_proto_v1_request_response_proto_rawDescGZIP(), []int{2} +} + +func (x *DescribeCallbackExecutionRequest) GetNamespaceId() string { + if x != nil { + return x.NamespaceId + } + return "" +} + +func (x *DescribeCallbackExecutionRequest) GetFrontendRequest() *v1.DescribeCallbackExecutionRequest { + if x != nil { + return x.FrontendRequest + } + return nil +} + +type DescribeCallbackExecutionResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + FrontendResponse *v1.DescribeCallbackExecutionResponse `protobuf:"bytes,1,opt,name=frontend_response,json=frontendResponse,proto3" json:"frontend_response,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DescribeCallbackExecutionResponse) Reset() { + *x = DescribeCallbackExecutionResponse{} + mi := &file_temporal_server_chasm_lib_callback_proto_v1_request_response_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DescribeCallbackExecutionResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DescribeCallbackExecutionResponse) ProtoMessage() {} + +func (x *DescribeCallbackExecutionResponse) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_callback_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 DescribeCallbackExecutionResponse.ProtoReflect.Descriptor instead. +func (*DescribeCallbackExecutionResponse) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_callback_proto_v1_request_response_proto_rawDescGZIP(), []int{3} +} + +func (x *DescribeCallbackExecutionResponse) GetFrontendResponse() *v1.DescribeCallbackExecutionResponse { + if x != nil { + return x.FrontendResponse + } + return nil +} + +type PollCallbackExecutionRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Internal namespace ID (UUID). + NamespaceId string `protobuf:"bytes,1,opt,name=namespace_id,json=namespaceId,proto3" json:"namespace_id,omitempty"` + FrontendRequest *v1.PollCallbackExecutionRequest `protobuf:"bytes,2,opt,name=frontend_request,json=frontendRequest,proto3" json:"frontend_request,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PollCallbackExecutionRequest) Reset() { + *x = PollCallbackExecutionRequest{} + mi := &file_temporal_server_chasm_lib_callback_proto_v1_request_response_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PollCallbackExecutionRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PollCallbackExecutionRequest) ProtoMessage() {} + +func (x *PollCallbackExecutionRequest) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_callback_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 PollCallbackExecutionRequest.ProtoReflect.Descriptor instead. +func (*PollCallbackExecutionRequest) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_callback_proto_v1_request_response_proto_rawDescGZIP(), []int{4} +} + +func (x *PollCallbackExecutionRequest) GetNamespaceId() string { + if x != nil { + return x.NamespaceId + } + return "" +} + +func (x *PollCallbackExecutionRequest) GetFrontendRequest() *v1.PollCallbackExecutionRequest { + if x != nil { + return x.FrontendRequest + } + return nil +} + +type PollCallbackExecutionResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + FrontendResponse *v1.PollCallbackExecutionResponse `protobuf:"bytes,1,opt,name=frontend_response,json=frontendResponse,proto3" json:"frontend_response,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PollCallbackExecutionResponse) Reset() { + *x = PollCallbackExecutionResponse{} + mi := &file_temporal_server_chasm_lib_callback_proto_v1_request_response_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PollCallbackExecutionResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PollCallbackExecutionResponse) ProtoMessage() {} + +func (x *PollCallbackExecutionResponse) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_callback_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 PollCallbackExecutionResponse.ProtoReflect.Descriptor instead. +func (*PollCallbackExecutionResponse) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_callback_proto_v1_request_response_proto_rawDescGZIP(), []int{5} +} + +func (x *PollCallbackExecutionResponse) GetFrontendResponse() *v1.PollCallbackExecutionResponse { + if x != nil { + return x.FrontendResponse + } + return nil +} + +type TerminateCallbackExecutionRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Internal namespace ID (UUID). + NamespaceId string `protobuf:"bytes,1,opt,name=namespace_id,json=namespaceId,proto3" json:"namespace_id,omitempty"` + FrontendRequest *v1.TerminateCallbackExecutionRequest `protobuf:"bytes,2,opt,name=frontend_request,json=frontendRequest,proto3" json:"frontend_request,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TerminateCallbackExecutionRequest) Reset() { + *x = TerminateCallbackExecutionRequest{} + mi := &file_temporal_server_chasm_lib_callback_proto_v1_request_response_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TerminateCallbackExecutionRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TerminateCallbackExecutionRequest) ProtoMessage() {} + +func (x *TerminateCallbackExecutionRequest) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_callback_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 TerminateCallbackExecutionRequest.ProtoReflect.Descriptor instead. +func (*TerminateCallbackExecutionRequest) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_callback_proto_v1_request_response_proto_rawDescGZIP(), []int{6} +} + +func (x *TerminateCallbackExecutionRequest) GetNamespaceId() string { + if x != nil { + return x.NamespaceId + } + return "" +} + +func (x *TerminateCallbackExecutionRequest) GetFrontendRequest() *v1.TerminateCallbackExecutionRequest { + if x != nil { + return x.FrontendRequest + } + return nil +} + +type TerminateCallbackExecutionResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + FrontendResponse *v1.TerminateCallbackExecutionResponse `protobuf:"bytes,1,opt,name=frontend_response,json=frontendResponse,proto3" json:"frontend_response,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TerminateCallbackExecutionResponse) Reset() { + *x = TerminateCallbackExecutionResponse{} + mi := &file_temporal_server_chasm_lib_callback_proto_v1_request_response_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TerminateCallbackExecutionResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TerminateCallbackExecutionResponse) ProtoMessage() {} + +func (x *TerminateCallbackExecutionResponse) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_callback_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 TerminateCallbackExecutionResponse.ProtoReflect.Descriptor instead. +func (*TerminateCallbackExecutionResponse) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_callback_proto_v1_request_response_proto_rawDescGZIP(), []int{7} +} + +func (x *TerminateCallbackExecutionResponse) GetFrontendResponse() *v1.TerminateCallbackExecutionResponse { + if x != nil { + return x.FrontendResponse + } + return nil +} + +type DeleteCallbackExecutionRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Internal namespace ID (UUID). + NamespaceId string `protobuf:"bytes,1,opt,name=namespace_id,json=namespaceId,proto3" json:"namespace_id,omitempty"` + FrontendRequest *v1.DeleteCallbackExecutionRequest `protobuf:"bytes,2,opt,name=frontend_request,json=frontendRequest,proto3" json:"frontend_request,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteCallbackExecutionRequest) Reset() { + *x = DeleteCallbackExecutionRequest{} + mi := &file_temporal_server_chasm_lib_callback_proto_v1_request_response_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteCallbackExecutionRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteCallbackExecutionRequest) ProtoMessage() {} + +func (x *DeleteCallbackExecutionRequest) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_callback_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 DeleteCallbackExecutionRequest.ProtoReflect.Descriptor instead. +func (*DeleteCallbackExecutionRequest) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_callback_proto_v1_request_response_proto_rawDescGZIP(), []int{8} +} + +func (x *DeleteCallbackExecutionRequest) GetNamespaceId() string { + if x != nil { + return x.NamespaceId + } + return "" +} + +func (x *DeleteCallbackExecutionRequest) GetFrontendRequest() *v1.DeleteCallbackExecutionRequest { + if x != nil { + return x.FrontendRequest + } + return nil +} + +type DeleteCallbackExecutionResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + FrontendResponse *v1.DeleteCallbackExecutionResponse `protobuf:"bytes,1,opt,name=frontend_response,json=frontendResponse,proto3" json:"frontend_response,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteCallbackExecutionResponse) Reset() { + *x = DeleteCallbackExecutionResponse{} + mi := &file_temporal_server_chasm_lib_callback_proto_v1_request_response_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteCallbackExecutionResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteCallbackExecutionResponse) ProtoMessage() {} + +func (x *DeleteCallbackExecutionResponse) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_callback_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 DeleteCallbackExecutionResponse.ProtoReflect.Descriptor instead. +func (*DeleteCallbackExecutionResponse) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_callback_proto_v1_request_response_proto_rawDescGZIP(), []int{9} +} + +func (x *DeleteCallbackExecutionResponse) GetFrontendResponse() *v1.DeleteCallbackExecutionResponse { + if x != nil { + return x.FrontendResponse + } + return nil +} + +var File_temporal_server_chasm_lib_callback_proto_v1_request_response_proto protoreflect.FileDescriptor + +const file_temporal_server_chasm_lib_callback_proto_v1_request_response_proto_rawDesc = "" + + "\n" + + "Btemporal/server/chasm/lib/callback/proto/v1/request_response.proto\x12,temporal.server.chasm.lib.callbacks.proto.v1\x1a6temporal/api/workflowservice/v1/request_response.proto\"\xad\x01\n" + + "\x1dStartCallbackExecutionRequest\x12!\n" + + "\fnamespace_id\x18\x01 \x01(\tR\vnamespaceId\x12i\n" + + "\x10frontend_request\x18\x02 \x01(\v2>.temporal.api.workflowservice.v1.StartCallbackExecutionRequestR\x0ffrontendRequest\"\x8e\x01\n" + + "\x1eStartCallbackExecutionResponse\x12l\n" + + "\x11frontend_response\x18\x01 \x01(\v2?.temporal.api.workflowservice.v1.StartCallbackExecutionResponseR\x10frontendResponse\"\xb3\x01\n" + + " DescribeCallbackExecutionRequest\x12!\n" + + "\fnamespace_id\x18\x01 \x01(\tR\vnamespaceId\x12l\n" + + "\x10frontend_request\x18\x02 \x01(\v2A.temporal.api.workflowservice.v1.DescribeCallbackExecutionRequestR\x0ffrontendRequest\"\x94\x01\n" + + "!DescribeCallbackExecutionResponse\x12o\n" + + "\x11frontend_response\x18\x01 \x01(\v2B.temporal.api.workflowservice.v1.DescribeCallbackExecutionResponseR\x10frontendResponse\"\xab\x01\n" + + "\x1cPollCallbackExecutionRequest\x12!\n" + + "\fnamespace_id\x18\x01 \x01(\tR\vnamespaceId\x12h\n" + + "\x10frontend_request\x18\x02 \x01(\v2=.temporal.api.workflowservice.v1.PollCallbackExecutionRequestR\x0ffrontendRequest\"\x8c\x01\n" + + "\x1dPollCallbackExecutionResponse\x12k\n" + + "\x11frontend_response\x18\x01 \x01(\v2>.temporal.api.workflowservice.v1.PollCallbackExecutionResponseR\x10frontendResponse\"\xb5\x01\n" + + "!TerminateCallbackExecutionRequest\x12!\n" + + "\fnamespace_id\x18\x01 \x01(\tR\vnamespaceId\x12m\n" + + "\x10frontend_request\x18\x02 \x01(\v2B.temporal.api.workflowservice.v1.TerminateCallbackExecutionRequestR\x0ffrontendRequest\"\x96\x01\n" + + "\"TerminateCallbackExecutionResponse\x12p\n" + + "\x11frontend_response\x18\x01 \x01(\v2C.temporal.api.workflowservice.v1.TerminateCallbackExecutionResponseR\x10frontendResponse\"\xaf\x01\n" + + "\x1eDeleteCallbackExecutionRequest\x12!\n" + + "\fnamespace_id\x18\x01 \x01(\tR\vnamespaceId\x12j\n" + + "\x10frontend_request\x18\x02 \x01(\v2?.temporal.api.workflowservice.v1.DeleteCallbackExecutionRequestR\x0ffrontendRequest\"\x90\x01\n" + + "\x1fDeleteCallbackExecutionResponse\x12m\n" + + "\x11frontend_response\x18\x01 \x01(\v2@.temporal.api.workflowservice.v1.DeleteCallbackExecutionResponseR\x10frontendResponseBGZEgo.temporal.io/server/chasm/lib/callbacks/gen/callbackspb;callbackspbb\x06proto3" + +var ( + file_temporal_server_chasm_lib_callback_proto_v1_request_response_proto_rawDescOnce sync.Once + file_temporal_server_chasm_lib_callback_proto_v1_request_response_proto_rawDescData []byte +) + +func file_temporal_server_chasm_lib_callback_proto_v1_request_response_proto_rawDescGZIP() []byte { + file_temporal_server_chasm_lib_callback_proto_v1_request_response_proto_rawDescOnce.Do(func() { + file_temporal_server_chasm_lib_callback_proto_v1_request_response_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_temporal_server_chasm_lib_callback_proto_v1_request_response_proto_rawDesc), len(file_temporal_server_chasm_lib_callback_proto_v1_request_response_proto_rawDesc))) + }) + return file_temporal_server_chasm_lib_callback_proto_v1_request_response_proto_rawDescData +} + +var file_temporal_server_chasm_lib_callback_proto_v1_request_response_proto_msgTypes = make([]protoimpl.MessageInfo, 10) +var file_temporal_server_chasm_lib_callback_proto_v1_request_response_proto_goTypes = []any{ + (*StartCallbackExecutionRequest)(nil), // 0: temporal.server.chasm.lib.callbacks.proto.v1.StartCallbackExecutionRequest + (*StartCallbackExecutionResponse)(nil), // 1: temporal.server.chasm.lib.callbacks.proto.v1.StartCallbackExecutionResponse + (*DescribeCallbackExecutionRequest)(nil), // 2: temporal.server.chasm.lib.callbacks.proto.v1.DescribeCallbackExecutionRequest + (*DescribeCallbackExecutionResponse)(nil), // 3: temporal.server.chasm.lib.callbacks.proto.v1.DescribeCallbackExecutionResponse + (*PollCallbackExecutionRequest)(nil), // 4: temporal.server.chasm.lib.callbacks.proto.v1.PollCallbackExecutionRequest + (*PollCallbackExecutionResponse)(nil), // 5: temporal.server.chasm.lib.callbacks.proto.v1.PollCallbackExecutionResponse + (*TerminateCallbackExecutionRequest)(nil), // 6: temporal.server.chasm.lib.callbacks.proto.v1.TerminateCallbackExecutionRequest + (*TerminateCallbackExecutionResponse)(nil), // 7: temporal.server.chasm.lib.callbacks.proto.v1.TerminateCallbackExecutionResponse + (*DeleteCallbackExecutionRequest)(nil), // 8: temporal.server.chasm.lib.callbacks.proto.v1.DeleteCallbackExecutionRequest + (*DeleteCallbackExecutionResponse)(nil), // 9: temporal.server.chasm.lib.callbacks.proto.v1.DeleteCallbackExecutionResponse + (*v1.StartCallbackExecutionRequest)(nil), // 10: temporal.api.workflowservice.v1.StartCallbackExecutionRequest + (*v1.StartCallbackExecutionResponse)(nil), // 11: temporal.api.workflowservice.v1.StartCallbackExecutionResponse + (*v1.DescribeCallbackExecutionRequest)(nil), // 12: temporal.api.workflowservice.v1.DescribeCallbackExecutionRequest + (*v1.DescribeCallbackExecutionResponse)(nil), // 13: temporal.api.workflowservice.v1.DescribeCallbackExecutionResponse + (*v1.PollCallbackExecutionRequest)(nil), // 14: temporal.api.workflowservice.v1.PollCallbackExecutionRequest + (*v1.PollCallbackExecutionResponse)(nil), // 15: temporal.api.workflowservice.v1.PollCallbackExecutionResponse + (*v1.TerminateCallbackExecutionRequest)(nil), // 16: temporal.api.workflowservice.v1.TerminateCallbackExecutionRequest + (*v1.TerminateCallbackExecutionResponse)(nil), // 17: temporal.api.workflowservice.v1.TerminateCallbackExecutionResponse + (*v1.DeleteCallbackExecutionRequest)(nil), // 18: temporal.api.workflowservice.v1.DeleteCallbackExecutionRequest + (*v1.DeleteCallbackExecutionResponse)(nil), // 19: temporal.api.workflowservice.v1.DeleteCallbackExecutionResponse +} +var file_temporal_server_chasm_lib_callback_proto_v1_request_response_proto_depIdxs = []int32{ + 10, // 0: temporal.server.chasm.lib.callbacks.proto.v1.StartCallbackExecutionRequest.frontend_request:type_name -> temporal.api.workflowservice.v1.StartCallbackExecutionRequest + 11, // 1: temporal.server.chasm.lib.callbacks.proto.v1.StartCallbackExecutionResponse.frontend_response:type_name -> temporal.api.workflowservice.v1.StartCallbackExecutionResponse + 12, // 2: temporal.server.chasm.lib.callbacks.proto.v1.DescribeCallbackExecutionRequest.frontend_request:type_name -> temporal.api.workflowservice.v1.DescribeCallbackExecutionRequest + 13, // 3: temporal.server.chasm.lib.callbacks.proto.v1.DescribeCallbackExecutionResponse.frontend_response:type_name -> temporal.api.workflowservice.v1.DescribeCallbackExecutionResponse + 14, // 4: temporal.server.chasm.lib.callbacks.proto.v1.PollCallbackExecutionRequest.frontend_request:type_name -> temporal.api.workflowservice.v1.PollCallbackExecutionRequest + 15, // 5: temporal.server.chasm.lib.callbacks.proto.v1.PollCallbackExecutionResponse.frontend_response:type_name -> temporal.api.workflowservice.v1.PollCallbackExecutionResponse + 16, // 6: temporal.server.chasm.lib.callbacks.proto.v1.TerminateCallbackExecutionRequest.frontend_request:type_name -> temporal.api.workflowservice.v1.TerminateCallbackExecutionRequest + 17, // 7: temporal.server.chasm.lib.callbacks.proto.v1.TerminateCallbackExecutionResponse.frontend_response:type_name -> temporal.api.workflowservice.v1.TerminateCallbackExecutionResponse + 18, // 8: temporal.server.chasm.lib.callbacks.proto.v1.DeleteCallbackExecutionRequest.frontend_request:type_name -> temporal.api.workflowservice.v1.DeleteCallbackExecutionRequest + 19, // 9: temporal.server.chasm.lib.callbacks.proto.v1.DeleteCallbackExecutionResponse.frontend_response:type_name -> temporal.api.workflowservice.v1.DeleteCallbackExecutionResponse + 10, // [10:10] is the sub-list for method output_type + 10, // [10:10] is the sub-list for method input_type + 10, // [10:10] is the sub-list for extension type_name + 10, // [10:10] is the sub-list for extension extendee + 0, // [0:10] is the sub-list for field type_name +} + +func init() { file_temporal_server_chasm_lib_callback_proto_v1_request_response_proto_init() } +func file_temporal_server_chasm_lib_callback_proto_v1_request_response_proto_init() { + if File_temporal_server_chasm_lib_callback_proto_v1_request_response_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_callback_proto_v1_request_response_proto_rawDesc), len(file_temporal_server_chasm_lib_callback_proto_v1_request_response_proto_rawDesc)), + NumEnums: 0, + NumMessages: 10, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_temporal_server_chasm_lib_callback_proto_v1_request_response_proto_goTypes, + DependencyIndexes: file_temporal_server_chasm_lib_callback_proto_v1_request_response_proto_depIdxs, + MessageInfos: file_temporal_server_chasm_lib_callback_proto_v1_request_response_proto_msgTypes, + }.Build() + File_temporal_server_chasm_lib_callback_proto_v1_request_response_proto = out.File + file_temporal_server_chasm_lib_callback_proto_v1_request_response_proto_goTypes = nil + file_temporal_server_chasm_lib_callback_proto_v1_request_response_proto_depIdxs = nil +} diff --git a/chasm/lib/callback/gen/callbackpb/v1/service.pb.go b/chasm/lib/callback/gen/callbackpb/v1/service.pb.go new file mode 100644 index 00000000000..2abe4351309 --- /dev/null +++ b/chasm/lib/callback/gen/callbackpb/v1/service.pb.go @@ -0,0 +1,90 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// plugins: +// protoc-gen-go +// protoc +// source: temporal/server/chasm/lib/callback/proto/v1/service.proto + +package callbackspb + +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_callback_proto_v1_service_proto protoreflect.FileDescriptor + +const file_temporal_server_chasm_lib_callback_proto_v1_service_proto_rawDesc = "" + + "\n" + + "9temporal/server/chasm/lib/callback/proto/v1/service.proto\x12,temporal.server.chasm.lib.callbacks.proto.v1\x1aBtemporal/server/chasm/lib/callback/proto/v1/request_response.proto\x1a0temporal/server/api/common/v1/api_category.proto\x1a.temporal/server/api/routing/v1/extension.proto2\x86\t\n" + + "\x0fCallbackService\x12\xdd\x01\n" + + "\x16StartCallbackExecution\x12K.temporal.server.chasm.lib.callbacks.proto.v1.StartCallbackExecutionRequest\x1aL.temporal.server.chasm.lib.callbacks.proto.v1.StartCallbackExecutionResponse\"(\x8a\xb5\x18\x02\b\x01\xd2\xc3\x18\x1e\x1a\x1cfrontend_request.callback_id\x12\xe6\x01\n" + + "\x19DescribeCallbackExecution\x12N.temporal.server.chasm.lib.callbacks.proto.v1.DescribeCallbackExecutionRequest\x1aO.temporal.server.chasm.lib.callbacks.proto.v1.DescribeCallbackExecutionResponse\"(\x8a\xb5\x18\x02\b\x01\xd2\xc3\x18\x1e\x1a\x1cfrontend_request.callback_id\x12\xda\x01\n" + + "\x15PollCallbackExecution\x12J.temporal.server.chasm.lib.callbacks.proto.v1.PollCallbackExecutionRequest\x1aK.temporal.server.chasm.lib.callbacks.proto.v1.PollCallbackExecutionResponse\"(\x8a\xb5\x18\x02\b\x02\xd2\xc3\x18\x1e\x1a\x1cfrontend_request.callback_id\x12\xe9\x01\n" + + "\x1aTerminateCallbackExecution\x12O.temporal.server.chasm.lib.callbacks.proto.v1.TerminateCallbackExecutionRequest\x1aP.temporal.server.chasm.lib.callbacks.proto.v1.TerminateCallbackExecutionResponse\"(\x8a\xb5\x18\x02\b\x01\xd2\xc3\x18\x1e\x1a\x1cfrontend_request.callback_id\x12\xe0\x01\n" + + "\x17DeleteCallbackExecution\x12L.temporal.server.chasm.lib.callbacks.proto.v1.DeleteCallbackExecutionRequest\x1aM.temporal.server.chasm.lib.callbacks.proto.v1.DeleteCallbackExecutionResponse\"(\x8a\xb5\x18\x02\b\x01\xd2\xc3\x18\x1e\x1a\x1cfrontend_request.callback_idBGZEgo.temporal.io/server/chasm/lib/callbacks/gen/callbackspb;callbackspbb\x06proto3" + +var file_temporal_server_chasm_lib_callback_proto_v1_service_proto_goTypes = []any{ + (*StartCallbackExecutionRequest)(nil), // 0: temporal.server.chasm.lib.callbacks.proto.v1.StartCallbackExecutionRequest + (*DescribeCallbackExecutionRequest)(nil), // 1: temporal.server.chasm.lib.callbacks.proto.v1.DescribeCallbackExecutionRequest + (*PollCallbackExecutionRequest)(nil), // 2: temporal.server.chasm.lib.callbacks.proto.v1.PollCallbackExecutionRequest + (*TerminateCallbackExecutionRequest)(nil), // 3: temporal.server.chasm.lib.callbacks.proto.v1.TerminateCallbackExecutionRequest + (*DeleteCallbackExecutionRequest)(nil), // 4: temporal.server.chasm.lib.callbacks.proto.v1.DeleteCallbackExecutionRequest + (*StartCallbackExecutionResponse)(nil), // 5: temporal.server.chasm.lib.callbacks.proto.v1.StartCallbackExecutionResponse + (*DescribeCallbackExecutionResponse)(nil), // 6: temporal.server.chasm.lib.callbacks.proto.v1.DescribeCallbackExecutionResponse + (*PollCallbackExecutionResponse)(nil), // 7: temporal.server.chasm.lib.callbacks.proto.v1.PollCallbackExecutionResponse + (*TerminateCallbackExecutionResponse)(nil), // 8: temporal.server.chasm.lib.callbacks.proto.v1.TerminateCallbackExecutionResponse + (*DeleteCallbackExecutionResponse)(nil), // 9: temporal.server.chasm.lib.callbacks.proto.v1.DeleteCallbackExecutionResponse +} +var file_temporal_server_chasm_lib_callback_proto_v1_service_proto_depIdxs = []int32{ + 0, // 0: temporal.server.chasm.lib.callbacks.proto.v1.CallbackService.StartCallbackExecution:input_type -> temporal.server.chasm.lib.callbacks.proto.v1.StartCallbackExecutionRequest + 1, // 1: temporal.server.chasm.lib.callbacks.proto.v1.CallbackService.DescribeCallbackExecution:input_type -> temporal.server.chasm.lib.callbacks.proto.v1.DescribeCallbackExecutionRequest + 2, // 2: temporal.server.chasm.lib.callbacks.proto.v1.CallbackService.PollCallbackExecution:input_type -> temporal.server.chasm.lib.callbacks.proto.v1.PollCallbackExecutionRequest + 3, // 3: temporal.server.chasm.lib.callbacks.proto.v1.CallbackService.TerminateCallbackExecution:input_type -> temporal.server.chasm.lib.callbacks.proto.v1.TerminateCallbackExecutionRequest + 4, // 4: temporal.server.chasm.lib.callbacks.proto.v1.CallbackService.DeleteCallbackExecution:input_type -> temporal.server.chasm.lib.callbacks.proto.v1.DeleteCallbackExecutionRequest + 5, // 5: temporal.server.chasm.lib.callbacks.proto.v1.CallbackService.StartCallbackExecution:output_type -> temporal.server.chasm.lib.callbacks.proto.v1.StartCallbackExecutionResponse + 6, // 6: temporal.server.chasm.lib.callbacks.proto.v1.CallbackService.DescribeCallbackExecution:output_type -> temporal.server.chasm.lib.callbacks.proto.v1.DescribeCallbackExecutionResponse + 7, // 7: temporal.server.chasm.lib.callbacks.proto.v1.CallbackService.PollCallbackExecution:output_type -> temporal.server.chasm.lib.callbacks.proto.v1.PollCallbackExecutionResponse + 8, // 8: temporal.server.chasm.lib.callbacks.proto.v1.CallbackService.TerminateCallbackExecution:output_type -> temporal.server.chasm.lib.callbacks.proto.v1.TerminateCallbackExecutionResponse + 9, // 9: temporal.server.chasm.lib.callbacks.proto.v1.CallbackService.DeleteCallbackExecution:output_type -> temporal.server.chasm.lib.callbacks.proto.v1.DeleteCallbackExecutionResponse + 5, // [5:10] is the sub-list for method output_type + 0, // [0:5] 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_callback_proto_v1_service_proto_init() } +func file_temporal_server_chasm_lib_callback_proto_v1_service_proto_init() { + if File_temporal_server_chasm_lib_callback_proto_v1_service_proto != nil { + return + } + file_temporal_server_chasm_lib_callback_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_callback_proto_v1_service_proto_rawDesc), len(file_temporal_server_chasm_lib_callback_proto_v1_service_proto_rawDesc)), + NumEnums: 0, + NumMessages: 0, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_temporal_server_chasm_lib_callback_proto_v1_service_proto_goTypes, + DependencyIndexes: file_temporal_server_chasm_lib_callback_proto_v1_service_proto_depIdxs, + }.Build() + File_temporal_server_chasm_lib_callback_proto_v1_service_proto = out.File + file_temporal_server_chasm_lib_callback_proto_v1_service_proto_goTypes = nil + file_temporal_server_chasm_lib_callback_proto_v1_service_proto_depIdxs = nil +} diff --git a/chasm/lib/callback/gen/callbackpb/v1/service_client.pb.go b/chasm/lib/callback/gen/callbackpb/v1/service_client.pb.go new file mode 100644 index 00000000000..7cba8c99a73 --- /dev/null +++ b/chasm/lib/callback/gen/callbackpb/v1/service_client.pb.go @@ -0,0 +1,275 @@ +// Code generated by protoc-gen-go-chasm. DO NOT EDIT. +package callbackspb + +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" + "google.golang.org/grpc" +) + +// CallbackServiceLayeredClient is a client for CallbackService. +type CallbackServiceLayeredClient struct { + metricsHandler metrics.Handler + numShards int32 + redirector history.Redirector[CallbackServiceClient] + retryPolicy backoff.RetryPolicy +} + +// NewCallbackServiceLayeredClient initializes a new CallbackServiceLayeredClient. +func NewCallbackServiceLayeredClient( + dc *dynamicconfig.Collection, + rpcFactory common.RPCFactory, + monitor membership.Monitor, + config *config.Persistence, + logger log.Logger, + metricsHandler metrics.Handler, +) (CallbackServiceClient, error) { + resolver, err := monitor.GetResolver(primitives.HistoryService) + if err != nil { + return nil, err + } + connections := history.NewConnectionPool(resolver, rpcFactory, NewCallbackServiceClient) + var redirector history.Redirector[CallbackServiceClient] + if dynamicconfig.HistoryClientOwnershipCachingEnabled.Get(dc)() { + redirector = history.NewCachingRedirector( + connections, + resolver, + logger, + dynamicconfig.HistoryClientOwnershipCachingStaleTTL.Get(dc), + ) + } else { + redirector = history.NewBasicRedirector(connections, resolver) + } + return &CallbackServiceLayeredClient{ + metricsHandler: metricsHandler, + redirector: redirector, + numShards: config.NumHistoryShards, + retryPolicy: common.CreateHistoryClientRetryPolicy(), + }, nil +} +func (c *CallbackServiceLayeredClient) callStartCallbackExecutionNoRetry( + ctx context.Context, + request *StartCallbackExecutionRequest, + opts ...grpc.CallOption, +) (*StartCallbackExecutionResponse, error) { + var response *StartCallbackExecutionResponse + 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("CallbackService.StartCallbackExecution"), + 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().GetCallbackId(), c.numShards) + op := func(ctx context.Context, client CallbackServiceClient) error { + var err error + ctx, cancel := context.WithTimeout(ctx, history.DefaultTimeout) + defer cancel() + response, err = client.StartCallbackExecution(ctx, request, opts...) + return err + } + err = c.redirector.Execute(ctx, shardID, op) + return response, err +} +func (c *CallbackServiceLayeredClient) StartCallbackExecution( + ctx context.Context, + request *StartCallbackExecutionRequest, + opts ...grpc.CallOption, +) (*StartCallbackExecutionResponse, error) { + call := func(ctx context.Context) (*StartCallbackExecutionResponse, error) { + return c.callStartCallbackExecutionNoRetry(ctx, request, opts...) + } + return backoff.ThrottleRetryContextWithReturn(ctx, call, c.retryPolicy, common.IsServiceClientTransientError) +} +func (c *CallbackServiceLayeredClient) callDescribeCallbackExecutionNoRetry( + ctx context.Context, + request *DescribeCallbackExecutionRequest, + opts ...grpc.CallOption, +) (*DescribeCallbackExecutionResponse, error) { + var response *DescribeCallbackExecutionResponse + 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("CallbackService.DescribeCallbackExecution"), + 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().GetCallbackId(), c.numShards) + op := func(ctx context.Context, client CallbackServiceClient) error { + var err error + ctx, cancel := context.WithTimeout(ctx, history.DefaultTimeout) + defer cancel() + response, err = client.DescribeCallbackExecution(ctx, request, opts...) + return err + } + err = c.redirector.Execute(ctx, shardID, op) + return response, err +} +func (c *CallbackServiceLayeredClient) DescribeCallbackExecution( + ctx context.Context, + request *DescribeCallbackExecutionRequest, + opts ...grpc.CallOption, +) (*DescribeCallbackExecutionResponse, error) { + call := func(ctx context.Context) (*DescribeCallbackExecutionResponse, error) { + return c.callDescribeCallbackExecutionNoRetry(ctx, request, opts...) + } + return backoff.ThrottleRetryContextWithReturn(ctx, call, c.retryPolicy, common.IsServiceClientTransientError) +} +func (c *CallbackServiceLayeredClient) callPollCallbackExecutionNoRetry( + ctx context.Context, + request *PollCallbackExecutionRequest, + opts ...grpc.CallOption, +) (*PollCallbackExecutionResponse, error) { + var response *PollCallbackExecutionResponse + 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("CallbackService.PollCallbackExecution"), + 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().GetCallbackId(), c.numShards) + op := func(ctx context.Context, client CallbackServiceClient) error { + var err error + ctx, cancel := context.WithTimeout(ctx, history.DefaultTimeout) + defer cancel() + response, err = client.PollCallbackExecution(ctx, request, opts...) + return err + } + err = c.redirector.Execute(ctx, shardID, op) + return response, err +} +func (c *CallbackServiceLayeredClient) PollCallbackExecution( + ctx context.Context, + request *PollCallbackExecutionRequest, + opts ...grpc.CallOption, +) (*PollCallbackExecutionResponse, error) { + call := func(ctx context.Context) (*PollCallbackExecutionResponse, error) { + return c.callPollCallbackExecutionNoRetry(ctx, request, opts...) + } + return backoff.ThrottleRetryContextWithReturn(ctx, call, c.retryPolicy, common.IsServiceClientTransientError) +} +func (c *CallbackServiceLayeredClient) callTerminateCallbackExecutionNoRetry( + ctx context.Context, + request *TerminateCallbackExecutionRequest, + opts ...grpc.CallOption, +) (*TerminateCallbackExecutionResponse, error) { + var response *TerminateCallbackExecutionResponse + 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("CallbackService.TerminateCallbackExecution"), + 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().GetCallbackId(), c.numShards) + op := func(ctx context.Context, client CallbackServiceClient) error { + var err error + ctx, cancel := context.WithTimeout(ctx, history.DefaultTimeout) + defer cancel() + response, err = client.TerminateCallbackExecution(ctx, request, opts...) + return err + } + err = c.redirector.Execute(ctx, shardID, op) + return response, err +} +func (c *CallbackServiceLayeredClient) TerminateCallbackExecution( + ctx context.Context, + request *TerminateCallbackExecutionRequest, + opts ...grpc.CallOption, +) (*TerminateCallbackExecutionResponse, error) { + call := func(ctx context.Context) (*TerminateCallbackExecutionResponse, error) { + return c.callTerminateCallbackExecutionNoRetry(ctx, request, opts...) + } + return backoff.ThrottleRetryContextWithReturn(ctx, call, c.retryPolicy, common.IsServiceClientTransientError) +} +func (c *CallbackServiceLayeredClient) callDeleteCallbackExecutionNoRetry( + ctx context.Context, + request *DeleteCallbackExecutionRequest, + opts ...grpc.CallOption, +) (*DeleteCallbackExecutionResponse, error) { + var response *DeleteCallbackExecutionResponse + 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("CallbackService.DeleteCallbackExecution"), + 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().GetCallbackId(), c.numShards) + op := func(ctx context.Context, client CallbackServiceClient) error { + var err error + ctx, cancel := context.WithTimeout(ctx, history.DefaultTimeout) + defer cancel() + response, err = client.DeleteCallbackExecution(ctx, request, opts...) + return err + } + err = c.redirector.Execute(ctx, shardID, op) + return response, err +} +func (c *CallbackServiceLayeredClient) DeleteCallbackExecution( + ctx context.Context, + request *DeleteCallbackExecutionRequest, + opts ...grpc.CallOption, +) (*DeleteCallbackExecutionResponse, error) { + call := func(ctx context.Context) (*DeleteCallbackExecutionResponse, error) { + return c.callDeleteCallbackExecutionNoRetry(ctx, request, opts...) + } + return backoff.ThrottleRetryContextWithReturn(ctx, call, c.retryPolicy, common.IsServiceClientTransientError) +} diff --git a/chasm/lib/callback/gen/callbackpb/v1/service_grpc.pb.go b/chasm/lib/callback/gen/callbackpb/v1/service_grpc.pb.go new file mode 100644 index 00000000000..5323ef5842d --- /dev/null +++ b/chasm/lib/callback/gen/callbackpb/v1/service_grpc.pb.go @@ -0,0 +1,258 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// plugins: +// - protoc-gen-go-grpc +// - protoc +// source: temporal/server/chasm/lib/callback/proto/v1/service.proto + +package callbackspb + +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 ( + CallbackService_StartCallbackExecution_FullMethodName = "/temporal.server.chasm.lib.callbacks.proto.v1.CallbackService/StartCallbackExecution" + CallbackService_DescribeCallbackExecution_FullMethodName = "/temporal.server.chasm.lib.callbacks.proto.v1.CallbackService/DescribeCallbackExecution" + CallbackService_PollCallbackExecution_FullMethodName = "/temporal.server.chasm.lib.callbacks.proto.v1.CallbackService/PollCallbackExecution" + CallbackService_TerminateCallbackExecution_FullMethodName = "/temporal.server.chasm.lib.callbacks.proto.v1.CallbackService/TerminateCallbackExecution" + CallbackService_DeleteCallbackExecution_FullMethodName = "/temporal.server.chasm.lib.callbacks.proto.v1.CallbackService/DeleteCallbackExecution" +) + +// CallbackServiceClient is the client API for CallbackService 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 CallbackServiceClient interface { + StartCallbackExecution(ctx context.Context, in *StartCallbackExecutionRequest, opts ...grpc.CallOption) (*StartCallbackExecutionResponse, error) + DescribeCallbackExecution(ctx context.Context, in *DescribeCallbackExecutionRequest, opts ...grpc.CallOption) (*DescribeCallbackExecutionResponse, error) + PollCallbackExecution(ctx context.Context, in *PollCallbackExecutionRequest, opts ...grpc.CallOption) (*PollCallbackExecutionResponse, error) + TerminateCallbackExecution(ctx context.Context, in *TerminateCallbackExecutionRequest, opts ...grpc.CallOption) (*TerminateCallbackExecutionResponse, error) + DeleteCallbackExecution(ctx context.Context, in *DeleteCallbackExecutionRequest, opts ...grpc.CallOption) (*DeleteCallbackExecutionResponse, error) +} + +type callbackServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewCallbackServiceClient(cc grpc.ClientConnInterface) CallbackServiceClient { + return &callbackServiceClient{cc} +} + +func (c *callbackServiceClient) StartCallbackExecution(ctx context.Context, in *StartCallbackExecutionRequest, opts ...grpc.CallOption) (*StartCallbackExecutionResponse, error) { + out := new(StartCallbackExecutionResponse) + err := c.cc.Invoke(ctx, CallbackService_StartCallbackExecution_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *callbackServiceClient) DescribeCallbackExecution(ctx context.Context, in *DescribeCallbackExecutionRequest, opts ...grpc.CallOption) (*DescribeCallbackExecutionResponse, error) { + out := new(DescribeCallbackExecutionResponse) + err := c.cc.Invoke(ctx, CallbackService_DescribeCallbackExecution_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *callbackServiceClient) PollCallbackExecution(ctx context.Context, in *PollCallbackExecutionRequest, opts ...grpc.CallOption) (*PollCallbackExecutionResponse, error) { + out := new(PollCallbackExecutionResponse) + err := c.cc.Invoke(ctx, CallbackService_PollCallbackExecution_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *callbackServiceClient) TerminateCallbackExecution(ctx context.Context, in *TerminateCallbackExecutionRequest, opts ...grpc.CallOption) (*TerminateCallbackExecutionResponse, error) { + out := new(TerminateCallbackExecutionResponse) + err := c.cc.Invoke(ctx, CallbackService_TerminateCallbackExecution_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *callbackServiceClient) DeleteCallbackExecution(ctx context.Context, in *DeleteCallbackExecutionRequest, opts ...grpc.CallOption) (*DeleteCallbackExecutionResponse, error) { + out := new(DeleteCallbackExecutionResponse) + err := c.cc.Invoke(ctx, CallbackService_DeleteCallbackExecution_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// CallbackServiceServer is the server API for CallbackService service. +// All implementations must embed UnimplementedCallbackServiceServer +// for forward compatibility +type CallbackServiceServer interface { + StartCallbackExecution(context.Context, *StartCallbackExecutionRequest) (*StartCallbackExecutionResponse, error) + DescribeCallbackExecution(context.Context, *DescribeCallbackExecutionRequest) (*DescribeCallbackExecutionResponse, error) + PollCallbackExecution(context.Context, *PollCallbackExecutionRequest) (*PollCallbackExecutionResponse, error) + TerminateCallbackExecution(context.Context, *TerminateCallbackExecutionRequest) (*TerminateCallbackExecutionResponse, error) + DeleteCallbackExecution(context.Context, *DeleteCallbackExecutionRequest) (*DeleteCallbackExecutionResponse, error) + mustEmbedUnimplementedCallbackServiceServer() +} + +// UnimplementedCallbackServiceServer must be embedded to have forward compatible implementations. +type UnimplementedCallbackServiceServer struct { +} + +func (UnimplementedCallbackServiceServer) StartCallbackExecution(context.Context, *StartCallbackExecutionRequest) (*StartCallbackExecutionResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method StartCallbackExecution not implemented") +} +func (UnimplementedCallbackServiceServer) DescribeCallbackExecution(context.Context, *DescribeCallbackExecutionRequest) (*DescribeCallbackExecutionResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method DescribeCallbackExecution not implemented") +} +func (UnimplementedCallbackServiceServer) PollCallbackExecution(context.Context, *PollCallbackExecutionRequest) (*PollCallbackExecutionResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method PollCallbackExecution not implemented") +} +func (UnimplementedCallbackServiceServer) TerminateCallbackExecution(context.Context, *TerminateCallbackExecutionRequest) (*TerminateCallbackExecutionResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method TerminateCallbackExecution not implemented") +} +func (UnimplementedCallbackServiceServer) DeleteCallbackExecution(context.Context, *DeleteCallbackExecutionRequest) (*DeleteCallbackExecutionResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method DeleteCallbackExecution not implemented") +} +func (UnimplementedCallbackServiceServer) mustEmbedUnimplementedCallbackServiceServer() {} + +// UnsafeCallbackServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to CallbackServiceServer will +// result in compilation errors. +type UnsafeCallbackServiceServer interface { + mustEmbedUnimplementedCallbackServiceServer() +} + +func RegisterCallbackServiceServer(s grpc.ServiceRegistrar, srv CallbackServiceServer) { + s.RegisterService(&CallbackService_ServiceDesc, srv) +} + +func _CallbackService_StartCallbackExecution_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(StartCallbackExecutionRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(CallbackServiceServer).StartCallbackExecution(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: CallbackService_StartCallbackExecution_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(CallbackServiceServer).StartCallbackExecution(ctx, req.(*StartCallbackExecutionRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _CallbackService_DescribeCallbackExecution_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DescribeCallbackExecutionRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(CallbackServiceServer).DescribeCallbackExecution(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: CallbackService_DescribeCallbackExecution_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(CallbackServiceServer).DescribeCallbackExecution(ctx, req.(*DescribeCallbackExecutionRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _CallbackService_PollCallbackExecution_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(PollCallbackExecutionRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(CallbackServiceServer).PollCallbackExecution(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: CallbackService_PollCallbackExecution_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(CallbackServiceServer).PollCallbackExecution(ctx, req.(*PollCallbackExecutionRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _CallbackService_TerminateCallbackExecution_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(TerminateCallbackExecutionRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(CallbackServiceServer).TerminateCallbackExecution(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: CallbackService_TerminateCallbackExecution_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(CallbackServiceServer).TerminateCallbackExecution(ctx, req.(*TerminateCallbackExecutionRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _CallbackService_DeleteCallbackExecution_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DeleteCallbackExecutionRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(CallbackServiceServer).DeleteCallbackExecution(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: CallbackService_DeleteCallbackExecution_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(CallbackServiceServer).DeleteCallbackExecution(ctx, req.(*DeleteCallbackExecutionRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// CallbackService_ServiceDesc is the grpc.ServiceDesc for CallbackService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var CallbackService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "temporal.server.chasm.lib.callbacks.proto.v1.CallbackService", + HandlerType: (*CallbackServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "StartCallbackExecution", + Handler: _CallbackService_StartCallbackExecution_Handler, + }, + { + MethodName: "DescribeCallbackExecution", + Handler: _CallbackService_DescribeCallbackExecution_Handler, + }, + { + MethodName: "PollCallbackExecution", + Handler: _CallbackService_PollCallbackExecution_Handler, + }, + { + MethodName: "TerminateCallbackExecution", + Handler: _CallbackService_TerminateCallbackExecution_Handler, + }, + { + MethodName: "DeleteCallbackExecution", + Handler: _CallbackService_DeleteCallbackExecution_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "temporal/server/chasm/lib/callback/proto/v1/service.proto", +} diff --git a/chasm/lib/callback/gen/callbackpb/v1/tasks.go-helpers.pb.go b/chasm/lib/callback/gen/callbackpb/v1/tasks.go-helpers.pb.go index a0181447c66..7c0fcca665b 100644 --- a/chasm/lib/callback/gen/callbackpb/v1/tasks.go-helpers.pb.go +++ b/chasm/lib/callback/gen/callbackpb/v1/tasks.go-helpers.pb.go @@ -78,3 +78,40 @@ func (this *BackoffTask) Equal(that interface{}) bool { return proto.Equal(this, that1) } + +// Marshal an object of type CompletionScheduleToCloseTimeoutTask to the protobuf v3 wire format +func (val *CompletionScheduleToCloseTimeoutTask) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type CompletionScheduleToCloseTimeoutTask from the protobuf v3 wire format +func (val *CompletionScheduleToCloseTimeoutTask) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *CompletionScheduleToCloseTimeoutTask) Size() int { + return proto.Size(val) +} + +// Equal returns whether two CompletionScheduleToCloseTimeoutTask 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 *CompletionScheduleToCloseTimeoutTask) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *CompletionScheduleToCloseTimeoutTask + switch t := that.(type) { + case *CompletionScheduleToCloseTimeoutTask: + that1 = t + case CompletionScheduleToCloseTimeoutTask: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} diff --git a/chasm/lib/callback/gen/callbackpb/v1/tasks.pb.go b/chasm/lib/callback/gen/callbackpb/v1/tasks.pb.go index 7354a359c8d..33e29da618d 100644 --- a/chasm/lib/callback/gen/callbackpb/v1/tasks.pb.go +++ b/chasm/lib/callback/gen/callbackpb/v1/tasks.pb.go @@ -112,6 +112,43 @@ func (x *BackoffTask) GetAttempt() int32 { return 0 } +// Fired when the callback completion's schedule-to-close timeout expires. +type CompletionScheduleToCloseTimeoutTask struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CompletionScheduleToCloseTimeoutTask) Reset() { + *x = CompletionScheduleToCloseTimeoutTask{} + mi := &file_temporal_server_chasm_lib_callback_proto_v1_tasks_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CompletionScheduleToCloseTimeoutTask) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CompletionScheduleToCloseTimeoutTask) ProtoMessage() {} + +func (x *CompletionScheduleToCloseTimeoutTask) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_callback_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 CompletionScheduleToCloseTimeoutTask.ProtoReflect.Descriptor instead. +func (*CompletionScheduleToCloseTimeoutTask) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_callback_proto_v1_tasks_proto_rawDescGZIP(), []int{2} +} + var File_temporal_server_chasm_lib_callback_proto_v1_tasks_proto protoreflect.FileDescriptor const file_temporal_server_chasm_lib_callback_proto_v1_tasks_proto_rawDesc = "" + @@ -120,7 +157,8 @@ const file_temporal_server_chasm_lib_callback_proto_v1_tasks_proto_rawDesc = "" "\x0eInvocationTask\x12\x18\n" + "\aattempt\x18\x01 \x01(\x05R\aattempt\"'\n" + "\vBackoffTask\x12\x18\n" + - "\aattempt\x18\x01 \x01(\x05R\aattemptBGZEgo.temporal.io/server/chasm/lib/callbacks/gen/callbackspb;callbackspbb\x06proto3" + "\aattempt\x18\x01 \x01(\x05R\aattempt\"&\n" + + "$CompletionScheduleToCloseTimeoutTaskBGZEgo.temporal.io/server/chasm/lib/callbacks/gen/callbackspb;callbackspbb\x06proto3" var ( file_temporal_server_chasm_lib_callback_proto_v1_tasks_proto_rawDescOnce sync.Once @@ -134,10 +172,11 @@ func file_temporal_server_chasm_lib_callback_proto_v1_tasks_proto_rawDescGZIP() return file_temporal_server_chasm_lib_callback_proto_v1_tasks_proto_rawDescData } -var file_temporal_server_chasm_lib_callback_proto_v1_tasks_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_temporal_server_chasm_lib_callback_proto_v1_tasks_proto_msgTypes = make([]protoimpl.MessageInfo, 3) var file_temporal_server_chasm_lib_callback_proto_v1_tasks_proto_goTypes = []any{ - (*InvocationTask)(nil), // 0: temporal.server.chasm.lib.callbacks.proto.v1.InvocationTask - (*BackoffTask)(nil), // 1: temporal.server.chasm.lib.callbacks.proto.v1.BackoffTask + (*InvocationTask)(nil), // 0: temporal.server.chasm.lib.callbacks.proto.v1.InvocationTask + (*BackoffTask)(nil), // 1: temporal.server.chasm.lib.callbacks.proto.v1.BackoffTask + (*CompletionScheduleToCloseTimeoutTask)(nil), // 2: temporal.server.chasm.lib.callbacks.proto.v1.CompletionScheduleToCloseTimeoutTask } var file_temporal_server_chasm_lib_callback_proto_v1_tasks_proto_depIdxs = []int32{ 0, // [0:0] is the sub-list for method output_type @@ -158,7 +197,7 @@ func file_temporal_server_chasm_lib_callback_proto_v1_tasks_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_temporal_server_chasm_lib_callback_proto_v1_tasks_proto_rawDesc), len(file_temporal_server_chasm_lib_callback_proto_v1_tasks_proto_rawDesc)), NumEnums: 0, - NumMessages: 2, + NumMessages: 3, NumExtensions: 0, NumServices: 0, }, diff --git a/chasm/lib/callback/handler.go b/chasm/lib/callback/handler.go new file mode 100644 index 00000000000..b70824a19df --- /dev/null +++ b/chasm/lib/callback/handler.go @@ -0,0 +1,352 @@ +package callback + +import ( + "context" + "errors" + "fmt" + + callbackpb "go.temporal.io/api/callback/v1" + commonpb "go.temporal.io/api/common/v1" + "go.temporal.io/api/serviceerror" + "go.temporal.io/api/workflowservice/v1" + "go.temporal.io/server/chasm" + callbackspb "go.temporal.io/server/chasm/lib/callback/gen/callbackpb/v1" + "go.temporal.io/server/common/contextutil" + "go.temporal.io/server/common/log" + "google.golang.org/protobuf/types/known/durationpb" + "google.golang.org/protobuf/types/known/timestamppb" +) + +type callbackHandler struct { + callbackspb.UnimplementedCallbackServiceServer + + config *Config + logger log.Logger +} + +func newCallbackHandler(config *Config, logger log.Logger) *callbackHandler { + return &callbackHandler{ + config: config, + logger: logger, + } +} + +func (h *callbackHandler) StartCallbackExecution( + ctx context.Context, + req *callbackspb.StartCallbackExecutionRequest, +) (resp *callbackspb.StartCallbackExecutionResponse, err error) { + frontendReq := req.FrontendRequest + + // Gather all the data necessary to create the Callback component. + input := &createStandaloneCallbackInput{ + CallbackID: frontendReq.GetCallbackId(), + RequestID: frontendReq.GetRequestId(), + CompletionScheduleToCloseTimeout: frontendReq.GetScheduleToCloseTimeout(), + Completion: frontendReq.GetCompletion(), + SearchAttributes: frontendReq.GetSearchAttributes().GetIndexedFields(), + } + + // Convert the API Callback to internal Callback proto. + if nexusCb := frontendReq.GetCallback().GetNexus(); nexusCb != nil { + input.Callback = &callbackspb.Callback{ + Variant: &callbackspb.Callback_Nexus_{ + Nexus: &callbackspb.Callback_Nexus{ + Url: nexusCb.GetUrl(), + Header: nexusCb.GetHeader(), + Token: nexusCb.GetToken(), + }, + }, + } + } + + // Create the CHASM Callback in so-called "standalone" mode, where it will be the root + // of the CHASM execution. + result, err := chasm.StartExecution( + ctx, + chasm.ExecutionKey{ + NamespaceID: req.NamespaceId, + BusinessID: frontendReq.GetCallbackId(), + }, + createStandaloneCallback, + input, + chasm.WithRequestID(frontendReq.GetRequestId()), + // Relying on these default policies. No configuration knobs are exposed to users. + chasm.WithBusinessIDPolicy( + chasm.BusinessIDReusePolicyAllowDuplicate, + chasm.BusinessIDConflictPolicyFail, + ), + ) + + // Like Workflow IDs, the Callback ID can be reused. But only one Callback with a given Callback ID + // can be executing at a given time. + var alreadyStartedErr *chasm.ExecutionAlreadyStartedError + if errors.As(err, &alreadyStartedErr) { + svcErr := serviceerror.NewCallbackExecutionAlreadyStarted( + "callback execution already started", + alreadyStartedErr.CurrentRequestID, + alreadyStartedErr.CurrentRunID, + frontendReq.GetCallbackId(), + ) + return nil, svcErr + } + if err != nil { + return nil, err + } + + return &callbackspb.StartCallbackExecutionResponse{ + FrontendResponse: &workflowservice.StartCallbackExecutionResponse{ + RunId: result.ExecutionKey.RunID, + }, + }, nil +} + +func (h *callbackHandler) DescribeCallbackExecution( + ctx context.Context, + req *callbackspb.DescribeCallbackExecutionRequest, +) (*callbackspb.DescribeCallbackExecutionResponse, error) { + + // Build the DescribeCallbackExecution proto. Closes over the req object. + buildDescriptionProto := func( + ctx chasm.Context, + c *Callback, + ) (*callbackspb.DescribeCallbackExecutionResponse, error) { + info, err := c.Describe(ctx) + if err != nil { + return nil, err + } + resp := &workflowservice.DescribeCallbackExecutionResponse{ + Info: info, + } + + if req.FrontendRequest.GetIncludeInput() { + resp.Input = c.SuppliedCompletion.Get(ctx) + } + if req.FrontendRequest.GetIncludeOutcome() { + resp.Outcome = c.Outcome(ctx) + } + + return &callbackspb.DescribeCallbackExecutionResponse{ + FrontendResponse: resp, + }, nil + } + + compRef := chasm.NewComponentRef[*Callback]( + chasm.ExecutionKey{ + NamespaceID: req.GetNamespaceId(), + BusinessID: req.FrontendRequest.GetCallbackId(), + RunID: req.FrontendRequest.GetRunId(), + }, + ) + + // Simple case. If no long-poll token is supplied, we just read and return + // the persisted state. + token := req.GetFrontendRequest().GetLongPollToken() + if len(token) == 0 { + return chasm.ReadComponent( + ctx, + compRef, + func( + c *Callback, + ctx chasm.Context, + req *callbackspb.DescribeCallbackExecutionRequest) (*callbackspb.DescribeCallbackExecutionResponse, error) { + return buildDescriptionProto(ctx, c) + }, + req) + } + + // Below, we send an empty non-error response on context deadline expiry. Here we compute a + // deadline that causes us to send that response before the caller's own deadline (see + // chasm.activity.longPollBuffer). We also cap the caller's deadline at + // chasm.activity.longPollTimeout. + targetNamespace := req.GetFrontendRequest().GetNamespace() + ctx, cancel := contextutil.WithDeadlineBuffer( + ctx, + h.config.LongPollTimeout(targetNamespace), + h.config.LongPollBuffer(targetNamespace), + ) + defer cancel() + + longpollReadFn := func( + c *Callback, + ctx chasm.Context, + req *callbackspb.DescribeCallbackExecutionRequest) (*callbackspb.DescribeCallbackExecutionResponse, bool, error) { + changed, err := chasm.ExecutionStateChanged(c, ctx, token) + if err != nil { + if errors.Is(err, chasm.ErrMalformedComponentRef) { + return nil, false, serviceerror.NewInvalidArgument("invalid long poll token") + } + if errors.Is(err, chasm.ErrInvalidComponentRef) { + return nil, false, serviceerror.NewInvalidArgument("long poll token does not match execution") + } + return nil, false, err + } + if changed { + response, err := buildDescriptionProto(ctx, c) + return response, true, err + } + return nil, false, nil + } + + // Now begin the polling, using our supplied reader. + response, _, err := chasm.PollComponent(ctx, compRef, longpollReadFn, req) + if err != nil && ctx.Err() != nil { + // Send empty non-error response on deadline expiry: caller should continue long-polling. + return &callbackspb.DescribeCallbackExecutionResponse{ + FrontendResponse: &workflowservice.DescribeCallbackExecutionResponse{}, + }, nil + } + return response, err +} + +func (h *callbackHandler) PollCallbackExecution( + ctx context.Context, + req *callbackspb.PollCallbackExecutionRequest, +) (resp *callbackspb.PollCallbackExecutionResponse, err error) { + + ref := chasm.NewComponentRef[*Callback]( + chasm.ExecutionKey{ + NamespaceID: req.NamespaceId, + BusinessID: req.FrontendRequest.GetCallbackId(), + RunID: req.FrontendRequest.GetRunId(), + }, + ) + + ns := req.FrontendRequest.GetNamespace() + ctx, cancel := contextutil.WithDeadlineBuffer( + ctx, + h.config.LongPollTimeout(ns), + h.config.LongPollBuffer(ns), + ) + defer cancel() + + resp, _, err = chasm.PollComponent(ctx, ref, func( + c *Callback, + ctx chasm.Context, + _ *callbackspb.PollCallbackExecutionRequest, + ) (*callbackspb.PollCallbackExecutionResponse, bool, error) { + if !c.LifecycleState(ctx).IsClosed() { + return nil, false, nil + } + return &callbackspb.PollCallbackExecutionResponse{ + FrontendResponse: &workflowservice.PollCallbackExecutionResponse{ + RunId: ctx.ExecutionKey().RunID, + Outcome: c.Outcome(ctx), + }, + }, true, nil + }, req) + + if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) { + // Send an empty non-error response as an invitation to resubmit the long-poll. + return &callbackspb.PollCallbackExecutionResponse{ + FrontendResponse: &workflowservice.PollCallbackExecutionResponse{}, + }, nil + } + return resp, err +} + +func (h *callbackHandler) TerminateCallbackExecution( + ctx context.Context, + req *callbackspb.TerminateCallbackExecutionRequest, +) (resp *callbackspb.TerminateCallbackExecutionResponse, err error) { + + resp, _, err = chasm.UpdateComponent( + ctx, + chasm.NewComponentRef[*Callback]( + chasm.ExecutionKey{ + NamespaceID: req.NamespaceId, + BusinessID: req.FrontendRequest.GetCallbackId(), + RunID: req.FrontendRequest.GetRunId(), + }, + ), + func(c *Callback, ctx chasm.MutableContext, _ *callbackspb.TerminateCallbackExecutionRequest) (*callbackspb.TerminateCallbackExecutionResponse, error) { + if _, err := c.Terminate(ctx, chasm.TerminateComponentRequest{ + Reason: req.FrontendRequest.GetReason(), + RequestID: req.FrontendRequest.GetRequestId(), + }); err != nil { + return nil, err + } + return &callbackspb.TerminateCallbackExecutionResponse{ + FrontendResponse: &workflowservice.TerminateCallbackExecutionResponse{}, + }, nil + }, + req, + ) + return resp, err +} + +func (h *callbackHandler) DeleteCallbackExecution( + ctx context.Context, + req *callbackspb.DeleteCallbackExecutionRequest, +) (resp *callbackspb.DeleteCallbackExecutionResponse, err error) { + + if err = chasm.DeleteExecution[*Callback]( + ctx, + chasm.ExecutionKey{ + NamespaceID: req.NamespaceId, + BusinessID: req.FrontendRequest.GetCallbackId(), + RunID: req.FrontendRequest.GetRunId(), + }, + chasm.DeleteExecutionRequest{ + TerminateComponentRequest: chasm.TerminateComponentRequest{ + Reason: "deleted", + }, + }, + ); err != nil { + return nil, err + } + + return &callbackspb.DeleteCallbackExecutionResponse{ + FrontendResponse: &workflowservice.DeleteCallbackExecutionResponse{}, + }, nil +} + +// createStandaloneCallbackInput is the bundle of inputs to the CHASM execution. +type createStandaloneCallbackInput struct { + RequestID string + Callback *callbackspb.Callback + CallbackID string + CompletionScheduleToCloseTimeout *durationpb.Duration + Completion *callbackpb.CallbackExecutionCompletion + SearchAttributes map[string]*commonpb.Payload +} + +// createStandaloneCallback constructs a new Callback component in "standalone" mode. +// The Callback is immediately transitioned to SCHEDULED state to begin invocation. +func createStandaloneCallback( + ctx chasm.MutableContext, + input *createStandaloneCallbackInput, +) (*Callback, error) { + now := timestamppb.Now() + + // Create child Callback component. + opts := newStandaloneCallbackOpts{ + RequestID: input.RequestID, + RegistrationTime: now, + Callback: input.Callback, + + CallbackID: input.CallbackID, + CompletionScheduleToCloseTimeout: input.CompletionScheduleToCloseTimeout, + Completion: input.Completion, + SearchAttributes: input.SearchAttributes, + } + cb := newStandaloneCallback(ctx, opts) + + // Immediately schedule the callback for invocation. + if err := TransitionScheduled.Apply(cb, ctx, EventScheduled{}); err != nil { + return nil, fmt.Errorf("failed to schedule callback: %w", err) + } + + // Schedule the timeout as applicable. + if durationProto := input.CompletionScheduleToCloseTimeout; durationProto != nil { + if duration := durationProto.AsDuration(); duration > 0 { + timeoutTime := now.AsTime().Add(duration) + ctx.AddTask( + cb, + chasm.TaskAttributes{ScheduledTime: timeoutTime}, + &callbackspb.CompletionScheduleToCloseTimeoutTask{}, + ) + } + } + + return cb, nil +} diff --git a/chasm/lib/callback/invocable_internal.go b/chasm/lib/callback/invocable_internal.go index 273c0a38634..75f465236ae 100644 --- a/chasm/lib/callback/invocable_internal.go +++ b/chasm/lib/callback/invocable_internal.go @@ -58,13 +58,14 @@ func (c invocableInternal) Invoke( task *callbackspb.InvocationTask, taskAttr chasm.TaskAttributes, ) invocationResult { - header := nexus.Header(c.callback.GetHeader()) - if header == nil { - header = nexus.Header{} + // Get the token from the dedicated Token field, falling back to the header for backwards compat. + encodedRef := c.callback.GetToken() + if encodedRef == "" { + header := nexus.Header(c.callback.GetHeader()) + if header != nil { + encodedRef = header.Get(commonnexus.CallbackTokenHeader) + } } - - // Get back the base64-encoded ComponentRef from the header. - encodedRef := header.Get(commonnexus.CallbackTokenHeader) if encodedRef == "" { return invocationResultFail{logInternalError(h.logger, "callback missing token", nil)} } diff --git a/chasm/lib/callback/invocable_outbound.go b/chasm/lib/callback/invocable_outbound.go index 6ed0a65d6dc..ccac789e159 100644 --- a/chasm/lib/callback/invocable_outbound.go +++ b/chasm/lib/callback/invocable_outbound.go @@ -15,6 +15,7 @@ import ( "go.temporal.io/server/common/namespace" commonnexus "go.temporal.io/server/common/nexus" "go.temporal.io/server/common/nexus/nexusrpc" + "go.temporal.io/server/components/nexusoperations" queuescommon "go.temporal.io/server/service/history/queues/common" queueserrors "go.temporal.io/server/service/history/queues/errors" ) @@ -28,7 +29,22 @@ type invocableOutbound struct { attempt int32 } +func (n invocableOutbound) isSystemCallback() bool { + c := n.callback + if c == nil { + return false + } + return c.Url == commonnexus.SystemCallbackURL +} + func (n invocableOutbound) WrapError(result invocationResult, err error) error { + // If the error is due to a completion of a Nexus operation being delivered before the + // operation has officially started, we want to avoid triggering the circuit breakers. + // Since the actual destination is working fine, and the failure is due to a data race. + if errors.Is(err, nexusoperations.ErrOperationNotStarted) { + return err + } + if retry, ok := result.(invocationResultRetry); ok { return queueserrors.NewDestinationDownError(retry.err.Error(), err) } @@ -66,8 +82,17 @@ func (n invocableOutbound) Invoke( }) // Make the call and record metrics. startTime := time.Now() - n.completion.Header = n.callback.Header + + // If the outbound call is to a standalone callback, then supply the Nexus + // operation's token in the request. + if n.callback.GetToken() != "" { + if n.completion.Header == nil { + n.completion.Header = nexus.Header{} + } + n.completion.Header.Set(commonnexus.CallbackTokenHeader, n.callback.GetToken()) + } + err := client.CompleteOperation(ctx, n.callback.Url, n.completion) namespaceTag := metrics.NamespaceTag(ns.Name().String()) @@ -79,6 +104,15 @@ func (n invocableOutbound) Invoke( if err != nil { retryable := isRetryableCallError(err) h.logger.Error("Callback request failed", tag.Error(err), tag.Bool("retryable", retryable)) + + // If the error from trying to resolve a Nexus operation that hasn't yet been marked + // as started, it is safe to retry. (n.WrapError will ensure repeated failures of this + // kind won't cause the SystemCallback to trip the circuit breaker.) + isErrNotStarted := errors.Is(err, nexusoperations.ErrOperationNotStarted) + if n.isSystemCallback() && isErrNotStarted { + retryable = true + } + if retryable { return invocationResultRetry{err} } diff --git a/chasm/lib/callback/library.go b/chasm/lib/callback/library.go index 838a88e1608..90785feb9f0 100644 --- a/chasm/lib/callback/library.go +++ b/chasm/lib/callback/library.go @@ -2,42 +2,60 @@ package callback import ( "go.temporal.io/server/chasm" + callbackspb "go.temporal.io/server/chasm/lib/callback/gen/callbackpb/v1" + "go.temporal.io/server/common/namespace" "google.golang.org/grpc" ) -type ( - Library struct { - chasm.UnimplementedLibrary - - InvocationTaskHandler *invocationTaskHandler - BackoffTaskHandler *backoffTaskHandler - } -) +// componentOnlyLibrary only containing the component definitions, but no implementation details. +type componentOnlyLibrary struct { + chasm.UnimplementedLibrary +} -func newLibrary( - InvocationTaskHandler *invocationTaskHandler, - BackoffTaskHandler *backoffTaskHandler, -) *Library { - return &Library{ - InvocationTaskHandler: InvocationTaskHandler, - BackoffTaskHandler: BackoffTaskHandler, - } +func newComponentOnlyLibrary(config *Config, namespaceRegistry namespace.Registry) *componentOnlyLibrary { + return &componentOnlyLibrary{} } -func (l *Library) Name() string { +func (l *componentOnlyLibrary) Name() string { return chasm.CallbackLibraryName } -func (l *Library) Components() []*chasm.RegistrableComponent { +func (l *componentOnlyLibrary) Components() []*chasm.RegistrableComponent { return []*chasm.RegistrableComponent{ chasm.NewRegistrableComponent[*Callback]( chasm.CallbackComponentName, chasm.WithDetached(), + chasm.WithBusinessIDAlias("CallbackId"), + chasm.WithSearchAttributes(executionStatusSearchAttribute), ), } } -func (l *Library) Tasks() []*chasm.RegistrableTask { +type library struct { + componentOnlyLibrary + + config *Config + InvocationTaskHandler *invocationTaskHandler + BackoffTaskHandler *backoffTaskHandler + CompletionScheduleToCloseTimeoutTaskHandler *CompletionScheduleToCloseTimeoutTaskHandler + callbackSvcHandler *callbackHandler +} + +func newLibrary( + InvocationTaskHandler *invocationTaskHandler, + BackoffTaskHandler *backoffTaskHandler, + CompletionScheduleToCloseTimeoutTaskHandler *CompletionScheduleToCloseTimeoutTaskHandler, + callbackSvcHandler *callbackHandler, +) *library { + return &library{ + InvocationTaskHandler: InvocationTaskHandler, + BackoffTaskHandler: BackoffTaskHandler, + CompletionScheduleToCloseTimeoutTaskHandler: CompletionScheduleToCloseTimeoutTaskHandler, + callbackSvcHandler: callbackSvcHandler, + } +} + +func (l *library) Tasks() []*chasm.RegistrableTask { return []*chasm.RegistrableTask{ chasm.NewRegistrableSideEffectTask( "invoke", @@ -47,8 +65,13 @@ func (l *Library) Tasks() []*chasm.RegistrableTask { "backoff", l.BackoffTaskHandler, ), + chasm.NewRegistrablePureTask( + "completionScheduleToCloseTimer", + l.CompletionScheduleToCloseTimeoutTaskHandler, + ), } } -func (l *Library) RegisterServices(server *grpc.Server) { +func (l *library) RegisterServices(server *grpc.Server) { + callbackspb.RegisterCallbackServiceServer(server, l.callbackSvcHandler) } diff --git a/chasm/lib/callback/proto/v1/message.proto b/chasm/lib/callback/proto/v1/message.proto index 057e5c470e0..b6d78a6c764 100644 --- a/chasm/lib/callback/proto/v1/message.proto +++ b/chasm/lib/callback/proto/v1/message.proto @@ -2,6 +2,7 @@ syntax = "proto3"; package temporal.server.chasm.lib.callbacks.proto.v1; +import "google/protobuf/duration.proto"; import "google/protobuf/timestamp.proto"; import "temporal/api/common/v1/message.proto"; import "temporal/api/failure/v1/message.proto"; @@ -33,6 +34,19 @@ message CallbackState { // Request ID that added the callback. string request_id = 9; + // Request ID that terminated the callback, if applicable. Used for idempotency. + string terminate_request_id = 10; + + // The time when the callback reached a terminal state. + google.protobuf.Timestamp close_time = 11; + + // (standalone only) User-supplied business ID set when StartCallbackExecution() is + // called. Used to identify the callback for operations like Describe- or Terminate-. + string callback_id = 12; + + // (standalone only) Schedule-to-close timeout from when StartCallbackExecution() + // is called to when the result gets delivered. + google.protobuf.Duration completion_schedule_to_close_timeout = 13; } // Status of a callback. @@ -49,6 +63,8 @@ enum CallbackStatus { CALLBACK_STATUS_FAILED = 4; // Callback has succeeded. CALLBACK_STATUS_SUCCEEDED = 5; + // Callback was terminated by request. + CALLBACK_STATUS_TERMINATED = 6; } message Callback { @@ -59,6 +75,8 @@ message Callback { string url = 1; // Header to attach to callback request. map header = 2; + // Token identifying the target callback to resolve. + string token = 3; } reserved 1; // For a generic callback mechanism to be added later. diff --git a/chasm/lib/callback/proto/v1/request_response.proto b/chasm/lib/callback/proto/v1/request_response.proto new file mode 100644 index 00000000000..75de45252e7 --- /dev/null +++ b/chasm/lib/callback/proto/v1/request_response.proto @@ -0,0 +1,62 @@ +syntax = "proto3"; + +package temporal.server.chasm.lib.callbacks.proto.v1; + +import "temporal/api/workflowservice/v1/request_response.proto"; + +option go_package = "go.temporal.io/server/chasm/lib/callbacks/gen/callbackspb;callbackspb"; + +message StartCallbackExecutionRequest { + // Internal namespace ID (UUID). + string namespace_id = 1; + + temporal.api.workflowservice.v1.StartCallbackExecutionRequest frontend_request = 2; +} + +message StartCallbackExecutionResponse { + temporal.api.workflowservice.v1.StartCallbackExecutionResponse frontend_response = 1; +} + +message DescribeCallbackExecutionRequest { + // Internal namespace ID (UUID). + string namespace_id = 1; + + temporal.api.workflowservice.v1.DescribeCallbackExecutionRequest frontend_request = 2; +} + +message DescribeCallbackExecutionResponse { + temporal.api.workflowservice.v1.DescribeCallbackExecutionResponse frontend_response = 1; +} + +message PollCallbackExecutionRequest { + // Internal namespace ID (UUID). + string namespace_id = 1; + + temporal.api.workflowservice.v1.PollCallbackExecutionRequest frontend_request = 2; +} + +message PollCallbackExecutionResponse { + temporal.api.workflowservice.v1.PollCallbackExecutionResponse frontend_response = 1; +} + +message TerminateCallbackExecutionRequest { + // Internal namespace ID (UUID). + string namespace_id = 1; + + temporal.api.workflowservice.v1.TerminateCallbackExecutionRequest frontend_request = 2; +} + +message TerminateCallbackExecutionResponse { + temporal.api.workflowservice.v1.TerminateCallbackExecutionResponse frontend_response = 1; +} + +message DeleteCallbackExecutionRequest { + // Internal namespace ID (UUID). + string namespace_id = 1; + + temporal.api.workflowservice.v1.DeleteCallbackExecutionRequest frontend_request = 2; +} + +message DeleteCallbackExecutionResponse { + temporal.api.workflowservice.v1.DeleteCallbackExecutionResponse frontend_response = 1; +} diff --git a/chasm/lib/callback/proto/v1/service.proto b/chasm/lib/callback/proto/v1/service.proto new file mode 100644 index 00000000000..1413d10ac4b --- /dev/null +++ b/chasm/lib/callback/proto/v1/service.proto @@ -0,0 +1,36 @@ +syntax = "proto3"; + +package temporal.server.chasm.lib.callbacks.proto.v1; + +import "chasm/lib/callback/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/callbacks/gen/callbackspb;callbackspb"; + +service CallbackService { + rpc StartCallbackExecution(StartCallbackExecutionRequest) returns (StartCallbackExecutionResponse) { + option (temporal.server.api.routing.v1.routing).business_id = "frontend_request.callback_id"; + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + rpc DescribeCallbackExecution(DescribeCallbackExecutionRequest) returns (DescribeCallbackExecutionResponse) { + option (temporal.server.api.routing.v1.routing).business_id = "frontend_request.callback_id"; + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + rpc PollCallbackExecution(PollCallbackExecutionRequest) returns (PollCallbackExecutionResponse) { + option (temporal.server.api.routing.v1.routing).business_id = "frontend_request.callback_id"; + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_LONG_POLL; + } + + rpc TerminateCallbackExecution(TerminateCallbackExecutionRequest) returns (TerminateCallbackExecutionResponse) { + option (temporal.server.api.routing.v1.routing).business_id = "frontend_request.callback_id"; + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + rpc DeleteCallbackExecution(DeleteCallbackExecutionRequest) returns (DeleteCallbackExecutionResponse) { + option (temporal.server.api.routing.v1.routing).business_id = "frontend_request.callback_id"; + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } +} diff --git a/chasm/lib/callback/proto/v1/tasks.proto b/chasm/lib/callback/proto/v1/tasks.proto index f4cb65faa6a..dde93a730f9 100644 --- a/chasm/lib/callback/proto/v1/tasks.proto +++ b/chasm/lib/callback/proto/v1/tasks.proto @@ -13,3 +13,6 @@ message BackoffTask { // The attempt number for this invocation. int32 attempt = 1; } + +// Fired when the callback completion's schedule-to-close timeout expires. +message CompletionScheduleToCloseTimeoutTask {} diff --git a/chasm/lib/callback/statemachine.go b/chasm/lib/callback/statemachine.go index 779b9773989..1d587658864 100644 --- a/chasm/lib/callback/statemachine.go +++ b/chasm/lib/callback/statemachine.go @@ -5,6 +5,7 @@ import ( "net/url" "time" + enumspb "go.temporal.io/api/enums/v1" failurepb "go.temporal.io/api/failure/v1" "go.temporal.io/server/chasm" callbackspb "go.temporal.io/server/chasm/lib/callback/gen/callbackpb/v1" @@ -37,7 +38,7 @@ var TransitionRescheduled = chasm.NewTransition( callbackspb.CALLBACK_STATUS_SCHEDULED, func(cb *Callback, ctx chasm.MutableContext, event EventRescheduled) error { cb.NextAttemptScheduleTime = nil - u, err := url.Parse(cb.Callback.GetNexus().Url) + u, err := url.Parse(cb.Callback.GetNexus().GetUrl()) if err != nil { return fmt.Errorf("failed to parse URL: %v: %w", cb.Callback, err) } @@ -61,7 +62,10 @@ var TransitionAttemptFailed = chasm.NewTransition( []callbackspb.CallbackStatus{callbackspb.CALLBACK_STATUS_SCHEDULED}, callbackspb.CALLBACK_STATUS_BACKING_OFF, func(cb *Callback, ctx chasm.MutableContext, event EventAttemptFailed) error { - cb.recordAttempt(event.Time) + now := ctx.Now(cb) + cb.recordAttempt(now) + cb.CloseTime = timestamppb.New(now) + // Use 0 for elapsed time as we don't limit the retry by time (for now). nextDelay := event.RetryPolicy.ComputeNextDelay(0, int(cb.Attempt), event.Err) nextAttemptScheduleTime := event.Time.Add(nextDelay) @@ -93,8 +97,11 @@ var TransitionFailed = chasm.NewTransition( []callbackspb.CallbackStatus{callbackspb.CALLBACK_STATUS_SCHEDULED}, callbackspb.CALLBACK_STATUS_FAILED, func(cb *Callback, ctx chasm.MutableContext, event EventFailed) error { - cb.recordAttempt(event.Time) - cb.LastAttemptFailure = &failurepb.Failure{ + now := ctx.Now(cb) + cb.recordAttempt(now) + cb.CloseTime = timestamppb.New(now) + + failure := &failurepb.Failure{ Message: event.Err.Error(), FailureInfo: &failurepb.Failure_ApplicationFailureInfo{ ApplicationFailureInfo: &failurepb.ApplicationFailureInfo{ @@ -102,6 +109,9 @@ var TransitionFailed = chasm.NewTransition( }, }, } + cb.LastAttemptFailure = failure + cb.TerminalFailure = chasm.NewDataField(ctx, failure) + return nil }, ) @@ -115,8 +125,69 @@ var TransitionSucceeded = chasm.NewTransition( []callbackspb.CallbackStatus{callbackspb.CALLBACK_STATUS_SCHEDULED}, callbackspb.CALLBACK_STATUS_SUCCEEDED, func(cb *Callback, ctx chasm.MutableContext, event EventSucceeded) error { - cb.recordAttempt(event.Time) + now := ctx.Now(cb) + cb.recordAttempt(now) cb.LastAttemptFailure = nil + cb.TerminalFailure = chasm.NewDataField[*failurepb.Failure](ctx, nil) + return nil + }, +) + +// EventTerminated is triggered when the callback is forcefully terminated. +type EventTerminated struct { + Reason string +} + +var TransitionTerminated = chasm.NewTransition( + []callbackspb.CallbackStatus{ + callbackspb.CALLBACK_STATUS_STANDBY, + callbackspb.CALLBACK_STATUS_SCHEDULED, + callbackspb.CALLBACK_STATUS_BACKING_OFF, + }, + callbackspb.CALLBACK_STATUS_TERMINATED, + func(cb *Callback, ctx chasm.MutableContext, event EventTerminated) error { + now := ctx.Now(cb) + cb.CloseTime = timestamppb.New(now) + + reason := event.Reason + if reason == "" { + reason = "callback execution terminated" + } + + failure := &failurepb.Failure{ + Message: reason, + FailureInfo: &failurepb.Failure_TerminatedFailureInfo{}, + } + cb.TerminalFailure = chasm.NewDataField(ctx, failure) + + return nil + }, +) + +// EventTimedOut is triggered when the callback's schedule-to-close timeout fires. +type EventTimedOut struct{} + +var TransitionTimedOut = chasm.NewTransition( + []callbackspb.CallbackStatus{ + callbackspb.CALLBACK_STATUS_STANDBY, + callbackspb.CALLBACK_STATUS_SCHEDULED, + callbackspb.CALLBACK_STATUS_BACKING_OFF, + }, + callbackspb.CALLBACK_STATUS_FAILED, + func(cb *Callback, ctx chasm.MutableContext, event EventTimedOut) error { + now := ctx.Now(cb) + cb.CloseTime = timestamppb.New(now) + + failure := &failurepb.Failure{ + Message: "callback execution timed out", + FailureInfo: &failurepb.Failure_TimeoutFailureInfo{ + TimeoutFailureInfo: &failurepb.TimeoutFailureInfo{ + TimeoutType: enumspb.TIMEOUT_TYPE_SCHEDULE_TO_CLOSE, + }, + }, + } + cb.TerminalFailure = chasm.NewDataField(ctx, failure) + return nil }, ) diff --git a/chasm/lib/callback/statemachine_test.go b/chasm/lib/callback/statemachine_test.go index dbefaf5d96c..6b7a4f9d40f 100644 --- a/chasm/lib/callback/statemachine_test.go +++ b/chasm/lib/callback/statemachine_test.go @@ -10,6 +10,7 @@ import ( callbackspb "go.temporal.io/server/chasm/lib/callback/gen/callbackpb/v1" "go.temporal.io/server/common/backoff" "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/timestamppb" ) func TestValidTransitions(t *testing.T) { @@ -20,7 +21,7 @@ func TestValidTransitions(t *testing.T) { Callback: &callbackspb.Callback{ Variant: &callbackspb.Callback_Nexus_{ Nexus: &callbackspb.Callback_Nexus{ - Url: "http://address:666/path/to/callback?query=string", + Url: "http://address:999/path/to/callback?query=string", }, }, }, @@ -28,85 +29,200 @@ func TestValidTransitions(t *testing.T) { } callback.SetStateMachineState(callbackspb.CALLBACK_STATUS_SCHEDULED) - // AttemptFailed - mctx := &chasm.MockMutableContext{} - err := TransitionAttemptFailed.Apply(callback, mctx, EventAttemptFailed{ - Time: currentTime, - Err: errors.New("test"), - RetryPolicy: backoff.NewExponentialRetryPolicy(time.Second), + t.Run("TransitionAttemptFailed", func(t *testing.T) { + mctx := &chasm.MockMutableContext{} + mctx.HandleNow = func(chasm.Component) time.Time { return currentTime } + + err := TransitionAttemptFailed.Apply(callback, mctx, EventAttemptFailed{ + Time: currentTime, + Err: errors.New("error message"), + RetryPolicy: backoff.NewExponentialRetryPolicy(time.Second), + }) + require.NoError(t, err) + + // Assert info object is updated. + require.Equal(t, callbackspb.CALLBACK_STATUS_BACKING_OFF, callback.StateMachineState()) + require.Equal(t, int32(1), callback.Attempt) + require.Equal(t, "error message", callback.LastAttemptFailure.Message) + require.False(t, callback.LastAttemptFailure.GetApplicationFailureInfo().NonRetryable) + require.Equal(t, currentTime, callback.LastAttemptCompleteTime.AsTime()) + dt := currentTime.Add(time.Second).Sub(callback.NextAttemptScheduleTime.AsTime()) + require.Less(t, dt, time.Millisecond*200) + + // Because of the retry policy, the first failure isn't terminal. + _, hasTermFailure := callback.TerminalFailure.TryGet(mctx) + require.False(t, hasTermFailure) + + // Assert backoff task is generated + require.Len(t, mctx.Tasks, 1) + require.IsType(t, &callbackspb.BackoffTask{}, mctx.Tasks[0].Payload) }) - require.NoError(t, err) - - // Assert info object is updated - require.Equal(t, callbackspb.CALLBACK_STATUS_BACKING_OFF, callback.StateMachineState()) - require.Equal(t, int32(1), callback.Attempt) - require.Equal(t, "test", callback.LastAttemptFailure.Message) - require.False(t, callback.LastAttemptFailure.GetApplicationFailureInfo().NonRetryable) - require.Equal(t, currentTime, callback.LastAttemptCompleteTime.AsTime()) - dt := currentTime.Add(time.Second).Sub(callback.NextAttemptScheduleTime.AsTime()) - require.Less(t, dt, time.Millisecond*200) - - // Assert backoff task is generated - require.Len(t, mctx.Tasks, 1) - require.IsType(t, &callbackspb.BackoffTask{}, mctx.Tasks[0].Payload) - - // Rescheduled - mctx = &chasm.MockMutableContext{} - err = TransitionRescheduled.Apply(callback, mctx, EventRescheduled{}) - require.NoError(t, err) - // Assert info object is updated only where needed - require.Equal(t, callbackspb.CALLBACK_STATUS_SCHEDULED, callback.StateMachineState()) - require.Equal(t, int32(1), callback.Attempt) - require.Equal(t, "test", callback.LastAttemptFailure.Message) - // Remains unmodified - require.Equal(t, currentTime, callback.LastAttemptCompleteTime.AsTime()) - require.Nil(t, callback.NextAttemptScheduleTime) - - // Assert callback task is generated - require.Len(t, mctx.Tasks, 1) - require.IsType(t, &callbackspb.InvocationTask{}, mctx.Tasks[0].Payload) + t.Run("TransitionRescheduled", func(t *testing.T) { + mctx := &chasm.MockMutableContext{} + mctx.HandleNow = func(chasm.Component) time.Time { return currentTime } + + err := TransitionRescheduled.Apply(callback, mctx, EventRescheduled{}) + require.NoError(t, err) + + // Assert info object is only partially updated. + require.Equal(t, callbackspb.CALLBACK_STATUS_SCHEDULED, callback.StateMachineState()) + // Unmodified + require.Equal(t, int32(1), callback.Attempt) + require.Equal(t, "error message", callback.LastAttemptFailure.Message) + require.Equal(t, currentTime, callback.LastAttemptCompleteTime.AsTime()) + require.Nil(t, callback.NextAttemptScheduleTime) + _, hasTermFailure := callback.TerminalFailure.TryGet(mctx) + require.False(t, hasTermFailure) + + // Assert callback task is generated. + require.Len(t, mctx.Tasks, 1) + require.IsType(t, &callbackspb.InvocationTask{}, mctx.Tasks[0].Payload) + }) - // Store the pre-succeeded state to test Failed later + // Store the pre-succeeded state to test Failed later. dup := &Callback{ CallbackState: proto.Clone(callback.CallbackState).(*callbackspb.CallbackState), } dup.Status = callback.StateMachineState() - // Succeeded - currentTime = currentTime.Add(time.Second) - mctx = &chasm.MockMutableContext{} - err = TransitionSucceeded.Apply(callback, mctx, EventSucceeded{Time: currentTime}) - require.NoError(t, err) + t.Run("TransitionSucceeded", func(t *testing.T) { + currentTime = currentTime.Add(time.Second) + mctx := &chasm.MockMutableContext{} + mctx.HandleNow = func(chasm.Component) time.Time { return currentTime } - // Assert info object is updated only where needed - require.Equal(t, callbackspb.CALLBACK_STATUS_SUCCEEDED, callback.StateMachineState()) - require.Equal(t, int32(2), callback.Attempt) - require.Nil(t, callback.LastAttemptFailure) - require.Equal(t, currentTime, callback.LastAttemptCompleteTime.AsTime()) - require.Nil(t, callback.NextAttemptScheduleTime) + err := TransitionSucceeded.Apply(callback, mctx, EventSucceeded{Time: currentTime}) + require.NoError(t, err) - // Assert no task is generated on success transition - require.Empty(t, mctx.Tasks) + // Assert info object is updated. + require.Equal(t, callbackspb.CALLBACK_STATUS_SUCCEEDED, callback.StateMachineState()) + require.Equal(t, int32(2), callback.Attempt) + require.Nil(t, callback.LastAttemptFailure) + require.Equal(t, currentTime, callback.LastAttemptCompleteTime.AsTime()) + require.Nil(t, callback.NextAttemptScheduleTime) - // Reset back to scheduled + // TerminalFailure may explicitly be set to nil. + termFailureValue, hasTermFailure := callback.TerminalFailure.TryGet(mctx) + require.True(t, !hasTermFailure || termFailureValue == nil) + + // Assert no task is generated on success transition + require.Empty(t, mctx.Tasks) + }) + + // Reset back to the scheduled state. callback = dup // Increment the time to ensure it's updated in the transition currentTime = currentTime.Add(time.Second) - // failed - mctx = &chasm.MockMutableContext{} - err = TransitionFailed.Apply(callback, mctx, EventFailed{Time: currentTime, Err: errors.New("failed")}) - require.NoError(t, err) + t.Run("TransitionFailed", func(t *testing.T) { + mctx := &chasm.MockMutableContext{} + mctx.HandleNow = func(chasm.Component) time.Time { return currentTime } + + err := TransitionFailed.Apply(callback, mctx, EventFailed{Time: currentTime, Err: errors.New("failed")}) + require.NoError(t, err) + + // Assert info object is updated. + require.Equal(t, callbackspb.CALLBACK_STATUS_FAILED, callback.StateMachineState()) + require.Equal(t, int32(2), callback.Attempt) + require.Equal(t, "failed", callback.LastAttemptFailure.Message) + require.True(t, callback.LastAttemptFailure.GetApplicationFailureInfo().NonRetryable) + require.Equal(t, currentTime, callback.LastAttemptCompleteTime.AsTime()) + require.Nil(t, callback.NextAttemptScheduleTime) + + // Check the TerminalFailure field is set. + require.NotNil(t, callback.TerminalFailure, "TerminalFailure not set") + got := callback.TerminalFailure.Get(mctx) + want := callback.LastAttemptFailure + require.True(t, proto.Equal(want, got), "TerminalFailure not as expected. Got %v, want %v.", got, want) + + // Assert no tasks generated. In terminal state. + require.Empty(t, mctx.Tasks) + }) +} + +func TestTerminatedTransition(t *testing.T) { + initialCallbackState := &callbackspb.CallbackState{ + RegistrationTime: timestamppb.New(time.Now()), + Callback: &callbackspb.Callback{ + Variant: &callbackspb.Callback_Nexus_{ + Nexus: &callbackspb.Callback_Nexus{ + Url: "http://address:999/path", + }, + }, + }, + } + initialCallback := &Callback{ + CallbackState: initialCallbackState, + } - // Assert info object is updated only where needed - require.Equal(t, callbackspb.CALLBACK_STATUS_FAILED, callback.StateMachineState()) - require.Equal(t, int32(2), callback.Attempt) - require.Equal(t, "failed", callback.LastAttemptFailure.Message) - require.True(t, callback.LastAttemptFailure.GetApplicationFailureInfo().NonRetryable) - require.Equal(t, currentTime, callback.LastAttemptCompleteTime.AsTime()) - require.Nil(t, callback.NextAttemptScheduleTime) + for _, src := range []callbackspb.CallbackStatus{ + callbackspb.CALLBACK_STATUS_STANDBY, + callbackspb.CALLBACK_STATUS_SCHEDULED, + callbackspb.CALLBACK_STATUS_BACKING_OFF, + } { + t.Run("from_"+src.String(), func(t *testing.T) { + currentTime := time.Now().UTC() + mctx := &chasm.MockMutableContext{} + mctx.HandleNow = func(chasm.Component) time.Time { return currentTime } + + cb := &Callback{ + CallbackState: proto.Clone(initialCallbackState).(*callbackspb.CallbackState), + } + cb.SetStateMachineState(src) + + err := TransitionTerminated.Apply(cb, mctx, EventTerminated{}) + require.NoError(t, err) + + // Confirm expected state changes. + require.Equal(t, callbackspb.CALLBACK_STATUS_TERMINATED, cb.StateMachineState()) + require.Equal(t, currentTime, cb.GetCloseTime().AsTime()) + // Other fields remain the same. + require.True(t, proto.Equal(initialCallbackState.Callback, cb.Callback)) + require.True(t, proto.Equal(initialCallbackState.RegistrationTime, cb.RegistrationTime)) + require.Equal(t, initialCallback.LastAttemptFailure, cb.LastAttemptFailure) + + // Confirm the Callback's terminal failure reason is set. + termFailure := cb.TerminalFailure.Get(mctx) + require.Equal(t, "callback execution terminated", termFailure.Message) + + // No new tasks were generated. + require.Empty(t, mctx.Tasks) + }) + } - // Assert task is not generated, failed is terminal - require.Empty(t, mctx.Tasks) + // Terminal states. Confirm the request should be rejected by CHASM. + for _, src := range []callbackspb.CallbackStatus{ + callbackspb.CALLBACK_STATUS_SUCCEEDED, + callbackspb.CALLBACK_STATUS_FAILED, + callbackspb.CALLBACK_STATUS_TERMINATED, + } { + t.Run("from_"+src.String(), func(t *testing.T) { + mctx := &chasm.MockMutableContext{} + + cb := &Callback{ + CallbackState: proto.Clone(initialCallbackState).(*callbackspb.CallbackState), + } + cb.SetStateMachineState(src) + + err := TransitionTerminated.Apply(cb, mctx, EventTerminated{}) + require.ErrorContains(t, err, "invalid transition from") + }) + } +} + +func TestSaveResult_TerminatedWhileInFlight(t *testing.T) { + // If the callback was terminated while an invocation was in-flight, + // saveResult should drop the result silently. + cb := &Callback{ + CallbackState: &callbackspb.CallbackState{ + Status: callbackspb.CALLBACK_STATUS_TERMINATED, + }, + } + mctx := &chasm.MockMutableContext{} + _, err := cb.saveResult(mctx, saveResultInput{ + result: invocationResultOK{}, + retryPolicy: backoff.NewExponentialRetryPolicy(time.Second), + }) + require.NoError(t, err) + require.Equal(t, callbackspb.CALLBACK_STATUS_TERMINATED, cb.StateMachineState()) } diff --git a/chasm/lib/callback/tasks.go b/chasm/lib/callback/tasks.go index b53efafea19..406b2468045 100644 --- a/chasm/lib/callback/tasks.go +++ b/chasm/lib/callback/tasks.go @@ -180,3 +180,30 @@ func (h *backoffTaskHandler) Validate( ) (bool, error) { return callback.Status == callbackspb.CALLBACK_STATUS_BACKING_OFF && callback.Attempt == task.Attempt, nil } + +// CompletionScheduleToCloseTimeoutTaskHandler handles schedule-to-close timeout for standalone callback executions. +type CompletionScheduleToCloseTimeoutTaskHandler struct { + chasm.PureTaskHandlerBase +} + +func NewCompletionScheduleToCloseTimeoutTaskHandler() *CompletionScheduleToCloseTimeoutTaskHandler { + return &CompletionScheduleToCloseTimeoutTaskHandler{} +} + +func (h *CompletionScheduleToCloseTimeoutTaskHandler) Validate( + _ chasm.Context, + callback *Callback, + _ chasm.TaskAttributes, + _ *callbackspb.CompletionScheduleToCloseTimeoutTask, +) (bool, error) { + return TransitionTimedOut.Possible(callback), nil +} + +func (h *CompletionScheduleToCloseTimeoutTaskHandler) Execute( + ctx chasm.MutableContext, + callback *Callback, + _ chasm.TaskAttributes, + _ *callbackspb.CompletionScheduleToCloseTimeoutTask, +) error { + return TransitionTimedOut.Apply(callback, ctx, EventTimedOut{}) +} diff --git a/chasm/lib/callback/tasks_test.go b/chasm/lib/callback/tasks_test.go index 71e5c881a64..c0793ed7961 100644 --- a/chasm/lib/callback/tasks_test.go +++ b/chasm/lib/callback/tasks_test.go @@ -192,7 +192,7 @@ func TestExecuteInvocationTaskNexus_Outcomes(t *testing.T) { } chasmRegistry := chasm.NewRegistry(logger) - err = chasmRegistry.Register(&Library{ + err = chasmRegistry.Register(&library{ InvocationTaskHandler: handler, }) require.NoError(t, err) @@ -547,7 +547,7 @@ func TestExecuteInvocationTaskChasm_Outcomes(t *testing.T) { } chasmRegistry := chasm.NewRegistry(logger) - err = chasmRegistry.Register(&Library{ + err = chasmRegistry.Register(&library{ InvocationTaskHandler: handler, }) require.NoError(t, err) diff --git a/chasm/lib/callback/validator.go b/chasm/lib/callback/validator.go index bc478ff8923..1543213e0bc 100644 --- a/chasm/lib/callback/validator.go +++ b/chasm/lib/callback/validator.go @@ -46,9 +46,17 @@ func (v *validator) Validate(namespaceName string, cbs []*commonpb.Callback) err } for _, cb := range cbs { + if cb == nil { + return serviceerror.NewInvalidArgument("Callback is not set") + } + switch variant := cb.GetVariant().(type) { case *commonpb.Callback_Nexus_: rawURL := variant.Nexus.GetUrl() + if rawURL == "" { + return serviceerror.NewInvalidArgument("Callback URL is not set") + } + if len(rawURL) > v.urlMaxLength(namespaceName) { return serviceerror.NewInvalidArgumentf( "invalid url: url length longer than max length allowed of %d", v.urlMaxLength(namespaceName), diff --git a/chasm/lib/workflow/workflow.go b/chasm/lib/workflow/workflow.go index df0355886bb..b79d244f926 100644 --- a/chasm/lib/workflow/workflow.go +++ b/chasm/lib/workflow/workflow.go @@ -110,7 +110,7 @@ func (w *Workflow) AddCompletionCallbacks( id := fmt.Sprintf("%s-%d", requestID, idx) // Create and add callback - callbackObj := callback.NewCallback(requestID, eventTime, &callbackspb.CallbackState{}, chasmCB) + callbackObj := callback.NewEmbeddedCallback(ctx, requestID, eventTime, chasmCB) w.Callbacks[id] = chasm.NewComponentField(ctx, callbackObj) } return nil diff --git a/client/frontend/client_gen.go b/client/frontend/client_gen.go index c72793f75b2..df0363fd252 100644 --- a/client/frontend/client_gen.go +++ b/client/frontend/client_gen.go @@ -19,6 +19,16 @@ func (c *clientImpl) CountActivityExecutions( return c.client.CountActivityExecutions(ctx, request, opts...) } +func (c *clientImpl) CountCallbackExecutions( + ctx context.Context, + request *workflowservice.CountCallbackExecutionsRequest, + opts ...grpc.CallOption, +) (*workflowservice.CountCallbackExecutionsResponse, error) { + ctx, cancel := c.createContext(ctx) + defer cancel() + return c.client.CountCallbackExecutions(ctx, request, opts...) +} + func (c *clientImpl) CountNexusOperationExecutions( ctx context.Context, request *workflowservice.CountNexusOperationExecutionsRequest, @@ -99,6 +109,16 @@ func (c *clientImpl) DeleteActivityExecution( return c.client.DeleteActivityExecution(ctx, request, opts...) } +func (c *clientImpl) DeleteCallbackExecution( + ctx context.Context, + request *workflowservice.DeleteCallbackExecutionRequest, + opts ...grpc.CallOption, +) (*workflowservice.DeleteCallbackExecutionResponse, error) { + ctx, cancel := c.createContext(ctx) + defer cancel() + return c.client.DeleteCallbackExecution(ctx, request, opts...) +} + func (c *clientImpl) DeleteNexusOperationExecution( ctx context.Context, request *workflowservice.DeleteNexusOperationExecutionRequest, @@ -189,6 +209,16 @@ func (c *clientImpl) DescribeBatchOperation( return c.client.DescribeBatchOperation(ctx, request, opts...) } +func (c *clientImpl) DescribeCallbackExecution( + ctx context.Context, + request *workflowservice.DescribeCallbackExecutionRequest, + opts ...grpc.CallOption, +) (*workflowservice.DescribeCallbackExecutionResponse, error) { + ctx, cancel := c.createContext(ctx) + defer cancel() + return c.client.DescribeCallbackExecution(ctx, request, opts...) +} + func (c *clientImpl) DescribeDeployment( ctx context.Context, request *workflowservice.DescribeDeploymentRequest, @@ -439,6 +469,16 @@ func (c *clientImpl) ListBatchOperations( return c.client.ListBatchOperations(ctx, request, opts...) } +func (c *clientImpl) ListCallbackExecutions( + ctx context.Context, + request *workflowservice.ListCallbackExecutionsRequest, + opts ...grpc.CallOption, +) (*workflowservice.ListCallbackExecutionsResponse, error) { + ctx, cancel := c.createContext(ctx) + defer cancel() + return c.client.ListCallbackExecutions(ctx, request, opts...) +} + func (c *clientImpl) ListClosedWorkflowExecutions( ctx context.Context, request *workflowservice.ListClosedWorkflowExecutionsRequest, @@ -619,6 +659,16 @@ func (c *clientImpl) PollActivityTaskQueue( return c.client.PollActivityTaskQueue(ctx, request, opts...) } +func (c *clientImpl) PollCallbackExecution( + ctx context.Context, + request *workflowservice.PollCallbackExecutionRequest, + opts ...grpc.CallOption, +) (*workflowservice.PollCallbackExecutionResponse, error) { + ctx, cancel := c.createContext(ctx) + defer cancel() + return c.client.PollCallbackExecution(ctx, request, opts...) +} + func (c *clientImpl) PollNexusOperationExecution( ctx context.Context, request *workflowservice.PollNexusOperationExecutionRequest, @@ -989,6 +1039,16 @@ func (c *clientImpl) StartBatchOperation( return c.client.StartBatchOperation(ctx, request, opts...) } +func (c *clientImpl) StartCallbackExecution( + ctx context.Context, + request *workflowservice.StartCallbackExecutionRequest, + opts ...grpc.CallOption, +) (*workflowservice.StartCallbackExecutionResponse, error) { + ctx, cancel := c.createContext(ctx) + defer cancel() + return c.client.StartCallbackExecution(ctx, request, opts...) +} + func (c *clientImpl) StartNexusOperationExecution( ctx context.Context, request *workflowservice.StartNexusOperationExecutionRequest, @@ -1029,6 +1089,16 @@ func (c *clientImpl) TerminateActivityExecution( return c.client.TerminateActivityExecution(ctx, request, opts...) } +func (c *clientImpl) TerminateCallbackExecution( + ctx context.Context, + request *workflowservice.TerminateCallbackExecutionRequest, + opts ...grpc.CallOption, +) (*workflowservice.TerminateCallbackExecutionResponse, error) { + ctx, cancel := c.createContext(ctx) + defer cancel() + return c.client.TerminateCallbackExecution(ctx, request, opts...) +} + func (c *clientImpl) TerminateNexusOperationExecution( ctx context.Context, request *workflowservice.TerminateNexusOperationExecutionRequest, diff --git a/client/frontend/metric_client_gen.go b/client/frontend/metric_client_gen.go index 247c90a2ed9..b87c42e8169 100644 --- a/client/frontend/metric_client_gen.go +++ b/client/frontend/metric_client_gen.go @@ -23,6 +23,20 @@ func (c *metricClient) CountActivityExecutions( return c.client.CountActivityExecutions(ctx, request, opts...) } +func (c *metricClient) CountCallbackExecutions( + ctx context.Context, + request *workflowservice.CountCallbackExecutionsRequest, + opts ...grpc.CallOption, +) (_ *workflowservice.CountCallbackExecutionsResponse, retError error) { + + metricsHandler, startTime := c.startMetricsRecording(ctx, "FrontendClientCountCallbackExecutions") + defer func() { + c.finishMetricsRecording(metricsHandler, startTime, retError) + }() + + return c.client.CountCallbackExecutions(ctx, request, opts...) +} + func (c *metricClient) CountNexusOperationExecutions( ctx context.Context, request *workflowservice.CountNexusOperationExecutionsRequest, @@ -135,6 +149,20 @@ func (c *metricClient) DeleteActivityExecution( return c.client.DeleteActivityExecution(ctx, request, opts...) } +func (c *metricClient) DeleteCallbackExecution( + ctx context.Context, + request *workflowservice.DeleteCallbackExecutionRequest, + opts ...grpc.CallOption, +) (_ *workflowservice.DeleteCallbackExecutionResponse, retError error) { + + metricsHandler, startTime := c.startMetricsRecording(ctx, "FrontendClientDeleteCallbackExecution") + defer func() { + c.finishMetricsRecording(metricsHandler, startTime, retError) + }() + + return c.client.DeleteCallbackExecution(ctx, request, opts...) +} + func (c *metricClient) DeleteNexusOperationExecution( ctx context.Context, request *workflowservice.DeleteNexusOperationExecutionRequest, @@ -261,6 +289,20 @@ func (c *metricClient) DescribeBatchOperation( return c.client.DescribeBatchOperation(ctx, request, opts...) } +func (c *metricClient) DescribeCallbackExecution( + ctx context.Context, + request *workflowservice.DescribeCallbackExecutionRequest, + opts ...grpc.CallOption, +) (_ *workflowservice.DescribeCallbackExecutionResponse, retError error) { + + metricsHandler, startTime := c.startMetricsRecording(ctx, "FrontendClientDescribeCallbackExecution") + defer func() { + c.finishMetricsRecording(metricsHandler, startTime, retError) + }() + + return c.client.DescribeCallbackExecution(ctx, request, opts...) +} + func (c *metricClient) DescribeDeployment( ctx context.Context, request *workflowservice.DescribeDeploymentRequest, @@ -611,6 +653,20 @@ func (c *metricClient) ListBatchOperations( return c.client.ListBatchOperations(ctx, request, opts...) } +func (c *metricClient) ListCallbackExecutions( + ctx context.Context, + request *workflowservice.ListCallbackExecutionsRequest, + opts ...grpc.CallOption, +) (_ *workflowservice.ListCallbackExecutionsResponse, retError error) { + + metricsHandler, startTime := c.startMetricsRecording(ctx, "FrontendClientListCallbackExecutions") + defer func() { + c.finishMetricsRecording(metricsHandler, startTime, retError) + }() + + return c.client.ListCallbackExecutions(ctx, request, opts...) +} + func (c *metricClient) ListClosedWorkflowExecutions( ctx context.Context, request *workflowservice.ListClosedWorkflowExecutionsRequest, @@ -863,6 +919,20 @@ func (c *metricClient) PollActivityTaskQueue( return c.client.PollActivityTaskQueue(ctx, request, opts...) } +func (c *metricClient) PollCallbackExecution( + ctx context.Context, + request *workflowservice.PollCallbackExecutionRequest, + opts ...grpc.CallOption, +) (_ *workflowservice.PollCallbackExecutionResponse, retError error) { + + metricsHandler, startTime := c.startMetricsRecording(ctx, "FrontendClientPollCallbackExecution") + defer func() { + c.finishMetricsRecording(metricsHandler, startTime, retError) + }() + + return c.client.PollCallbackExecution(ctx, request, opts...) +} + func (c *metricClient) PollNexusOperationExecution( ctx context.Context, request *workflowservice.PollNexusOperationExecutionRequest, @@ -1381,6 +1451,20 @@ func (c *metricClient) StartBatchOperation( return c.client.StartBatchOperation(ctx, request, opts...) } +func (c *metricClient) StartCallbackExecution( + ctx context.Context, + request *workflowservice.StartCallbackExecutionRequest, + opts ...grpc.CallOption, +) (_ *workflowservice.StartCallbackExecutionResponse, retError error) { + + metricsHandler, startTime := c.startMetricsRecording(ctx, "FrontendClientStartCallbackExecution") + defer func() { + c.finishMetricsRecording(metricsHandler, startTime, retError) + }() + + return c.client.StartCallbackExecution(ctx, request, opts...) +} + func (c *metricClient) StartNexusOperationExecution( ctx context.Context, request *workflowservice.StartNexusOperationExecutionRequest, @@ -1437,6 +1521,20 @@ func (c *metricClient) TerminateActivityExecution( return c.client.TerminateActivityExecution(ctx, request, opts...) } +func (c *metricClient) TerminateCallbackExecution( + ctx context.Context, + request *workflowservice.TerminateCallbackExecutionRequest, + opts ...grpc.CallOption, +) (_ *workflowservice.TerminateCallbackExecutionResponse, retError error) { + + metricsHandler, startTime := c.startMetricsRecording(ctx, "FrontendClientTerminateCallbackExecution") + defer func() { + c.finishMetricsRecording(metricsHandler, startTime, retError) + }() + + return c.client.TerminateCallbackExecution(ctx, request, opts...) +} + func (c *metricClient) TerminateNexusOperationExecution( ctx context.Context, request *workflowservice.TerminateNexusOperationExecutionRequest, diff --git a/client/frontend/retryable_client_gen.go b/client/frontend/retryable_client_gen.go index 7a5a63cf4de..89b4bc9f5ca 100644 --- a/client/frontend/retryable_client_gen.go +++ b/client/frontend/retryable_client_gen.go @@ -26,6 +26,21 @@ func (c *retryableClient) CountActivityExecutions( return resp, err } +func (c *retryableClient) CountCallbackExecutions( + ctx context.Context, + request *workflowservice.CountCallbackExecutionsRequest, + opts ...grpc.CallOption, +) (*workflowservice.CountCallbackExecutionsResponse, error) { + var resp *workflowservice.CountCallbackExecutionsResponse + op := func(ctx context.Context) error { + var err error + resp, err = c.client.CountCallbackExecutions(ctx, request, opts...) + return err + } + err := backoff.ThrottleRetryContext(ctx, op, c.policy, c.isRetryable) + return resp, err +} + func (c *retryableClient) CountNexusOperationExecutions( ctx context.Context, request *workflowservice.CountNexusOperationExecutionsRequest, @@ -146,6 +161,21 @@ func (c *retryableClient) DeleteActivityExecution( return resp, err } +func (c *retryableClient) DeleteCallbackExecution( + ctx context.Context, + request *workflowservice.DeleteCallbackExecutionRequest, + opts ...grpc.CallOption, +) (*workflowservice.DeleteCallbackExecutionResponse, error) { + var resp *workflowservice.DeleteCallbackExecutionResponse + op := func(ctx context.Context) error { + var err error + resp, err = c.client.DeleteCallbackExecution(ctx, request, opts...) + return err + } + err := backoff.ThrottleRetryContext(ctx, op, c.policy, c.isRetryable) + return resp, err +} + func (c *retryableClient) DeleteNexusOperationExecution( ctx context.Context, request *workflowservice.DeleteNexusOperationExecutionRequest, @@ -281,6 +311,21 @@ func (c *retryableClient) DescribeBatchOperation( return resp, err } +func (c *retryableClient) DescribeCallbackExecution( + ctx context.Context, + request *workflowservice.DescribeCallbackExecutionRequest, + opts ...grpc.CallOption, +) (*workflowservice.DescribeCallbackExecutionResponse, error) { + var resp *workflowservice.DescribeCallbackExecutionResponse + op := func(ctx context.Context) error { + var err error + resp, err = c.client.DescribeCallbackExecution(ctx, request, opts...) + return err + } + err := backoff.ThrottleRetryContext(ctx, op, c.policy, c.isRetryable) + return resp, err +} + func (c *retryableClient) DescribeDeployment( ctx context.Context, request *workflowservice.DescribeDeploymentRequest, @@ -656,6 +701,21 @@ func (c *retryableClient) ListBatchOperations( return resp, err } +func (c *retryableClient) ListCallbackExecutions( + ctx context.Context, + request *workflowservice.ListCallbackExecutionsRequest, + opts ...grpc.CallOption, +) (*workflowservice.ListCallbackExecutionsResponse, error) { + var resp *workflowservice.ListCallbackExecutionsResponse + op := func(ctx context.Context) error { + var err error + resp, err = c.client.ListCallbackExecutions(ctx, request, opts...) + return err + } + err := backoff.ThrottleRetryContext(ctx, op, c.policy, c.isRetryable) + return resp, err +} + func (c *retryableClient) ListClosedWorkflowExecutions( ctx context.Context, request *workflowservice.ListClosedWorkflowExecutionsRequest, @@ -926,6 +986,21 @@ func (c *retryableClient) PollActivityTaskQueue( return resp, err } +func (c *retryableClient) PollCallbackExecution( + ctx context.Context, + request *workflowservice.PollCallbackExecutionRequest, + opts ...grpc.CallOption, +) (*workflowservice.PollCallbackExecutionResponse, error) { + var resp *workflowservice.PollCallbackExecutionResponse + op := func(ctx context.Context) error { + var err error + resp, err = c.client.PollCallbackExecution(ctx, request, opts...) + return err + } + err := backoff.ThrottleRetryContext(ctx, op, c.policy, c.isRetryable) + return resp, err +} + func (c *retryableClient) PollNexusOperationExecution( ctx context.Context, request *workflowservice.PollNexusOperationExecutionRequest, @@ -1481,6 +1556,21 @@ func (c *retryableClient) StartBatchOperation( return resp, err } +func (c *retryableClient) StartCallbackExecution( + ctx context.Context, + request *workflowservice.StartCallbackExecutionRequest, + opts ...grpc.CallOption, +) (*workflowservice.StartCallbackExecutionResponse, error) { + var resp *workflowservice.StartCallbackExecutionResponse + op := func(ctx context.Context) error { + var err error + resp, err = c.client.StartCallbackExecution(ctx, request, opts...) + return err + } + err := backoff.ThrottleRetryContext(ctx, op, c.policy, c.isRetryable) + return resp, err +} + func (c *retryableClient) StartNexusOperationExecution( ctx context.Context, request *workflowservice.StartNexusOperationExecutionRequest, @@ -1541,6 +1631,21 @@ func (c *retryableClient) TerminateActivityExecution( return resp, err } +func (c *retryableClient) TerminateCallbackExecution( + ctx context.Context, + request *workflowservice.TerminateCallbackExecutionRequest, + opts ...grpc.CallOption, +) (*workflowservice.TerminateCallbackExecutionResponse, error) { + var resp *workflowservice.TerminateCallbackExecutionResponse + op := func(ctx context.Context) error { + var err error + resp, err = c.client.TerminateCallbackExecution(ctx, request, opts...) + return err + } + err := backoff.ThrottleRetryContext(ctx, op, c.policy, c.isRetryable) + return resp, err +} + func (c *retryableClient) TerminateNexusOperationExecution( ctx context.Context, request *workflowservice.TerminateNexusOperationExecutionRequest, diff --git a/common/api/metadata.go b/common/api/metadata.go index bb584d38000..9962280693e 100644 --- a/common/api/metadata.go +++ b/common/api/metadata.go @@ -89,20 +89,27 @@ var ( "RespondActivityTaskCanceled": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, "RespondActivityTaskCanceledById": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, "CountActivityExecutions": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingNone}, + "CountCallbackExecutions": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingNone}, "CountNexusOperationExecutions": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingNone}, "DeleteActivityExecution": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, + "DeleteCallbackExecution": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, "DeleteNexusOperationExecution": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, "DescribeActivityExecution": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingCapable}, + "DescribeCallbackExecution": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingCapable}, "DescribeNexusOperationExecution": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingCapable}, "PollActivityExecution": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingAlways}, + "PollCallbackExecution": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingAlways}, "PollNexusOperationExecution": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingAlways}, "ListActivityExecutions": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingNone}, + "ListCallbackExecutions": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingNone}, "ListNexusOperationExecutions": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingNone}, "RequestCancelActivityExecution": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, "RequestCancelNexusOperationExecution": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, "StartActivityExecution": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, + "StartCallbackExecution": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, "StartNexusOperationExecution": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, "TerminateActivityExecution": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, + "TerminateCallbackExecution": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, "TerminateNexusOperationExecution": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, "PollNexusTaskQueue": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingAlways}, "RespondNexusTaskCompleted": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, diff --git a/common/rpc/interceptor/logtags/workflow_service_server_gen.go b/common/rpc/interceptor/logtags/workflow_service_server_gen.go index a03c1e7c1f1..5fb3c1c6f3a 100644 --- a/common/rpc/interceptor/logtags/workflow_service_server_gen.go +++ b/common/rpc/interceptor/logtags/workflow_service_server_gen.go @@ -13,6 +13,10 @@ func (wt *WorkflowTags) extractFromWorkflowServiceServerMessage(message any) []t return nil case *workflowservice.CountActivityExecutionsResponse: return nil + case *workflowservice.CountCallbackExecutionsRequest: + return nil + case *workflowservice.CountCallbackExecutionsResponse: + return nil case *workflowservice.CountNexusOperationExecutionsRequest: return nil case *workflowservice.CountNexusOperationExecutionsResponse: @@ -48,6 +52,12 @@ func (wt *WorkflowTags) extractFromWorkflowServiceServerMessage(message any) []t } case *workflowservice.DeleteActivityExecutionResponse: return nil + case *workflowservice.DeleteCallbackExecutionRequest: + return []tag.Tag{ + tag.WorkflowRunID(r.GetRunId()), + } + case *workflowservice.DeleteCallbackExecutionResponse: + return nil case *workflowservice.DeleteNexusOperationExecutionRequest: return []tag.Tag{ tag.OperationID(r.GetOperationId()), @@ -95,6 +105,12 @@ func (wt *WorkflowTags) extractFromWorkflowServiceServerMessage(message any) []t return nil case *workflowservice.DescribeBatchOperationResponse: return nil + case *workflowservice.DescribeCallbackExecutionRequest: + return []tag.Tag{ + tag.WorkflowRunID(r.GetRunId()), + } + case *workflowservice.DescribeCallbackExecutionResponse: + return nil case *workflowservice.DescribeDeploymentRequest: return nil case *workflowservice.DescribeDeploymentResponse: @@ -209,6 +225,10 @@ func (wt *WorkflowTags) extractFromWorkflowServiceServerMessage(message any) []t return nil case *workflowservice.ListBatchOperationsResponse: return nil + case *workflowservice.ListCallbackExecutionsRequest: + return nil + case *workflowservice.ListCallbackExecutionsResponse: + return nil case *workflowservice.ListClosedWorkflowExecutionsRequest: return nil case *workflowservice.ListClosedWorkflowExecutionsResponse: @@ -299,6 +319,14 @@ func (wt *WorkflowTags) extractFromWorkflowServiceServerMessage(message any) []t tag.WorkflowID(r.GetWorkflowExecution().GetWorkflowId()), tag.WorkflowRunID(r.GetWorkflowExecution().GetRunId()), } + case *workflowservice.PollCallbackExecutionRequest: + return []tag.Tag{ + tag.WorkflowRunID(r.GetRunId()), + } + case *workflowservice.PollCallbackExecutionResponse: + return []tag.Tag{ + tag.WorkflowRunID(r.GetRunId()), + } case *workflowservice.PollNexusOperationExecutionRequest: return []tag.Tag{ tag.OperationID(r.GetOperationId()), @@ -515,6 +543,12 @@ func (wt *WorkflowTags) extractFromWorkflowServiceServerMessage(message any) []t return nil case *workflowservice.StartBatchOperationResponse: return nil + case *workflowservice.StartCallbackExecutionRequest: + return nil + case *workflowservice.StartCallbackExecutionResponse: + return []tag.Tag{ + tag.WorkflowRunID(r.GetRunId()), + } case *workflowservice.StartNexusOperationExecutionRequest: return []tag.Tag{ tag.OperationID(r.GetOperationId()), @@ -542,6 +576,12 @@ func (wt *WorkflowTags) extractFromWorkflowServiceServerMessage(message any) []t } case *workflowservice.TerminateActivityExecutionResponse: return nil + case *workflowservice.TerminateCallbackExecutionRequest: + return []tag.Tag{ + tag.WorkflowRunID(r.GetRunId()), + } + case *workflowservice.TerminateCallbackExecutionResponse: + return nil case *workflowservice.TerminateNexusOperationExecutionRequest: return []tag.Tag{ tag.OperationID(r.GetOperationId()), diff --git a/common/rpc/interceptor/redirection.go b/common/rpc/interceptor/redirection.go index 05e3bc73cf5..276662e2e84 100644 --- a/common/rpc/interceptor/redirection.go +++ b/common/rpc/interceptor/redirection.go @@ -156,6 +156,14 @@ var ( "TerminateActivityExecution": func() any { return &workflowservice.TerminateActivityExecutionResponse{} }, "DeleteActivityExecution": func() any { return &workflowservice.DeleteActivityExecutionResponse{} }, + "StartCallbackExecution": func() any { return &workflowservice.StartCallbackExecutionResponse{} }, + "DescribeCallbackExecution": func() any { return &workflowservice.DescribeCallbackExecutionResponse{} }, + "PollCallbackExecution": func() any { return &workflowservice.PollCallbackExecutionResponse{} }, + "CountCallbackExecutions": func() any { return &workflowservice.CountCallbackExecutionsResponse{} }, + "ListCallbackExecutions": func() any { return &workflowservice.ListCallbackExecutionsResponse{} }, + "TerminateCallbackExecution": func() any { return &workflowservice.TerminateCallbackExecutionResponse{} }, + "DeleteCallbackExecution": func() any { return &workflowservice.DeleteCallbackExecutionResponse{} }, + "CountNexusOperationExecutions": func() any { return &workflowservice.CountNexusOperationExecutionsResponse{} }, "DeleteNexusOperationExecution": func() any { return &workflowservice.DeleteNexusOperationExecutionResponse{} }, "DescribeNexusOperationExecution": func() any { return &workflowservice.DescribeNexusOperationExecutionResponse{} }, diff --git a/common/rpc/interceptor/redirection_test.go b/common/rpc/interceptor/redirection_test.go index ddc180b4b00..a1a46a68510 100644 --- a/common/rpc/interceptor/redirection_test.go +++ b/common/rpc/interceptor/redirection_test.go @@ -213,6 +213,14 @@ func (s *redirectionInterceptorSuite) TestGlobalAPI() { "TerminateActivityExecution": {}, "DeleteActivityExecution": {}, + "StartCallbackExecution": {}, + "DescribeCallbackExecution": {}, + "PollCallbackExecution": {}, + "ListCallbackExecutions": {}, + "CountCallbackExecutions": {}, + "TerminateCallbackExecution": {}, + "DeleteCallbackExecution": {}, + "CountNexusOperationExecutions": {}, "DeleteNexusOperationExecution": {}, "DescribeNexusOperationExecution": {}, diff --git a/common/testing/mockapi/workflowservicemock/v1/service_grpc.pb.mock.go b/common/testing/mockapi/workflowservicemock/v1/service_grpc.pb.mock.go index 34a676ad5a9..42996b2cb90 100644 --- a/common/testing/mockapi/workflowservicemock/v1/service_grpc.pb.mock.go +++ b/common/testing/mockapi/workflowservicemock/v1/service_grpc.pb.mock.go @@ -62,6 +62,26 @@ func (mr *MockWorkflowServiceClientMockRecorder) CountActivityExecutions(ctx, in return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CountActivityExecutions", reflect.TypeOf((*MockWorkflowServiceClient)(nil).CountActivityExecutions), varargs...) } +// CountCallbackExecutions mocks base method. +func (m *MockWorkflowServiceClient) CountCallbackExecutions(ctx context.Context, in *workflowservice.CountCallbackExecutionsRequest, opts ...grpc.CallOption) (*workflowservice.CountCallbackExecutionsResponse, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, in} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "CountCallbackExecutions", varargs...) + ret0, _ := ret[0].(*workflowservice.CountCallbackExecutionsResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CountCallbackExecutions indicates an expected call of CountCallbackExecutions. +func (mr *MockWorkflowServiceClientMockRecorder) CountCallbackExecutions(ctx, in any, opts ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, in}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CountCallbackExecutions", reflect.TypeOf((*MockWorkflowServiceClient)(nil).CountCallbackExecutions), varargs...) +} + // CountNexusOperationExecutions mocks base method. func (m *MockWorkflowServiceClient) CountNexusOperationExecutions(ctx context.Context, in *workflowservice.CountNexusOperationExecutionsRequest, opts ...grpc.CallOption) (*workflowservice.CountNexusOperationExecutionsResponse, error) { m.ctrl.T.Helper() @@ -222,6 +242,26 @@ func (mr *MockWorkflowServiceClientMockRecorder) DeleteActivityExecution(ctx, in return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteActivityExecution", reflect.TypeOf((*MockWorkflowServiceClient)(nil).DeleteActivityExecution), varargs...) } +// DeleteCallbackExecution mocks base method. +func (m *MockWorkflowServiceClient) DeleteCallbackExecution(ctx context.Context, in *workflowservice.DeleteCallbackExecutionRequest, opts ...grpc.CallOption) (*workflowservice.DeleteCallbackExecutionResponse, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, in} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "DeleteCallbackExecution", varargs...) + ret0, _ := ret[0].(*workflowservice.DeleteCallbackExecutionResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DeleteCallbackExecution indicates an expected call of DeleteCallbackExecution. +func (mr *MockWorkflowServiceClientMockRecorder) DeleteCallbackExecution(ctx, in any, opts ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, in}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteCallbackExecution", reflect.TypeOf((*MockWorkflowServiceClient)(nil).DeleteCallbackExecution), varargs...) +} + // DeleteNexusOperationExecution mocks base method. func (m *MockWorkflowServiceClient) DeleteNexusOperationExecution(ctx context.Context, in *workflowservice.DeleteNexusOperationExecutionRequest, opts ...grpc.CallOption) (*workflowservice.DeleteNexusOperationExecutionResponse, error) { m.ctrl.T.Helper() @@ -402,6 +442,26 @@ func (mr *MockWorkflowServiceClientMockRecorder) DescribeBatchOperation(ctx, in return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeBatchOperation", reflect.TypeOf((*MockWorkflowServiceClient)(nil).DescribeBatchOperation), varargs...) } +// DescribeCallbackExecution mocks base method. +func (m *MockWorkflowServiceClient) DescribeCallbackExecution(ctx context.Context, in *workflowservice.DescribeCallbackExecutionRequest, opts ...grpc.CallOption) (*workflowservice.DescribeCallbackExecutionResponse, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, in} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "DescribeCallbackExecution", varargs...) + ret0, _ := ret[0].(*workflowservice.DescribeCallbackExecutionResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DescribeCallbackExecution indicates an expected call of DescribeCallbackExecution. +func (mr *MockWorkflowServiceClientMockRecorder) DescribeCallbackExecution(ctx, in any, opts ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, in}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeCallbackExecution", reflect.TypeOf((*MockWorkflowServiceClient)(nil).DescribeCallbackExecution), varargs...) +} + // DescribeDeployment mocks base method. func (m *MockWorkflowServiceClient) DescribeDeployment(ctx context.Context, in *workflowservice.DescribeDeploymentRequest, opts ...grpc.CallOption) (*workflowservice.DescribeDeploymentResponse, error) { m.ctrl.T.Helper() @@ -902,6 +962,26 @@ func (mr *MockWorkflowServiceClientMockRecorder) ListBatchOperations(ctx, in any return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListBatchOperations", reflect.TypeOf((*MockWorkflowServiceClient)(nil).ListBatchOperations), varargs...) } +// ListCallbackExecutions mocks base method. +func (m *MockWorkflowServiceClient) ListCallbackExecutions(ctx context.Context, in *workflowservice.ListCallbackExecutionsRequest, opts ...grpc.CallOption) (*workflowservice.ListCallbackExecutionsResponse, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, in} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "ListCallbackExecutions", varargs...) + ret0, _ := ret[0].(*workflowservice.ListCallbackExecutionsResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListCallbackExecutions indicates an expected call of ListCallbackExecutions. +func (mr *MockWorkflowServiceClientMockRecorder) ListCallbackExecutions(ctx, in any, opts ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, in}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListCallbackExecutions", reflect.TypeOf((*MockWorkflowServiceClient)(nil).ListCallbackExecutions), varargs...) +} + // ListClosedWorkflowExecutions mocks base method. func (m *MockWorkflowServiceClient) ListClosedWorkflowExecutions(ctx context.Context, in *workflowservice.ListClosedWorkflowExecutionsRequest, opts ...grpc.CallOption) (*workflowservice.ListClosedWorkflowExecutionsResponse, error) { m.ctrl.T.Helper() @@ -1262,6 +1342,26 @@ func (mr *MockWorkflowServiceClientMockRecorder) PollActivityTaskQueue(ctx, in a return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PollActivityTaskQueue", reflect.TypeOf((*MockWorkflowServiceClient)(nil).PollActivityTaskQueue), varargs...) } +// PollCallbackExecution mocks base method. +func (m *MockWorkflowServiceClient) PollCallbackExecution(ctx context.Context, in *workflowservice.PollCallbackExecutionRequest, opts ...grpc.CallOption) (*workflowservice.PollCallbackExecutionResponse, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, in} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "PollCallbackExecution", varargs...) + ret0, _ := ret[0].(*workflowservice.PollCallbackExecutionResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PollCallbackExecution indicates an expected call of PollCallbackExecution. +func (mr *MockWorkflowServiceClientMockRecorder) PollCallbackExecution(ctx, in any, opts ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, in}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PollCallbackExecution", reflect.TypeOf((*MockWorkflowServiceClient)(nil).PollCallbackExecution), varargs...) +} + // PollNexusOperationExecution mocks base method. func (m *MockWorkflowServiceClient) PollNexusOperationExecution(ctx context.Context, in *workflowservice.PollNexusOperationExecutionRequest, opts ...grpc.CallOption) (*workflowservice.PollNexusOperationExecutionResponse, error) { m.ctrl.T.Helper() @@ -2002,6 +2102,26 @@ func (mr *MockWorkflowServiceClientMockRecorder) StartBatchOperation(ctx, in any return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StartBatchOperation", reflect.TypeOf((*MockWorkflowServiceClient)(nil).StartBatchOperation), varargs...) } +// StartCallbackExecution mocks base method. +func (m *MockWorkflowServiceClient) StartCallbackExecution(ctx context.Context, in *workflowservice.StartCallbackExecutionRequest, opts ...grpc.CallOption) (*workflowservice.StartCallbackExecutionResponse, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, in} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "StartCallbackExecution", varargs...) + ret0, _ := ret[0].(*workflowservice.StartCallbackExecutionResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// StartCallbackExecution indicates an expected call of StartCallbackExecution. +func (mr *MockWorkflowServiceClientMockRecorder) StartCallbackExecution(ctx, in any, opts ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, in}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StartCallbackExecution", reflect.TypeOf((*MockWorkflowServiceClient)(nil).StartCallbackExecution), varargs...) +} + // StartNexusOperationExecution mocks base method. func (m *MockWorkflowServiceClient) StartNexusOperationExecution(ctx context.Context, in *workflowservice.StartNexusOperationExecutionRequest, opts ...grpc.CallOption) (*workflowservice.StartNexusOperationExecutionResponse, error) { m.ctrl.T.Helper() @@ -2082,6 +2202,26 @@ func (mr *MockWorkflowServiceClientMockRecorder) TerminateActivityExecution(ctx, return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TerminateActivityExecution", reflect.TypeOf((*MockWorkflowServiceClient)(nil).TerminateActivityExecution), varargs...) } +// TerminateCallbackExecution mocks base method. +func (m *MockWorkflowServiceClient) TerminateCallbackExecution(ctx context.Context, in *workflowservice.TerminateCallbackExecutionRequest, opts ...grpc.CallOption) (*workflowservice.TerminateCallbackExecutionResponse, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, in} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "TerminateCallbackExecution", varargs...) + ret0, _ := ret[0].(*workflowservice.TerminateCallbackExecutionResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// TerminateCallbackExecution indicates an expected call of TerminateCallbackExecution. +func (mr *MockWorkflowServiceClientMockRecorder) TerminateCallbackExecution(ctx, in any, opts ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, in}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TerminateCallbackExecution", reflect.TypeOf((*MockWorkflowServiceClient)(nil).TerminateCallbackExecution), varargs...) +} + // TerminateNexusOperationExecution mocks base method. func (m *MockWorkflowServiceClient) TerminateNexusOperationExecution(ctx context.Context, in *workflowservice.TerminateNexusOperationExecutionRequest, opts ...grpc.CallOption) (*workflowservice.TerminateNexusOperationExecutionResponse, error) { m.ctrl.T.Helper() diff --git a/components/nexusoperations/completion.go b/components/nexusoperations/completion.go index cf37247030b..ec10497f7ca 100644 --- a/components/nexusoperations/completion.go +++ b/components/nexusoperations/completion.go @@ -16,6 +16,11 @@ import ( "google.golang.org/protobuf/types/known/timestamppb" ) +// ErrOperationNotStarted is returned when a completion arrives before the operation has +// started and no operation token is provided. This error is used by the callback invocation +// layer to detect this specific condition and retry without triggering the circuit breaker. +var ErrOperationNotStarted = serviceerror.NewFailedPrecondition("nexus operation not started") + func handleSuccessfulOperationResult( node *hsm.Node, operation Operation, @@ -136,6 +141,14 @@ func fabricateStartedEventIfMissing( return nil } + // If the operation hasn't started yet and the completion doesn't include an operation token, + // reject the request. This handles the race where a completion arrives before the start + // handler returns with the operation token. The caller will retry and by then the start + // handler will have returned and recorded the token. + if operationToken == "" { + return ErrOperationNotStarted + } + eventID, err := hsm.EventIDFromToken(operation.ScheduledEventToken) if err != nil { return err diff --git a/go.mod b/go.mod index dbe232a6248..6e6dbdca09f 100644 --- a/go.mod +++ b/go.mod @@ -63,7 +63,7 @@ require ( go.opentelemetry.io/otel/sdk v1.43.0 go.opentelemetry.io/otel/sdk/metric v1.43.0 go.opentelemetry.io/otel/trace v1.43.0 - go.temporal.io/api v1.62.12-0.20260430203359-15c391664683 + go.temporal.io/api v1.62.12-0.20260506203937-27ab43932052 // DO NOT SUBMIT -- Branch chrsmith/standalone-callbacks go.temporal.io/auto-scaled-workers v0.0.0-20260407181057-edd947d743d2 go.temporal.io/sdk v1.41.1 go.uber.org/fx v1.24.0 diff --git a/go.sum b/go.sum index 957bed1529f..9d452e6683c 100644 --- a/go.sum +++ b/go.sum @@ -469,8 +469,8 @@ go.opentelemetry.io/proto/slim/otlp/collector/profiles/v1development v0.3.0 h1:R go.opentelemetry.io/proto/slim/otlp/collector/profiles/v1development v0.3.0/go.mod h1:I89cynRj8y+383o7tEQVg2SVA6SRgDVIouWPUVXjx0U= go.opentelemetry.io/proto/slim/otlp/profiles/v1development v0.3.0 h1:CQvJSldHRUN6Z8jsUeYv8J0lXRvygALXIzsmAeCcZE0= go.opentelemetry.io/proto/slim/otlp/profiles/v1development v0.3.0/go.mod h1:xSQ+mEfJe/GjK1LXEyVOoSI1N9JV9ZI923X5kup43W4= -go.temporal.io/api v1.62.12-0.20260430203359-15c391664683 h1:GtwQjX9hN0pRjuneBpl/xvcu9Xl9llAt4GjKrlpP0sg= -go.temporal.io/api v1.62.12-0.20260430203359-15c391664683/go.mod h1:iaxoP/9OXMJcQkETTECfwYq4cw/bj4nwov8b3ZLVnXM= +go.temporal.io/api v1.62.12-0.20260506203937-27ab43932052 h1:zTB6uMLzdBsLksMH073JTuLfcS0S53+Bm5Kxestwnz4= +go.temporal.io/api v1.62.12-0.20260506203937-27ab43932052/go.mod h1:iaxoP/9OXMJcQkETTECfwYq4cw/bj4nwov8b3ZLVnXM= go.temporal.io/auto-scaled-workers v0.0.0-20260407181057-edd947d743d2 h1:1hKeH3GyR6YD6LKMHGCZ76t6h1Sgha0hXVQBxWi3dlQ= go.temporal.io/auto-scaled-workers v0.0.0-20260407181057-edd947d743d2/go.mod h1:T8dnzVPeO+gaUTj9eDgm/lT2lZH4+JXNvrGaQGyVi50= go.temporal.io/sdk v1.41.1 h1:yOpvsHyDD1lNuwlGBv/SUodCPhjv9nDeC9lLHW/fJUA= diff --git a/service/frontend/configs/quotas.go b/service/frontend/configs/quotas.go index 4d8dcc42c3a..ba8bd19806f 100644 --- a/service/frontend/configs/quotas.go +++ b/service/frontend/configs/quotas.go @@ -92,6 +92,7 @@ var ( "/temporal.api.workflowservice.v1.WorkflowService/CreateSchedule": 1, "/temporal.api.workflowservice.v1.WorkflowService/StartBatchOperation": 1, "/temporal.api.workflowservice.v1.WorkflowService/StartActivityExecution": 1, + "/temporal.api.workflowservice.v1.WorkflowService/StartCallbackExecution": 1, "/temporal.api.workflowservice.v1.WorkflowService/StartNexusOperationExecution": 1, DispatchNexusTaskByNamespaceAndTaskQueueAPIName: 1, DispatchNexusTaskByEndpointAPIName: 1, @@ -145,7 +146,9 @@ var ( "/temporal.api.workflowservice.v1.WorkflowService/UpdateTaskQueueConfig": 2, "/temporal.api.workflowservice.v1.WorkflowService/RequestCancelActivityExecution": 2, "/temporal.api.workflowservice.v1.WorkflowService/TerminateActivityExecution": 2, + "/temporal.api.workflowservice.v1.WorkflowService/TerminateCallbackExecution": 2, "/temporal.api.workflowservice.v1.WorkflowService/DeleteActivityExecution": 2, + "/temporal.api.workflowservice.v1.WorkflowService/DeleteCallbackExecution": 2, "/temporal.api.workflowservice.v1.WorkflowService/RequestCancelNexusOperationExecution": 2, "/temporal.api.workflowservice.v1.WorkflowService/TerminateNexusOperationExecution": 2, "/temporal.api.workflowservice.v1.WorkflowService/DeleteNexusOperationExecution": 2, @@ -155,6 +158,7 @@ var ( // P3: Status Querying APIs "/temporal.api.workflowservice.v1.WorkflowService/DescribeWorkflowExecution": 3, "/temporal.api.workflowservice.v1.WorkflowService/DescribeActivityExecution": 3, + "/temporal.api.workflowservice.v1.WorkflowService/DescribeCallbackExecution": 3, "/temporal.api.workflowservice.v1.WorkflowService/DescribeTaskQueue": 3, "/temporal.api.workflowservice.v1.WorkflowService/GetWorkerBuildIdCompatibility": 3, "/temporal.api.workflowservice.v1.WorkflowService/GetWorkerVersioningRules": 3, @@ -181,7 +185,8 @@ var ( // P4: Poll APIs and other low priority APIs "/temporal.api.workflowservice.v1.WorkflowService/PollNexusOperationExecution": 4, - "/temporal.api.workflowservice.v1.WorkflowService/PollActivityExecution": 4, // TODO(saa-preview): should it be 4 or 3? + "/temporal.api.workflowservice.v1.WorkflowService/PollActivityExecution": 4, // TODO(saa-preview): Should it be 4 or 3? Same PollCallback. + "/temporal.api.workflowservice.v1.WorkflowService/PollCallbackExecution": 4, "/temporal.api.workflowservice.v1.WorkflowService/PollWorkflowTaskQueue": 4, "/temporal.api.workflowservice.v1.WorkflowService/PollActivityTaskQueue": 4, "/temporal.api.workflowservice.v1.WorkflowService/PollWorkflowExecutionUpdate": 4, @@ -216,7 +221,9 @@ var ( "/temporal.api.workflowservice.v1.WorkflowService/ListWorkers": 1, "/temporal.api.workflowservice.v1.WorkflowService/DescribeWorker": 1, "/temporal.api.workflowservice.v1.WorkflowService/CountActivityExecutions": 1, + "/temporal.api.workflowservice.v1.WorkflowService/CountCallbackExecutions": 1, "/temporal.api.workflowservice.v1.WorkflowService/ListActivityExecutions": 1, + "/temporal.api.workflowservice.v1.WorkflowService/ListCallbackExecutions": 1, "/temporal.api.workflowservice.v1.WorkflowService/CountNexusOperationExecutions": 1, "/temporal.api.workflowservice.v1.WorkflowService/ListNexusOperationExecutions": 1, diff --git a/service/frontend/configs/quotas_test.go b/service/frontend/configs/quotas_test.go index 173b28f2cda..615ac6eb974 100644 --- a/service/frontend/configs/quotas_test.go +++ b/service/frontend/configs/quotas_test.go @@ -106,6 +106,8 @@ func (s *quotasSuite) TestVisibilityAPIs() { "/temporal.api.workflowservice.v1.WorkflowService/CountActivityExecutions": {}, "/temporal.api.workflowservice.v1.WorkflowService/ListActivityExecutions": {}, + "/temporal.api.workflowservice.v1.WorkflowService/CountCallbackExecutions": {}, + "/temporal.api.workflowservice.v1.WorkflowService/ListCallbackExecutions": {}, "/temporal.api.workflowservice.v1.WorkflowService/CountNexusOperationExecutions": {}, "/temporal.api.workflowservice.v1.WorkflowService/ListNexusOperationExecutions": {}, } diff --git a/service/frontend/fx.go b/service/frontend/fx.go index a40077a8aa8..45dd0127083 100644 --- a/service/frontend/fx.go +++ b/service/frontend/fx.go @@ -130,6 +130,7 @@ var Module = fx.Options( chasmnexus.Module, chasmworkflow.Module, activity.FrontendModule, + callback.FrontendModule, fx.Provide(visibility.ChasmVisibilityManagerProvider), fx.Provide(chasm.ChasmVisibilityInterceptorProvider), ) @@ -888,6 +889,7 @@ func HandlerProvider( healthInterceptor *interceptor.HealthInterceptor, scheduleSpecBuilder *scheduler.SpecBuilder, activityHandler activity.FrontendHandler, + callbackHandler callback.FrontendHandler, callbackValidator callback.Validator, nexusOperationHandler chasmnexus.FrontendHandler, registry *chasm.Registry, @@ -927,6 +929,7 @@ func HandlerProvider( scheduleSpecBuilder, httpEnabled(cfg, serviceName), activityHandler, + callbackHandler, nexusOperationHandler, registry, workerDeploymentReadRateLimiter, diff --git a/service/frontend/service.go b/service/frontend/service.go index ac561bec1a1..954385c7e3b 100644 --- a/service/frontend/service.go +++ b/service/frontend/service.go @@ -10,6 +10,7 @@ import ( "go.temporal.io/api/workflowservice/v1" "go.temporal.io/server/api/adminservice/v1" "go.temporal.io/server/chasm/lib/activity" + "go.temporal.io/server/chasm/lib/callback" chasmnexus "go.temporal.io/server/chasm/lib/nexusoperation" "go.temporal.io/server/common/dynamicconfig" "go.temporal.io/server/common/log" @@ -237,6 +238,7 @@ type Config struct { // CHASM archetypes Activity *activity.Config + Callback *callback.Config } // IsExperimentAllowed checks if an experiment is enabled for a given namespace in the dynamic config. @@ -404,7 +406,9 @@ func NewConfig( HTTPAllowedHosts: dynamicconfig.FrontendHTTPAllowedHosts.Get(dc), AllowedExperiments: dynamicconfig.FrontendAllowedExperiments.Get(dc), + // CHASM component configurations. Activity: activity.ConfigProvider(dc), + Callback: callback.ConfigProvider(dc), } } diff --git a/service/frontend/workflow_handler.go b/service/frontend/workflow_handler.go index 85263168f5f..c823760b84f 100644 --- a/service/frontend/workflow_handler.go +++ b/service/frontend/workflow_handler.go @@ -110,15 +110,18 @@ const ( ) type ( - // ActivityHandler is the activity frontend handler, aliased to avoid embedding name collision. - ActivityHandler = activity.FrontendHandler - // NexusOperationHandler is the nexus operation frontend handler, aliased to avoid embedding name collision. + // Aliases for CHASM components to avoid name collisions. + + ActivityHandler = activity.FrontendHandler + CallbackHandler = callback.FrontendHandler NexusOperationHandler = chasmnexus.FrontendHandler // WorkflowHandler - gRPC handler interface for workflowservice WorkflowHandler struct { workflowservice.UnsafeWorkflowServiceServer + ActivityHandler + CallbackHandler NexusOperationHandler status int32 @@ -329,12 +332,14 @@ func NewWorkflowHandler( scheduleSpecBuilder *scheduler.SpecBuilder, httpEnabled bool, activityHandler activity.FrontendHandler, + callbackHandler callback.FrontendHandler, nexusOperationHandler chasmnexus.FrontendHandler, registry *chasm.Registry, workerDeploymentReadRateLimiter quotas.RequestRateLimiter, ) *WorkflowHandler { handler := &WorkflowHandler{ ActivityHandler: activityHandler, + CallbackHandler: callbackHandler, NexusOperationHandler: nexusOperationHandler, status: common.DaemonStatusInitialized, callbackValidator: callbackValidator, diff --git a/service/frontend/workflow_handler_test.go b/service/frontend/workflow_handler_test.go index d39f7a47c17..bce54487f54 100644 --- a/service/frontend/workflow_handler_test.go +++ b/service/frontend/workflow_handler_test.go @@ -209,6 +209,7 @@ func (s *WorkflowHandlerSuite) getWorkflowHandler(config *Config) *WorkflowHandl scheduler.NewSpecBuilder(), true, nil, // Not testing activity handler here + nil, // Not testing callback handler here nexusoperation.NewFrontendHandler( nil, nil, diff --git a/service/history/fx.go b/service/history/fx.go index bc34c37ee2b..c98edb97e7c 100644 --- a/service/history/fx.go +++ b/service/history/fx.go @@ -8,6 +8,7 @@ import ( "go.temporal.io/server/api/historyservice/v1" "go.temporal.io/server/chasm" "go.temporal.io/server/chasm/lib/activity" + "go.temporal.io/server/chasm/lib/callback" chasmnexus "go.temporal.io/server/chasm/lib/nexusoperation" chasmworkflow "go.temporal.io/server/chasm/lib/workflow" "go.temporal.io/server/common" @@ -99,6 +100,7 @@ var Module = fx.Options( hsmnexusoperations.Module, fx.Invoke(hsmnexusworkflow.RegisterCommandHandlers), activity.HistoryModule, + callback.HistoryModule, chasmnexus.Module, chasmworkflow.Module, ) diff --git a/temporal/fx.go b/temporal/fx.go index 8905c1b5567..1b35c13493b 100644 --- a/temporal/fx.go +++ b/temporal/fx.go @@ -21,7 +21,6 @@ import ( "go.temporal.io/api/serviceerror" persistencespb "go.temporal.io/server/api/persistence/v1" "go.temporal.io/server/chasm" - chasmcallback "go.temporal.io/server/chasm/lib/callback" chasmscheduler "go.temporal.io/server/chasm/lib/scheduler" "go.temporal.io/server/client" "go.temporal.io/server/common/archiver" @@ -155,7 +154,6 @@ var ( ChasmLibraryOptions = fx.Options( chasm.Module, chasmscheduler.Module, - chasmcallback.Module, ) ) diff --git a/tests/standalone_callbacks_test.go b/tests/standalone_callbacks_test.go new file mode 100644 index 00000000000..7c7bf9e2e57 --- /dev/null +++ b/tests/standalone_callbacks_test.go @@ -0,0 +1,1244 @@ +package tests + +import ( + "context" + "fmt" + "net/http" + "net/http/httptest" + "slices" + "testing" + "time" + + "github.com/google/uuid" + "github.com/nexus-rpc/sdk-go/nexus" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + callbackpb "go.temporal.io/api/callback/v1" + commonpb "go.temporal.io/api/common/v1" + enumspb "go.temporal.io/api/enums/v1" + failurepb "go.temporal.io/api/failure/v1" + historypb "go.temporal.io/api/history/v1" + nexuspb "go.temporal.io/api/nexus/v1" + "go.temporal.io/api/operatorservice/v1" + "go.temporal.io/api/workflowservice/v1" + "go.temporal.io/sdk/client" + "go.temporal.io/sdk/temporal" + "go.temporal.io/sdk/worker" + "go.temporal.io/sdk/workflow" + "go.temporal.io/server/chasm/lib/callback" + "go.temporal.io/server/common/dynamicconfig" + commonnexus "go.temporal.io/server/common/nexus" + "go.temporal.io/server/common/nexus/nexustest" + "go.temporal.io/server/common/testing/parallelsuite" + hsmcallbacks "go.temporal.io/server/components/callbacks" + "go.temporal.io/server/tests/testcore" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/durationpb" +) + +// Test suite for the Nexus "Standalone Callbacks". Which are Nexus operations corresponding to +// aysynchronous actions that take place outside of Temporal. (e.g. waiting for a payment to +// be processed, or webhook to be delivered, etc.) + +// Minimal information that an external service would need to report the results of a callback. +type externalRequestInfo struct { + Namespace string + Token string + URL string + + // Result of the callback, success or failure. + Result *callbackpb.CallbackExecutionCompletion +} + +// Result of the fake service after receiving a request. If the `StartCallbackExecution` request +// was accepted, reports the CallbackID, RunID. Otherwise the error. +type externalRequestResult struct { + CallbackID string + + // Mutually exclusive with Error. + RunID string + Error error +} + +// fakeExternalService simulates a service doing work asynchronously outside of Temporal. +// Test cases will create a Nexus handler that will start a Nexus operation, and then +// pass the context information to the fake service (via externalRequestInfo). The fake +// service will then notify Temporal the work is done (via StartCallbackExecution), and +// then put metadata into the `requestResults` channel. +type fakeExternalService struct { + incommingRequests chan<- externalRequestInfo + requestResults <-chan externalRequestResult +} + +// startFakeExternalService starts a new fake service Goroutine, using the supplied client. +// Will shut down and close its channels when the given context is complete. +func startFakeExternalService(ctx context.Context, c workflowservice.WorkflowServiceClient) *fakeExternalService { + // Channels are buffered so that tests don't block on reads/writes. + input := make(chan externalRequestInfo, 4) + output := make(chan externalRequestResult, 4) + + // Logic of the actual faux return, processing requests. Reads requests to do work (from + // a Nexus handler), and then reports the results out-of-band from the Nexus operation. + go func() { + defer close(input) + defer close(output) + + for { + select { + case incommingRequest := <-input: + // Uniquely identify the callback execution. + callbackID := "faux-svc-callback-" + uuid.NewString() + + targetCallback := &commonpb.Callback{ + Variant: &commonpb.Callback_Nexus_{ + Nexus: &commonpb.Callback_Nexus{ + Url: incommingRequest.URL, + Token: incommingRequest.Token, + }, + }, + } + + resp, err := c.StartCallbackExecution(ctx, &workflowservice.StartCallbackExecutionRequest{ + Namespace: incommingRequest.Namespace, + Identity: "faux-external-service", + RequestId: uuid.NewString(), + CallbackId: callbackID, + Callback: targetCallback, + Input: &workflowservice.StartCallbackExecutionRequest_Completion{ + Completion: incommingRequest.Result, + }, + ScheduleToCloseTimeout: durationpb.New(10 * time.Second), + }) + + // Make the result available to the testcase. + output <- externalRequestResult{ + CallbackID: callbackID, + RunID: resp.GetRunId(), + Error: err, + } + case <-ctx.Done(): + return + } + } + }() + + return &fakeExternalService{ + incommingRequests: input, + requestResults: output, + } +} + +func TestStandaloneCallbackSuite(t *testing.T) { + parallelsuite.Run(t, &StandaloneCallbackSuite{}) +} + +type StandaloneCallbackSuite struct { + parallelsuite.Suite[*StandaloneCallbackSuite] +} + +func (s *StandaloneCallbackSuite) newEnv() *testcore.TestEnv { + return testcore.NewEnv(s.T(), + testcore.WithDynamicConfig(dynamicconfig.EnableChasm, true), + testcore.WithDynamicConfig(dynamicconfig.EnableCHASMCallbacks, true), + testcore.WithDynamicConfig(callback.EnableStandaloneExecutions, true), + + // QUIRK: The configuration for setting the callback allow list + // is in the HSM code. The chasm/lib/callback AllowedAddresses + // field isn't used. + testcore.WithDynamicConfig(hsmcallbacks.AllowedAddresses, []any{ + map[string]any{"Pattern": "*", "AllowInsecure": true}, + }), + ) +} + +// Calls StartCallbackExecution, reporting the result of a callback that doesn't exist. +// +// This isn't expected to return an error, since the API is written to support the external +// operation having completed before the Temporal/Nexus side registration has completed. +// +// However, since the Nexus callback isn't even registered, the callback execution will +// aways result in timing out. +func (s *StandaloneCallbackSuite) callStartCallbackExecutionToBogusCallback( + ctx context.Context, + env *testcore.TestEnv, + callbackID string, + timeout time.Duration, +) *workflowservice.StartCallbackExecutionResponse { + s.T().Helper() + cb := &commonpb.Callback{ + Variant: &commonpb.Callback_Nexus_{ + Nexus: &commonpb.Callback_Nexus{ + Url: "http://localhost:1/nonexistent", + }, + }, + } + + completion := &callbackpb.CallbackExecutionCompletion{ + Result: &callbackpb.CallbackExecutionCompletion_Success{ + Success: testcore.MustToPayload(s.T(), "some-result"), + }, + } + + return s.callStartCallbackExecution(ctx, env, callbackID, cb, completion, timeout) +} + +// Call the StartCallbackExecution API with the given parameters. +func (s *StandaloneCallbackSuite) callStartCallbackExecution( + ctx context.Context, + env *testcore.TestEnv, + callbackID string, + cb *commonpb.Callback, + completion *callbackpb.CallbackExecutionCompletion, + timeout time.Duration, +) *workflowservice.StartCallbackExecutionResponse { + s.T().Helper() + + frontend := env.FrontendClient() + namespace := env.Namespace() + + resp, err := frontend.StartCallbackExecution(ctx, &workflowservice.StartCallbackExecutionRequest{ + Namespace: namespace.String(), + Identity: "startCallbackExecution", + RequestId: uuid.NewString(), + CallbackId: callbackID, + Callback: cb, + Input: &workflowservice.StartCallbackExecutionRequest_Completion{ + Completion: completion, + }, + ScheduleToCloseTimeout: durationpb.New(timeout), + }) + s.NoError(err, "Error calling StartCallbackExecution to bogus callback") + + return resp +} + +// TestBasicaOperation tests that a Nexus operation started by a workflow can be completed using the +// StartCallbackExecution API to deliver the Nexus completion to the operation's callback URL. +// +// Flow: +// 1. A caller workflow (`callerWf`) starts a Nexus operation via an external endpoint. +// 2. The Nexus handler (`nexusHandler`) starts the operation asynchronously and captures +// the callback URL and token. +// 3. The Nexus handler passes the data to a fake external service (`fakeSvc`), which then +// StartCallbackExecution API with a successful payload. +// 4. The CHASM callback execution delivers the Nexus completion to the callback URL. +// 5. The caller workflow receives the operation's result and completes. +// +// The test is ran in two variants. First, where the out-of-band service reports a successful +// compoetion result. The second reports a failure. Causing the calling workflow to fail. +func (s *StandaloneCallbackSuite) TestBasicOperation() { + + // Implementation of the test scenario, standing up the workflow, Nexus operation, + // external service, etc. + // + // Takes a CallbackExecutionCompletion to be reported by the external service, and a + // verification function to test the calling Workflow behaved as expected. + runStandaloneCallbackScenario := func( + s *StandaloneCallbackSuite, + env *testcore.TestEnv, + ctx context.Context, + completionResult *callbackpb.CallbackExecutionCompletion, + workflowRunVerificationFn func(client.WorkflowRun), + ) { + taskQueue := testcore.RandomizeStr(s.T().Name()) + + // Fake External Service + // + // Start a fake external service to handle async requests, and report + // their results to Temporal. + fakeSvc := startFakeExternalService(ctx, env.FrontendClient()) + + // Nexus Handler + // + // Set up an external Nexus handler that starts operations asynchronously + // and sends the callback URL and token to the fake service. The Nexus + // handler terminates, while the fake service reports results out-of-band. + nexusEndpointName := testcore.RandomizedNexusEndpoint(s.T().Name()) + const ( + nexusSvcName = "nexus-service" + nexusSvcOp = "nexus-operation" + ) + + nexusHandler := nexustest.Handler{ + OnStartOperation: func( + ctx context.Context, + service, operation string, + input *nexus.LazyValue, + options nexus.StartOperationOptions, + ) (nexus.HandlerStartOperationResult[any], error) { + s.Equal(nexusSvcName, service) + s.Equal(nexusSvcOp, operation) + + // Send the request to the external service to do the work. + fakeSvc.incommingRequests <- externalRequestInfo{ + Namespace: env.Namespace().String(), + Token: options.CallbackHeader.Get(commonnexus.CallbackTokenHeader), + URL: options.CallbackURL, + + Result: completionResult, + } + + // End the Nexus operation. + return &nexus.HandlerStartOperationResultAsync{ + OperationToken: fmt.Sprintf("operation-token-%s", uuid.NewString()), + }, nil + }, + } + + // Start the Nexus server. + listenAddr := nexustest.AllocListenAddress() + nexustest.NewNexusServer(s.T(), listenAddr, nexusHandler) + + // Register the Nexus endpoint with the Temporal service. + createNexusEndpointReq := &operatorservice.CreateNexusEndpointRequest{ + Spec: &nexuspb.EndpointSpec{ + Name: nexusEndpointName, + Target: &nexuspb.EndpointTarget{ + Variant: &nexuspb.EndpointTarget_External_{ + External: &nexuspb.EndpointTarget_External{ + Url: "http://" + listenAddr, + }, + }, + }, + }, + } + _, err := env.OperatorClient().CreateNexusEndpoint(ctx, createNexusEndpointReq) + s.NoError(err, "Error registering Nexus endpoint") + + // Calling Workflow + // + // This will create the Nexus operation, invoking the Handler workflow. The + // workflow will then block until the Nexus operation completes, which will + // not be until the fake external service has reported the callback's result. + callerWf := func(ctx workflow.Context) (string, error) { + c := workflow.NewNexusClient(nexusEndpointName, nexusSvcName) + fut := c.ExecuteOperation(ctx, nexusSvcOp, "input", workflow.NexusOperationOptions{}) + + var nexusOpResult string + err := fut.Get(ctx, &nexusOpResult) + return nexusOpResult, err + } + + // Run the Test + // + // Construct and start the calling workflow Worker. Then wait for completion. + callerWfWorker := worker.New(env.SdkClient(), taskQueue, worker.Options{}) + callerWfWorker.RegisterWorkflow(callerWf) + s.NoError(callerWfWorker.Start(), "Error starting calling workflow Worker") + defer callerWfWorker.Stop() + + // Start + startOpts := client.StartWorkflowOptions{ + TaskQueue: taskQueue, + } + callerWfRun, err := env.SdkClient().ExecuteWorkflow(ctx, startOpts, callerWf) + s.NoError(err, "Error running the caller Workflow") + + // Defer to a caller-supplied verification function to wait on the caller + // workflow's result and verify the success/failure as applicable. + workflowRunVerificationFn(callerWfRun) + + // If the fake service was called correctly, we expect to see the result + // of it doing the out-of-band work. + fakeSvcResult := <-fakeSvc.requestResults + s.NoError(fakeSvcResult.Error) + + // Additional Verification + // + // Use the Describe and Poll APIs to fetch the callback execution. + descResp, err := env.FrontendClient().DescribeCallbackExecution(ctx, &workflowservice.DescribeCallbackExecutionRequest{ + Namespace: env.Namespace().String(), + CallbackId: fakeSvcResult.CallbackID, + IncludeInput: true, + IncludeOutcome: false, + }) + s.NoError(err, "Error describing the callback execution") + s.True(proto.Equal(completionResult, descResp.Input)) + s.Nil(descResp.Outcome) + + gotInfo := descResp.GetInfo() + s.NotNil(gotInfo, "Got nil Info in response") + s.Equal(fakeSvcResult.CallbackID, gotInfo.GetCallbackId()) + s.NotNil(gotInfo.GetCreateTime()) + + // Confirm the outcome of the callback was a successful delivery. + // (Even if it was for a failed completion.) + s.Equal(enumspb.CALLBACK_EXECUTION_STATUS_SUCCEEDED, gotInfo.GetStatus()) + s.Equal(enumspb.CALLBACK_STATE_SUCCEEDED, gotInfo.GetState()) + + // Poll to verify the outcome as well. + pollResp, err := env.FrontendClient().PollCallbackExecution(ctx, &workflowservice.PollCallbackExecutionRequest{ + Namespace: env.Namespace().String(), + CallbackId: fakeSvcResult.CallbackID, + }) + s.NoError(err, "Error polling completed callback execution") + + outcome := pollResp.GetOutcome() + s.NotNil(outcome, "Got nil Outcome") + + // Confirm the outcome of the callback was a successful delivery. + // (Even if it was for a failed completion.) + s.NotNil(outcome.GetSuccess()) + s.Nil(outcome.GetFailure()) + } + + s.Run("success", func(s *StandaloneCallbackSuite) { + env := s.newEnv() + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + wantPayloadStr := "successfully delivered payload via external svc" + successCompletion := &callbackpb.CallbackExecutionCompletion{ + Result: &callbackpb.CallbackExecutionCompletion_Success{ + Success: testcore.MustToPayload(s.T(), wantPayloadStr), + }, + } + + verifyWorkflowRunFn := func(workflowRun client.WorkflowRun) { + var gotPayload string + s.NoError(workflowRun.Get(ctx, &gotPayload)) + s.Equal(wantPayloadStr, gotPayload) + } + + runStandaloneCallbackScenario(s, env, ctx, successCompletion, verifyWorkflowRunFn) + }) + + s.Run("failure", func(s *StandaloneCallbackSuite) { + env := s.newEnv() + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + failureCompletion := &callbackpb.CallbackExecutionCompletion{ + Result: &callbackpb.CallbackExecutionCompletion_Failure{ + Failure: &failurepb.Failure{ + Message: "operation failed from standalone callback", + FailureInfo: &failurepb.Failure_ApplicationFailureInfo{ + ApplicationFailureInfo: &failurepb.ApplicationFailureInfo{ + NonRetryable: true, + }, + }, + }, + }, + } + + verifyWorkflowRunFn := func(workflowRun client.WorkflowRun) { + // The workflow should fail with a NexusOperationError wrapping the failure. + var unusedResult string + err := workflowRun.Get(ctx, &unusedResult) + + // Confirm the (super-long) error contains the key information. + s.ErrorContains(err, "workflow execution error") + s.ErrorContains(err, workflowRun.GetRunID()) + s.ErrorContains(err, "nexus operation completed unsuccessfully") + s.ErrorContains(err, "operation failed from standalone callback") + + // Confirm the error's type is correct as well. + var wee *temporal.WorkflowExecutionError + s.ErrorAs(err, &wee) + + var noe *temporal.NexusOperationError + s.ErrorAs(wee, &noe) + s.Contains(noe.Error(), "operation failed from standalone callback") + } + + runStandaloneCallbackScenario(s, env, ctx, failureCompletion, verifyWorkflowRunFn) + }) +} + +// TestPollCallbackExecution tests that PollCallbackExecution long-polls for the outcome +// of a callback execution. It returns an empty response when the poll times out, and +// the CallbackExecutionOutcome when the callback reaches a terminal state. +func (s *StandaloneCallbackSuite) TestPollCallbackExecution() { + env := s.newEnv() + + s.Run("returns_empty_for_non_terminal", func(s *StandaloneCallbackSuite) { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + callbackID := "poll-test-" + uuid.NewString() + + // Report the result of a non-existent, non-routable callback. The CallbackExecution + // will linger for 1m before timing out. + s.callStartCallbackExecutionToBogusCallback(ctx, env, callbackID, time.Minute) + + // Poll a non-terminal callback with a short timeout. Should return an empty response. + // NOTE: Passing a shorter timeout like 1s will cause failure with "Workflow is busy." + shortCtx, shortCancel := context.WithTimeout(ctx, 2*time.Second) + defer shortCancel() + pollResp, err := env.FrontendClient().PollCallbackExecution(shortCtx, &workflowservice.PollCallbackExecutionRequest{ + Namespace: env.Namespace().String(), + CallbackId: callbackID, + }) + s.NoError(err) + s.Nil(pollResp.GetOutcome()) + + // Terminate the CallbackExecution resource, then poll should return the outcome. + _, err = env.FrontendClient().TerminateCallbackExecution(ctx, &workflowservice.TerminateCallbackExecutionRequest{ + Namespace: env.Namespace().String(), + CallbackId: callbackID, + Identity: s.T().Name(), + RequestId: uuid.NewString(), + Reason: "testing poll behavior", + }) + s.NoError(err, "Unable to terminate CallbackExecution") + + pollResp, err = env.FrontendClient().PollCallbackExecution(ctx, &workflowservice.PollCallbackExecutionRequest{ + Namespace: env.Namespace().String(), + CallbackId: callbackID, + }) + s.NoError(err) + s.NotNil(pollResp.GetOutcome().GetFailure()) + s.Equal("testing poll behavior", pollResp.GetOutcome().GetFailure().GetMessage()) + }) + + s.Run("blocks_until_complete", func(s *StandaloneCallbackSuite) { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + callbackID := "poll-blocks-" + uuid.NewString() + s.callStartCallbackExecutionToBogusCallback(ctx, env, callbackID, time.Minute) + + // Start a long-poll in a goroutine. + type pollResult struct { + resp *workflowservice.PollCallbackExecutionResponse + err error + } + resultCh := make(chan pollResult, 1) + go func() { + resp, err := env.FrontendClient().PollCallbackExecution(ctx, &workflowservice.PollCallbackExecutionRequest{ + Namespace: env.Namespace().String(), + CallbackId: callbackID, + }) + resultCh <- pollResult{resp: resp, err: err} + }() + + // Verify the poll is still blocking and hasn't returned yet. + select { + case <-resultCh: + s.Fail("expected poll to block, but it returned before terminate") + case <-time.After(500 * time.Millisecond): + } + + // Terminate the CallbackExecution. Confirm that the poll result (from Goroutine) has completed. + _, err := env.FrontendClient().TerminateCallbackExecution(ctx, &workflowservice.TerminateCallbackExecutionRequest{ + Namespace: env.Namespace().String(), + CallbackId: callbackID, + Identity: "test", + RequestId: uuid.NewString(), + Reason: "testing poll blocks", + }) + s.NoError(err) + + result := <-resultCh + s.NoError(result.err) + s.NotNil(result.resp.GetOutcome().GetFailure()) + s.Equal("testing poll blocks", result.resp.GetOutcome().GetFailure().GetMessage()) + }) + + s.Run("returns_run_id", func(s *StandaloneCallbackSuite) { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + callbackID := "poll-runid-" + uuid.NewString() + startResp := s.callStartCallbackExecutionToBogusCallback(ctx, env, callbackID, time.Minute) + + gotRunID := startResp.GetRunId() + + // Terminate so poll returns immediately. + _, err := env.FrontendClient().TerminateCallbackExecution(ctx, &workflowservice.TerminateCallbackExecutionRequest{ + Namespace: env.Namespace().String(), + CallbackId: callbackID, + Identity: "test", + RequestId: uuid.NewString(), + Reason: "testing run_id", + }) + s.NoError(err) + + // Confirm the Poll result includes the RunID of the initial StartCallbackExecution request. + pollResp, err := env.FrontendClient().PollCallbackExecution(ctx, &workflowservice.PollCallbackExecutionRequest{ + Namespace: env.Namespace().String(), + CallbackId: callbackID, + }) + s.NoError(err) + s.Equal(gotRunID, pollResp.GetRunId()) + s.NotNil(pollResp.GetOutcome().GetFailure()) + }) + + s.Run("poll_after_timeout", func(s *StandaloneCallbackSuite) { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + callbackID := "poll-timeout-" + uuid.NewString() + // Start with a very short schedule-to-close timeout so it times out quickly. + s.callStartCallbackExecutionToBogusCallback(ctx, env, callbackID, 500*time.Millisecond) + + // Wait for the callback to time out, then poll for the outcome. + const ( + waitUpTo = 3 * time.Second + checkInterval = 200 * time.Millisecond + ) + s.EventuallyWithT(func(t *assert.CollectT) { + pollResp, err := env.FrontendClient().PollCallbackExecution(ctx, &workflowservice.PollCallbackExecutionRequest{ + Namespace: env.Namespace().String(), + CallbackId: callbackID, + }) + require.NoError(t, err) + require.NotNil(t, pollResp.GetOutcome()) + + require.NotNil(t, pollResp.GetOutcome().GetFailure()) + require.NotNil(t, pollResp.GetOutcome().GetFailure().GetTimeoutFailureInfo()) + require.Equal(t, enumspb.TIMEOUT_TYPE_SCHEDULE_TO_CLOSE, pollResp.GetOutcome().GetFailure().GetTimeoutFailureInfo().GetTimeoutType()) + }, waitUpTo, checkInterval) + }) +} + +// TestDeleteCallbackExecution verifies that a standalone callback execution can be deleted. +// Delete terminates the callback if it's still running, then marks it for cleanup. +func (s *StandaloneCallbackSuite) TestDeleteCallbackExecution() { + env := s.newEnv() + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + // Create a callback that points to a non-existent URL so it won't complete on its own. + // The callback will be in SCHEDULED/BACKING_OFF state when we delete it. + callbackID := "delete-test-" + uuid.NewString() + startResp := s.callStartCallbackExecutionToBogusCallback(ctx, env, callbackID, time.Minute) + runID := startResp.GetRunId() + + // Describe using run_id to verify it was created. + descResp, err := env.FrontendClient().DescribeCallbackExecution(ctx, &workflowservice.DescribeCallbackExecutionRequest{ + Namespace: env.Namespace().String(), + CallbackId: callbackID, + RunId: runID, + }) + s.NoError(err) + s.Equal(callbackID, descResp.GetInfo().GetCallbackId()) + s.Equal(runID, descResp.GetInfo().GetRunId()) + s.Equal(enumspb.CALLBACK_EXECUTION_STATUS_RUNNING, descResp.GetInfo().GetStatus()) + + // Delete with wrong run_id should fail. + _, err = env.FrontendClient().DeleteCallbackExecution(ctx, &workflowservice.DeleteCallbackExecutionRequest{ + Namespace: env.Namespace().String(), + CallbackId: callbackID, + RunId: uuid.NewString(), + }) + s.ErrorContains(err, fmt.Sprintf("callback not found for ID: %s", callbackID)) + + // Delete the callback execution using correct run_id. + _, err = env.FrontendClient().DeleteCallbackExecution(ctx, &workflowservice.DeleteCallbackExecutionRequest{ + Namespace: env.Namespace().String(), + CallbackId: callbackID, + RunId: runID, + }) + s.NoError(err) + + // Describe after delete — the callback should eventually be not found. + const ( + waitUpTo = 3 * time.Second + checkInterval = 100 * time.Millisecond + ) + s.EventuallyWithT(func(t *assert.CollectT) { + _, err := env.FrontendClient().DescribeCallbackExecution(ctx, &workflowservice.DescribeCallbackExecutionRequest{ + Namespace: env.Namespace().String(), + CallbackId: callbackID, + RunId: runID, + }) + require.ErrorContains(t, err, "not found") + }, waitUpTo, checkInterval) +} + +// TestStartCallbackExecution_InvalidArguments verifies request validation. +func (s *StandaloneCallbackSuite) TestStartCallbackExecution_InvalidArguments() { + env := s.newEnv() + + validCallback := &commonpb.Callback{ + Variant: &commonpb.Callback_Nexus_{ + Nexus: &commonpb.Callback_Nexus{ + Url: "http://localhost:1/callback", + }, + }, + } + validCompletion := &callbackpb.CallbackExecutionCompletion{ + Result: &callbackpb.CallbackExecutionCompletion_Success{ + Success: testcore.MustToPayload(s.T(), "result"), + }, + } + + tests := []struct { + name string + mutate func(req *workflowservice.StartCallbackExecutionRequest) + errMsg string + }{ + { + name: "missing callback_id", + mutate: func(req *workflowservice.StartCallbackExecutionRequest) { + req.CallbackId = "" + }, + errMsg: "CallbackId is not set", + }, + { + name: "missing callback", + mutate: func(req *workflowservice.StartCallbackExecutionRequest) { + req.Callback = nil + }, + errMsg: "Callback is not set", + }, + { + name: "missing callback URL", + mutate: func(req *workflowservice.StartCallbackExecutionRequest) { + req.Callback = &commonpb.Callback{ + Variant: &commonpb.Callback_Nexus_{ + Nexus: &commonpb.Callback_Nexus{Url: ""}, + }, + } + }, + errMsg: "Callback URL is not set", + }, + { + name: "invalid callback URL scheme", + mutate: func(req *workflowservice.StartCallbackExecutionRequest) { + req.Callback = &commonpb.Callback{ + Variant: &commonpb.Callback_Nexus_{ + Nexus: &commonpb.Callback_Nexus{Url: "ftp://example.com/callback"}, + }, + } + }, + errMsg: "unknown scheme", + }, + { + name: "callback URL missing host", + mutate: func(req *workflowservice.StartCallbackExecutionRequest) { + req.Callback = &commonpb.Callback{ + Variant: &commonpb.Callback_Nexus_{ + Nexus: &commonpb.Callback_Nexus{Url: "http:///callback"}, + }, + } + }, + errMsg: "missing host", + }, + { + name: "missing completion", + mutate: func(req *workflowservice.StartCallbackExecutionRequest) { + req.Input = nil + }, + errMsg: "Completion is not set", + }, + { + name: "empty completion", + mutate: func(req *workflowservice.StartCallbackExecutionRequest) { + req.Input = &workflowservice.StartCallbackExecutionRequest_Completion{Completion: &callbackpb.CallbackExecutionCompletion{}} + }, + errMsg: "Completion must have either success or failure set", + }, + { + name: "missing schedule_to_close_timeout", + mutate: func(req *workflowservice.StartCallbackExecutionRequest) { + req.ScheduleToCloseTimeout = nil + }, + errMsg: "ScheduleToCloseTimeout must be set", + }, + } + + for _, tc := range tests { + s.Run(tc.name, func(s *StandaloneCallbackSuite) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + + req := &workflowservice.StartCallbackExecutionRequest{ + Namespace: env.Namespace().String(), + Identity: "test", + RequestId: uuid.NewString(), + CallbackId: "validation-test-" + uuid.NewString(), + Callback: validCallback, + Input: &workflowservice.StartCallbackExecutionRequest_Completion{Completion: validCompletion}, + ScheduleToCloseTimeout: durationpb.New(time.Minute), + } + tc.mutate(req) + _, err := env.FrontendClient().StartCallbackExecution(ctx, req) + s.Error(err) + s.Contains(err.Error(), tc.errMsg) + }) + } +} + +// TestStartCallbackExecution_DuplicateID verifies that starting a callback with +// an already-used callback_id returns an AlreadyExists error with a different request_id, +// and that the same request_id is idempotent (returns the existing run_id without error). +func (s *StandaloneCallbackSuite) TestStartCallbackExecution_DuplicateID() { + env := s.newEnv() + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + defer cancel() + + callbackID := "dup-test-" + uuid.NewString() + requestID := uuid.NewString() + + // Build the request explicitly so we can reuse the same request_id. + req := &workflowservice.StartCallbackExecutionRequest{ + Namespace: env.Namespace().String(), + Identity: "test", + RequestId: requestID, + CallbackId: callbackID, + Callback: &commonpb.Callback{ + Variant: &commonpb.Callback_Nexus_{ + Nexus: &commonpb.Callback_Nexus{ + Url: "http://localhost:1/nonexistent", + }, + }, + }, + Input: &workflowservice.StartCallbackExecutionRequest_Completion{Completion: &callbackpb.CallbackExecutionCompletion{ + Result: &callbackpb.CallbackExecutionCompletion_Success{ + Success: testcore.MustToPayload(s.T(), "some-result"), + }, + }}, + ScheduleToCloseTimeout: durationpb.New(time.Minute), + } + + // First call succeeds. + startResp, err := env.FrontendClient().StartCallbackExecution(ctx, req) + s.NoError(err) + existingRunID := startResp.GetRunId() + s.NotEmpty(existingRunID) + + // Same callback_id + same request_id should be idempotent (return existing run_id). + dupResp, err := env.FrontendClient().StartCallbackExecution(ctx, req) + s.NoError(err) + s.Equal(existingRunID, dupResp.GetRunId()) + + // Same callback_id + different request_id should fail with serviceerror.CallbackExecutionAlreadyStarted. + req.RequestId = uuid.NewString() + _, err = env.FrontendClient().StartCallbackExecution(ctx, req) + s.Error(err) + s.Contains(err.Error(), "callback execution already started") +} + +// TestListAndCountCallbackExecutions tests that standalone callback executions +// can be listed and counted via the visibility APIs, and verifies the returned data. +func (s *StandaloneCallbackSuite) TestListAndCountCallbackExecutions() { + env := s.newEnv() + ctx, cancel := context.WithTimeout(context.Background(), time.Second*60) + defer cancel() + + // Create two callback executions with known IDs. + callbackIDs := make([]string, 2) + for i := range 2 { + callbackIDs[i] = fmt.Sprintf("list-test-%d-%s", i, uuid.NewString()) + s.callStartCallbackExecutionToBogusCallback(ctx, env, callbackIDs[i], time.Minute) + } + + // List callback executions. Visibility indexing happens be async, so use EventuallyWithT. + const ( + waitUpTo = 5 * time.Second + checkInterval = 200 * time.Millisecond + ) + + // Verify returned data includes our callback IDs and has valid fields. + s.EventuallyWithT(func(t *assert.CollectT) { + listResp, err := env.FrontendClient().ListCallbackExecutions(ctx, &workflowservice.ListCallbackExecutionsRequest{ + Namespace: env.Namespace().String(), + PageSize: 10, + }) + require.NoError(t, err) + require.GreaterOrEqual(t, len(listResp.GetExecutions()), 2) + + // Collect returned callback IDs and verify fields. + foundIDs := make(map[string]bool) + for _, exec := range listResp.GetExecutions() { + foundIDs[exec.GetCallbackId()] = true + require.NotEmpty(t, exec.GetCallbackId()) + require.NotNil(t, exec.GetCreateTime()) + } + for _, id := range callbackIDs { + require.True(t, foundIDs[id], "expected callback %s in list response", id) + } + }, waitUpTo, checkInterval, "Didn't find expected results from ListCallbackExecutions") + + // List with ExecutionStatus query filter — newly started callbacks should be "Running". + s.EventuallyWithT(func(t *assert.CollectT) { + listResp, err := env.FrontendClient().ListCallbackExecutions(ctx, &workflowservice.ListCallbackExecutionsRequest{ + Namespace: env.Namespace().String(), + PageSize: 10, + Query: fmt.Sprintf(`ExecutionStatus = "Running" AND CallbackId = %q`, callbackIDs[0]), + }) + require.NoError(t, err) + require.Len(t, listResp.GetExecutions(), 1) + require.Equal(t, callbackIDs[0], listResp.GetExecutions()[0].GetCallbackId()) + }, waitUpTo, checkInterval, "Didn't find Running callback") + + // Terminate one callback to test filtering by terminal status. + _, err := env.FrontendClient().TerminateCallbackExecution(ctx, &workflowservice.TerminateCallbackExecutionRequest{ + Namespace: env.Namespace().String(), + CallbackId: callbackIDs[1], + Identity: "test", + RequestId: uuid.NewString(), + Reason: "testing list filter", + }) + s.NoError(err) + + // List with ExecutionStatus = "Terminated" should find the terminated callback. + s.EventuallyWithT(func(t *assert.CollectT) { + listResp, err := env.FrontendClient().ListCallbackExecutions(ctx, &workflowservice.ListCallbackExecutionsRequest{ + Namespace: env.Namespace().String(), + PageSize: 10, + Query: fmt.Sprintf(`ExecutionStatus = "Terminated" AND CallbackId = %q`, callbackIDs[1]), + }) + require.NoError(t, err) + require.Len(t, listResp.GetExecutions(), 1) + require.Equal(t, callbackIDs[1], listResp.GetExecutions()[0].GetCallbackId()) + }, waitUpTo, checkInterval, "Didn't find Terminated callbacks") + + // Count callback executions. + s.EventuallyWithT(func(t *assert.CollectT) { + countResp, err := env.FrontendClient().CountCallbackExecutions(ctx, &workflowservice.CountCallbackExecutionsRequest{ + Namespace: env.Namespace().String(), + }) + require.NoError(t, err) + require.GreaterOrEqual(t, countResp.GetCount(), int64(2)) + }, 10*time.Second, 200*time.Millisecond) + + // Count with ExecutionStatus filter should only count scheduled callbacks. + s.EventuallyWithT(func(t *assert.CollectT) { + countResp, err := env.FrontendClient().CountCallbackExecutions(ctx, &workflowservice.CountCallbackExecutionsRequest{ + Namespace: env.Namespace().String(), + Query: `ExecutionStatus = "Running"`, + }) + require.NoError(t, err) + require.GreaterOrEqual(t, countResp.GetCount(), int64(1)) + }, waitUpTo, checkInterval) +} + +// TestStartCallbackExecution_SearchAttributes tests that search attributes provided at start +// are persisted and can be used to query callback executions via list filtering. +func (s *StandaloneCallbackSuite) TestStartCallbackExecution_SearchAttributes() { + env := s.newEnv() + ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) + defer cancel() + + callbackID := "sa-test-" + uuid.NewString() + saValue := "sa-test-value-" + uuid.NewString() + + _, err := env.FrontendClient().StartCallbackExecution(ctx, &workflowservice.StartCallbackExecutionRequest{ + Namespace: env.Namespace().String(), + Identity: "test", + RequestId: uuid.NewString(), + CallbackId: callbackID, + Callback: &commonpb.Callback{ + Variant: &commonpb.Callback_Nexus_{ + Nexus: &commonpb.Callback_Nexus{ + Url: "http://localhost:1/nonexistent", + }, + }, + }, + Input: &workflowservice.StartCallbackExecutionRequest_Completion{Completion: &callbackpb.CallbackExecutionCompletion{ + Result: &callbackpb.CallbackExecutionCompletion_Success{ + Success: testcore.MustToPayload(s.T(), "some-result"), + }, + }}, + ScheduleToCloseTimeout: durationpb.New(time.Minute), + SearchAttributes: &commonpb.SearchAttributes{ + IndexedFields: map[string]*commonpb.Payload{ + "CustomKeywordField": testcore.MustToPayload(s.T(), saValue), + }, + }, + }) + s.NoError(err) + + // Verify the search attribute is queryable via list. + s.EventuallyWithT(func(t *assert.CollectT) { + listResp, err := env.FrontendClient().ListCallbackExecutions(ctx, &workflowservice.ListCallbackExecutionsRequest{ + Namespace: env.Namespace().String(), + PageSize: 10, + Query: fmt.Sprintf(`CustomKeywordField = %q AND CallbackId = %q`, saValue, callbackID), + }) + require.NoError(t, err) + require.Len(t, listResp.GetExecutions(), 1) + require.Equal(t, callbackID, listResp.GetExecutions()[0].GetCallbackId()) + }, 10*time.Second, 200*time.Millisecond) +} + +// TestTerminateCallbackExecution tests terminate, run_id validation, and request ID idempotency. +func (s *StandaloneCallbackSuite) TestTerminateCallbackExecution() { + env := s.newEnv() + ctx, cancel := context.WithTimeout(context.Background(), time.Second*20) + defer cancel() + + callbackID := "terminate-test-" + uuid.NewString() + startResp := s.callStartCallbackExecutionToBogusCallback(ctx, env, callbackID, time.Minute) + requestID := uuid.NewString() + runID := startResp.GetRunId() + + // Wrong run_id should fail for describe, poll, and terminate. + wrongRunID := uuid.NewString() + + _, err := env.FrontendClient().DescribeCallbackExecution(ctx, &workflowservice.DescribeCallbackExecutionRequest{ + Namespace: env.Namespace().String(), + CallbackId: callbackID, + RunId: wrongRunID, + }) + s.Error(err) + + shortCtx, shortCancel := context.WithTimeout(ctx, time.Second*2) + defer shortCancel() + _, err = env.FrontendClient().PollCallbackExecution(shortCtx, &workflowservice.PollCallbackExecutionRequest{ + Namespace: env.Namespace().String(), + CallbackId: callbackID, + RunId: wrongRunID, + }) + s.Error(err) + + _, err = env.FrontendClient().TerminateCallbackExecution(ctx, &workflowservice.TerminateCallbackExecutionRequest{ + Namespace: env.Namespace().String(), + CallbackId: callbackID, + RunId: wrongRunID, + Identity: "test", + RequestId: uuid.NewString(), + Reason: "wrong run_id", + }) + s.Error(err) + + // Terminate with correct run_id and known request ID. + _, err = env.FrontendClient().TerminateCallbackExecution(ctx, &workflowservice.TerminateCallbackExecutionRequest{ + Namespace: env.Namespace().String(), + CallbackId: callbackID, + RunId: runID, + Identity: "test", + RequestId: requestID, + Reason: "testing terminate", + }) + s.NoError(err) + + // Describe after terminate — should be TERMINATED with correct run_id. + descResp, err := env.FrontendClient().DescribeCallbackExecution(ctx, &workflowservice.DescribeCallbackExecutionRequest{ + Namespace: env.Namespace().String(), + CallbackId: callbackID, + RunId: runID, + }) + s.NoError(err) + s.Equal(enumspb.CALLBACK_EXECUTION_STATUS_TERMINATED, descResp.GetInfo().GetStatus()) + s.Equal(enumspb.CALLBACK_STATE_TERMINATED, descResp.GetInfo().GetState()) + s.NotNil(descResp.GetInfo().GetCloseTime()) + s.Equal(runID, descResp.GetInfo().GetRunId()) + + // Poll to verify the outcome. + pollResp, err := env.FrontendClient().PollCallbackExecution(ctx, &workflowservice.PollCallbackExecutionRequest{ + Namespace: env.Namespace().String(), + CallbackId: callbackID, + RunId: runID, + }) + s.NoError(err) + s.NotNil(pollResp.GetOutcome().GetFailure()) + s.Equal("testing terminate", pollResp.GetOutcome().GetFailure().GetMessage()) + + // Same request ID should be a no-op (idempotent). + _, err = env.FrontendClient().TerminateCallbackExecution(ctx, &workflowservice.TerminateCallbackExecutionRequest{ + Namespace: env.Namespace().String(), + CallbackId: callbackID, + Identity: "test", + RequestId: requestID, + Reason: "testing terminate", + }) + s.NoError(err) + + // Different request ID should return FailedPrecondition. + _, err = env.FrontendClient().TerminateCallbackExecution(ctx, &workflowservice.TerminateCallbackExecutionRequest{ + Namespace: env.Namespace().String(), + CallbackId: callbackID, + Identity: "test", + RequestId: uuid.NewString(), + Reason: "different request", + }) + s.Error(err) + s.Contains(err.Error(), "already terminated with request ID") +} + +// TestCallbackExecutionFailedOutcome tests that when a callback fails with a non-retryable error +// (e.g., a 400 response from the target), the poll outcome contains the failure details. +func (s *StandaloneCallbackSuite) TestCallbackExecutionFailedOutcome() { + env := s.newEnv() + ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) + defer cancel() + + // Start an HTTP server that always returns 400 Bad Request (non-retryable). + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusBadRequest) + })) + defer srv.Close() + + callbackID := "failed-outcome-test-" + uuid.NewString() + cb := &commonpb.Callback{ + Variant: &commonpb.Callback_Nexus_{ + Nexus: &commonpb.Callback_Nexus{Url: srv.URL + "/callback"}, + }, + } + completion := &callbackpb.CallbackExecutionCompletion{ + Result: &callbackpb.CallbackExecutionCompletion_Success{ + Success: testcore.MustToPayload(s.T(), "some-result"), + }, + } + s.callStartCallbackExecution(ctx, env, callbackID, cb, completion, time.Minute) + + // Poll for the outcome — the callback should eventually fail with a non-retryable error. + pollResp, err := env.FrontendClient().PollCallbackExecution(ctx, &workflowservice.PollCallbackExecutionRequest{ + Namespace: env.Namespace().String(), + CallbackId: callbackID, + }) + s.NoError(err) + s.NotNil(pollResp.GetOutcome().GetFailure()) + s.Contains(pollResp.GetOutcome().GetFailure().GetMessage(), "handler error (BAD_REQUEST)") + s.True(pollResp.GetOutcome().GetFailure().GetApplicationFailureInfo().GetNonRetryable()) +} + +// TestNexusOperationCompletionBeforeStartHandlerReturns tests that a standalone callback can +// complete a Nexus operation even when the callback execution is started *before* the Nexus +// start handler returns to the caller. This exercises the race where the completion arrives +// while the operation is still in SCHEDULED state (i.e., before transitioning to STARTED). +// +// Flow: +// 1. A caller workflow starts a Nexus operation via an external endpoint. +// 2. The external Nexus handler captures the callback URL/token and calls +// StartCallbackExecution to deliver the completion *before* returning async. +// 3. The handler then returns an async result. +// 4. The operation can be completed from SCHEDULED state directly, so the workflow +// receives the result regardless of the start handler timing. +func (s *StandaloneCallbackSuite) TestNexusOperationCompletionBeforeStartHandlerReturns() { + env := s.newEnv() + ctx, cancel := context.WithTimeout(context.Background(), time.Second*60) + defer cancel() + + taskQueue := testcore.RandomizeStr(s.T().Name()) + endpointName := testcore.RandomizedNexusEndpoint(s.T().Name()) + + // Set up an external Nexus handler that starts the standalone callback *inside* + // the start handler — before returning the async result — to simulate a race + // where the completion is delivered before the caller processes the start response. + h := nexustest.Handler{ + OnStartOperation: func( + ctx context.Context, + service, operation string, + input *nexus.LazyValue, + options nexus.StartOperationOptions, + ) (nexus.HandlerStartOperationResult[any], error) { + token := options.CallbackHeader.Get(commonnexus.CallbackTokenHeader) + callbackURL := options.CallbackURL + + // Start the standalone callback execution to deliver the completion + // BEFORE returning from this handler. + callbackID := "race-callback-" + uuid.NewString() + cb := &commonpb.Callback{ + Variant: &commonpb.Callback_Nexus_{ + Nexus: &commonpb.Callback_Nexus{ + Url: callbackURL, + Token: token, + }, + }, + } + completion := &callbackpb.CallbackExecutionCompletion{ + Result: &callbackpb.CallbackExecutionCompletion_Success{ + Success: testcore.MustToPayload(s.T(), "result-before-start-returns"), + }, + } + s.callStartCallbackExecution(ctx, env, callbackID, cb, completion, time.Minute) + + // Now return the async result — the completion has already been + // delivered and the operation should already be completed. + return &nexus.HandlerStartOperationResultAsync{ + OperationToken: "test", + }, nil + }, + } + listenAddr := nexustest.AllocListenAddress() + nexustest.NewNexusServer(s.T(), listenAddr, h) + + _, err := env.OperatorClient().CreateNexusEndpoint(ctx, &operatorservice.CreateNexusEndpointRequest{ + Spec: &nexuspb.EndpointSpec{ + Name: endpointName, + Target: &nexuspb.EndpointTarget{ + Variant: &nexuspb.EndpointTarget_External_{ + External: &nexuspb.EndpointTarget_External{ + Url: "http://" + listenAddr, + }, + }, + }, + }, + }) + s.NoError(err) + + // Register a caller workflow that starts a Nexus operation and waits for its result. + callerWf := func(ctx workflow.Context) (string, error) { + c := workflow.NewNexusClient(endpointName, "service") + fut := c.ExecuteOperation(ctx, "operation", "input", workflow.NexusOperationOptions{}) + var result string + err := fut.Get(ctx, &result) + return result, err + } + + w := worker.New(env.SdkClient(), taskQueue, worker.Options{}) + w.RegisterWorkflow(callerWf) + s.NoError(w.Start()) + defer w.Stop() + + // Start the caller workflow. + run, err := env.SdkClient().ExecuteWorkflow(ctx, client.StartWorkflowOptions{ + TaskQueue: taskQueue, + }, callerWf) + s.NoError(err) + + // The standalone callback delivers the completion even though it was started + // before the start handler returned. The operation transitions directly from + // SCHEDULED to SUCCEEDED. + var result string + s.NoError(run.Get(ctx, &result)) + s.Equal("result-before-start-returns", result) + + // Verify the operation token is recorded in the caller workflow's history. + histResp, err := env.FrontendClient().GetWorkflowExecutionHistory(ctx, &workflowservice.GetWorkflowExecutionHistoryRequest{ + Namespace: env.Namespace().String(), + Execution: &commonpb.WorkflowExecution{ + WorkflowId: run.GetID(), + RunId: run.GetRunID(), + }, + }) + s.NoError(err) + startedIdx := slices.IndexFunc(histResp.History.Events, func(e *historypb.HistoryEvent) bool { + return e.GetNexusOperationStartedEventAttributes() != nil + }) + s.NotEqual(-1, startedIdx, "expected NexusOperationStarted event in history") + s.Equal("test", histResp.History.Events[startedIdx].GetNexusOperationStartedEventAttributes().GetOperationToken()) +} + +// TestScheduleToCloseTimeout verifies that a callback execution transitions to FAILED +// when its schedule-to-close timeout expires before the callback succeeds. +func (s *StandaloneCallbackSuite) TestScheduleToCloseTimeout() { + env := s.newEnv() + ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) + defer cancel() + + // Short timeout so it fires quickly during the test. + callbackID := "timeout-test-" + uuid.NewString() + s.callStartCallbackExecutionToBogusCallback(ctx, env, callbackID, 2*time.Second) + + // Poll until the callback reaches a terminal state due to timeout. + pollResp, err := env.FrontendClient().PollCallbackExecution(ctx, &workflowservice.PollCallbackExecutionRequest{ + Namespace: env.Namespace().String(), + CallbackId: callbackID, + }) + s.NoError(err) + s.NotNil(pollResp.GetOutcome(), "expected terminal outcome after timeout") + s.NotNil(pollResp.GetOutcome().GetFailure(), "expected failure outcome after timeout") + s.Contains(pollResp.GetOutcome().GetFailure().GetMessage(), "timed out") + s.NotNil(pollResp.GetOutcome().GetFailure().GetTimeoutFailureInfo()) + s.Equal(enumspb.TIMEOUT_TYPE_SCHEDULE_TO_CLOSE, pollResp.GetOutcome().GetFailure().GetTimeoutFailureInfo().GetTimeoutType()) + + // Describe should show FAILED state with timeout failure. + descResp, err := env.FrontendClient().DescribeCallbackExecution(ctx, &workflowservice.DescribeCallbackExecutionRequest{ + Namespace: env.Namespace().String(), + CallbackId: callbackID, + IncludeOutcome: true, + }) + s.NoError(err) + s.Equal(enumspb.CALLBACK_EXECUTION_STATUS_FAILED, descResp.GetInfo().GetStatus()) + s.Equal(enumspb.CALLBACK_STATE_FAILED, descResp.GetInfo().GetState()) + s.NotNil(descResp.GetInfo().GetCloseTime()) + s.NotNil(descResp.GetOutcome().GetFailure()) + s.NotNil(descResp.GetOutcome().GetFailure().GetTimeoutFailureInfo()) + s.Equal(enumspb.TIMEOUT_TYPE_SCHEDULE_TO_CLOSE, descResp.GetOutcome().GetFailure().GetTimeoutFailureInfo().GetTimeoutType()) +}