diff --git a/chasm/callback.go b/chasm/callback.go index 46392fb4998..5264572da99 100644 --- a/chasm/callback.go +++ b/chasm/callback.go @@ -1,10 +1,12 @@ package chasm const ( - CallbackLibraryName = "callback" - CallbackComponentName = "callback" + CallbackLibraryName = "callback" + CallbackComponentName = "callback" + CallbackExecutionComponentName = "callback_execution" ) var ( - CallbackComponentID = GenerateTypeID(FullyQualifiedName(CallbackLibraryName, CallbackComponentName)) + CallbackComponentID = GenerateTypeID(FullyQualifiedName(CallbackLibraryName, CallbackComponentName)) + CallbackExecutionComponentID = GenerateTypeID(FullyQualifiedName(CallbackLibraryName, CallbackExecutionComponentName)) ) diff --git a/chasm/lib/callback/callback_execution.go b/chasm/lib/callback/callback_execution.go new file mode 100644 index 00000000000..842e6007946 --- /dev/null +++ b/chasm/lib/callback/callback_execution.go @@ -0,0 +1,290 @@ +package callback + +import ( + "fmt" + + "github.com/nexus-rpc/sdk-go/nexus" + 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" + "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" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/durationpb" + "google.golang.org/protobuf/types/known/timestamppb" +) + +var ( + executionStatusSearchAttribute = chasm.NewSearchAttributeKeyword( + "ExecutionStatus", + chasm.SearchAttributeFieldLowCardinalityKeyword01, + ) + + _ chasm.RootComponent = (*CallbackExecution)(nil) + _ CompletionSource = (*CallbackExecution)(nil) + _ chasm.VisibilitySearchAttributesProvider = (*CallbackExecution)(nil) + _ chasm.VisibilityMemoProvider = (*CallbackExecution)(nil) +) + +// CallbackExecution is a top-level CHASM entity that manages a standalone callback. +// It owns a child Callback component and implements CompletionSource to provide +// stored Nexus completion data for invocation. +type CallbackExecution struct { + chasm.UnimplementedComponent + + // Persisted state + *callbackspb.CallbackExecutionState + + // Child callback component + Callback chasm.Field[*Callback] + + // Visibility sub-component for search attributes and memo indexing. + Visibility chasm.Field[*chasm.Visibility] +} + +// StartCallbackExecutionInput contains validated fields from the start request. +type StartCallbackExecutionInput struct { + CallbackID string + RequestID string + Callback *callbackspb.Callback + SuccessCompletion *commonpb.Payload + FailureCompletion *failurepb.Failure + ScheduleToCloseTimeout *durationpb.Duration //nolint:revive // keeping full type name for clarity + SearchAttributes map[string]*commonpb.Payload +} + +// CreateCallbackExecution constructs a new CallbackExecution entity with a child Callback. +// The child Callback is immediately transitioned to SCHEDULED state to begin invocation. +func CreateCallbackExecution( + ctx chasm.MutableContext, + input *StartCallbackExecutionInput, +) (*CallbackExecution, error) { + now := timestamppb.Now() + + state := &callbackspb.CallbackExecutionState{ + CallbackId: input.CallbackID, + CreateTime: now, + ScheduleToCloseTimeout: input.ScheduleToCloseTimeout, + } + + // Store the completion payload. + if input.SuccessCompletion != nil { + state.Completion = &callbackspb.CallbackExecutionState_SuccessCompletion{ + SuccessCompletion: input.SuccessCompletion, + } + } else if input.FailureCompletion != nil { + state.Completion = &callbackspb.CallbackExecutionState_FailureCompletion{ + FailureCompletion: input.FailureCompletion, + } + } + + // Create child Callback component. + cb := NewCallback( + input.RequestID, + now, + &callbackspb.CallbackState{}, + input.Callback, + ) + + exec := &CallbackExecution{ + CallbackExecutionState: state, + } + exec.Callback = chasm.NewComponentField(ctx, cb) + + visibility := chasm.NewVisibilityWithData(ctx, input.SearchAttributes, nil) + exec.Visibility = chasm.NewComponentField(ctx, visibility) + + // 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 task if ScheduleToCloseTimeout is set. + if input.ScheduleToCloseTimeout != nil { + if timeout := input.ScheduleToCloseTimeout.AsDuration(); timeout > 0 { + ctx.AddTask( + cb, + chasm.TaskAttributes{ + ScheduledTime: now.AsTime().Add(timeout), + }, + &callbackspb.ScheduleToCloseTimeoutTask{}, + ) + } + } + + return exec, nil +} + +// LifecycleState delegates to the child Callback's lifecycle state. +func (e *CallbackExecution) LifecycleState(ctx chasm.Context) chasm.LifecycleState { + cb := e.Callback.Get(ctx) + return cb.LifecycleState(ctx) +} + +// 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 (e *CallbackExecution) Terminate( + ctx chasm.MutableContext, + req chasm.TerminateComponentRequest, +) (chasm.TerminateComponentResponse, error) { + cb := e.Callback.Get(ctx) + if cb.LifecycleState(ctx).IsClosed() { + if e.TerminateRequestId == "" { + // Completed organically (succeeded/failed/timed out), not via Terminate. + return chasm.TerminateComponentResponse{}, serviceerror.NewFailedPreconditionf( + "callback execution already in terminal state %v", cb.Status) + } + if e.TerminateRequestId != req.RequestID { + return chasm.TerminateComponentResponse{}, serviceerror.NewFailedPreconditionf( + "already terminated with request ID %s", e.TerminateRequestId) + } + return chasm.TerminateComponentResponse{}, nil + } + if err := TransitionTerminated.Apply(cb, ctx, EventTerminated{Reason: req.Reason}); err != nil { + return chasm.TerminateComponentResponse{}, fmt.Errorf("failed to terminate callback: %w", err) + } + e.TerminateRequestId = req.RequestID + return chasm.TerminateComponentResponse{}, nil +} + +// Describe returns CallbackExecutionInfo for the describe RPC. +func (e *CallbackExecution) Describe(ctx chasm.Context) (*callbackpb.CallbackExecutionInfo, error) { + cb := e.Callback.Get(ctx) + apiCb, err := cb.ToAPICallback() + if err != nil { + return nil, err + } + + info := &callbackpb.CallbackExecutionInfo{ + CallbackId: e.CallbackId, + RunId: ctx.ExecutionKey().RunID, + Callback: apiCb, + Status: callbackStatusToAPIExecutionStatus(cb.Status), + State: callbackStatusToAPIState(cb.Status), + Attempt: cb.Attempt, + CreateTime: e.CreateTime, + LastAttemptCompleteTime: cb.LastAttemptCompleteTime, + LastAttemptFailure: cb.LastAttemptFailure, + NextAttemptScheduleTime: cb.NextAttemptScheduleTime, + CloseTime: cb.CloseTime, + ScheduleToCloseTimeout: e.ScheduleToCloseTimeout, + StateTransitionCount: ctx.StateTransitionCount(), + } + return info, nil +} + +// GetOutcome returns the callback execution outcome if the execution is in a terminal state. +func (e *CallbackExecution) GetOutcome(ctx chasm.Context) (*callbackpb.CallbackExecutionOutcome, error) { + cb := e.Callback.Get(ctx) + switch cb.Status { + case callbackspb.CALLBACK_STATUS_SUCCEEDED: + return &callbackpb.CallbackExecutionOutcome{ + Value: &callbackpb.CallbackExecutionOutcome_Success{}, + }, nil + case callbackspb.CALLBACK_STATUS_FAILED, + callbackspb.CALLBACK_STATUS_TERMINATED: + return &callbackpb.CallbackExecutionOutcome{ + Value: &callbackpb.CallbackExecutionOutcome_Failure{ + Failure: cb.GetFailure(), + }, + }, nil + default: + return nil, nil + } +} + +// 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 + } +} + +// SearchAttributes implements chasm.VisibilitySearchAttributesProvider. +func (e *CallbackExecution) SearchAttributes(ctx chasm.Context) []chasm.SearchAttributeKeyValue { + cb := e.Callback.Get(ctx) + return []chasm.SearchAttributeKeyValue{ + executionStatusSearchAttribute.Value(callbackStatusToAPIExecutionStatus(cb.Status).String()), + } +} + +// Memo implements chasm.VisibilityMemoProvider. Returns the CallbackExecutionListInfo +// as the memo for visibility queries. +func (e *CallbackExecution) Memo(ctx chasm.Context) proto.Message { + cb := e.Callback.Get(ctx) + return &callbackpb.CallbackExecutionListInfo{ + CallbackId: e.CallbackId, + Status: callbackStatusToAPIExecutionStatus(cb.Status), + CreateTime: e.CreateTime, + CloseTime: cb.CloseTime, + } +} + +// GetNexusCompletion implements CompletionSource. It converts the stored completion +// payload to nexusrpc.CompleteOperationOptions for use by the Callback invocation logic. +func (e *CallbackExecution) GetNexusCompletion( + ctx chasm.Context, + requestID string, +) (nexusrpc.CompleteOperationOptions, error) { + opts := nexusrpc.CompleteOperationOptions{ + StartTime: e.CreateTime.AsTime(), + } + switch c := e.Completion.(type) { + case *callbackspb.CallbackExecutionState_SuccessCompletion: + opts.Result = c.SuccessCompletion + return opts, nil + case *callbackspb.CallbackExecutionState_FailureCompletion: + f, err := commonnexus.TemporalFailureToNexusFailure(c.FailureCompletion) + if err != nil { + return nexusrpc.CompleteOperationOptions{}, fmt.Errorf("failed to convert failure: %w", err) + } + opErr := &nexus.OperationError{ + State: nexus.OperationStateFailed, + Message: "operation failed", + Cause: &nexus.FailureError{Failure: f}, + } + if err := nexusrpc.MarkAsWrapperError(nexusrpc.DefaultFailureConverter(), opErr); err != nil { + return nexusrpc.CompleteOperationOptions{}, fmt.Errorf("failed to mark wrapper error: %w", err) + } + opts.Error = opErr + return opts, nil + default: + return nexusrpc.CompleteOperationOptions{}, fmt.Errorf("empty completion payload") + } +} diff --git a/chasm/lib/callback/chasm_invocation.go b/chasm/lib/callback/chasm_invocation.go index be1c5d6c5a8..b522b825e3a 100644 --- a/chasm/lib/callback/chasm_invocation.go +++ b/chasm/lib/callback/chasm_invocation.go @@ -56,13 +56,14 @@ func (c chasmInvocation) Invoke( task *callbackspb.InvocationTask, taskAttr chasm.TaskAttributes, ) invocationResult { - header := nexus.Header(c.nexus.GetHeader()) - if header == nil { - header = nexus.Header{} + // Get the token from the dedicated Token field, falling back to the header for backward compat. + encodedRef := c.nexus.GetToken() + if encodedRef == "" { + header := nexus.Header(c.nexus.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/component.go b/chasm/lib/callback/component.go index c288ff76c23..a8574f81958 100644 --- a/chasm/lib/callback/component.go +++ b/chasm/lib/callback/component.go @@ -5,6 +5,7 @@ import ( "time" 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" @@ -52,7 +53,11 @@ 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 +72,16 @@ func (c *Callback) SetStateMachineState(status callbackspb.CallbackStatus) { c.Status = status } +// GetFailure returns the failure for a closed callback. It checks the dedicated Failure field +// (set by external failures like timeout or terminate) first, then falls back to LastAttemptFailure +// (set by invocation attempt failures). +func (c *Callback) GetFailure() *failurepb.Failure { + if c.Failure != nil { + return c.Failure + } + return c.LastAttemptFailure +} + func (c *Callback) recordAttempt(ts time.Time) { c.Attempt++ c.LastAttemptCompleteTime = timestamppb.New(ts) @@ -117,6 +132,11 @@ 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 — drop it silently. + 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)}) @@ -128,6 +148,13 @@ func (c *Callback) saveResult( RetryPolicy: input.retryPolicy, }) return nil, err + case invocationResultRetryNoCB: + err := TransitionAttemptFailed.Apply(c, ctx, EventAttemptFailed{ + Time: ctx.Now(c), + Err: r.err, + RetryPolicy: input.retryPolicy, + }) + return nil, err case invocationResultFail: err := TransitionFailed.Apply(c, ctx, EventFailed{ Time: ctx.Now(c), diff --git a/chasm/lib/callback/config.go b/chasm/lib/callback/config.go index c0b4aadf485..2f9a5f064f7 100644 --- a/chasm/lib/callback/config.go +++ b/chasm/lib/callback/config.go @@ -14,31 +14,58 @@ import ( "google.golang.org/grpc/status" ) -var RequestTimeout = dynamicconfig.NewDestinationDurationSetting( - "callback.request.timeout", - time.Second*10, - `RequestTimeout is the timeout for executing a single callback request.`, -) +var ( + Enabled = dynamicconfig.NewNamespaceBoolSetting( + "callback.enableStandaloneExecutions", + true, + `Toggles standalone callback execution functionality on the server.`, + ) -var RetryPolicyInitialInterval = dynamicconfig.NewGlobalDurationSetting( - "callback.retryPolicy.initialInterval", - time.Second, - `The initial backoff interval between every callback request attempt for a given callback.`, -) + RequestTimeout = dynamicconfig.NewDestinationDurationSetting( + "callback.request.timeout", + time.Second*10, + `RequestTimeout is the timeout for executing a single callback request.`, + ) + + RetryPolicyInitialInterval = dynamicconfig.NewGlobalDurationSetting( + "callback.retryPolicy.initialInterval", + time.Second, + `The initial backoff interval between every callback request attempt for a given callback.`, + ) + + RetryPolicyMaximumInterval = dynamicconfig.NewGlobalDurationSetting( + "callback.retryPolicy.maxInterval", + time.Hour, + `The maximum backoff interval between every callback request attempt for a given callback.`, + ) + + LongPollTimeout = dynamicconfig.NewNamespaceDurationSetting( + "callback.longPollTimeout", + 20*time.Second, + `Timeout for callback execution long-poll requests.`, + ) -var RetryPolicyMaximumInterval = dynamicconfig.NewGlobalDurationSetting( - "callback.retryPolicy.maxInterval", - time.Hour, - `The maximum backoff interval between every callback request attempt for a given callback.`, + 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.`, + ) ) type Config struct { - RequestTimeout dynamicconfig.DurationPropertyFnWithDestinationFilter - RetryPolicy func() backoff.RetryPolicy + Enabled dynamicconfig.BoolPropertyFnWithNamespaceFilter + RequestTimeout dynamicconfig.DurationPropertyFnWithDestinationFilter + RetryPolicy func() backoff.RetryPolicy + LongPollTimeout dynamicconfig.DurationPropertyFnWithNamespaceFilter + LongPollBuffer dynamicconfig.DurationPropertyFnWithNamespaceFilter + CallbackURLMaxLength dynamicconfig.IntPropertyFnWithNamespaceFilter + AllowedAddresses dynamicconfig.TypedPropertyFnWithNamespaceFilter[AddressMatchRules] } func configProvider(dc *dynamicconfig.Collection) *Config { return &Config{ + Enabled: Enabled.Get(dc), RequestTimeout: RequestTimeout.Get(dc), RetryPolicy: func() backoff.RetryPolicy { return backoff.NewExponentialRetryPolicy( @@ -49,6 +76,10 @@ func configProvider(dc *dynamicconfig.Collection) *Config { backoff.NoInterval, ) }, + LongPollTimeout: LongPollTimeout.Get(dc), + LongPollBuffer: LongPollBuffer.Get(dc), + CallbackURLMaxLength: dynamicconfig.FrontendCallbackURLMaxLength.Get(dc), + AllowedAddresses: AllowedAddresses.Get(dc), } } diff --git a/chasm/lib/callback/frontend.go b/chasm/lib/callback/frontend.go new file mode 100644 index 00000000000..0a62524de93 --- /dev/null +++ b/chasm/lib/callback/frontend.go @@ -0,0 +1,334 @@ +package callback + +import ( + "context" + "fmt" + + callbackpb "go.temporal.io/api/callback/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" +) + +type ( + // CallbackExecutionFrontendHandler defines the frontend interface for standalone callback execution RPCs. + CallbackExecutionFrontendHandler 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) + TerminateCallbackExecution(context.Context, *workflowservice.TerminateCallbackExecutionRequest) (*workflowservice.TerminateCallbackExecutionResponse, error) + DeleteCallbackExecution(context.Context, *workflowservice.DeleteCallbackExecutionRequest) (*workflowservice.DeleteCallbackExecutionResponse, error) + ListCallbackExecutions(context.Context, *workflowservice.ListCallbackExecutionsRequest) (*workflowservice.ListCallbackExecutionsResponse, error) + CountCallbackExecutions(context.Context, *workflowservice.CountCallbackExecutionsRequest) (*workflowservice.CountCallbackExecutionsResponse, error) + IsStandaloneCallbackEnabled(namespaceName string) bool + } + + callbackExecutionFrontendHandler struct { + client callbackspb.CallbackExecutionServiceClient + config *Config + logger log.Logger + namespaceRegistry namespace.Registry + } +) + +var ErrStandaloneCallbackDisabled = serviceerror.NewUnimplemented("standalone callback executions are not enabled") + +// NewCallbackExecutionFrontendHandler creates a new frontend handler for standalone callback executions. +func NewCallbackExecutionFrontendHandler( + client callbackspb.CallbackExecutionServiceClient, + config *Config, + logger log.Logger, + namespaceRegistry namespace.Registry, +) CallbackExecutionFrontendHandler { + return &callbackExecutionFrontendHandler{ + client: client, + config: config, + logger: logger, + namespaceRegistry: namespaceRegistry, + } +} + +// validateStartCallbackExecutionRequest checks that required fields are set and structurally valid +// before the request reaches the CHASM handler. +func validateStartCallbackExecutionRequest(request *workflowservice.StartCallbackExecutionRequest, config *Config) error { + // Validate callback is set and is the Nexus variant. + cb := request.GetCallback() + if cb == nil { + return serviceerror.NewInvalidArgument("Callback is not set on request.") + } + nexusCb := cb.GetNexus() + if nexusCb == nil { + return serviceerror.NewInvalidArgument("Only Nexus callback variant is supported.") + } + if nexusCb.GetUrl() == "" { + return serviceerror.NewInvalidArgument("Callback URL is not set.") + } + + // Validate callback URL structure and length. + if err := validateCallbackURL(nexusCb.GetUrl(), request.GetNamespace(), config); err != nil { + return err + } + + // Validate completion is set. + completion := request.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 schedule_to_close_timeout. + if request.GetScheduleToCloseTimeout() == nil || request.GetScheduleToCloseTimeout().AsDuration() <= 0 { + return serviceerror.NewInvalidArgument("ScheduleToCloseTimeout must be set and positive.") + } + + return nil +} + +// validateCallbackURL checks that the callback URL does not exceed the configured maximum length +// and passes the standard address match rules validation (scheme, host, allowlist). +func validateCallbackURL(rawURL string, namespaceName string, config *Config) error { + maxLen := config.CallbackURLMaxLength(namespaceName) + if len(rawURL) > maxLen { + return serviceerror.NewInvalidArgument(fmt.Sprintf("Callback URL length exceeds maximum allowed length of %d.", maxLen)) + } + return config.AllowedAddresses(namespaceName).Validate(rawURL) +} + +func (h *callbackExecutionFrontendHandler) IsStandaloneCallbackEnabled(namespaceName string) bool { + return h.config.Enabled(namespaceName) +} + +// StartCallbackExecution creates a new standalone callback execution that will deliver the +// provided Nexus completion payload to the target callback URL with retries. +func (h *callbackExecutionFrontendHandler) StartCallbackExecution( + ctx context.Context, + request *workflowservice.StartCallbackExecutionRequest, +) (*workflowservice.StartCallbackExecutionResponse, error) { + if !h.config.Enabled(request.GetNamespace()) { + return nil, ErrStandaloneCallbackDisabled + } + if request.GetCallbackId() == "" { + return nil, serviceerror.NewInvalidArgument("CallbackId is not set on request.") + } + + if err := validateStartCallbackExecutionRequest(request, h.config); err != nil { + return nil, err + } + + namespaceID, err := h.namespaceRegistry.GetNamespaceID(namespace.Name(request.GetNamespace())) + 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. +func (h *callbackExecutionFrontendHandler) DescribeCallbackExecution( + ctx context.Context, + request *workflowservice.DescribeCallbackExecutionRequest, +) (*workflowservice.DescribeCallbackExecutionResponse, error) { + if !h.config.Enabled(request.GetNamespace()) { + return nil, ErrStandaloneCallbackDisabled + } + if request.GetCallbackId() == "" { + return nil, serviceerror.NewInvalidArgument("CallbackId is not set on request.") + } + + namespaceID, err := h.namespaceRegistry.GetNamespaceID(namespace.Name(request.GetNamespace())) + 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 *callbackExecutionFrontendHandler) PollCallbackExecution( + ctx context.Context, + request *workflowservice.PollCallbackExecutionRequest, +) (*workflowservice.PollCallbackExecutionResponse, error) { + if !h.config.Enabled(request.GetNamespace()) { + return nil, ErrStandaloneCallbackDisabled + } + if request.GetCallbackId() == "" { + return nil, serviceerror.NewInvalidArgument("CallbackId is not set on request.") + } + + namespaceID, err := h.namespaceRegistry.GetNamespaceID(namespace.Name(request.GetNamespace())) + 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 *callbackExecutionFrontendHandler) TerminateCallbackExecution( + ctx context.Context, + request *workflowservice.TerminateCallbackExecutionRequest, +) (*workflowservice.TerminateCallbackExecutionResponse, error) { + if !h.config.Enabled(request.GetNamespace()) { + return nil, ErrStandaloneCallbackDisabled + } + if request.GetCallbackId() == "" { + return nil, serviceerror.NewInvalidArgument("CallbackId is not set on request.") + } + + namespaceID, err := h.namespaceRegistry.GetNamespaceID(namespace.Name(request.GetNamespace())) + 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 *callbackExecutionFrontendHandler) DeleteCallbackExecution( + ctx context.Context, + request *workflowservice.DeleteCallbackExecutionRequest, +) (*workflowservice.DeleteCallbackExecutionResponse, error) { + if !h.config.Enabled(request.GetNamespace()) { + return nil, ErrStandaloneCallbackDisabled + } + if request.GetCallbackId() == "" { + return nil, serviceerror.NewInvalidArgument("CallbackId is not set on request.") + } + + namespaceID, err := h.namespaceRegistry.GetNamespaceID(namespace.Name(request.GetNamespace())) + 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 *callbackExecutionFrontendHandler) ListCallbackExecutions( + ctx context.Context, + request *workflowservice.ListCallbackExecutionsRequest, +) (*workflowservice.ListCallbackExecutionsResponse, error) { + if !h.config.Enabled(request.GetNamespace()) { + return nil, ErrStandaloneCallbackDisabled + } + + namespaceName := namespace.Name(request.GetNamespace()) + if _, err := h.namespaceRegistry.GetNamespaceID(namespaceName); err != nil { + return nil, err + } + + resp, err := chasm.ListExecutions[*CallbackExecution, *callbackpb.CallbackExecutionListInfo]( + ctx, + &chasm.ListExecutionsRequest{ + NamespaceName: namespaceName.String(), + PageSize: int(request.GetPageSize()), + NextPageToken: request.GetNextPageToken(), + Query: request.GetQuery(), + }, + ) + if err != nil { + return nil, err + } + + executions := make([]*callbackpb.CallbackExecutionListInfo, len(resp.Executions)) + for i, ex := range resp.Executions { + listInfo := ex.ChasmMemo + if listInfo == nil { + listInfo = &callbackpb.CallbackExecutionListInfo{ + CallbackId: ex.BusinessID, + } + } + executions[i] = listInfo + } + + 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 *callbackExecutionFrontendHandler) CountCallbackExecutions( + ctx context.Context, + request *workflowservice.CountCallbackExecutionsRequest, +) (*workflowservice.CountCallbackExecutionsResponse, error) { + if !h.config.Enabled(request.GetNamespace()) { + return nil, ErrStandaloneCallbackDisabled + } + + namespaceName := namespace.Name(request.GetNamespace()) + if _, err := h.namespaceRegistry.GetNamespaceID(namespaceName); err != nil { + return nil, err + } + + resp, err := chasm.CountExecutions[*CallbackExecution]( + ctx, + &chasm.CountExecutionsRequest{ + NamespaceName: namespaceName.String(), + Query: request.GetQuery(), + }, + ) + if err != nil { + return nil, err + } + + 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/fx.go b/chasm/lib/callback/fx.go index a02f6fea054..3bbc40eef8b 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" @@ -53,12 +54,20 @@ func httpCallerProviderProvider( return m.Get, nil } +var FrontendModule = fx.Module( + "callback-execution-frontend", + fx.Provide(callbackspb.NewCallbackExecutionServiceLayeredClient), + fx.Provide(NewCallbackExecutionFrontendHandler), +) + var Module = fx.Module( "chasm.lib.callback", fx.Provide(configProvider), fx.Provide(httpCallerProviderProvider), fx.Provide(NewInvocationTaskHandler), fx.Provide(NewBackoffTaskHandler), + fx.Provide(NewScheduleToCloseTimeoutTaskHandler), + fx.Provide(newCallbackExecutionHandler), fx.Provide(newLibrary), fx.Invoke(register), ) 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..7557874488f 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 @@ -44,6 +44,43 @@ func (this *CallbackState) Equal(that interface{}) bool { return proto.Equal(this, that1) } +// Marshal an object of type CallbackExecutionState to the protobuf v3 wire format +func (val *CallbackExecutionState) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type CallbackExecutionState from the protobuf v3 wire format +func (val *CallbackExecutionState) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *CallbackExecutionState) Size() int { + return proto.Size(val) +} + +// Equal returns whether two CallbackExecutionState 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 *CallbackExecutionState) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *CallbackExecutionState + switch t := that.(type) { + case *CallbackExecutionState: + that1 = t + case CallbackExecutionState: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + // Marshal an object of type Callback to the protobuf v3 wire format func (val *Callback) Marshal() ([]byte, error) { return proto.Marshal(val) @@ -89,6 +126,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..e6f599d958c 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 via TerminateCallbackExecution. + 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,7 +133,12 @@ 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"` + RequestId string `protobuf:"bytes,9,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + // The time when the callback reached a terminal state. + CloseTime *timestamppb.Timestamp `protobuf:"bytes,10,opt,name=close_time,json=closeTime,proto3" json:"close_time,omitempty"` + // Failure from an external termination (timeout or terminate), stored separately + // from last_attempt_failure so the last invocation attempt's failure is preserved. + Failure *v1.Failure `protobuf:"bytes,11,opt,name=failure,proto3" json:"failure,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -217,6 +229,147 @@ func (x *CallbackState) GetRequestId() string { return "" } +func (x *CallbackState) GetCloseTime() *timestamppb.Timestamp { + if x != nil { + return x.CloseTime + } + return nil +} + +func (x *CallbackState) GetFailure() *v1.Failure { + if x != nil { + return x.Failure + } + return nil +} + +// Persisted state for a standalone CallbackExecution entity. +// The CallbackExecution is a top-level CHASM entity that owns a child Callback component +// and implements CompletionSource to provide stored completion data. +type CallbackExecutionState struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The Nexus completion payload to deliver to the callback URL. + // For success: contains the result Payload. + // For failure: contains the Failure details. + // + // Types that are valid to be assigned to Completion: + // + // *CallbackExecutionState_SuccessCompletion + // *CallbackExecutionState_FailureCompletion + Completion isCallbackExecutionState_Completion `protobuf_oneof:"completion"` + // The time when the execution was created. + CreateTime *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty"` + // The callback ID (business ID). + CallbackId string `protobuf:"bytes,4,opt,name=callback_id,json=callbackId,proto3" json:"callback_id,omitempty"` + // Schedule-to-close timeout from the start request. + ScheduleToCloseTimeout *durationpb.Duration `protobuf:"bytes,5,opt,name=schedule_to_close_timeout,json=scheduleToCloseTimeout,proto3" json:"schedule_to_close_timeout,omitempty"` + // The request ID from the terminate request, used for idempotency. + TerminateRequestId string `protobuf:"bytes,6,opt,name=terminate_request_id,json=terminateRequestId,proto3" json:"terminate_request_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CallbackExecutionState) Reset() { + *x = CallbackExecutionState{} + mi := &file_temporal_server_chasm_lib_callback_proto_v1_message_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CallbackExecutionState) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CallbackExecutionState) ProtoMessage() {} + +func (x *CallbackExecutionState) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_callback_proto_v1_message_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CallbackExecutionState.ProtoReflect.Descriptor instead. +func (*CallbackExecutionState) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_callback_proto_v1_message_proto_rawDescGZIP(), []int{1} +} + +func (x *CallbackExecutionState) GetCompletion() isCallbackExecutionState_Completion { + if x != nil { + return x.Completion + } + return nil +} + +func (x *CallbackExecutionState) GetSuccessCompletion() *v11.Payload { + if x != nil { + if x, ok := x.Completion.(*CallbackExecutionState_SuccessCompletion); ok { + return x.SuccessCompletion + } + } + return nil +} + +func (x *CallbackExecutionState) GetFailureCompletion() *v1.Failure { + if x != nil { + if x, ok := x.Completion.(*CallbackExecutionState_FailureCompletion); ok { + return x.FailureCompletion + } + } + return nil +} + +func (x *CallbackExecutionState) GetCreateTime() *timestamppb.Timestamp { + if x != nil { + return x.CreateTime + } + return nil +} + +func (x *CallbackExecutionState) GetCallbackId() string { + if x != nil { + return x.CallbackId + } + return "" +} + +func (x *CallbackExecutionState) GetScheduleToCloseTimeout() *durationpb.Duration { + if x != nil { + return x.ScheduleToCloseTimeout + } + return nil +} + +func (x *CallbackExecutionState) GetTerminateRequestId() string { + if x != nil { + return x.TerminateRequestId + } + return "" +} + +type isCallbackExecutionState_Completion interface { + isCallbackExecutionState_Completion() +} + +type CallbackExecutionState_SuccessCompletion struct { + // Deliver a successful Nexus operation completion with this result payload. + SuccessCompletion *v11.Payload `protobuf:"bytes,1,opt,name=success_completion,json=successCompletion,proto3,oneof"` +} + +type CallbackExecutionState_FailureCompletion struct { + // Deliver a failed Nexus operation completion with this failure. + FailureCompletion *v1.Failure `protobuf:"bytes,2,opt,name=failure_completion,json=failureCompletion,proto3,oneof"` +} + +func (*CallbackExecutionState_SuccessCompletion) isCallbackExecutionState_Completion() {} + +func (*CallbackExecutionState_FailureCompletion) isCallbackExecutionState_Completion() {} + type Callback struct { state protoimpl.MessageState `protogen:"open.v1"` // Types that are valid to be assigned to Variant: @@ -230,7 +383,7 @@ type Callback struct { func (x *Callback) Reset() { *x = Callback{} - mi := &file_temporal_server_chasm_lib_callback_proto_v1_message_proto_msgTypes[1] + mi := &file_temporal_server_chasm_lib_callback_proto_v1_message_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -242,7 +395,7 @@ func (x *Callback) String() string { func (*Callback) ProtoMessage() {} func (x *Callback) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_chasm_lib_callback_proto_v1_message_proto_msgTypes[1] + mi := &file_temporal_server_chasm_lib_callback_proto_v1_message_proto_msgTypes[2] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -255,7 +408,7 @@ func (x *Callback) ProtoReflect() protoreflect.Message { // Deprecated: Use Callback.ProtoReflect.Descriptor instead. func (*Callback) Descriptor() ([]byte, []int) { - return file_temporal_server_chasm_lib_callback_proto_v1_message_proto_rawDescGZIP(), []int{1} + return file_temporal_server_chasm_lib_callback_proto_v1_message_proto_rawDescGZIP(), []int{2} } func (x *Callback) GetVariant() isCallback_Variant { @@ -300,7 +453,7 @@ type CallbackState_WorkflowClosed struct { func (x *CallbackState_WorkflowClosed) Reset() { *x = CallbackState_WorkflowClosed{} - mi := &file_temporal_server_chasm_lib_callback_proto_v1_message_proto_msgTypes[2] + mi := &file_temporal_server_chasm_lib_callback_proto_v1_message_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -312,7 +465,7 @@ func (x *CallbackState_WorkflowClosed) String() string { func (*CallbackState_WorkflowClosed) ProtoMessage() {} func (x *CallbackState_WorkflowClosed) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_chasm_lib_callback_proto_v1_message_proto_msgTypes[2] + mi := &file_temporal_server_chasm_lib_callback_proto_v1_message_proto_msgTypes[3] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -336,14 +489,16 @@ 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"` + // Standard token to use for completion. + Token string `protobuf:"bytes,3,opt,name=token,proto3" json:"token,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *Callback_Nexus) Reset() { *x = Callback_Nexus{} - mi := &file_temporal_server_chasm_lib_callback_proto_v1_message_proto_msgTypes[3] + mi := &file_temporal_server_chasm_lib_callback_proto_v1_message_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -355,7 +510,7 @@ func (x *Callback_Nexus) String() string { func (*Callback_Nexus) ProtoMessage() {} func (x *Callback_Nexus) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_chasm_lib_callback_proto_v1_message_proto_msgTypes[3] + mi := &file_temporal_server_chasm_lib_callback_proto_v1_message_proto_msgTypes[4] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -368,7 +523,7 @@ func (x *Callback_Nexus) ProtoReflect() protoreflect.Message { // Deprecated: Use Callback_Nexus.ProtoReflect.Descriptor instead. func (*Callback_Nexus) Descriptor() ([]byte, []int) { - return file_temporal_server_chasm_lib_callback_proto_v1_message_proto_rawDescGZIP(), []int{1, 0} + return file_temporal_server_chasm_lib_callback_proto_v1_message_proto_rawDescGZIP(), []int{2, 0} } func (x *Callback_Nexus) GetUrl() string { @@ -385,11 +540,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\"\xca\x05\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 +561,42 @@ 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\x129\n" + + "\n" + + "close_time\x18\n" + + " \x01(\v2\x1a.google.protobuf.TimestampR\tcloseTime\x12:\n" + + "\afailure\x18\v \x01(\v2 .temporal.api.failure.v1.FailureR\afailure\x1a\x10\n" + + "\x0eWorkflowClosed\"\xb1\x03\n" + + "\x16CallbackExecutionState\x12P\n" + + "\x12success_completion\x18\x01 \x01(\v2\x1f.temporal.api.common.v1.PayloadH\x00R\x11successCompletion\x12Q\n" + + "\x12failure_completion\x18\x02 \x01(\v2 .temporal.api.failure.v1.FailureH\x00R\x11failureCompletion\x12;\n" + + "\vcreate_time\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampR\n" + + "createTime\x12\x1f\n" + + "\vcallback_id\x18\x04 \x01(\tR\n" + + "callbackId\x12T\n" + + "\x19schedule_to_close_timeout\x18\x05 \x01(\v2\x19.google.protobuf.DurationR\x16scheduleToCloseTimeout\x120\n" + + "\x14terminate_request_id\x18\x06 \x01(\tR\x12terminateRequestIdB\f\n" + + "\n" + + "completion\"\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 @@ -432,33 +611,42 @@ func file_temporal_server_chasm_lib_callback_proto_v1_message_proto_rawDescGZIP( } var file_temporal_server_chasm_lib_callback_proto_v1_message_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_temporal_server_chasm_lib_callback_proto_v1_message_proto_msgTypes = make([]protoimpl.MessageInfo, 5) +var file_temporal_server_chasm_lib_callback_proto_v1_message_proto_msgTypes = make([]protoimpl.MessageInfo, 6) var file_temporal_server_chasm_lib_callback_proto_v1_message_proto_goTypes = []any{ (CallbackStatus)(0), // 0: temporal.server.chasm.lib.callbacks.proto.v1.CallbackStatus (*CallbackState)(nil), // 1: temporal.server.chasm.lib.callbacks.proto.v1.CallbackState - (*Callback)(nil), // 2: temporal.server.chasm.lib.callbacks.proto.v1.Callback - (*CallbackState_WorkflowClosed)(nil), // 3: temporal.server.chasm.lib.callbacks.proto.v1.CallbackState.WorkflowClosed - (*Callback_Nexus)(nil), // 4: temporal.server.chasm.lib.callbacks.proto.v1.Callback.Nexus - 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 + (*CallbackExecutionState)(nil), // 2: temporal.server.chasm.lib.callbacks.proto.v1.CallbackExecutionState + (*Callback)(nil), // 3: temporal.server.chasm.lib.callbacks.proto.v1.Callback + (*CallbackState_WorkflowClosed)(nil), // 4: temporal.server.chasm.lib.callbacks.proto.v1.CallbackState.WorkflowClosed + (*Callback_Nexus)(nil), // 5: temporal.server.chasm.lib.callbacks.proto.v1.Callback.Nexus + nil, // 6: temporal.server.chasm.lib.callbacks.proto.v1.Callback.Nexus.HeaderEntry + (*timestamppb.Timestamp)(nil), // 7: google.protobuf.Timestamp + (*v1.Failure)(nil), // 8: temporal.api.failure.v1.Failure + (*v11.Payload)(nil), // 9: temporal.api.common.v1.Payload + (*durationpb.Duration)(nil), // 10: google.protobuf.Duration + (*v11.Link)(nil), // 11: 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 + 3, // 0: temporal.server.chasm.lib.callbacks.proto.v1.CallbackState.callback:type_name -> temporal.server.chasm.lib.callbacks.proto.v1.Callback + 7, // 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 + 7, // 3: temporal.server.chasm.lib.callbacks.proto.v1.CallbackState.last_attempt_complete_time:type_name -> google.protobuf.Timestamp + 8, // 4: temporal.server.chasm.lib.callbacks.proto.v1.CallbackState.last_attempt_failure:type_name -> temporal.api.failure.v1.Failure + 7, // 5: temporal.server.chasm.lib.callbacks.proto.v1.CallbackState.next_attempt_schedule_time:type_name -> google.protobuf.Timestamp + 7, // 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.failure:type_name -> temporal.api.failure.v1.Failure + 9, // 8: temporal.server.chasm.lib.callbacks.proto.v1.CallbackExecutionState.success_completion:type_name -> temporal.api.common.v1.Payload + 8, // 9: temporal.server.chasm.lib.callbacks.proto.v1.CallbackExecutionState.failure_completion:type_name -> temporal.api.failure.v1.Failure + 7, // 10: temporal.server.chasm.lib.callbacks.proto.v1.CallbackExecutionState.create_time:type_name -> google.protobuf.Timestamp + 10, // 11: temporal.server.chasm.lib.callbacks.proto.v1.CallbackExecutionState.schedule_to_close_timeout:type_name -> google.protobuf.Duration + 5, // 12: temporal.server.chasm.lib.callbacks.proto.v1.Callback.nexus:type_name -> temporal.server.chasm.lib.callbacks.proto.v1.Callback.Nexus + 11, // 13: temporal.server.chasm.lib.callbacks.proto.v1.Callback.links:type_name -> temporal.api.common.v1.Link + 6, // 14: temporal.server.chasm.lib.callbacks.proto.v1.Callback.Nexus.header:type_name -> temporal.server.chasm.lib.callbacks.proto.v1.Callback.Nexus.HeaderEntry + 15, // [15:15] is the sub-list for method output_type + 15, // [15:15] is the sub-list for method input_type + 15, // [15:15] is the sub-list for extension type_name + 15, // [15:15] is the sub-list for extension extendee + 0, // [0:15] is the sub-list for field type_name } func init() { file_temporal_server_chasm_lib_callback_proto_v1_message_proto_init() } @@ -467,6 +655,10 @@ func file_temporal_server_chasm_lib_callback_proto_v1_message_proto_init() { return } file_temporal_server_chasm_lib_callback_proto_v1_message_proto_msgTypes[1].OneofWrappers = []any{ + (*CallbackExecutionState_SuccessCompletion)(nil), + (*CallbackExecutionState_FailureCompletion)(nil), + } + file_temporal_server_chasm_lib_callback_proto_v1_message_proto_msgTypes[2].OneofWrappers = []any{ (*Callback_Nexus_)(nil), } type x struct{} @@ -475,7 +667,7 @@ func file_temporal_server_chasm_lib_callback_proto_v1_message_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_temporal_server_chasm_lib_callback_proto_v1_message_proto_rawDesc), len(file_temporal_server_chasm_lib_callback_proto_v1_message_proto_rawDesc)), NumEnums: 1, - NumMessages: 5, + NumMessages: 6, NumExtensions: 0, NumServices: 0, }, 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..5c90c9f612b --- /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\x1a.temporal/server/api/routing/v1/extension.proto\x1a0temporal/server/api/common/v1/api_category.proto2\x8f\t\n" + + "\x18CallbackExecutionService\x12\xdd\x01\n" + + "\x16StartCallbackExecution\x12K.temporal.server.chasm.lib.callbacks.proto.v1.StartCallbackExecutionRequest\x1aL.temporal.server.chasm.lib.callbacks.proto.v1.StartCallbackExecutionResponse\"(\x92\xc4\x03\x1e\x1a\x1cfrontend_request.callback_id\x8a\xb5\x18\x02\b\x01\x12\xe6\x01\n" + + "\x19DescribeCallbackExecution\x12N.temporal.server.chasm.lib.callbacks.proto.v1.DescribeCallbackExecutionRequest\x1aO.temporal.server.chasm.lib.callbacks.proto.v1.DescribeCallbackExecutionResponse\"(\x92\xc4\x03\x1e\x1a\x1cfrontend_request.callback_id\x8a\xb5\x18\x02\b\x01\x12\xda\x01\n" + + "\x15PollCallbackExecution\x12J.temporal.server.chasm.lib.callbacks.proto.v1.PollCallbackExecutionRequest\x1aK.temporal.server.chasm.lib.callbacks.proto.v1.PollCallbackExecutionResponse\"(\x92\xc4\x03\x1e\x1a\x1cfrontend_request.callback_id\x8a\xb5\x18\x02\b\x01\x12\xe9\x01\n" + + "\x1aTerminateCallbackExecution\x12O.temporal.server.chasm.lib.callbacks.proto.v1.TerminateCallbackExecutionRequest\x1aP.temporal.server.chasm.lib.callbacks.proto.v1.TerminateCallbackExecutionResponse\"(\x92\xc4\x03\x1e\x1a\x1cfrontend_request.callback_id\x8a\xb5\x18\x02\b\x01\x12\xe0\x01\n" + + "\x17DeleteCallbackExecution\x12L.temporal.server.chasm.lib.callbacks.proto.v1.DeleteCallbackExecutionRequest\x1aM.temporal.server.chasm.lib.callbacks.proto.v1.DeleteCallbackExecutionResponse\"(\x92\xc4\x03\x1e\x1a\x1cfrontend_request.callback_id\x8a\xb5\x18\x02\b\x01BGZEgo.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.CallbackExecutionService.StartCallbackExecution:input_type -> temporal.server.chasm.lib.callbacks.proto.v1.StartCallbackExecutionRequest + 1, // 1: temporal.server.chasm.lib.callbacks.proto.v1.CallbackExecutionService.DescribeCallbackExecution:input_type -> temporal.server.chasm.lib.callbacks.proto.v1.DescribeCallbackExecutionRequest + 2, // 2: temporal.server.chasm.lib.callbacks.proto.v1.CallbackExecutionService.PollCallbackExecution:input_type -> temporal.server.chasm.lib.callbacks.proto.v1.PollCallbackExecutionRequest + 3, // 3: temporal.server.chasm.lib.callbacks.proto.v1.CallbackExecutionService.TerminateCallbackExecution:input_type -> temporal.server.chasm.lib.callbacks.proto.v1.TerminateCallbackExecutionRequest + 4, // 4: temporal.server.chasm.lib.callbacks.proto.v1.CallbackExecutionService.DeleteCallbackExecution:input_type -> temporal.server.chasm.lib.callbacks.proto.v1.DeleteCallbackExecutionRequest + 5, // 5: temporal.server.chasm.lib.callbacks.proto.v1.CallbackExecutionService.StartCallbackExecution:output_type -> temporal.server.chasm.lib.callbacks.proto.v1.StartCallbackExecutionResponse + 6, // 6: temporal.server.chasm.lib.callbacks.proto.v1.CallbackExecutionService.DescribeCallbackExecution:output_type -> temporal.server.chasm.lib.callbacks.proto.v1.DescribeCallbackExecutionResponse + 7, // 7: temporal.server.chasm.lib.callbacks.proto.v1.CallbackExecutionService.PollCallbackExecution:output_type -> temporal.server.chasm.lib.callbacks.proto.v1.PollCallbackExecutionResponse + 8, // 8: temporal.server.chasm.lib.callbacks.proto.v1.CallbackExecutionService.TerminateCallbackExecution:output_type -> temporal.server.chasm.lib.callbacks.proto.v1.TerminateCallbackExecutionResponse + 9, // 9: temporal.server.chasm.lib.callbacks.proto.v1.CallbackExecutionService.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..0f3ba5ac895 --- /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" +) + +// CallbackExecutionServiceLayeredClient is a client for CallbackExecutionService. +type CallbackExecutionServiceLayeredClient struct { + metricsHandler metrics.Handler + numShards int32 + redirector history.Redirector[CallbackExecutionServiceClient] + retryPolicy backoff.RetryPolicy +} + +// NewCallbackExecutionServiceLayeredClient initializes a new CallbackExecutionServiceLayeredClient. +func NewCallbackExecutionServiceLayeredClient( + dc *dynamicconfig.Collection, + rpcFactory common.RPCFactory, + monitor membership.Monitor, + config *config.Persistence, + logger log.Logger, + metricsHandler metrics.Handler, +) (CallbackExecutionServiceClient, error) { + resolver, err := monitor.GetResolver(primitives.HistoryService) + if err != nil { + return nil, err + } + connections := history.NewConnectionPool(resolver, rpcFactory, NewCallbackExecutionServiceClient) + var redirector history.Redirector[CallbackExecutionServiceClient] + if dynamicconfig.HistoryClientOwnershipCachingEnabled.Get(dc)() { + redirector = history.NewCachingRedirector( + connections, + resolver, + logger, + dynamicconfig.HistoryClientOwnershipCachingStaleTTL.Get(dc), + ) + } else { + redirector = history.NewBasicRedirector(connections, resolver) + } + return &CallbackExecutionServiceLayeredClient{ + metricsHandler: metricsHandler, + redirector: redirector, + numShards: config.NumHistoryShards, + retryPolicy: common.CreateHistoryClientRetryPolicy(), + }, nil +} +func (c *CallbackExecutionServiceLayeredClient) 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("CallbackExecutionService.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 CallbackExecutionServiceClient) 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 *CallbackExecutionServiceLayeredClient) 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 *CallbackExecutionServiceLayeredClient) 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("CallbackExecutionService.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 CallbackExecutionServiceClient) 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 *CallbackExecutionServiceLayeredClient) 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 *CallbackExecutionServiceLayeredClient) 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("CallbackExecutionService.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 CallbackExecutionServiceClient) 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 *CallbackExecutionServiceLayeredClient) 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 *CallbackExecutionServiceLayeredClient) 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("CallbackExecutionService.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 CallbackExecutionServiceClient) 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 *CallbackExecutionServiceLayeredClient) 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 *CallbackExecutionServiceLayeredClient) 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("CallbackExecutionService.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 CallbackExecutionServiceClient) 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 *CallbackExecutionServiceLayeredClient) 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..fda5ea54262 --- /dev/null +++ b/chasm/lib/callback/gen/callbackpb/v1/service_grpc.pb.go @@ -0,0 +1,259 @@ +// 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 ( + CallbackExecutionService_StartCallbackExecution_FullMethodName = "/temporal.server.chasm.lib.callbacks.proto.v1.CallbackExecutionService/StartCallbackExecution" + CallbackExecutionService_DescribeCallbackExecution_FullMethodName = "/temporal.server.chasm.lib.callbacks.proto.v1.CallbackExecutionService/DescribeCallbackExecution" + CallbackExecutionService_PollCallbackExecution_FullMethodName = "/temporal.server.chasm.lib.callbacks.proto.v1.CallbackExecutionService/PollCallbackExecution" + CallbackExecutionService_TerminateCallbackExecution_FullMethodName = "/temporal.server.chasm.lib.callbacks.proto.v1.CallbackExecutionService/TerminateCallbackExecution" + CallbackExecutionService_DeleteCallbackExecution_FullMethodName = "/temporal.server.chasm.lib.callbacks.proto.v1.CallbackExecutionService/DeleteCallbackExecution" +) + +// CallbackExecutionServiceClient is the client API for CallbackExecutionService 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 CallbackExecutionServiceClient 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 callbackExecutionServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewCallbackExecutionServiceClient(cc grpc.ClientConnInterface) CallbackExecutionServiceClient { + return &callbackExecutionServiceClient{cc} +} + +func (c *callbackExecutionServiceClient) StartCallbackExecution(ctx context.Context, in *StartCallbackExecutionRequest, opts ...grpc.CallOption) (*StartCallbackExecutionResponse, error) { + out := new(StartCallbackExecutionResponse) + err := c.cc.Invoke(ctx, CallbackExecutionService_StartCallbackExecution_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *callbackExecutionServiceClient) DescribeCallbackExecution(ctx context.Context, in *DescribeCallbackExecutionRequest, opts ...grpc.CallOption) (*DescribeCallbackExecutionResponse, error) { + out := new(DescribeCallbackExecutionResponse) + err := c.cc.Invoke(ctx, CallbackExecutionService_DescribeCallbackExecution_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *callbackExecutionServiceClient) PollCallbackExecution(ctx context.Context, in *PollCallbackExecutionRequest, opts ...grpc.CallOption) (*PollCallbackExecutionResponse, error) { + out := new(PollCallbackExecutionResponse) + err := c.cc.Invoke(ctx, CallbackExecutionService_PollCallbackExecution_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *callbackExecutionServiceClient) TerminateCallbackExecution(ctx context.Context, in *TerminateCallbackExecutionRequest, opts ...grpc.CallOption) (*TerminateCallbackExecutionResponse, error) { + out := new(TerminateCallbackExecutionResponse) + err := c.cc.Invoke(ctx, CallbackExecutionService_TerminateCallbackExecution_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *callbackExecutionServiceClient) DeleteCallbackExecution(ctx context.Context, in *DeleteCallbackExecutionRequest, opts ...grpc.CallOption) (*DeleteCallbackExecutionResponse, error) { + out := new(DeleteCallbackExecutionResponse) + err := c.cc.Invoke(ctx, CallbackExecutionService_DeleteCallbackExecution_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// CallbackExecutionServiceServer is the server API for CallbackExecutionService service. +// All implementations must embed UnimplementedCallbackExecutionServiceServer +// for forward compatibility +type CallbackExecutionServiceServer 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) + mustEmbedUnimplementedCallbackExecutionServiceServer() +} + +// UnimplementedCallbackExecutionServiceServer must be embedded to have forward compatible implementations. +type UnimplementedCallbackExecutionServiceServer struct { +} + +func (UnimplementedCallbackExecutionServiceServer) StartCallbackExecution(context.Context, *StartCallbackExecutionRequest) (*StartCallbackExecutionResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method StartCallbackExecution not implemented") +} +func (UnimplementedCallbackExecutionServiceServer) DescribeCallbackExecution(context.Context, *DescribeCallbackExecutionRequest) (*DescribeCallbackExecutionResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method DescribeCallbackExecution not implemented") +} +func (UnimplementedCallbackExecutionServiceServer) PollCallbackExecution(context.Context, *PollCallbackExecutionRequest) (*PollCallbackExecutionResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method PollCallbackExecution not implemented") +} +func (UnimplementedCallbackExecutionServiceServer) TerminateCallbackExecution(context.Context, *TerminateCallbackExecutionRequest) (*TerminateCallbackExecutionResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method TerminateCallbackExecution not implemented") +} +func (UnimplementedCallbackExecutionServiceServer) DeleteCallbackExecution(context.Context, *DeleteCallbackExecutionRequest) (*DeleteCallbackExecutionResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method DeleteCallbackExecution not implemented") +} +func (UnimplementedCallbackExecutionServiceServer) mustEmbedUnimplementedCallbackExecutionServiceServer() { +} + +// UnsafeCallbackExecutionServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to CallbackExecutionServiceServer will +// result in compilation errors. +type UnsafeCallbackExecutionServiceServer interface { + mustEmbedUnimplementedCallbackExecutionServiceServer() +} + +func RegisterCallbackExecutionServiceServer(s grpc.ServiceRegistrar, srv CallbackExecutionServiceServer) { + s.RegisterService(&CallbackExecutionService_ServiceDesc, srv) +} + +func _CallbackExecutionService_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.(CallbackExecutionServiceServer).StartCallbackExecution(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: CallbackExecutionService_StartCallbackExecution_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(CallbackExecutionServiceServer).StartCallbackExecution(ctx, req.(*StartCallbackExecutionRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _CallbackExecutionService_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.(CallbackExecutionServiceServer).DescribeCallbackExecution(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: CallbackExecutionService_DescribeCallbackExecution_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(CallbackExecutionServiceServer).DescribeCallbackExecution(ctx, req.(*DescribeCallbackExecutionRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _CallbackExecutionService_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.(CallbackExecutionServiceServer).PollCallbackExecution(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: CallbackExecutionService_PollCallbackExecution_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(CallbackExecutionServiceServer).PollCallbackExecution(ctx, req.(*PollCallbackExecutionRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _CallbackExecutionService_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.(CallbackExecutionServiceServer).TerminateCallbackExecution(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: CallbackExecutionService_TerminateCallbackExecution_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(CallbackExecutionServiceServer).TerminateCallbackExecution(ctx, req.(*TerminateCallbackExecutionRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _CallbackExecutionService_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.(CallbackExecutionServiceServer).DeleteCallbackExecution(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: CallbackExecutionService_DeleteCallbackExecution_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(CallbackExecutionServiceServer).DeleteCallbackExecution(ctx, req.(*DeleteCallbackExecutionRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// CallbackExecutionService_ServiceDesc is the grpc.ServiceDesc for CallbackExecutionService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var CallbackExecutionService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "temporal.server.chasm.lib.callbacks.proto.v1.CallbackExecutionService", + HandlerType: (*CallbackExecutionServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "StartCallbackExecution", + Handler: _CallbackExecutionService_StartCallbackExecution_Handler, + }, + { + MethodName: "DescribeCallbackExecution", + Handler: _CallbackExecutionService_DescribeCallbackExecution_Handler, + }, + { + MethodName: "PollCallbackExecution", + Handler: _CallbackExecutionService_PollCallbackExecution_Handler, + }, + { + MethodName: "TerminateCallbackExecution", + Handler: _CallbackExecutionService_TerminateCallbackExecution_Handler, + }, + { + MethodName: "DeleteCallbackExecution", + Handler: _CallbackExecutionService_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..0ea4db28c6c 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 ScheduleToCloseTimeoutTask to the protobuf v3 wire format +func (val *ScheduleToCloseTimeoutTask) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type ScheduleToCloseTimeoutTask from the protobuf v3 wire format +func (val *ScheduleToCloseTimeoutTask) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *ScheduleToCloseTimeoutTask) Size() int { + return proto.Size(val) +} + +// Equal returns whether two ScheduleToCloseTimeoutTask 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 *ScheduleToCloseTimeoutTask) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *ScheduleToCloseTimeoutTask + switch t := that.(type) { + case *ScheduleToCloseTimeoutTask: + that1 = t + case ScheduleToCloseTimeoutTask: + 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..a0f6b7eb630 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 execution's schedule-to-close timeout expires. +type ScheduleToCloseTimeoutTask struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ScheduleToCloseTimeoutTask) Reset() { + *x = ScheduleToCloseTimeoutTask{} + 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 *ScheduleToCloseTimeoutTask) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ScheduleToCloseTimeoutTask) ProtoMessage() {} + +func (x *ScheduleToCloseTimeoutTask) 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 ScheduleToCloseTimeoutTask.ProtoReflect.Descriptor instead. +func (*ScheduleToCloseTimeoutTask) 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\"\x1c\n" + + "\x1aScheduleToCloseTimeoutTaskBGZEgo.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 + (*ScheduleToCloseTimeoutTask)(nil), // 2: temporal.server.chasm.lib.callbacks.proto.v1.ScheduleToCloseTimeoutTask } 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..90c85ecf98a --- /dev/null +++ b/chasm/lib/callback/handler.go @@ -0,0 +1,246 @@ +package callback + +import ( + "context" + "errors" + "fmt" + + "go.temporal.io/api/errordetails/v1" + "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/grpc/codes" + "google.golang.org/grpc/status" +) + +type callbackExecutionHandler struct { + callbackspb.UnimplementedCallbackExecutionServiceServer + + config *Config + logger log.Logger +} + +func newCallbackExecutionHandler(config *Config, logger log.Logger) *callbackExecutionHandler { + return &callbackExecutionHandler{ + config: config, + logger: logger, + } +} + +func (h *callbackExecutionHandler) StartCallbackExecution( + ctx context.Context, + req *callbackspb.StartCallbackExecutionRequest, +) (resp *callbackspb.StartCallbackExecutionResponse, err error) { + defer log.CapturePanic(h.logger, &err) + + frontendReq := req.FrontendRequest + + input := &StartCallbackExecutionInput{ + CallbackID: frontendReq.GetCallbackId(), + RequestID: frontendReq.GetRequestId(), + ScheduleToCloseTimeout: frontendReq.GetScheduleToCloseTimeout(), + 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(), + }, + }, + } + } + + // Extract completion payload. + if completion := frontendReq.GetCompletion(); completion != nil { + input.SuccessCompletion = completion.GetSuccess() + input.FailureCompletion = completion.GetFailure() + } + + result, err := chasm.StartExecution( + ctx, + chasm.ExecutionKey{ + NamespaceID: req.NamespaceId, + BusinessID: frontendReq.GetCallbackId(), + }, + CreateCallbackExecution, + input, + chasm.WithRequestID(frontendReq.GetRequestId()), + ) + + var alreadyStartedErr *chasm.ExecutionAlreadyStartedError + if errors.As(err, &alreadyStartedErr) { + st := status.New(codes.AlreadyExists, fmt.Sprintf("callback execution %q already exists", frontendReq.GetCallbackId())) + st, _ = st.WithDetails(&errordetails.CallbackExecutionAlreadyStartedFailure{ + StartRequestId: alreadyStartedErr.CurrentRequestID, + RunId: alreadyStartedErr.CurrentRunID, + }) + return nil, st.Err() + } + if err != nil { + return nil, err + } + + return &callbackspb.StartCallbackExecutionResponse{ + FrontendResponse: &workflowservice.StartCallbackExecutionResponse{ + RunId: result.ExecutionKey.RunID, + }, + }, nil +} + +func (h *callbackExecutionHandler) DescribeCallbackExecution( + ctx context.Context, + req *callbackspb.DescribeCallbackExecutionRequest, +) (resp *callbackspb.DescribeCallbackExecutionResponse, err error) { + defer log.CapturePanic(h.logger, &err) + + resp, err = chasm.ReadComponent( + ctx, + chasm.NewComponentRef[*CallbackExecution]( + chasm.ExecutionKey{ + NamespaceID: req.NamespaceId, + BusinessID: req.FrontendRequest.GetCallbackId(), + RunID: req.FrontendRequest.GetRunId(), + }, + ), + func(e *CallbackExecution, ctx chasm.Context, req *callbackspb.DescribeCallbackExecutionRequest) (*callbackspb.DescribeCallbackExecutionResponse, error) { + info, err := e.Describe(ctx) + if err != nil { + return nil, err + } + resp := &workflowservice.DescribeCallbackExecutionResponse{ + Info: info, + } + if req.FrontendRequest.GetIncludeOutcome() { + outcome, err := e.GetOutcome(ctx) + if err != nil { + return nil, err + } + resp.Outcome = outcome + } + return &callbackspb.DescribeCallbackExecutionResponse{ + FrontendResponse: resp, + }, nil + }, + req, + ) + return resp, err +} + +// PollCallbackExecution long-polls for callback execution outcome. It returns an empty non-error +// response on context deadline expiry, to indicate that the state being waited for was not reached. +// Callers should interpret this as an invitation to resubmit their long-poll request. +func (h *callbackExecutionHandler) PollCallbackExecution( + ctx context.Context, + req *callbackspb.PollCallbackExecutionRequest, +) (resp *callbackspb.PollCallbackExecutionResponse, err error) { + defer log.CapturePanic(h.logger, &err) + + ref := chasm.NewComponentRef[*CallbackExecution]( + 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( + e *CallbackExecution, + ctx chasm.Context, + _ *callbackspb.PollCallbackExecutionRequest, + ) (*callbackspb.PollCallbackExecutionResponse, bool, error) { + if !e.LifecycleState(ctx).IsClosed() { + return nil, false, nil + } + outcome, err := e.GetOutcome(ctx) + if err != nil { + return nil, false, err + } + return &callbackspb.PollCallbackExecutionResponse{ + FrontendResponse: &workflowservice.PollCallbackExecutionResponse{ + RunId: ctx.ExecutionKey().RunID, + Outcome: outcome, + }, + }, 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 *callbackExecutionHandler) TerminateCallbackExecution( + ctx context.Context, + req *callbackspb.TerminateCallbackExecutionRequest, +) (resp *callbackspb.TerminateCallbackExecutionResponse, err error) { + defer log.CapturePanic(h.logger, &err) + + resp, _, err = chasm.UpdateComponent( + ctx, + chasm.NewComponentRef[*CallbackExecution]( + chasm.ExecutionKey{ + NamespaceID: req.NamespaceId, + BusinessID: req.FrontendRequest.GetCallbackId(), + RunID: req.FrontendRequest.GetRunId(), + }, + ), + func(e *CallbackExecution, ctx chasm.MutableContext, _ *callbackspb.TerminateCallbackExecutionRequest) (*callbackspb.TerminateCallbackExecutionResponse, error) { + if _, err := e.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 *callbackExecutionHandler) DeleteCallbackExecution( + ctx context.Context, + req *callbackspb.DeleteCallbackExecutionRequest, +) (resp *callbackspb.DeleteCallbackExecutionResponse, err error) { + defer log.CapturePanic(h.logger, &err) + + if err = chasm.DeleteExecution[*CallbackExecution]( + 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 +} diff --git a/chasm/lib/callback/library.go b/chasm/lib/callback/library.go index 1eee0c74195..969e7282516 100644 --- a/chasm/lib/callback/library.go +++ b/chasm/lib/callback/library.go @@ -2,6 +2,7 @@ package callback import ( "go.temporal.io/server/chasm" + callbackspb "go.temporal.io/server/chasm/lib/callback/gen/callbackpb/v1" "google.golang.org/grpc" ) @@ -9,18 +10,24 @@ type ( Library struct { chasm.UnimplementedLibrary - InvocationTaskHandler *InvocationTaskHandler - BackoffTaskHandler *BackoffTaskHandler + InvocationTaskHandler *InvocationTaskHandler + BackoffTaskHandler *BackoffTaskHandler + ScheduleToCloseTimeoutTaskHandler *ScheduleToCloseTimeoutTaskHandler + callbackExecutionHandler *callbackExecutionHandler } ) func newLibrary( InvocationTaskHandler *InvocationTaskHandler, BackoffTaskHandler *BackoffTaskHandler, + ScheduleToCloseTimeoutTaskHandler *ScheduleToCloseTimeoutTaskHandler, + callbackExecutionHandler *callbackExecutionHandler, ) *Library { return &Library{ - InvocationTaskHandler: InvocationTaskHandler, - BackoffTaskHandler: BackoffTaskHandler, + InvocationTaskHandler: InvocationTaskHandler, + BackoffTaskHandler: BackoffTaskHandler, + ScheduleToCloseTimeoutTaskHandler: ScheduleToCloseTimeoutTaskHandler, + callbackExecutionHandler: callbackExecutionHandler, } } @@ -34,6 +41,11 @@ func (l *Library) Components() []*chasm.RegistrableComponent { chasm.CallbackComponentName, chasm.WithDetached(), ), + chasm.NewRegistrableComponent[*CallbackExecution]( + chasm.CallbackExecutionComponentName, + chasm.WithBusinessIDAlias("CallbackId"), + chasm.WithSearchAttributes(executionStatusSearchAttribute), + ), } } @@ -47,8 +59,13 @@ func (l *Library) Tasks() []*chasm.RegistrableTask { "backoff", l.BackoffTaskHandler, ), + chasm.NewRegistrablePureTask( + "schedule_to_close_timeout", + l.ScheduleToCloseTimeoutTaskHandler, + ), } } func (l *Library) RegisterServices(server *grpc.Server) { + callbackspb.RegisterCallbackExecutionServiceServer(server, l.callbackExecutionHandler) } diff --git a/chasm/lib/callback/nexus_invocation.go b/chasm/lib/callback/nexus_invocation.go index 669c9c488ba..cdddbfa2650 100644 --- a/chasm/lib/callback/nexus_invocation.go +++ b/chasm/lib/callback/nexus_invocation.go @@ -5,6 +5,7 @@ import ( "errors" "net/http" "net/http/httptrace" + "strings" "time" "github.com/nexus-rpc/sdk-go/nexus" @@ -16,6 +17,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" ) @@ -36,6 +38,8 @@ func (n nexusInvocation) WrapError(result invocationResult, err error) error { if retry, ok := result.(invocationResultRetry); ok { return queueserrors.NewDestinationDownError(retry.err.Error(), err) } + // invocationResultRetryNoCB is intentionally NOT wrapped as DestinationDownError + // to avoid triggering the circuit breaker for transient server-side conditions. return err } @@ -72,6 +76,12 @@ func (n nexusInvocation) Invoke( startTime := time.Now() n.completion.Header = n.nexus.Header + if n.nexus.GetToken() != "" { + if n.completion.Header == nil { + n.completion.Header = nexus.Header{} + } + n.completion.Header.Set(commonnexus.CallbackTokenHeader, n.nexus.GetToken()) + } err := client.CompleteOperation(ctx, n.nexus.Url, n.completion) namespaceTag := metrics.NamespaceTag(ns.Name().String()) @@ -84,6 +94,12 @@ func (n nexusInvocation) Invoke( retryable := isRetryableCallError(err) h.logger.Error("Callback request failed", tag.Error(err), tag.Bool("retryable", retryable)) if retryable { + // Check if this is a transient "operation not started yet" error. + // This should not trigger the circuit breaker since the destination + // is reachable — the operation just hasn't been started by its handler yet. + if isOperationNotStartedError(err) { + return invocationResultRetryNoCB{err} + } return invocationResultRetry{err} } return invocationResultFail{err} @@ -99,6 +115,16 @@ func isRetryableCallError(err error) bool { return true } +// isOperationNotStartedError detects the specific "operation not started yet" error +// returned when a completion arrives before the Nexus start handler has returned. +func isOperationNotStartedError(err error) bool { + var handlerError *nexus.HandlerError + if errors.As(err, &handlerError) { + return strings.Contains(handlerError.Message, nexusoperations.ErrMsgOperationNotStarted) + } + return false +} + func outcomeTag(callCtx context.Context, callErr error) string { if callErr != nil { if callCtx.Err() != nil { diff --git a/chasm/lib/callback/proto/v1/message.proto b/chasm/lib/callback/proto/v1/message.proto index 057e5c470e0..5aef337f375 100644 --- a/chasm/lib/callback/proto/v1/message.proto +++ b/chasm/lib/callback/proto/v1/message.proto @@ -2,12 +2,13 @@ syntax = "proto3"; package temporal.server.chasm.lib.callbacks.proto.v1; +option go_package = "go.temporal.io/server/chasm/lib/callbacks/gen/callbackspb;callbackspb"; + +import "google/protobuf/duration.proto"; import "google/protobuf/timestamp.proto"; import "temporal/api/common/v1/message.proto"; import "temporal/api/failure/v1/message.proto"; -option go_package = "go.temporal.io/server/chasm/lib/callbacks/gen/callbackspb;callbackspb"; - message CallbackState { // Trigger for when the workflow is closed. message WorkflowClosed {} @@ -33,6 +34,11 @@ message CallbackState { // Request ID that added the callback. string request_id = 9; + // The time when the callback reached a terminal state. + google.protobuf.Timestamp close_time = 10; + // Failure from an external termination (timeout or terminate), stored separately + // from last_attempt_failure so the last invocation attempt's failure is preserved. + temporal.api.failure.v1.Failure failure = 11; } // Status of a callback. @@ -49,6 +55,31 @@ enum CallbackStatus { CALLBACK_STATUS_FAILED = 4; // Callback has succeeded. CALLBACK_STATUS_SUCCEEDED = 5; + // Callback was terminated via TerminateCallbackExecution. + CALLBACK_STATUS_TERMINATED = 6; +} + +// Persisted state for a standalone CallbackExecution entity. +// The CallbackExecution is a top-level CHASM entity that owns a child Callback component +// and implements CompletionSource to provide stored completion data. +message CallbackExecutionState { + // The Nexus completion payload to deliver to the callback URL. + // For success: contains the result Payload. + // For failure: contains the Failure details. + oneof completion { + // Deliver a successful Nexus operation completion with this result payload. + temporal.api.common.v1.Payload success_completion = 1; + // Deliver a failed Nexus operation completion with this failure. + temporal.api.failure.v1.Failure failure_completion = 2; + } + // The time when the execution was created. + google.protobuf.Timestamp create_time = 3; + // The callback ID (business ID). + string callback_id = 4; + // Schedule-to-close timeout from the start request. + google.protobuf.Duration schedule_to_close_timeout = 5; + // The request ID from the terminate request, used for idempotency. + string terminate_request_id = 6; } message Callback { @@ -59,6 +90,8 @@ message Callback { string url = 1; // Header to attach to callback request. map header = 2; + // Standard token to use for completion. + 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..a8f7089d8e2 --- /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; + +option go_package = "go.temporal.io/server/chasm/lib/callbacks/gen/callbackspb;callbackspb"; + +import "temporal/api/workflowservice/v1/request_response.proto"; + +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..b4e3a62410c --- /dev/null +++ b/chasm/lib/callback/proto/v1/service.proto @@ -0,0 +1,36 @@ +syntax = "proto3"; + +package temporal.server.chasm.lib.callbacks.proto.v1; + +option go_package = "go.temporal.io/server/chasm/lib/callbacks/gen/callbackspb;callbackspb"; + +import "chasm/lib/callback/proto/v1/request_response.proto"; +import "temporal/server/api/routing/v1/extension.proto"; +import "temporal/server/api/common/v1/api_category.proto"; + +service CallbackExecutionService { + 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_STANDARD; + } + + 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..e913855f99c 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 execution's schedule-to-close timeout expires. +message ScheduleToCloseTimeoutTask {} diff --git a/chasm/lib/callback/statemachine.go b/chasm/lib/callback/statemachine.go index 779b9773989..70a477fa5f5 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) } @@ -94,6 +95,7 @@ var TransitionFailed = chasm.NewTransition( callbackspb.CALLBACK_STATUS_FAILED, func(cb *Callback, ctx chasm.MutableContext, event EventFailed) error { cb.recordAttempt(event.Time) + cb.CloseTime = timestamppb.New(event.Time) cb.LastAttemptFailure = &failurepb.Failure{ Message: event.Err.Error(), FailureInfo: &failurepb.Failure_ApplicationFailureInfo{ @@ -112,11 +114,64 @@ type EventSucceeded struct { } var TransitionSucceeded = chasm.NewTransition( - []callbackspb.CallbackStatus{callbackspb.CALLBACK_STATUS_SCHEDULED}, + []callbackspb.CallbackStatus{ + callbackspb.CALLBACK_STATUS_SCHEDULED, + }, callbackspb.CALLBACK_STATUS_SUCCEEDED, func(cb *Callback, ctx chasm.MutableContext, event EventSucceeded) error { cb.recordAttempt(event.Time) + cb.CloseTime = timestamppb.New(event.Time) cb.LastAttemptFailure = 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 { + cb.CloseTime = timestamppb.New(ctx.Now(cb)) + reason := event.Reason + if reason == "" { + reason = "callback execution terminated" + } + cb.Failure = &failurepb.Failure{ + Message: reason, + FailureInfo: &failurepb.Failure_TerminatedFailureInfo{}, + } + 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 { + cb.CloseTime = timestamppb.New(ctx.Now(cb)) + cb.Failure = &failurepb.Failure{ + Message: "callback execution timed out", + FailureInfo: &failurepb.Failure_TimeoutFailureInfo{ + TimeoutFailureInfo: &failurepb.TimeoutFailureInfo{ + TimeoutType: enumspb.TIMEOUT_TYPE_SCHEDULE_TO_CLOSE, + }, + }, + } + return nil + }, +) diff --git a/chasm/lib/callback/statemachine_test.go b/chasm/lib/callback/statemachine_test.go index dbefaf5d96c..2b17ba5a1d9 100644 --- a/chasm/lib/callback/statemachine_test.go +++ b/chasm/lib/callback/statemachine_test.go @@ -110,3 +110,82 @@ func TestValidTransitions(t *testing.T) { // Assert task is not generated, failed is terminal require.Empty(t, mctx.Tasks) } + +func TestTerminatedTransition(t *testing.T) { + callback := &Callback{ + CallbackState: &callbackspb.CallbackState{ + Callback: &callbackspb.Callback{ + Variant: &callbackspb.Callback_Nexus_{ + Nexus: &callbackspb.Callback_Nexus{ + Url: "http://address:666/path", + }, + }, + }, + }, + } + + 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) { + cb := &Callback{CallbackState: proto.Clone(callback.CallbackState).(*callbackspb.CallbackState)} + cb.SetStateMachineState(src) + mctx := &chasm.MockMutableContext{} + err := TransitionTerminated.Apply(cb, mctx, EventTerminated{}) + require.NoError(t, err) + require.Equal(t, callbackspb.CALLBACK_STATUS_TERMINATED, cb.StateMachineState()) + }) + } +} + +func TestSaveResult_RetryNoCB(t *testing.T) { + // invocationResultRetryNoCB should transition to BACKING_OFF just like + // invocationResultRetry, but without triggering the circuit breaker + // (circuit breaker is handled in WrapError, not saveResult). + cb := &Callback{ + CallbackState: &callbackspb.CallbackState{ + Callback: &callbackspb.Callback{ + Variant: &callbackspb.Callback_Nexus_{ + Nexus: &callbackspb.Callback_Nexus{ + Url: "http://address:666/path", + }, + }, + }, + Status: callbackspb.CALLBACK_STATUS_SCHEDULED, + }, + } + mctx := &chasm.MockMutableContext{} + _, err := cb.saveResult(mctx, saveResultInput{ + result: invocationResultRetryNoCB{err: errors.New("operation not started")}, + 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, "operation not started", cb.LastAttemptFailure.Message) + require.False(t, cb.LastAttemptFailure.GetApplicationFailureInfo().NonRetryable) + require.NotNil(t, cb.NextAttemptScheduleTime) + + // Assert backoff task is generated + require.Len(t, mctx.Tasks, 1) + require.IsType(t, &callbackspb.BackoffTask{}, mctx.Tasks[0].Payload) +} + +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 b4703e3a6b1..977469af122 100644 --- a/chasm/lib/callback/tasks.go +++ b/chasm/lib/callback/tasks.go @@ -102,6 +102,19 @@ func (r invocationResultRetry) error() error { return r.err } +// invocationResultRetryNoCB marks an invocation as failed with the intent to retry +// but should NOT trigger the circuit breaker. Used for transient server-side conditions +// like an operation that hasn't started yet. +type invocationResultRetryNoCB struct { + err error +} + +func (invocationResultRetryNoCB) mustImplementInvocationResult() {} + +func (r invocationResultRetryNoCB) error() error { + return r.err +} + type callbackInvokable interface { // Invoke executes the callback logic and returns the invocation result. Invoke(ctx context.Context, ns *namespace.Namespace, h InvocationTaskHandler, task *callbackspb.InvocationTask, taskAttr chasm.TaskAttributes) invocationResult @@ -193,3 +206,30 @@ func (h *BackoffTaskHandler) Validate( // Validate that the callback is in BACKING_OFF state return callback.Status == callbackspb.CALLBACK_STATUS_BACKING_OFF && callback.Attempt == task.Attempt, nil } + +// ScheduleToCloseTimeoutTaskHandler handles schedule-to-close timeout for standalone callback executions. +type ScheduleToCloseTimeoutTaskHandler struct { + chasm.PureTaskHandlerBase +} + +func NewScheduleToCloseTimeoutTaskHandler() *ScheduleToCloseTimeoutTaskHandler { + return &ScheduleToCloseTimeoutTaskHandler{} +} + +func (h *ScheduleToCloseTimeoutTaskHandler) Validate( + _ chasm.Context, + callback *Callback, + _ chasm.TaskAttributes, + _ *callbackspb.ScheduleToCloseTimeoutTask, +) (bool, error) { + return TransitionTimedOut.Possible(callback), nil +} + +func (h *ScheduleToCloseTimeoutTaskHandler) Execute( + ctx chasm.MutableContext, + callback *Callback, + _ chasm.TaskAttributes, + _ *callbackspb.ScheduleToCloseTimeoutTask, +) error { + return TransitionTimedOut.Apply(callback, ctx, EventTimedOut{}) +} diff --git a/chasm/lib/callback/tasks_test.go b/chasm/lib/callback/tasks_test.go index 82248a80fde..da3eac37d8e 100644 --- a/chasm/lib/callback/tasks_test.go +++ b/chasm/lib/callback/tasks_test.go @@ -4,7 +4,9 @@ import ( "context" "encoding/base64" "errors" + "io" "net/http" + "strings" "testing" "time" @@ -24,6 +26,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" "go.temporal.io/server/common/resource" "go.temporal.io/server/service/history/queues/common" queueserrors "go.temporal.io/server/service/history/queues/errors" @@ -111,6 +114,32 @@ func TestExecuteInvocationTaskNexus_Outcomes(t *testing.T) { require.Equal(t, callbackspb.CALLBACK_STATUS_BACKING_OFF, cb.Status) }, }, + { + name: "operation-not-started-retries-without-circuit-breaker", + caller: func(r *http.Request) (*http.Response, error) { + // Simulate the "operation not started yet" response from the server. + // The server converts serviceerror.NewUnavailable(msg) via + // ConvertGRPCError → HandlerError, then WriteFailure serializes it + // through the FailureConverter as a structured Failure JSON with + // metadata indicating it's a HandlerError. + body := `{"message":"` + nexusoperations.ErrMsgOperationNotStarted + + `","metadata":{"type":"nexus.HandlerError"},"details":{"type":"UNAVAILABLE"}}` + return &http.Response{ + StatusCode: http.StatusServiceUnavailable, + Header: http.Header{"Content-Type": []string{"application/json"}}, + Body: io.NopCloser(strings.NewReader(body)), + }, nil + }, + expectedMetricOutcome: "handler-error:UNAVAILABLE", + assertOutcome: func(t *testing.T, cb *Callback, err error) { + // Should retry. + require.Equal(t, callbackspb.CALLBACK_STATUS_BACKING_OFF, cb.Status) + // Must NOT trigger the circuit breaker. + var destDownErr *queueserrors.DestinationDownError + require.False(t, errors.As(err, &destDownErr), + "invocationResultRetryNoCB must not produce a DestinationDownError") + }, + }, { name: "non-retryable-http-error", caller: func(r *http.Request) (*http.Response, error) { diff --git a/client/frontend/client_gen.go b/client/frontend/client_gen.go index 7b3f3e3aa58..23a249bba32 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) CountSchedules( ctx context.Context, request *workflowservice.CountSchedulesRequest, @@ -69,6 +79,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) DeleteSchedule( ctx context.Context, request *workflowservice.DeleteScheduleRequest, @@ -149,6 +169,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, @@ -389,6 +419,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, @@ -549,6 +589,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) PollNexusTaskQueue( ctx context.Context, request *workflowservice.PollNexusTaskQueueRequest, @@ -889,6 +939,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) StartWorkflowExecution( ctx context.Context, request *workflowservice.StartWorkflowExecutionRequest, @@ -919,6 +979,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) TerminateWorkflowExecution( ctx context.Context, request *workflowservice.TerminateWorkflowExecutionRequest, diff --git a/client/frontend/metric_client_gen.go b/client/frontend/metric_client_gen.go index ad15ce5dc48..e1c9328538b 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) CountSchedules( ctx context.Context, request *workflowservice.CountSchedulesRequest, @@ -93,6 +107,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) DeleteSchedule( ctx context.Context, request *workflowservice.DeleteScheduleRequest, @@ -205,6 +233,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, @@ -541,6 +583,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, @@ -765,6 +821,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) PollNexusTaskQueue( ctx context.Context, request *workflowservice.PollNexusTaskQueueRequest, @@ -1241,6 +1311,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) StartWorkflowExecution( ctx context.Context, request *workflowservice.StartWorkflowExecutionRequest, @@ -1283,6 +1367,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) TerminateWorkflowExecution( ctx context.Context, request *workflowservice.TerminateWorkflowExecutionRequest, diff --git a/client/frontend/retryable_client_gen.go b/client/frontend/retryable_client_gen.go index a46a3bbfd27..146e76c9589 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) CountSchedules( ctx context.Context, request *workflowservice.CountSchedulesRequest, @@ -101,6 +116,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) DeleteSchedule( ctx context.Context, request *workflowservice.DeleteScheduleRequest, @@ -221,6 +251,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, @@ -581,6 +626,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, @@ -821,6 +881,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) PollNexusTaskQueue( ctx context.Context, request *workflowservice.PollNexusTaskQueueRequest, @@ -1331,6 +1406,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) StartWorkflowExecution( ctx context.Context, request *workflowservice.StartWorkflowExecutionRequest, @@ -1376,6 +1466,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) TerminateWorkflowExecution( ctx context.Context, request *workflowservice.TerminateWorkflowExecutionRequest, diff --git a/cmd/tools/getproto/files.go b/cmd/tools/getproto/files.go index 5a269db0083..4cf6a31ab34 100644 --- a/cmd/tools/getproto/files.go +++ b/cmd/tools/getproto/files.go @@ -8,6 +8,7 @@ import ( activity "go.temporal.io/api/activity/v1" batch "go.temporal.io/api/batch/v1" + callback "go.temporal.io/api/callback/v1" command "go.temporal.io/api/command/v1" common "go.temporal.io/api/common/v1" deployment "go.temporal.io/api/deployment/v1" @@ -47,6 +48,7 @@ func init() { importMap["google/protobuf/wrappers.proto"] = wrapperspb.File_google_protobuf_wrappers_proto importMap["temporal/api/activity/v1/message.proto"] = activity.File_temporal_api_activity_v1_message_proto importMap["temporal/api/batch/v1/message.proto"] = batch.File_temporal_api_batch_v1_message_proto + importMap["temporal/api/callback/v1/message.proto"] = callback.File_temporal_api_callback_v1_message_proto importMap["temporal/api/command/v1/message.proto"] = command.File_temporal_api_command_v1_message_proto importMap["temporal/api/common/v1/message.proto"] = common.File_temporal_api_common_v1_message_proto importMap["temporal/api/deployment/v1/message.proto"] = deployment.File_temporal_api_deployment_v1_message_proto diff --git a/common/dynamicconfig/constants.go b/common/dynamicconfig/constants.go index b1935cdca19..1e5fd7d5169 100644 --- a/common/dynamicconfig/constants.go +++ b/common/dynamicconfig/constants.go @@ -2824,7 +2824,7 @@ that task will be sent to DLQ.`, EnableChasm = NewNamespaceBoolSetting( "history.enableChasm", - false, + true, "Use real chasm tree implementation instead of the noop one", ) @@ -2857,7 +2857,7 @@ to the CHASM (V2) implementation on active scheduler workflows.`, EnableCHASMCallbacks = NewNamespaceBoolSetting( "history.enableCHASMCallbacks", - false, + true, `Controls whether new callbacks are created using the CHASM implementation instead of the previous HSM backed implementation.`, ) diff --git a/common/rpc/interceptor/logtags/workflow_service_server_gen.go b/common/rpc/interceptor/logtags/workflow_service_server_gen.go index 6f847107d14..2b3aa0772ec 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.CountSchedulesRequest: return nil case *workflowservice.CountSchedulesResponse: @@ -36,6 +40,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.DeleteScheduleRequest: return nil case *workflowservice.DeleteScheduleResponse: @@ -76,6 +86,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: @@ -181,6 +197,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: @@ -259,6 +279,12 @@ 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 nil case *workflowservice.PollNexusTaskQueueRequest: return nil case *workflowservice.PollNexusTaskQueueResponse: @@ -451,6 +477,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.StartWorkflowExecutionRequest: return []tag.Tag{ tag.WorkflowID(r.GetWorkflowId()), @@ -470,6 +502,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.TerminateWorkflowExecutionRequest: return []tag.Tag{ tag.WorkflowID(r.GetWorkflowExecution().GetWorkflowId()), 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 0ff2f78a3a9..a5e31d0426e 100644 --- a/common/testing/mockapi/workflowservicemock/v1/service_grpc.pb.mock.go +++ b/common/testing/mockapi/workflowservicemock/v1/service_grpc.pb.mock.go @@ -3,7 +3,7 @@ // // Generated by this command: // -// mockgen -package workflowservicemock -destination workflowservicemock/v1/service_grpc.pb.mock.go go.temporal.io/api/workflowservice/v1 WorkflowServiceClient +// mockgen -package workflowservicemock -destination common/testing/mockapi/workflowservicemock/v1/service_grpc.pb.mock.go go.temporal.io/api/workflowservice/v1 WorkflowServiceClient // // Package workflowservicemock is a generated GoMock package. @@ -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...) +} + // CountSchedules mocks base method. func (m *MockWorkflowServiceClient) CountSchedules(ctx context.Context, in *workflowservice.CountSchedulesRequest, opts ...grpc.CallOption) (*workflowservice.CountSchedulesResponse, error) { m.ctrl.T.Helper() @@ -162,6 +182,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...) +} + // DeleteSchedule mocks base method. func (m *MockWorkflowServiceClient) DeleteSchedule(ctx context.Context, in *workflowservice.DeleteScheduleRequest, opts ...grpc.CallOption) (*workflowservice.DeleteScheduleResponse, error) { m.ctrl.T.Helper() @@ -322,6 +362,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() @@ -802,6 +862,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() @@ -1122,6 +1202,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...) +} + // PollNexusTaskQueue mocks base method. func (m *MockWorkflowServiceClient) PollNexusTaskQueue(ctx context.Context, in *workflowservice.PollNexusTaskQueueRequest, opts ...grpc.CallOption) (*workflowservice.PollNexusTaskQueueResponse, error) { m.ctrl.T.Helper() @@ -1802,6 +1902,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...) +} + // StartWorkflowExecution mocks base method. func (m *MockWorkflowServiceClient) StartWorkflowExecution(ctx context.Context, in *workflowservice.StartWorkflowExecutionRequest, opts ...grpc.CallOption) (*workflowservice.StartWorkflowExecutionResponse, error) { m.ctrl.T.Helper() @@ -1862,6 +1982,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...) +} + // TerminateWorkflowExecution mocks base method. func (m *MockWorkflowServiceClient) TerminateWorkflowExecution(ctx context.Context, in *workflowservice.TerminateWorkflowExecutionRequest, opts ...grpc.CallOption) (*workflowservice.TerminateWorkflowExecutionResponse, error) { m.ctrl.T.Helper() diff --git a/components/nexusoperations/completion.go b/components/nexusoperations/completion.go index cf37247030b..92af2076b32 100644 --- a/components/nexusoperations/completion.go +++ b/components/nexusoperations/completion.go @@ -16,6 +16,12 @@ import ( "google.golang.org/protobuf/types/known/timestamppb" ) +// ErrMsgOperationNotStarted is the error message returned when a completion arrives before +// the operation has started and no operation token is provided. This message is used by the +// callback invocation layer to detect this specific condition and retry without triggering +// the circuit breaker. +const ErrMsgOperationNotStarted = "nexus operation not started yet" + func handleSuccessfulOperationResult( node *hsm.Node, operation Operation, @@ -136,6 +142,14 @@ func fabricateStartedEventIfMissing( return nil } + // If the operation hasn't started yet and the completion doesn't include an operation token, + // reject with a retryable error. 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 serviceerror.NewUnavailable(ErrMsgOperationNotStarted) + } + eventID, err := hsm.EventIDFromToken(operation.ScheduledEventToken) if err != nil { return err diff --git a/components/nexusoperations/frontend/handler.go b/components/nexusoperations/frontend/handler.go index dbc1ca5a285..44b086227d3 100644 --- a/components/nexusoperations/frontend/handler.go +++ b/components/nexusoperations/frontend/handler.go @@ -224,6 +224,12 @@ func (h *completionHandler) CompleteOperation(ctx context.Context, r *nexusrpc.C if errors.As(err, ¬FoundErr) { return commonnexus.ConvertGRPCError(err, true) } + var unavailableErr *serviceerror.Unavailable + if errors.As(err, &unavailableErr) { + // Expose the message so callers can detect specific conditions + // like "operation not started yet" and avoid circuit breaking. + return commonnexus.ConvertGRPCError(err, true) + } return commonnexus.ConvertGRPCError(err, false) } return nil diff --git a/go.mod b/go.mod index 7ecccf7dd30..9c898ee5770 100644 --- a/go.mod +++ b/go.mod @@ -59,7 +59,7 @@ require ( go.opentelemetry.io/otel/sdk v1.40.0 go.opentelemetry.io/otel/sdk/metric v1.40.0 go.opentelemetry.io/otel/trace v1.40.0 - go.temporal.io/api v1.62.6-0.20260318231552-70f7dc9970b6 + go.temporal.io/api v1.62.7-0.20260403231116-52df46d186c1 go.temporal.io/sdk v1.41.1 go.uber.org/fx v1.24.0 go.uber.org/mock v0.6.0 diff --git a/go.sum b/go.sum index a3e3a54ecc9..72d504ab17e 100644 --- a/go.sum +++ b/go.sum @@ -376,8 +376,8 @@ go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZY go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4= go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE= -go.temporal.io/api v1.62.6-0.20260318231552-70f7dc9970b6 h1:N3/HbW7JdvRGJHP2woHEJwJ/vFM9gCy+yIM5qWJwSQo= -go.temporal.io/api v1.62.6-0.20260318231552-70f7dc9970b6/go.mod h1:iaxoP/9OXMJcQkETTECfwYq4cw/bj4nwov8b3ZLVnXM= +go.temporal.io/api v1.62.7-0.20260403231116-52df46d186c1 h1:TxJYMUBM+t+JbPGQnnZyEyCBgQdf9izyr6pn/yS4FQA= +go.temporal.io/api v1.62.7-0.20260403231116-52df46d186c1/go.mod h1:iaxoP/9OXMJcQkETTECfwYq4cw/bj4nwov8b3ZLVnXM= go.temporal.io/sdk v1.41.1 h1:yOpvsHyDD1lNuwlGBv/SUodCPhjv9nDeC9lLHW/fJUA= go.temporal.io/sdk v1.41.1/go.mod h1:/InXQT5guZ6AizYzpmzr5avQ/GMgq1ZObcKlKE2AhTc= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= diff --git a/service/frontend/fx.go b/service/frontend/fx.go index 99df01452ae..5612ede7357 100644 --- a/service/frontend/fx.go +++ b/service/frontend/fx.go @@ -8,6 +8,7 @@ import ( "go.temporal.io/server/api/adminservice/v1" "go.temporal.io/server/chasm" "go.temporal.io/server/chasm/lib/activity" + callback "go.temporal.io/server/chasm/lib/callback" "go.temporal.io/server/chasm/lib/scheduler/gen/schedulerpb/v1" "go.temporal.io/server/client" "go.temporal.io/server/common" @@ -116,6 +117,7 @@ var Module = fx.Options( fx.Invoke(ServiceLifetimeHooks), fx.Invoke(EndpointRegistryLifetimeHooks), fx.Provide(schedulerpb.NewSchedulerServiceLayeredClient), + callback.FrontendModule, nexusfrontend.Module, activity.FrontendModule, fx.Provide(visibility.ChasmVisibilityManagerProvider), @@ -804,6 +806,7 @@ func HandlerProvider( matchingClient resource.MatchingClient, workerDeploymentStoreClient workerdeployment.Client, schedulerClient schedulerpb.SchedulerServiceClient, + callbackExecutionFrontendHandler callback.CallbackExecutionFrontendHandler, archiverProvider provider.ArchiverProvider, metricsHandler metrics.Handler, payloadSerializer serialization.Serializer, @@ -840,6 +843,7 @@ func HandlerProvider( matchingClient, workerDeploymentStoreClient, schedulerClient, + callbackExecutionFrontendHandler, archiverProvider, payloadSerializer, namespaceRegistry, diff --git a/service/frontend/workflow_handler.go b/service/frontend/workflow_handler.go index a2d32844852..44db6e9506f 100644 --- a/service/frontend/workflow_handler.go +++ b/service/frontend/workflow_handler.go @@ -36,6 +36,7 @@ import ( taskqueuespb "go.temporal.io/server/api/taskqueue/v1" "go.temporal.io/server/chasm" "go.temporal.io/server/chasm/lib/activity" + callback "go.temporal.io/server/chasm/lib/callback" chasmscheduler "go.temporal.io/server/chasm/lib/scheduler" "go.temporal.io/server/chasm/lib/scheduler/gen/schedulerpb/v1" "go.temporal.io/server/client/frontend" @@ -114,6 +115,7 @@ type ( WorkflowHandler struct { workflowservice.UnsafeWorkflowServiceServer activity.FrontendHandler + callback.CallbackExecutionFrontendHandler status int32 @@ -165,6 +167,7 @@ func NewWorkflowHandler( matchingClient matchingservice.MatchingServiceClient, workerDeploymentClient workerdeployment.Client, schedulerClient schedulerpb.SchedulerServiceClient, + callbackExecutionFrontendHandler callback.CallbackExecutionFrontendHandler, archiverProvider provider.ArchiverProvider, payloadSerializer serialization.Serializer, namespaceRegistry namespace.Registry, @@ -198,22 +201,23 @@ func NewWorkflowHandler( timeSource, config, ), - getDefaultWorkflowRetrySettings: config.DefaultWorkflowRetryPolicy, - visibilityMgr: visibilityMgr, - logger: logger, - throttledLogger: throttledLogger, - persistenceExecutionName: persistenceExecutionName, - clusterMetadataManager: clusterMetadataManager, - clusterMetadata: clusterMetadata, - historyClient: historyClient, - matchingClient: matchingClient, - workerDeploymentClient: workerDeploymentClient, - schedulerClient: schedulerClient, - archiverProvider: archiverProvider, - payloadSerializer: payloadSerializer, - namespaceRegistry: namespaceRegistry, - saProvider: saProvider, - saMapperProvider: saMapperProvider, + getDefaultWorkflowRetrySettings: config.DefaultWorkflowRetryPolicy, + visibilityMgr: visibilityMgr, + logger: logger, + throttledLogger: throttledLogger, + persistenceExecutionName: persistenceExecutionName, + clusterMetadataManager: clusterMetadataManager, + clusterMetadata: clusterMetadata, + historyClient: historyClient, + matchingClient: matchingClient, + workerDeploymentClient: workerDeploymentClient, + schedulerClient: schedulerClient, + CallbackExecutionFrontendHandler: callbackExecutionFrontendHandler, + archiverProvider: archiverProvider, + payloadSerializer: payloadSerializer, + namespaceRegistry: namespaceRegistry, + saProvider: saProvider, + saMapperProvider: saMapperProvider, saValidator: searchattribute.NewValidator( saProvider, saMapperProvider, diff --git a/service/frontend/workflow_handler_test.go b/service/frontend/workflow_handler_test.go index c202ddbddc2..258ae57dd9d 100644 --- a/service/frontend/workflow_handler_test.go +++ b/service/frontend/workflow_handler_test.go @@ -180,6 +180,7 @@ func (s *WorkflowHandlerSuite) getWorkflowHandler(config *Config) *WorkflowHandl s.mockResource.GetMatchingClient(), nil, nil, // Not initializing the scheduler client here. + nil, // Not initializing the callback execution client here. s.mockResource.GetArchiverProvider(), s.mockResource.GetPayloadSerializer(), s.mockResource.GetNamespaceRegistry(), diff --git a/tests/standalone_callback_test.go b/tests/standalone_callback_test.go new file mode 100644 index 00000000000..4ca0c3fcf1c --- /dev/null +++ b/tests/standalone_callback_test.go @@ -0,0 +1,1137 @@ +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/suite" + 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" + callback "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/tests/testcore" + "google.golang.org/protobuf/types/known/durationpb" +) + +type StandaloneCallbackSuite struct { + testcore.FunctionalTestBase +} + +func TestStandaloneCallbackSuite(t *testing.T) { + t.Parallel() + suite.Run(t, new(StandaloneCallbackSuite)) +} + +func (s *StandaloneCallbackSuite) SetupSuite() { + s.SetupSuiteWithCluster( + testcore.WithDynamicConfigOverrides(map[dynamicconfig.Key]any{ + dynamicconfig.EnableChasm.Key(): true, + dynamicconfig.EnableCHASMCallbacks.Key(): true, + callback.Enabled.Key(): true, + callback.AllowedAddresses.Key(): []any{ + map[string]any{"Pattern": "*", "AllowInsecure": true}, + }, + }), + ) +} + +// startCallbackExecution creates a standalone callback execution with the given parameters. +// If cb is nil, a default non-routable callback is used. +// If completion is nil, a default success completion is used. +func (s *StandaloneCallbackSuite) startCallbackExecution( + ctx context.Context, + callbackID string, + cb *commonpb.Callback, + completion *callbackpb.CallbackExecutionCompletion, + timeout time.Duration, +) (*workflowservice.StartCallbackExecutionResponse, error) { + s.T().Helper() + if cb == nil { + cb = &commonpb.Callback{ + Variant: &commonpb.Callback_Nexus_{ + Nexus: &commonpb.Callback_Nexus{ + Url: "http://localhost:1/nonexistent", + }, + }, + } + } + if completion == nil { + completion = &callbackpb.CallbackExecutionCompletion{ + Result: &callbackpb.CallbackExecutionCompletion_Success{ + Success: testcore.MustToPayload(s.T(), "some-result"), + }, + } + } + return s.FrontendClient().StartCallbackExecution(ctx, &workflowservice.StartCallbackExecutionRequest{ + Namespace: s.Namespace().String(), + Identity: "test", + RequestId: uuid.NewString(), + CallbackId: callbackID, + Callback: cb, + Input: &workflowservice.StartCallbackExecutionRequest_Completion{Completion: completion}, + ScheduleToCloseTimeout: durationpb.New(timeout), + }) +} + +// mustStartCallbackExecution calls startCallbackExecution and asserts no error and non-empty run_id. +func (s *StandaloneCallbackSuite) mustStartCallbackExecution( + ctx context.Context, + callbackID string, + cb *commonpb.Callback, + completion *callbackpb.CallbackExecutionCompletion, + timeout time.Duration, +) *workflowservice.StartCallbackExecutionResponse { + s.T().Helper() + resp, err := s.startCallbackExecution(ctx, callbackID, cb, completion, timeout) + s.NoError(err) + s.NotEmpty(resp.GetRunId()) + return resp +} + +// TestNexusOperationCompletionViaStandaloneCallback tests that a Nexus operation started by a +// workflow can be completed by using the standalone StartCallbackExecution API to deliver the +// Nexus completion to the operation's callback URL. +// +// Flow: +// 1. A caller workflow starts a Nexus operation via an external endpoint +// 2. The external Nexus handler starts the operation asynchronously and captures the callback URL/token +// 3. StartCallbackExecution is called with the callback URL/token and a success payload +// 4. The CHASM callback execution delivers the Nexus completion to the callback URL +// 5. The caller workflow receives the operation result and completes +func (s *StandaloneCallbackSuite) TestNexusOperationCompletionViaStandaloneCallback() { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*60) + defer cancel() + + taskQueue := testcore.RandomizeStr(s.T().Name()) + endpointName := testcore.RandomizedNexusEndpoint(s.T().Name()) + + // Channel to transmit callback info from the Nexus handler to the goroutine + // that will call StartCallbackExecution (simulating an external system receiving + // the callback details and independently completing the operation). + type callbackInfo struct { + Token string + URL string + } + callbackCh := make(chan callbackInfo, 1) + + // Set up an external Nexus handler that starts operations asynchronously + // and sends the callback URL + token to the completion goroutine. + h := nexustest.Handler{ + OnStartOperation: func( + ctx context.Context, + service, operation string, + input *nexus.LazyValue, + options nexus.StartOperationOptions, + ) (nexus.HandlerStartOperationResult[any], error) { + callbackCh <- callbackInfo{ + Token: options.CallbackHeader.Get(commonnexus.CallbackTokenHeader), + URL: options.CallbackURL, + } + return &nexus.HandlerStartOperationResultAsync{ + OperationToken: "test", + }, nil + }, + } + listenAddr := nexustest.AllocListenAddress() + nexustest.NewNexusServer(s.T(), listenAddr, h) + + _, err := s.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) + + // Goroutine that waits for the callback info from the Nexus handler and then + // calls StartCallbackExecution to deliver the completion — simulating an + // external system that receives the callback details out-of-band and + // independently completes the Nexus operation. + callbackID := "test-callback-" + uuid.NewString() + completionErrCh := make(chan error, 1) + go func() { + select { + case info := <-callbackCh: + _, err := s.startCallbackExecution(ctx, callbackID, &commonpb.Callback{ + Variant: &commonpb.Callback_Nexus_{ + Nexus: &commonpb.Callback_Nexus{ + Url: info.URL, + Token: info.Token, + }, + }, + }, &callbackpb.CallbackExecutionCompletion{ + Result: &callbackpb.CallbackExecutionCompletion_Success{ + Success: testcore.MustToPayload(s.T(), "result-from-standalone-callback"), + }, + }, time.Minute) + completionErrCh <- err + case <-ctx.Done(): + completionErrCh <- ctx.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(s.SdkClient(), taskQueue, worker.Options{}) + w.RegisterWorkflow(callerWf) + s.NoError(w.Start()) + defer w.Stop() + + // Start the caller workflow. + run, err := s.SdkClient().ExecuteWorkflow(ctx, client.StartWorkflowOptions{ + TaskQueue: taskQueue, + }, callerWf) + s.NoError(err) + + // Verify the StartCallbackExecution call succeeded. + s.NoError(<-completionErrCh) + + // The standalone callback delivers the completion to the Nexus callback URL, + // which completes the Nexus operation in the caller workflow. + var result string + s.NoError(run.Get(ctx, &result)) + s.Equal("result-from-standalone-callback", result) + + // Describe the callback execution and verify its state. + descResp, err := s.FrontendClient().DescribeCallbackExecution(ctx, &workflowservice.DescribeCallbackExecutionRequest{ + Namespace: s.Namespace().String(), + CallbackId: callbackID, + }) + s.NoError(err) + s.NotNil(descResp.GetInfo()) + s.Equal(callbackID, descResp.GetInfo().GetCallbackId()) + s.NotNil(descResp.GetInfo().GetCreateTime()) + s.Equal(enumspb.CALLBACK_EXECUTION_STATUS_SUCCEEDED, descResp.GetInfo().GetStatus()) + s.Equal(enumspb.CALLBACK_STATE_SUCCEEDED, descResp.GetInfo().GetState()) + + // Poll to verify the outcome is a success with no failure. + pollResp, err := s.FrontendClient().PollCallbackExecution(ctx, &workflowservice.PollCallbackExecutionRequest{ + Namespace: s.Namespace().String(), + CallbackId: callbackID, + }) + s.NoError(err) + s.NotNil(pollResp.GetOutcome().GetSuccess()) + s.Nil(pollResp.GetOutcome().GetFailure()) +} + +// 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 outcome when the callback reaches a terminal state. +func (s *StandaloneCallbackSuite) TestPollCallbackExecution() { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) + defer cancel() + + s.Run("returns_empty_for_non_terminal", func() { + callbackID := "poll-test-" + uuid.NewString() + s.mustStartCallbackExecution(ctx, callbackID, nil, nil, time.Minute) + + // Poll a non-terminal callback with a short timeout — should return an empty response. + shortCtx, shortCancel := context.WithTimeout(ctx, time.Second*2) + defer shortCancel() + pollResp, err := s.FrontendClient().PollCallbackExecution(shortCtx, &workflowservice.PollCallbackExecutionRequest{ + Namespace: s.Namespace().String(), + CallbackId: callbackID, + }) + s.NoError(err) + s.Nil(pollResp.GetOutcome()) + + // Terminate, then poll should return the outcome. + _, err = s.FrontendClient().TerminateCallbackExecution(ctx, &workflowservice.TerminateCallbackExecutionRequest{ + Namespace: s.Namespace().String(), + CallbackId: callbackID, + Identity: "test", + RequestId: uuid.NewString(), + Reason: "testing poll", + }) + s.NoError(err) + + pollResp, err = s.FrontendClient().PollCallbackExecution(ctx, &workflowservice.PollCallbackExecutionRequest{ + Namespace: s.Namespace().String(), + CallbackId: callbackID, + }) + s.NoError(err) + s.NotNil(pollResp.GetOutcome().GetFailure()) + s.Equal("testing poll", pollResp.GetOutcome().GetFailure().GetMessage()) + }) + + s.Run("blocks_until_complete", func() { + callbackID := "poll-blocks-" + uuid.NewString() + s.mustStartCallbackExecution(ctx, callbackID, nil, nil, 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 := s.FrontendClient().PollCallbackExecution(ctx, &workflowservice.PollCallbackExecutionRequest{ + Namespace: s.Namespace().String(), + CallbackId: callbackID, + }) + resultCh <- pollResult{resp: resp, err: err} + }() + + // Verify the poll is still blocking (hasn't returned yet). + select { + case <-resultCh: + s.Fail("expected poll to block, but it returned before terminate") + case <-time.After(500 * time.Millisecond): + } + + _, err := s.FrontendClient().TerminateCallbackExecution(ctx, &workflowservice.TerminateCallbackExecutionRequest{ + Namespace: s.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() { + callbackID := "poll-runid-" + uuid.NewString() + startResp := s.mustStartCallbackExecution(ctx, callbackID, nil, nil, time.Minute) + runID := startResp.GetRunId() + + // Terminate so poll returns immediately. + _, err := s.FrontendClient().TerminateCallbackExecution(ctx, &workflowservice.TerminateCallbackExecutionRequest{ + Namespace: s.Namespace().String(), + CallbackId: callbackID, + Identity: "test", + RequestId: uuid.NewString(), + Reason: "testing run_id", + }) + s.NoError(err) + + pollResp, err := s.FrontendClient().PollCallbackExecution(ctx, &workflowservice.PollCallbackExecutionRequest{ + Namespace: s.Namespace().String(), + CallbackId: callbackID, + }) + s.NoError(err) + s.Equal(runID, pollResp.GetRunId()) + s.NotNil(pollResp.GetOutcome().GetFailure()) + }) + + s.Run("poll_after_timeout", func() { + callbackID := "poll-timeout-" + uuid.NewString() + // Start with a very short schedule-to-close timeout so it times out quickly. + s.mustStartCallbackExecution(ctx, callbackID, nil, nil, time.Second) + + // Wait for the callback to time out, then poll for the outcome. + s.EventuallyWithT(func(t *assert.CollectT) { + pollResp, err := s.FrontendClient().PollCallbackExecution(ctx, &workflowservice.PollCallbackExecutionRequest{ + Namespace: s.Namespace().String(), + CallbackId: callbackID, + }) + if !assert.NoError(t, err) { + return + } + if !assert.NotNil(t, pollResp.GetOutcome()) { + return + } + assert.NotNil(t, pollResp.GetOutcome().GetFailure()) + assert.NotNil(t, pollResp.GetOutcome().GetFailure().GetTimeoutFailureInfo()) + assert.Equal(t, enumspb.TIMEOUT_TYPE_SCHEDULE_TO_CLOSE, pollResp.GetOutcome().GetFailure().GetTimeoutFailureInfo().GetTimeoutType()) + }, 10*time.Second, 200*time.Millisecond) + }) +} + +// TestDeleteCallbackExecution tests 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() { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*20) + 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.mustStartCallbackExecution(ctx, callbackID, nil, nil, time.Minute) + runID := startResp.GetRunId() + + // Describe using run_id to verify it was created. + descResp, err := s.FrontendClient().DescribeCallbackExecution(ctx, &workflowservice.DescribeCallbackExecutionRequest{ + Namespace: s.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 = s.FrontendClient().DeleteCallbackExecution(ctx, &workflowservice.DeleteCallbackExecutionRequest{ + Namespace: s.Namespace().String(), + CallbackId: callbackID, + RunId: uuid.NewString(), + }) + s.Error(err) + + // Delete the callback execution using correct run_id. + _, err = s.FrontendClient().DeleteCallbackExecution(ctx, &workflowservice.DeleteCallbackExecutionRequest{ + Namespace: s.Namespace().String(), + CallbackId: callbackID, + RunId: runID, + }) + s.NoError(err) + + // Describe after delete — the callback should eventually be not found. + s.EventuallyWithT(func(t *assert.CollectT) { + _, err := s.FrontendClient().DescribeCallbackExecution(ctx, &workflowservice.DescribeCallbackExecutionRequest{ + Namespace: s.Namespace().String(), + CallbackId: callbackID, + RunId: runID, + }) + assert.ErrorContains(t, err, "not found") + }, 5*time.Second, 100*time.Millisecond) +} + +// TestNexusOperationFailureViaStandaloneCallback tests that a standalone callback can deliver +// a failure completion to a Nexus operation, causing the caller workflow to receive the failure. +func (s *StandaloneCallbackSuite) TestNexusOperationFailureViaStandaloneCallback() { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*20) + defer cancel() + + taskQueue := testcore.RandomizeStr(s.T().Name()) + endpointName := testcore.RandomizedNexusEndpoint(s.T().Name()) + + type callbackInfo struct { + Token string + URL string + } + callbackCh := make(chan callbackInfo, 1) + + h := nexustest.Handler{ + OnStartOperation: func( + ctx context.Context, + service, operation string, + input *nexus.LazyValue, + options nexus.StartOperationOptions, + ) (nexus.HandlerStartOperationResult[any], error) { + callbackCh <- callbackInfo{ + Token: options.CallbackHeader.Get(commonnexus.CallbackTokenHeader), + URL: options.CallbackURL, + } + return &nexus.HandlerStartOperationResultAsync{ + OperationToken: "test", + }, nil + }, + } + listenAddr := nexustest.AllocListenAddress() + nexustest.NewNexusServer(s.T(), listenAddr, h) + + _, err := s.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) + + // Goroutine delivers a FAILURE completion via StartCallbackExecution. + completionErrCh := make(chan error, 1) + go func() { + select { + case info := <-callbackCh: + callbackID := "failure-callback-" + uuid.NewString() + _, err := s.startCallbackExecution(ctx, callbackID, &commonpb.Callback{ + Variant: &commonpb.Callback_Nexus_{ + Nexus: &commonpb.Callback_Nexus{ + Url: info.URL, + Token: info.Token, + }, + }, + }, &callbackpb.CallbackExecutionCompletion{ + Result: &callbackpb.CallbackExecutionCompletion_Failure{ + Failure: &failurepb.Failure{ + Message: "operation failed from standalone callback", + FailureInfo: &failurepb.Failure_ApplicationFailureInfo{ + ApplicationFailureInfo: &failurepb.ApplicationFailureInfo{ + NonRetryable: true, + }, + }, + }, + }, + }, time.Minute) + completionErrCh <- err + case <-ctx.Done(): + completionErrCh <- ctx.Err() + } + }() + + // Caller workflow starts a Nexus operation and expects a failure. + 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(s.SdkClient(), taskQueue, worker.Options{}) + w.RegisterWorkflow(callerWf) + s.NoError(w.Start()) + defer w.Stop() + + run, err := s.SdkClient().ExecuteWorkflow(ctx, client.StartWorkflowOptions{ + TaskQueue: taskQueue, + }, callerWf) + s.NoError(err) + + // Verify StartCallbackExecution succeeded. + s.NoError(<-completionErrCh) + + // The workflow should fail with a NexusOperationError wrapping the failure. + var result string + err = run.Get(ctx, &result) + s.Error(err) + + 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") +} + +// TestStartCallbackExecution_InvalidArguments verifies request validation. +func (s *StandaloneCallbackSuite) TestStartCallbackExecution_InvalidArguments() { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + defer cancel() + + 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() { + req := &workflowservice.StartCallbackExecutionRequest{ + Namespace: s.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 := s.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() { + 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: s.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 := s.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 := s.FrontendClient().StartCallbackExecution(ctx, req) + s.NoError(err) + s.Equal(existingRunID, dupResp.GetRunId()) + + // Same callback_id + different request_id should fail with AlreadyExists. + req.RequestId = uuid.NewString() + _, err = s.FrontendClient().StartCallbackExecution(ctx, req) + s.Error(err) + s.Contains(err.Error(), "already exists") +} + +// TestListAndCountCallbackExecutions tests that standalone callback executions +// can be listed and counted via the visibility APIs, and verifies the returned data. +func (s *StandaloneCallbackSuite) TestListAndCountCallbackExecutions() { + 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.mustStartCallbackExecution(ctx, callbackIDs[i], nil, nil, time.Minute) + } + + // List callback executions — visibility indexing may be async, so use EventuallyWithT. + // Verify returned data includes our callback IDs and has valid fields. + s.EventuallyWithT(func(t *assert.CollectT) { + listResp, err := s.FrontendClient().ListCallbackExecutions(ctx, &workflowservice.ListCallbackExecutionsRequest{ + Namespace: s.Namespace().String(), + PageSize: 10, + }) + if !assert.NoError(t, err) { + return + } + if !assert.GreaterOrEqual(t, len(listResp.GetExecutions()), 2) { + return + } + + // Collect returned callback IDs and verify fields. + foundIDs := make(map[string]bool) + for _, exec := range listResp.GetExecutions() { + foundIDs[exec.GetCallbackId()] = true + assert.NotEmpty(t, exec.GetCallbackId()) + assert.NotNil(t, exec.GetCreateTime()) + } + for _, id := range callbackIDs { + assert.True(t, foundIDs[id], "expected callback %s in list response", id) + } + }, 10*time.Second, 200*time.Millisecond) + + // List with ExecutionStatus query filter — newly started callbacks should be "Running". + s.EventuallyWithT(func(t *assert.CollectT) { + listResp, err := s.FrontendClient().ListCallbackExecutions(ctx, &workflowservice.ListCallbackExecutionsRequest{ + Namespace: s.Namespace().String(), + PageSize: 10, + Query: fmt.Sprintf(`ExecutionStatus = "Running" AND CallbackId = %q`, callbackIDs[0]), + }) + if !assert.NoError(t, err) { + return + } + if !assert.Equal(t, 1, len(listResp.GetExecutions())) { + return + } + assert.Equal(t, callbackIDs[0], listResp.GetExecutions()[0].GetCallbackId()) + }, 10*time.Second, 200*time.Millisecond) + + // Terminate one callback to test filtering by terminal status. + _, err := s.FrontendClient().TerminateCallbackExecution(ctx, &workflowservice.TerminateCallbackExecutionRequest{ + Namespace: s.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 := s.FrontendClient().ListCallbackExecutions(ctx, &workflowservice.ListCallbackExecutionsRequest{ + Namespace: s.Namespace().String(), + PageSize: 10, + Query: fmt.Sprintf(`ExecutionStatus = "Terminated" AND CallbackId = %q`, callbackIDs[1]), + }) + if !assert.NoError(t, err) { + return + } + if !assert.Equal(t, 1, len(listResp.GetExecutions())) { + return + } + assert.Equal(t, callbackIDs[1], listResp.GetExecutions()[0].GetCallbackId()) + }, 10*time.Second, 200*time.Millisecond) + + // Count callback executions. + s.EventuallyWithT(func(t *assert.CollectT) { + countResp, err := s.FrontendClient().CountCallbackExecutions(ctx, &workflowservice.CountCallbackExecutionsRequest{ + Namespace: s.Namespace().String(), + }) + if !assert.NoError(t, err) { + return + } + assert.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 := s.FrontendClient().CountCallbackExecutions(ctx, &workflowservice.CountCallbackExecutionsRequest{ + Namespace: s.Namespace().String(), + Query: `ExecutionStatus = "Running"`, + }) + if !assert.NoError(t, err) { + return + } + assert.GreaterOrEqual(t, countResp.GetCount(), int64(1)) + }, 10*time.Second, 200*time.Millisecond) +} + +// 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() { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) + defer cancel() + + callbackID := "sa-test-" + uuid.NewString() + saValue := "sa-test-value-" + uuid.NewString() + + _, err := s.FrontendClient().StartCallbackExecution(ctx, &workflowservice.StartCallbackExecutionRequest{ + Namespace: s.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 := s.FrontendClient().ListCallbackExecutions(ctx, &workflowservice.ListCallbackExecutionsRequest{ + Namespace: s.Namespace().String(), + PageSize: 10, + Query: fmt.Sprintf(`CustomKeywordField = %q AND CallbackId = %q`, saValue, callbackID), + }) + if !assert.NoError(t, err) { + return + } + if !assert.Equal(t, 1, len(listResp.GetExecutions())) { + return + } + assert.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() { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*20) + defer cancel() + + callbackID := "terminate-test-" + uuid.NewString() + startResp := s.mustStartCallbackExecution(ctx, callbackID, nil, nil, time.Minute) + requestID := uuid.NewString() + runID := startResp.GetRunId() + + // Wrong run_id should fail for describe, poll, and terminate. + wrongRunID := uuid.NewString() + + _, err := s.FrontendClient().DescribeCallbackExecution(ctx, &workflowservice.DescribeCallbackExecutionRequest{ + Namespace: s.Namespace().String(), + CallbackId: callbackID, + RunId: wrongRunID, + }) + s.Error(err) + + shortCtx, shortCancel := context.WithTimeout(ctx, time.Second*2) + defer shortCancel() + _, err = s.FrontendClient().PollCallbackExecution(shortCtx, &workflowservice.PollCallbackExecutionRequest{ + Namespace: s.Namespace().String(), + CallbackId: callbackID, + RunId: wrongRunID, + }) + s.Error(err) + + _, err = s.FrontendClient().TerminateCallbackExecution(ctx, &workflowservice.TerminateCallbackExecutionRequest{ + Namespace: s.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 = s.FrontendClient().TerminateCallbackExecution(ctx, &workflowservice.TerminateCallbackExecutionRequest{ + Namespace: s.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 := s.FrontendClient().DescribeCallbackExecution(ctx, &workflowservice.DescribeCallbackExecutionRequest{ + Namespace: s.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 := s.FrontendClient().PollCallbackExecution(ctx, &workflowservice.PollCallbackExecutionRequest{ + Namespace: s.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 = s.FrontendClient().TerminateCallbackExecution(ctx, &workflowservice.TerminateCallbackExecutionRequest{ + Namespace: s.Namespace().String(), + CallbackId: callbackID, + Identity: "test", + RequestId: requestID, + Reason: "testing terminate", + }) + s.NoError(err) + + // Different request ID should return FailedPrecondition. + _, err = s.FrontendClient().TerminateCallbackExecution(ctx, &workflowservice.TerminateCallbackExecutionRequest{ + Namespace: s.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() { + 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() + s.mustStartCallbackExecution(ctx, callbackID, &commonpb.Callback{ + Variant: &commonpb.Callback_Nexus_{ + Nexus: &commonpb.Callback_Nexus{Url: srv.URL + "/callback"}, + }, + }, nil, time.Minute) + + // Poll for the outcome — the callback should eventually fail with a non-retryable error. + pollResp, err := s.FrontendClient().PollCallbackExecution(ctx, &workflowservice.PollCallbackExecutionRequest{ + Namespace: s.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() { + 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() + _, err := s.startCallbackExecution(ctx, callbackID, &commonpb.Callback{ + Variant: &commonpb.Callback_Nexus_{ + Nexus: &commonpb.Callback_Nexus{ + Url: callbackURL, + Token: token, + }, + }, + }, &callbackpb.CallbackExecutionCompletion{ + Result: &callbackpb.CallbackExecutionCompletion_Success{ + Success: testcore.MustToPayload(s.T(), "result-before-start-returns"), + }, + }, time.Minute) + if err != nil { + return nil, nexus.NewOperationErrorf(nexus.OperationStateFailed, "StartCallbackExecution failed inside handler: %w", err) + } + + // 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 := s.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(s.SdkClient(), taskQueue, worker.Options{}) + w.RegisterWorkflow(callerWf) + s.NoError(w.Start()) + defer w.Stop() + + // Start the caller workflow. + run, err := s.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 := s.FrontendClient().GetWorkflowExecutionHistory(ctx, &workflowservice.GetWorkflowExecutionHistoryRequest{ + Namespace: s.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() { + 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.mustStartCallbackExecution(ctx, callbackID, nil, nil, 2*time.Second) + + // Poll until the callback reaches a terminal state due to timeout. + pollResp, err := s.FrontendClient().PollCallbackExecution(ctx, &workflowservice.PollCallbackExecutionRequest{ + Namespace: s.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 := s.FrontendClient().DescribeCallbackExecution(ctx, &workflowservice.DescribeCallbackExecutionRequest{ + Namespace: s.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()) +}