From f60ad80897178a74ec16dd1a7fa1a6372982901a Mon Sep 17 00:00:00 2001 From: Stephan Behnke Date: Thu, 12 Mar 2026 10:11:31 -0700 Subject: [PATCH 01/11] Standalone Nexus Operations API (#9458) Add API boilerplate for standalone Nexus Operations. - [x] built - [ ] run locally and tested manually - [x] covered by existing tests - [ ] added new unit test(s) - [ ] added new functional test(s) --- chasm/lib/nexusoperation/frontend.go | 72 ++++++++ chasm/lib/nexusoperation/fx.go | 6 + client/frontend/client_gen.go | 80 +++++++++ client/frontend/metric_client_gen.go | 112 ++++++++++++ client/frontend/retryable_client_gen.go | 120 +++++++++++++ common/api/metadata.go | 8 + .../logtags/workflow_service_server_gen.go | 55 ++++++ common/rpc/interceptor/redirection.go | 9 + common/rpc/interceptor/redirection_test.go | 9 + .../v1/service_grpc.pb.mock.go | 160 ++++++++++++++++++ go.mod | 2 +- go.sum | 6 +- service/frontend/configs/quotas.go | 34 ++-- service/frontend/configs/quotas_test.go | 3 + service/frontend/fx.go | 4 + service/frontend/workflow_handler.go | 21 ++- service/frontend/workflow_handler_test.go | 1 + 17 files changed, 680 insertions(+), 22 deletions(-) create mode 100644 chasm/lib/nexusoperation/frontend.go diff --git a/chasm/lib/nexusoperation/frontend.go b/chasm/lib/nexusoperation/frontend.go new file mode 100644 index 0000000000..35e41a558c --- /dev/null +++ b/chasm/lib/nexusoperation/frontend.go @@ -0,0 +1,72 @@ +package nexusoperation + +import ( + "context" + + "go.temporal.io/api/serviceerror" + "go.temporal.io/api/workflowservice/v1" + "go.temporal.io/server/common/log" + "go.temporal.io/server/common/namespace" +) + +// FrontendHandler provides the frontend-facing API for standalone Nexus operations. +type FrontendHandler interface { + StartNexusOperationExecution(context.Context, *workflowservice.StartNexusOperationExecutionRequest) (*workflowservice.StartNexusOperationExecutionResponse, error) + DescribeNexusOperationExecution(context.Context, *workflowservice.DescribeNexusOperationExecutionRequest) (*workflowservice.DescribeNexusOperationExecutionResponse, error) + PollNexusOperationExecution(context.Context, *workflowservice.PollNexusOperationExecutionRequest) (*workflowservice.PollNexusOperationExecutionResponse, error) + ListNexusOperationExecutions(context.Context, *workflowservice.ListNexusOperationExecutionsRequest) (*workflowservice.ListNexusOperationExecutionsResponse, error) + CountNexusOperationExecutions(context.Context, *workflowservice.CountNexusOperationExecutionsRequest) (*workflowservice.CountNexusOperationExecutionsResponse, error) + RequestCancelNexusOperationExecution(context.Context, *workflowservice.RequestCancelNexusOperationExecutionRequest) (*workflowservice.RequestCancelNexusOperationExecutionResponse, error) + TerminateNexusOperationExecution(context.Context, *workflowservice.TerminateNexusOperationExecutionRequest) (*workflowservice.TerminateNexusOperationExecutionResponse, error) + DeleteNexusOperationExecution(context.Context, *workflowservice.DeleteNexusOperationExecutionRequest) (*workflowservice.DeleteNexusOperationExecutionResponse, error) +} + +type frontendHandler struct { + config *Config + logger log.Logger + namespaceRegistry namespace.Registry +} + +func NewFrontendHandler( + config *Config, + logger log.Logger, + namespaceRegistry namespace.Registry, +) FrontendHandler { + return &frontendHandler{ + config: config, + logger: logger, + namespaceRegistry: namespaceRegistry, + } +} + +func (h *frontendHandler) StartNexusOperationExecution(context.Context, *workflowservice.StartNexusOperationExecutionRequest) (*workflowservice.StartNexusOperationExecutionResponse, error) { + return nil, serviceerror.NewUnimplemented("StartNexusOperationExecution not implemented") +} + +func (h *frontendHandler) DescribeNexusOperationExecution(context.Context, *workflowservice.DescribeNexusOperationExecutionRequest) (*workflowservice.DescribeNexusOperationExecutionResponse, error) { + return nil, serviceerror.NewUnimplemented("DescribeNexusOperationExecution not implemented") +} + +func (h *frontendHandler) PollNexusOperationExecution(context.Context, *workflowservice.PollNexusOperationExecutionRequest) (*workflowservice.PollNexusOperationExecutionResponse, error) { + return nil, serviceerror.NewUnimplemented("PollNexusOperationExecution not implemented") +} + +func (h *frontendHandler) ListNexusOperationExecutions(context.Context, *workflowservice.ListNexusOperationExecutionsRequest) (*workflowservice.ListNexusOperationExecutionsResponse, error) { + return nil, serviceerror.NewUnimplemented("ListNexusOperationExecutions not implemented") +} + +func (h *frontendHandler) CountNexusOperationExecutions(context.Context, *workflowservice.CountNexusOperationExecutionsRequest) (*workflowservice.CountNexusOperationExecutionsResponse, error) { + return nil, serviceerror.NewUnimplemented("CountNexusOperationExecutions not implemented") +} + +func (h *frontendHandler) RequestCancelNexusOperationExecution(context.Context, *workflowservice.RequestCancelNexusOperationExecutionRequest) (*workflowservice.RequestCancelNexusOperationExecutionResponse, error) { + return nil, serviceerror.NewUnimplemented("RequestCancelNexusOperationExecution not implemented") +} + +func (h *frontendHandler) TerminateNexusOperationExecution(context.Context, *workflowservice.TerminateNexusOperationExecutionRequest) (*workflowservice.TerminateNexusOperationExecutionResponse, error) { + return nil, serviceerror.NewUnimplemented("TerminateNexusOperationExecution not implemented") +} + +func (h *frontendHandler) DeleteNexusOperationExecution(context.Context, *workflowservice.DeleteNexusOperationExecutionRequest) (*workflowservice.DeleteNexusOperationExecutionResponse, error) { + return nil, serviceerror.NewUnimplemented("DeleteNexusOperationExecution not implemented") +} diff --git a/chasm/lib/nexusoperation/fx.go b/chasm/lib/nexusoperation/fx.go index dcaf022ffb..804802799c 100644 --- a/chasm/lib/nexusoperation/fx.go +++ b/chasm/lib/nexusoperation/fx.go @@ -43,6 +43,12 @@ var Module = fx.Module( fx.Invoke(register), ) +var FrontendModule = fx.Module( + "chasm.lib.nexusoperation.frontend", + fx.Provide(configProvider), + fx.Provide(NewFrontendHandler), +) + func register( registry *chasm.Registry, library *Library, diff --git a/client/frontend/client_gen.go b/client/frontend/client_gen.go index bcbcabdfc8..c1dd736d51 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) CountNexusOperationExecutions( + ctx context.Context, + request *workflowservice.CountNexusOperationExecutionsRequest, + opts ...grpc.CallOption, +) (*workflowservice.CountNexusOperationExecutionsResponse, error) { + ctx, cancel := c.createContext(ctx) + defer cancel() + return c.client.CountNexusOperationExecutions(ctx, request, opts...) +} + func (c *clientImpl) CountSchedules( ctx context.Context, request *workflowservice.CountSchedulesRequest, @@ -89,6 +99,16 @@ func (c *clientImpl) DeleteActivityExecution( return c.client.DeleteActivityExecution(ctx, request, opts...) } +func (c *clientImpl) DeleteNexusOperationExecution( + ctx context.Context, + request *workflowservice.DeleteNexusOperationExecutionRequest, + opts ...grpc.CallOption, +) (*workflowservice.DeleteNexusOperationExecutionResponse, error) { + ctx, cancel := c.createContext(ctx) + defer cancel() + return c.client.DeleteNexusOperationExecution(ctx, request, opts...) +} + func (c *clientImpl) DeleteSchedule( ctx context.Context, request *workflowservice.DeleteScheduleRequest, @@ -189,6 +209,16 @@ func (c *clientImpl) DescribeNamespace( return c.client.DescribeNamespace(ctx, request, opts...) } +func (c *clientImpl) DescribeNexusOperationExecution( + ctx context.Context, + request *workflowservice.DescribeNexusOperationExecutionRequest, + opts ...grpc.CallOption, +) (*workflowservice.DescribeNexusOperationExecutionResponse, error) { + ctx, cancel := c.createContext(ctx) + defer cancel() + return c.client.DescribeNexusOperationExecution(ctx, request, opts...) +} + func (c *clientImpl) DescribeSchedule( ctx context.Context, request *workflowservice.DescribeScheduleRequest, @@ -439,6 +469,16 @@ func (c *clientImpl) ListNamespaces( return c.client.ListNamespaces(ctx, request, opts...) } +func (c *clientImpl) ListNexusOperationExecutions( + ctx context.Context, + request *workflowservice.ListNexusOperationExecutionsRequest, + opts ...grpc.CallOption, +) (*workflowservice.ListNexusOperationExecutionsResponse, error) { + ctx, cancel := c.createContext(ctx) + defer cancel() + return c.client.ListNexusOperationExecutions(ctx, request, opts...) +} + func (c *clientImpl) ListOpenWorkflowExecutions( ctx context.Context, request *workflowservice.ListOpenWorkflowExecutionsRequest, @@ -569,6 +609,16 @@ func (c *clientImpl) PollActivityTaskQueue( return c.client.PollActivityTaskQueue(ctx, request, opts...) } +func (c *clientImpl) PollNexusOperationExecution( + ctx context.Context, + request *workflowservice.PollNexusOperationExecutionRequest, + opts ...grpc.CallOption, +) (*workflowservice.PollNexusOperationExecutionResponse, error) { + ctx, cancel := c.createContext(ctx) + defer cancel() + return c.client.PollNexusOperationExecution(ctx, request, opts...) +} + func (c *clientImpl) PollNexusTaskQueue( ctx context.Context, request *workflowservice.PollNexusTaskQueueRequest, @@ -659,6 +709,16 @@ func (c *clientImpl) RequestCancelActivityExecution( return c.client.RequestCancelActivityExecution(ctx, request, opts...) } +func (c *clientImpl) RequestCancelNexusOperationExecution( + ctx context.Context, + request *workflowservice.RequestCancelNexusOperationExecutionRequest, + opts ...grpc.CallOption, +) (*workflowservice.RequestCancelNexusOperationExecutionResponse, error) { + ctx, cancel := c.createContext(ctx) + defer cancel() + return c.client.RequestCancelNexusOperationExecution(ctx, request, opts...) +} + func (c *clientImpl) RequestCancelWorkflowExecution( ctx context.Context, request *workflowservice.RequestCancelWorkflowExecutionRequest, @@ -909,6 +969,16 @@ func (c *clientImpl) StartBatchOperation( return c.client.StartBatchOperation(ctx, request, opts...) } +func (c *clientImpl) StartNexusOperationExecution( + ctx context.Context, + request *workflowservice.StartNexusOperationExecutionRequest, + opts ...grpc.CallOption, +) (*workflowservice.StartNexusOperationExecutionResponse, error) { + ctx, cancel := c.createContext(ctx) + defer cancel() + return c.client.StartNexusOperationExecution(ctx, request, opts...) +} + func (c *clientImpl) StartWorkflowExecution( ctx context.Context, request *workflowservice.StartWorkflowExecutionRequest, @@ -939,6 +1009,16 @@ func (c *clientImpl) TerminateActivityExecution( return c.client.TerminateActivityExecution(ctx, request, opts...) } +func (c *clientImpl) TerminateNexusOperationExecution( + ctx context.Context, + request *workflowservice.TerminateNexusOperationExecutionRequest, + opts ...grpc.CallOption, +) (*workflowservice.TerminateNexusOperationExecutionResponse, error) { + ctx, cancel := c.createContext(ctx) + defer cancel() + return c.client.TerminateNexusOperationExecution(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 2115b2836c..1595908bcb 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) CountNexusOperationExecutions( + ctx context.Context, + request *workflowservice.CountNexusOperationExecutionsRequest, + opts ...grpc.CallOption, +) (_ *workflowservice.CountNexusOperationExecutionsResponse, retError error) { + + metricsHandler, startTime := c.startMetricsRecording(ctx, "FrontendClientCountNexusOperationExecutions") + defer func() { + c.finishMetricsRecording(metricsHandler, startTime, retError) + }() + + return c.client.CountNexusOperationExecutions(ctx, request, opts...) +} + func (c *metricClient) CountSchedules( ctx context.Context, request *workflowservice.CountSchedulesRequest, @@ -121,6 +135,20 @@ func (c *metricClient) DeleteActivityExecution( return c.client.DeleteActivityExecution(ctx, request, opts...) } +func (c *metricClient) DeleteNexusOperationExecution( + ctx context.Context, + request *workflowservice.DeleteNexusOperationExecutionRequest, + opts ...grpc.CallOption, +) (_ *workflowservice.DeleteNexusOperationExecutionResponse, retError error) { + + metricsHandler, startTime := c.startMetricsRecording(ctx, "FrontendClientDeleteNexusOperationExecution") + defer func() { + c.finishMetricsRecording(metricsHandler, startTime, retError) + }() + + return c.client.DeleteNexusOperationExecution(ctx, request, opts...) +} + func (c *metricClient) DeleteSchedule( ctx context.Context, request *workflowservice.DeleteScheduleRequest, @@ -261,6 +289,20 @@ func (c *metricClient) DescribeNamespace( return c.client.DescribeNamespace(ctx, request, opts...) } +func (c *metricClient) DescribeNexusOperationExecution( + ctx context.Context, + request *workflowservice.DescribeNexusOperationExecutionRequest, + opts ...grpc.CallOption, +) (_ *workflowservice.DescribeNexusOperationExecutionResponse, retError error) { + + metricsHandler, startTime := c.startMetricsRecording(ctx, "FrontendClientDescribeNexusOperationExecution") + defer func() { + c.finishMetricsRecording(metricsHandler, startTime, retError) + }() + + return c.client.DescribeNexusOperationExecution(ctx, request, opts...) +} + func (c *metricClient) DescribeSchedule( ctx context.Context, request *workflowservice.DescribeScheduleRequest, @@ -611,6 +653,20 @@ func (c *metricClient) ListNamespaces( return c.client.ListNamespaces(ctx, request, opts...) } +func (c *metricClient) ListNexusOperationExecutions( + ctx context.Context, + request *workflowservice.ListNexusOperationExecutionsRequest, + opts ...grpc.CallOption, +) (_ *workflowservice.ListNexusOperationExecutionsResponse, retError error) { + + metricsHandler, startTime := c.startMetricsRecording(ctx, "FrontendClientListNexusOperationExecutions") + defer func() { + c.finishMetricsRecording(metricsHandler, startTime, retError) + }() + + return c.client.ListNexusOperationExecutions(ctx, request, opts...) +} + func (c *metricClient) ListOpenWorkflowExecutions( ctx context.Context, request *workflowservice.ListOpenWorkflowExecutionsRequest, @@ -793,6 +849,20 @@ func (c *metricClient) PollActivityTaskQueue( return c.client.PollActivityTaskQueue(ctx, request, opts...) } +func (c *metricClient) PollNexusOperationExecution( + ctx context.Context, + request *workflowservice.PollNexusOperationExecutionRequest, + opts ...grpc.CallOption, +) (_ *workflowservice.PollNexusOperationExecutionResponse, retError error) { + + metricsHandler, startTime := c.startMetricsRecording(ctx, "FrontendClientPollNexusOperationExecution") + defer func() { + c.finishMetricsRecording(metricsHandler, startTime, retError) + }() + + return c.client.PollNexusOperationExecution(ctx, request, opts...) +} + func (c *metricClient) PollNexusTaskQueue( ctx context.Context, request *workflowservice.PollNexusTaskQueueRequest, @@ -919,6 +989,20 @@ func (c *metricClient) RequestCancelActivityExecution( return c.client.RequestCancelActivityExecution(ctx, request, opts...) } +func (c *metricClient) RequestCancelNexusOperationExecution( + ctx context.Context, + request *workflowservice.RequestCancelNexusOperationExecutionRequest, + opts ...grpc.CallOption, +) (_ *workflowservice.RequestCancelNexusOperationExecutionResponse, retError error) { + + metricsHandler, startTime := c.startMetricsRecording(ctx, "FrontendClientRequestCancelNexusOperationExecution") + defer func() { + c.finishMetricsRecording(metricsHandler, startTime, retError) + }() + + return c.client.RequestCancelNexusOperationExecution(ctx, request, opts...) +} + func (c *metricClient) RequestCancelWorkflowExecution( ctx context.Context, request *workflowservice.RequestCancelWorkflowExecutionRequest, @@ -1269,6 +1353,20 @@ func (c *metricClient) StartBatchOperation( return c.client.StartBatchOperation(ctx, request, opts...) } +func (c *metricClient) StartNexusOperationExecution( + ctx context.Context, + request *workflowservice.StartNexusOperationExecutionRequest, + opts ...grpc.CallOption, +) (_ *workflowservice.StartNexusOperationExecutionResponse, retError error) { + + metricsHandler, startTime := c.startMetricsRecording(ctx, "FrontendClientStartNexusOperationExecution") + defer func() { + c.finishMetricsRecording(metricsHandler, startTime, retError) + }() + + return c.client.StartNexusOperationExecution(ctx, request, opts...) +} + func (c *metricClient) StartWorkflowExecution( ctx context.Context, request *workflowservice.StartWorkflowExecutionRequest, @@ -1311,6 +1409,20 @@ func (c *metricClient) TerminateActivityExecution( return c.client.TerminateActivityExecution(ctx, request, opts...) } +func (c *metricClient) TerminateNexusOperationExecution( + ctx context.Context, + request *workflowservice.TerminateNexusOperationExecutionRequest, + opts ...grpc.CallOption, +) (_ *workflowservice.TerminateNexusOperationExecutionResponse, retError error) { + + metricsHandler, startTime := c.startMetricsRecording(ctx, "FrontendClientTerminateNexusOperationExecution") + defer func() { + c.finishMetricsRecording(metricsHandler, startTime, retError) + }() + + return c.client.TerminateNexusOperationExecution(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 9915f8f84e..b062c5393d 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) CountNexusOperationExecutions( + ctx context.Context, + request *workflowservice.CountNexusOperationExecutionsRequest, + opts ...grpc.CallOption, +) (*workflowservice.CountNexusOperationExecutionsResponse, error) { + var resp *workflowservice.CountNexusOperationExecutionsResponse + op := func(ctx context.Context) error { + var err error + resp, err = c.client.CountNexusOperationExecutions(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, @@ -131,6 +146,21 @@ func (c *retryableClient) DeleteActivityExecution( return resp, err } +func (c *retryableClient) DeleteNexusOperationExecution( + ctx context.Context, + request *workflowservice.DeleteNexusOperationExecutionRequest, + opts ...grpc.CallOption, +) (*workflowservice.DeleteNexusOperationExecutionResponse, error) { + var resp *workflowservice.DeleteNexusOperationExecutionResponse + op := func(ctx context.Context) error { + var err error + resp, err = c.client.DeleteNexusOperationExecution(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, @@ -281,6 +311,21 @@ func (c *retryableClient) DescribeNamespace( return resp, err } +func (c *retryableClient) DescribeNexusOperationExecution( + ctx context.Context, + request *workflowservice.DescribeNexusOperationExecutionRequest, + opts ...grpc.CallOption, +) (*workflowservice.DescribeNexusOperationExecutionResponse, error) { + var resp *workflowservice.DescribeNexusOperationExecutionResponse + op := func(ctx context.Context) error { + var err error + resp, err = c.client.DescribeNexusOperationExecution(ctx, request, opts...) + return err + } + err := backoff.ThrottleRetryContext(ctx, op, c.policy, c.isRetryable) + return resp, err +} + func (c *retryableClient) DescribeSchedule( ctx context.Context, request *workflowservice.DescribeScheduleRequest, @@ -656,6 +701,21 @@ func (c *retryableClient) ListNamespaces( return resp, err } +func (c *retryableClient) ListNexusOperationExecutions( + ctx context.Context, + request *workflowservice.ListNexusOperationExecutionsRequest, + opts ...grpc.CallOption, +) (*workflowservice.ListNexusOperationExecutionsResponse, error) { + var resp *workflowservice.ListNexusOperationExecutionsResponse + op := func(ctx context.Context) error { + var err error + resp, err = c.client.ListNexusOperationExecutions(ctx, request, opts...) + return err + } + err := backoff.ThrottleRetryContext(ctx, op, c.policy, c.isRetryable) + return resp, err +} + func (c *retryableClient) ListOpenWorkflowExecutions( ctx context.Context, request *workflowservice.ListOpenWorkflowExecutionsRequest, @@ -851,6 +911,21 @@ func (c *retryableClient) PollActivityTaskQueue( return resp, err } +func (c *retryableClient) PollNexusOperationExecution( + ctx context.Context, + request *workflowservice.PollNexusOperationExecutionRequest, + opts ...grpc.CallOption, +) (*workflowservice.PollNexusOperationExecutionResponse, error) { + var resp *workflowservice.PollNexusOperationExecutionResponse + op := func(ctx context.Context) error { + var err error + resp, err = c.client.PollNexusOperationExecution(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, @@ -986,6 +1061,21 @@ func (c *retryableClient) RequestCancelActivityExecution( return resp, err } +func (c *retryableClient) RequestCancelNexusOperationExecution( + ctx context.Context, + request *workflowservice.RequestCancelNexusOperationExecutionRequest, + opts ...grpc.CallOption, +) (*workflowservice.RequestCancelNexusOperationExecutionResponse, error) { + var resp *workflowservice.RequestCancelNexusOperationExecutionResponse + op := func(ctx context.Context) error { + var err error + resp, err = c.client.RequestCancelNexusOperationExecution(ctx, request, opts...) + return err + } + err := backoff.ThrottleRetryContext(ctx, op, c.policy, c.isRetryable) + return resp, err +} + func (c *retryableClient) RequestCancelWorkflowExecution( ctx context.Context, request *workflowservice.RequestCancelWorkflowExecutionRequest, @@ -1361,6 +1451,21 @@ func (c *retryableClient) StartBatchOperation( return resp, err } +func (c *retryableClient) StartNexusOperationExecution( + ctx context.Context, + request *workflowservice.StartNexusOperationExecutionRequest, + opts ...grpc.CallOption, +) (*workflowservice.StartNexusOperationExecutionResponse, error) { + var resp *workflowservice.StartNexusOperationExecutionResponse + op := func(ctx context.Context) error { + var err error + resp, err = c.client.StartNexusOperationExecution(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, @@ -1406,6 +1511,21 @@ func (c *retryableClient) TerminateActivityExecution( return resp, err } +func (c *retryableClient) TerminateNexusOperationExecution( + ctx context.Context, + request *workflowservice.TerminateNexusOperationExecutionRequest, + opts ...grpc.CallOption, +) (*workflowservice.TerminateNexusOperationExecutionResponse, error) { + var resp *workflowservice.TerminateNexusOperationExecutionResponse + op := func(ctx context.Context) error { + var err error + resp, err = c.client.TerminateNexusOperationExecution(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/common/api/metadata.go b/common/api/metadata.go index 9cca5ad0e0..3098d38b41 100644 --- a/common/api/metadata.go +++ b/common/api/metadata.go @@ -175,6 +175,14 @@ var ( "UpdateTaskQueueConfig": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, "FetchWorkerConfig": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingNone}, "UpdateWorkerConfig": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, + "DescribeNexusOperationExecution": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingCapable}, + "ListNexusOperationExecutions": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingNone}, + "StartNexusOperationExecution": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, + "PollNexusOperationExecution": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingAlways}, + "CountNexusOperationExecutions": {Scope: ScopeNamespace, Access: AccessReadOnly, Polling: PollingNone}, + "RequestCancelNexusOperationExecution": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, + "TerminateNexusOperationExecution": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, + "DeleteNexusOperationExecution": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, "PauseWorkflowExecution": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, "UnpauseWorkflowExecution": {Scope: ScopeNamespace, Access: AccessWrite, Polling: PollingNone}, } diff --git a/common/rpc/interceptor/logtags/workflow_service_server_gen.go b/common/rpc/interceptor/logtags/workflow_service_server_gen.go index efbccad9a6..b0d7624779 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.CountNexusOperationExecutionsRequest: + return nil + case *workflowservice.CountNexusOperationExecutionsResponse: + return nil case *workflowservice.CountSchedulesRequest: return nil case *workflowservice.CountSchedulesResponse: @@ -44,6 +48,13 @@ func (wt *WorkflowTags) extractFromWorkflowServiceServerMessage(message any) []t } case *workflowservice.DeleteActivityExecutionResponse: return nil + case *workflowservice.DeleteNexusOperationExecutionRequest: + return []tag.Tag{ + tag.OperationID(r.GetOperationId()), + tag.ChasmRunID(r.GetRunId()), + } + case *workflowservice.DeleteNexusOperationExecutionResponse: + return nil case *workflowservice.DeleteScheduleRequest: return nil case *workflowservice.DeleteScheduleResponse: @@ -92,6 +103,15 @@ func (wt *WorkflowTags) extractFromWorkflowServiceServerMessage(message any) []t return nil case *workflowservice.DescribeNamespaceResponse: return nil + case *workflowservice.DescribeNexusOperationExecutionRequest: + return []tag.Tag{ + tag.OperationID(r.GetOperationId()), + tag.ChasmRunID(r.GetRunId()), + } + case *workflowservice.DescribeNexusOperationExecutionResponse: + return []tag.Tag{ + tag.WorkflowRunID(r.GetRunId()), + } case *workflowservice.DescribeScheduleRequest: return nil case *workflowservice.DescribeScheduleResponse: @@ -201,6 +221,10 @@ func (wt *WorkflowTags) extractFromWorkflowServiceServerMessage(message any) []t return nil case *workflowservice.ListNamespacesResponse: return nil + case *workflowservice.ListNexusOperationExecutionsRequest: + return nil + case *workflowservice.ListNexusOperationExecutionsResponse: + return nil case *workflowservice.ListOpenWorkflowExecutionsRequest: return nil case *workflowservice.ListOpenWorkflowExecutionsResponse: @@ -267,6 +291,15 @@ func (wt *WorkflowTags) extractFromWorkflowServiceServerMessage(message any) []t tag.WorkflowID(r.GetWorkflowExecution().GetWorkflowId()), tag.WorkflowRunID(r.GetWorkflowExecution().GetRunId()), } + case *workflowservice.PollNexusOperationExecutionRequest: + return []tag.Tag{ + tag.OperationID(r.GetOperationId()), + tag.ChasmRunID(r.GetRunId()), + } + case *workflowservice.PollNexusOperationExecutionResponse: + return []tag.Tag{ + tag.WorkflowRunID(r.GetRunId()), + } case *workflowservice.PollNexusTaskQueueRequest: return nil case *workflowservice.PollNexusTaskQueueResponse: @@ -322,6 +355,13 @@ func (wt *WorkflowTags) extractFromWorkflowServiceServerMessage(message any) []t } case *workflowservice.RequestCancelActivityExecutionResponse: return nil + case *workflowservice.RequestCancelNexusOperationExecutionRequest: + return []tag.Tag{ + tag.OperationID(r.GetOperationId()), + tag.ChasmRunID(r.GetRunId()), + } + case *workflowservice.RequestCancelNexusOperationExecutionResponse: + return nil case *workflowservice.RequestCancelWorkflowExecutionRequest: return []tag.Tag{ tag.WorkflowID(r.GetWorkflowExecution().GetWorkflowId()), @@ -459,6 +499,14 @@ func (wt *WorkflowTags) extractFromWorkflowServiceServerMessage(message any) []t return nil case *workflowservice.StartBatchOperationResponse: return nil + case *workflowservice.StartNexusOperationExecutionRequest: + return []tag.Tag{ + tag.OperationID(r.GetOperationId()), + } + case *workflowservice.StartNexusOperationExecutionResponse: + return []tag.Tag{ + tag.WorkflowRunID(r.GetRunId()), + } case *workflowservice.StartWorkflowExecutionRequest: return []tag.Tag{ tag.WorkflowID(r.GetWorkflowId()), @@ -478,6 +526,13 @@ func (wt *WorkflowTags) extractFromWorkflowServiceServerMessage(message any) []t } case *workflowservice.TerminateActivityExecutionResponse: return nil + case *workflowservice.TerminateNexusOperationExecutionRequest: + return []tag.Tag{ + tag.OperationID(r.GetOperationId()), + tag.ChasmRunID(r.GetRunId()), + } + case *workflowservice.TerminateNexusOperationExecutionResponse: + return nil case *workflowservice.TerminateWorkflowExecutionRequest: return []tag.Tag{ tag.WorkflowID(r.GetWorkflowExecution().GetWorkflowId()), diff --git a/common/rpc/interceptor/redirection.go b/common/rpc/interceptor/redirection.go index 334a65a82e..5d2e06f7da 100644 --- a/common/rpc/interceptor/redirection.go +++ b/common/rpc/interceptor/redirection.go @@ -151,6 +151,15 @@ var ( "RequestCancelActivityExecution": func() any { return &workflowservice.RequestCancelActivityExecutionResponse{} }, "TerminateActivityExecution": func() any { return &workflowservice.TerminateActivityExecutionResponse{} }, "DeleteActivityExecution": func() any { return &workflowservice.DeleteActivityExecutionResponse{} }, + + "DescribeNexusOperationExecution": func() any { return &workflowservice.DescribeNexusOperationExecutionResponse{} }, + "ListNexusOperationExecutions": func() any { return &workflowservice.ListNexusOperationExecutionsResponse{} }, + "StartNexusOperationExecution": func() any { return &workflowservice.StartNexusOperationExecutionResponse{} }, + "PollNexusOperationExecution": func() any { return &workflowservice.PollNexusOperationExecutionResponse{} }, + "CountNexusOperationExecutions": func() any { return &workflowservice.CountNexusOperationExecutionsResponse{} }, + "RequestCancelNexusOperationExecution": func() any { return &workflowservice.RequestCancelNexusOperationExecutionResponse{} }, + "TerminateNexusOperationExecution": func() any { return &workflowservice.TerminateNexusOperationExecutionResponse{} }, + "DeleteNexusOperationExecution": func() any { return &workflowservice.DeleteNexusOperationExecutionResponse{} }, } ) diff --git a/common/rpc/interceptor/redirection_test.go b/common/rpc/interceptor/redirection_test.go index 52dcb6c3b6..9ba7201689 100644 --- a/common/rpc/interceptor/redirection_test.go +++ b/common/rpc/interceptor/redirection_test.go @@ -208,6 +208,15 @@ func (s *redirectionInterceptorSuite) TestGlobalAPI() { "RequestCancelActivityExecution": {}, "TerminateActivityExecution": {}, "DeleteActivityExecution": {}, + + "CountNexusOperationExecutions": {}, + "DeleteNexusOperationExecution": {}, + "DescribeNexusOperationExecution": {}, + "ListNexusOperationExecutions": {}, + "PollNexusOperationExecution": {}, + "RequestCancelNexusOperationExecution": {}, + "StartNexusOperationExecution": {}, + "TerminateNexusOperationExecution": {}, }, apis) } 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 babc78844f..785d81bb6c 100644 --- a/common/testing/mockapi/workflowservicemock/v1/service_grpc.pb.mock.go +++ b/common/testing/mockapi/workflowservicemock/v1/service_grpc.pb.mock.go @@ -62,6 +62,26 @@ func (mr *MockWorkflowServiceClientMockRecorder) CountActivityExecutions(ctx, in return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CountActivityExecutions", reflect.TypeOf((*MockWorkflowServiceClient)(nil).CountActivityExecutions), varargs...) } +// CountNexusOperationExecutions mocks base method. +func (m *MockWorkflowServiceClient) CountNexusOperationExecutions(ctx context.Context, in *workflowservice.CountNexusOperationExecutionsRequest, opts ...grpc.CallOption) (*workflowservice.CountNexusOperationExecutionsResponse, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, in} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "CountNexusOperationExecutions", varargs...) + ret0, _ := ret[0].(*workflowservice.CountNexusOperationExecutionsResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CountNexusOperationExecutions indicates an expected call of CountNexusOperationExecutions. +func (mr *MockWorkflowServiceClientMockRecorder) CountNexusOperationExecutions(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, "CountNexusOperationExecutions", reflect.TypeOf((*MockWorkflowServiceClient)(nil).CountNexusOperationExecutions), 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() @@ -202,6 +222,26 @@ func (mr *MockWorkflowServiceClientMockRecorder) DeleteActivityExecution(ctx, in return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteActivityExecution", reflect.TypeOf((*MockWorkflowServiceClient)(nil).DeleteActivityExecution), varargs...) } +// DeleteNexusOperationExecution mocks base method. +func (m *MockWorkflowServiceClient) DeleteNexusOperationExecution(ctx context.Context, in *workflowservice.DeleteNexusOperationExecutionRequest, opts ...grpc.CallOption) (*workflowservice.DeleteNexusOperationExecutionResponse, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, in} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "DeleteNexusOperationExecution", varargs...) + ret0, _ := ret[0].(*workflowservice.DeleteNexusOperationExecutionResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DeleteNexusOperationExecution indicates an expected call of DeleteNexusOperationExecution. +func (mr *MockWorkflowServiceClientMockRecorder) DeleteNexusOperationExecution(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, "DeleteNexusOperationExecution", reflect.TypeOf((*MockWorkflowServiceClient)(nil).DeleteNexusOperationExecution), 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() @@ -402,6 +442,26 @@ func (mr *MockWorkflowServiceClientMockRecorder) DescribeNamespace(ctx, in any, return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeNamespace", reflect.TypeOf((*MockWorkflowServiceClient)(nil).DescribeNamespace), varargs...) } +// DescribeNexusOperationExecution mocks base method. +func (m *MockWorkflowServiceClient) DescribeNexusOperationExecution(ctx context.Context, in *workflowservice.DescribeNexusOperationExecutionRequest, opts ...grpc.CallOption) (*workflowservice.DescribeNexusOperationExecutionResponse, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, in} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "DescribeNexusOperationExecution", varargs...) + ret0, _ := ret[0].(*workflowservice.DescribeNexusOperationExecutionResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DescribeNexusOperationExecution indicates an expected call of DescribeNexusOperationExecution. +func (mr *MockWorkflowServiceClientMockRecorder) DescribeNexusOperationExecution(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, "DescribeNexusOperationExecution", reflect.TypeOf((*MockWorkflowServiceClient)(nil).DescribeNexusOperationExecution), varargs...) +} + // DescribeSchedule mocks base method. func (m *MockWorkflowServiceClient) DescribeSchedule(ctx context.Context, in *workflowservice.DescribeScheduleRequest, opts ...grpc.CallOption) (*workflowservice.DescribeScheduleResponse, error) { m.ctrl.T.Helper() @@ -902,6 +962,26 @@ func (mr *MockWorkflowServiceClientMockRecorder) ListNamespaces(ctx, in any, opt return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListNamespaces", reflect.TypeOf((*MockWorkflowServiceClient)(nil).ListNamespaces), varargs...) } +// ListNexusOperationExecutions mocks base method. +func (m *MockWorkflowServiceClient) ListNexusOperationExecutions(ctx context.Context, in *workflowservice.ListNexusOperationExecutionsRequest, opts ...grpc.CallOption) (*workflowservice.ListNexusOperationExecutionsResponse, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, in} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "ListNexusOperationExecutions", varargs...) + ret0, _ := ret[0].(*workflowservice.ListNexusOperationExecutionsResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListNexusOperationExecutions indicates an expected call of ListNexusOperationExecutions. +func (mr *MockWorkflowServiceClientMockRecorder) ListNexusOperationExecutions(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, "ListNexusOperationExecutions", reflect.TypeOf((*MockWorkflowServiceClient)(nil).ListNexusOperationExecutions), varargs...) +} + // ListOpenWorkflowExecutions mocks base method. func (m *MockWorkflowServiceClient) ListOpenWorkflowExecutions(ctx context.Context, in *workflowservice.ListOpenWorkflowExecutionsRequest, opts ...grpc.CallOption) (*workflowservice.ListOpenWorkflowExecutionsResponse, error) { m.ctrl.T.Helper() @@ -1162,6 +1242,26 @@ func (mr *MockWorkflowServiceClientMockRecorder) PollActivityTaskQueue(ctx, in a return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PollActivityTaskQueue", reflect.TypeOf((*MockWorkflowServiceClient)(nil).PollActivityTaskQueue), varargs...) } +// PollNexusOperationExecution mocks base method. +func (m *MockWorkflowServiceClient) PollNexusOperationExecution(ctx context.Context, in *workflowservice.PollNexusOperationExecutionRequest, opts ...grpc.CallOption) (*workflowservice.PollNexusOperationExecutionResponse, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, in} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "PollNexusOperationExecution", varargs...) + ret0, _ := ret[0].(*workflowservice.PollNexusOperationExecutionResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PollNexusOperationExecution indicates an expected call of PollNexusOperationExecution. +func (mr *MockWorkflowServiceClientMockRecorder) PollNexusOperationExecution(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, "PollNexusOperationExecution", reflect.TypeOf((*MockWorkflowServiceClient)(nil).PollNexusOperationExecution), 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() @@ -1342,6 +1442,26 @@ func (mr *MockWorkflowServiceClientMockRecorder) RequestCancelActivityExecution( return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RequestCancelActivityExecution", reflect.TypeOf((*MockWorkflowServiceClient)(nil).RequestCancelActivityExecution), varargs...) } +// RequestCancelNexusOperationExecution mocks base method. +func (m *MockWorkflowServiceClient) RequestCancelNexusOperationExecution(ctx context.Context, in *workflowservice.RequestCancelNexusOperationExecutionRequest, opts ...grpc.CallOption) (*workflowservice.RequestCancelNexusOperationExecutionResponse, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, in} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "RequestCancelNexusOperationExecution", varargs...) + ret0, _ := ret[0].(*workflowservice.RequestCancelNexusOperationExecutionResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// RequestCancelNexusOperationExecution indicates an expected call of RequestCancelNexusOperationExecution. +func (mr *MockWorkflowServiceClientMockRecorder) RequestCancelNexusOperationExecution(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, "RequestCancelNexusOperationExecution", reflect.TypeOf((*MockWorkflowServiceClient)(nil).RequestCancelNexusOperationExecution), varargs...) +} + // RequestCancelWorkflowExecution mocks base method. func (m *MockWorkflowServiceClient) RequestCancelWorkflowExecution(ctx context.Context, in *workflowservice.RequestCancelWorkflowExecutionRequest, opts ...grpc.CallOption) (*workflowservice.RequestCancelWorkflowExecutionResponse, error) { m.ctrl.T.Helper() @@ -1842,6 +1962,26 @@ func (mr *MockWorkflowServiceClientMockRecorder) StartBatchOperation(ctx, in any return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StartBatchOperation", reflect.TypeOf((*MockWorkflowServiceClient)(nil).StartBatchOperation), varargs...) } +// StartNexusOperationExecution mocks base method. +func (m *MockWorkflowServiceClient) StartNexusOperationExecution(ctx context.Context, in *workflowservice.StartNexusOperationExecutionRequest, opts ...grpc.CallOption) (*workflowservice.StartNexusOperationExecutionResponse, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, in} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "StartNexusOperationExecution", varargs...) + ret0, _ := ret[0].(*workflowservice.StartNexusOperationExecutionResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// StartNexusOperationExecution indicates an expected call of StartNexusOperationExecution. +func (mr *MockWorkflowServiceClientMockRecorder) StartNexusOperationExecution(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, "StartNexusOperationExecution", reflect.TypeOf((*MockWorkflowServiceClient)(nil).StartNexusOperationExecution), 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() @@ -1902,6 +2042,26 @@ func (mr *MockWorkflowServiceClientMockRecorder) TerminateActivityExecution(ctx, return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TerminateActivityExecution", reflect.TypeOf((*MockWorkflowServiceClient)(nil).TerminateActivityExecution), varargs...) } +// TerminateNexusOperationExecution mocks base method. +func (m *MockWorkflowServiceClient) TerminateNexusOperationExecution(ctx context.Context, in *workflowservice.TerminateNexusOperationExecutionRequest, opts ...grpc.CallOption) (*workflowservice.TerminateNexusOperationExecutionResponse, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, in} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "TerminateNexusOperationExecution", varargs...) + ret0, _ := ret[0].(*workflowservice.TerminateNexusOperationExecutionResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// TerminateNexusOperationExecution indicates an expected call of TerminateNexusOperationExecution. +func (mr *MockWorkflowServiceClientMockRecorder) TerminateNexusOperationExecution(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, "TerminateNexusOperationExecution", reflect.TypeOf((*MockWorkflowServiceClient)(nil).TerminateNexusOperationExecution), 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/go.mod b/go.mod index a8031c011e..f23481376a 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.8 + go.temporal.io/api v1.62.9-0.20260413170224-bfc609451c3d go.temporal.io/auto-scaled-workers v0.0.0-20260407181057-edd947d743d2 go.temporal.io/sdk v1.41.1 go.uber.org/fx v1.24.0 diff --git a/go.sum b/go.sum index fa7633c649..171475c3d2 100644 --- a/go.sum +++ b/go.sum @@ -440,8 +440,10 @@ 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.8 h1:g8RAZmdebYODoNa2GLA4M4TsXNe1096WV3n26C4+fdw= -go.temporal.io/api v1.62.8/go.mod h1:iaxoP/9OXMJcQkETTECfwYq4cw/bj4nwov8b3ZLVnXM= +go.temporal.io/api v1.62.7-0.20260409145211-9f50bee01930 h1:K9Ch2w/vTHQwPVZj9Ft8G+Zb1cdEj7NmlCCCXnR9elI= +go.temporal.io/api v1.62.7-0.20260409145211-9f50bee01930/go.mod h1:iaxoP/9OXMJcQkETTECfwYq4cw/bj4nwov8b3ZLVnXM= +go.temporal.io/api v1.62.9-0.20260413170224-bfc609451c3d h1:eYMXtXe15odN5Dut+riDlmbMOZ+QeUlBJRAkUY1lRi8= +go.temporal.io/api v1.62.9-0.20260413170224-bfc609451c3d/go.mod h1:iaxoP/9OXMJcQkETTECfwYq4cw/bj4nwov8b3ZLVnXM= go.temporal.io/auto-scaled-workers v0.0.0-20260407181057-edd947d743d2 h1:1hKeH3GyR6YD6LKMHGCZ76t6h1Sgha0hXVQBxWi3dlQ= go.temporal.io/auto-scaled-workers v0.0.0-20260407181057-edd947d743d2/go.mod h1:T8dnzVPeO+gaUTj9eDgm/lT2lZH4+JXNvrGaQGyVi50= go.temporal.io/sdk v1.41.1 h1:yOpvsHyDD1lNuwlGBv/SUodCPhjv9nDeC9lLHW/fJUA= diff --git a/service/frontend/configs/quotas.go b/service/frontend/configs/quotas.go index bef6821cbc..98ec1647bb 100644 --- a/service/frontend/configs/quotas.go +++ b/service/frontend/configs/quotas.go @@ -35,26 +35,26 @@ var ( // from their corresponding quota, which is determined by // dynamicconfig.FrontendMaxConcurrentLongRunningRequestsPerInstance. If the value is not set, // then the method is not considered a long-running request and the number of concurrent - // requests will not be throttled. The Poll* methods here are long-running because they block - // until there is a task available. GetWorkflowExecutionHistory and DescribeActivityExecution - // methods are blocking only if WaitNewEvent/LongPollToken are set, otherwise they are not - // long-running. The QueryWorkflow and UpdateWorkflowExecution methods are long-running because - // they both block until a background WFT is complete. + // requests will not be throttled. ExecutionAPICountLimitOverride = map[string]int{ + // These methods here are long-running because they block until there is a task available. "/temporal.api.workflowservice.v1.WorkflowService/PollActivityTaskQueue": 1, "/temporal.api.workflowservice.v1.WorkflowService/PollWorkflowTaskQueue": 1, "/temporal.api.workflowservice.v1.WorkflowService/PollWorkflowExecutionUpdate": 1, - "/temporal.api.workflowservice.v1.WorkflowService/QueryWorkflow": 1, - "/temporal.api.workflowservice.v1.WorkflowService/UpdateWorkflowExecution": 1, "/temporal.api.workflowservice.v1.WorkflowService/PollNexusTaskQueue": 1, + "/temporal.api.workflowservice.v1.WorkflowService/PollActivityExecution": 1, + "/temporal.api.workflowservice.v1.WorkflowService/PollNexusOperationExecution": 1, - // Long-running if activity outcome is not already available - "/temporal.api.workflowservice.v1.WorkflowService/PollActivityExecution": 1, - // Long-running if certain request parameters are set - "/temporal.api.workflowservice.v1.WorkflowService/GetWorkflowExecutionHistory": 1, - "/temporal.api.workflowservice.v1.WorkflowService/DescribeActivityExecution": 1, + // These methods are long-running because they block until a background WFT is complete. + "/temporal.api.workflowservice.v1.WorkflowService/QueryWorkflow": 1, + "/temporal.api.workflowservice.v1.WorkflowService/UpdateWorkflowExecution": 1, - // potentially long-running, depending on the operations + // These methods are blocking only if WaitNewEvent/LongPollToken are set, otherwise they are not. + "/temporal.api.workflowservice.v1.WorkflowService/DescribeNexusOperationExecution": 1, + "/temporal.api.workflowservice.v1.WorkflowService/GetWorkflowExecutionHistory": 1, + "/temporal.api.workflowservice.v1.WorkflowService/DescribeActivityExecution": 1, + + // Potentially long-running, depending on the operations. "/temporal.api.workflowservice.v1.WorkflowService/ExecuteMultiOperation": 1, // Dispatching a Nexus task is a potentially long running RPC, it's classified in the same bucket as QueryWorkflow. @@ -92,6 +92,7 @@ var ( "/temporal.api.workflowservice.v1.WorkflowService/CreateSchedule": 1, "/temporal.api.workflowservice.v1.WorkflowService/StartBatchOperation": 1, "/temporal.api.workflowservice.v1.WorkflowService/StartActivityExecution": 1, + "/temporal.api.workflowservice.v1.WorkflowService/StartNexusOperationExecution": 1, DispatchNexusTaskByNamespaceAndTaskQueueAPIName: 1, DispatchNexusTaskByEndpointAPIName: 1, @@ -141,6 +142,9 @@ var ( "/temporal.api.workflowservice.v1.WorkflowService/RequestCancelActivityExecution": 2, "/temporal.api.workflowservice.v1.WorkflowService/TerminateActivityExecution": 2, "/temporal.api.workflowservice.v1.WorkflowService/DeleteActivityExecution": 2, + "/temporal.api.workflowservice.v1.WorkflowService/RequestCancelNexusOperationExecution": 2, + "/temporal.api.workflowservice.v1.WorkflowService/TerminateNexusOperationExecution": 2, + "/temporal.api.workflowservice.v1.WorkflowService/DeleteNexusOperationExecution": 2, "/temporal.api.workflowservice.v1.WorkflowService/PauseWorkflowExecution": 2, "/temporal.api.workflowservice.v1.WorkflowService/UnpauseWorkflowExecution": 2, @@ -159,6 +163,7 @@ var ( "/temporal.api.workflowservice.v1.WorkflowService/GetCurrentDeployment": 3, // [cleanup-wv-pre-release] "/temporal.api.workflowservice.v1.WorkflowService/DescribeWorkerDeploymentVersion": 3, "/temporal.api.workflowservice.v1.WorkflowService/DescribeWorkerDeployment": 3, + "/temporal.api.workflowservice.v1.WorkflowService/DescribeNexusOperationExecution": 3, "/temporal.api.workflowservice.v1.WorkflowService/ValidateWorkerDeploymentVersionComputeConfig": 3, // P3: Progress APIs for reporting cancellations and failures. @@ -171,6 +176,7 @@ var ( "/temporal.api.workflowservice.v1.WorkflowService/RespondNexusTaskFailed": 3, // P4: Poll APIs and other low priority APIs + "/temporal.api.workflowservice.v1.WorkflowService/PollNexusOperationExecution": 4, "/temporal.api.workflowservice.v1.WorkflowService/PollActivityExecution": 4, // TODO(saa-preview): should it be 4 or 3? "/temporal.api.workflowservice.v1.WorkflowService/PollWorkflowTaskQueue": 4, "/temporal.api.workflowservice.v1.WorkflowService/PollActivityTaskQueue": 4, @@ -207,6 +213,8 @@ var ( "/temporal.api.workflowservice.v1.WorkflowService/DescribeWorker": 1, "/temporal.api.workflowservice.v1.WorkflowService/CountActivityExecutions": 1, "/temporal.api.workflowservice.v1.WorkflowService/ListActivityExecutions": 1, + "/temporal.api.workflowservice.v1.WorkflowService/CountNexusOperationExecutions": 1, + "/temporal.api.workflowservice.v1.WorkflowService/ListNexusOperationExecutions": 1, // APIs that rely on visibility "/temporal.api.workflowservice.v1.WorkflowService/GetWorkerTaskReachability": 1, diff --git a/service/frontend/configs/quotas_test.go b/service/frontend/configs/quotas_test.go index dcd5107d23..06663dc901 100644 --- a/service/frontend/configs/quotas_test.go +++ b/service/frontend/configs/quotas_test.go @@ -106,6 +106,9 @@ func (s *quotasSuite) TestVisibilityAPIs() { "/temporal.api.workflowservice.v1.WorkflowService/CountActivityExecutions": {}, "/temporal.api.workflowservice.v1.WorkflowService/ListActivityExecutions": {}, + + "/temporal.api.workflowservice.v1.WorkflowService/CountNexusOperationExecutions": {}, + "/temporal.api.workflowservice.v1.WorkflowService/ListNexusOperationExecutions": {}, } var service workflowservice.WorkflowServiceServer diff --git a/service/frontend/fx.go b/service/frontend/fx.go index 893f5d479e..900eddab1a 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" + "go.temporal.io/server/chasm/lib/nexusoperation" "go.temporal.io/server/chasm/lib/scheduler/gen/schedulerpb/v1" "go.temporal.io/server/client" "go.temporal.io/server/common" @@ -118,6 +119,7 @@ var Module = fx.Options( fx.Provide(schedulerpb.NewSchedulerServiceLayeredClient), nexusfrontend.Module, activity.FrontendModule, + nexusoperation.FrontendModule, fx.Provide(visibility.ChasmVisibilityManagerProvider), fx.Provide(chasm.ChasmVisibilityInterceptorProvider), ) @@ -820,6 +822,7 @@ func HandlerProvider( healthInterceptor *interceptor.HealthInterceptor, scheduleSpecBuilder *scheduler.SpecBuilder, activityHandler activity.FrontendHandler, + nexusOperationHandler nexusoperation.FrontendHandler, registry *chasm.Registry, frontendServiceResolver membership.ServiceResolver, ) Handler { @@ -856,6 +859,7 @@ func HandlerProvider( scheduleSpecBuilder, httpEnabled(cfg, serviceName), activityHandler, + nexusOperationHandler, registry, workerDeploymentReadRateLimiter, ) diff --git a/service/frontend/workflow_handler.go b/service/frontend/workflow_handler.go index 0f7cac9dcf..eeaf2c0b57 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" + "go.temporal.io/server/chasm/lib/nexusoperation" chasmscheduler "go.temporal.io/server/chasm/lib/scheduler" "go.temporal.io/server/chasm/lib/scheduler/gen/schedulerpb/v1" "go.temporal.io/server/client/frontend" @@ -110,10 +111,16 @@ const ( ) type ( + // ActivityHandler is the activity frontend handler, aliased to avoid embedding name collision. + ActivityHandler = activity.FrontendHandler + // NexusOperationHandler is the nexus operation frontend handler, aliased to avoid embedding name collision. + NexusOperationHandler = nexusoperation.FrontendHandler + // WorkflowHandler - gRPC handler interface for workflowservice WorkflowHandler struct { workflowservice.UnsafeWorkflowServiceServer - activity.FrontendHandler + ActivityHandler + NexusOperationHandler status int32 @@ -321,15 +328,17 @@ func NewWorkflowHandler( scheduleSpecBuilder *scheduler.SpecBuilder, httpEnabled bool, activityHandler activity.FrontendHandler, + nexusOperationHandler nexusoperation.FrontendHandler, registry *chasm.Registry, workerDeploymentReadRateLimiter quotas.RequestRateLimiter, ) *WorkflowHandler { handler := &WorkflowHandler{ - FrontendHandler: activityHandler, - status: common.DaemonStatusInitialized, - config: config, - tokenSerializer: tasktoken.NewSerializer(), - versionChecker: headers.NewDefaultVersionChecker(), + ActivityHandler: activityHandler, + NexusOperationHandler: nexusOperationHandler, + status: common.DaemonStatusInitialized, + config: config, + tokenSerializer: tasktoken.NewSerializer(), + versionChecker: headers.NewDefaultVersionChecker(), namespaceHandler: newNamespaceHandler( logger, persistenceMetadataManager, diff --git a/service/frontend/workflow_handler_test.go b/service/frontend/workflow_handler_test.go index 8726d6bf07..62c2f7c9cc 100644 --- a/service/frontend/workflow_handler_test.go +++ b/service/frontend/workflow_handler_test.go @@ -194,6 +194,7 @@ func (s *WorkflowHandlerSuite) getWorkflowHandler(config *Config) *WorkflowHandl scheduler.NewSpecBuilder(), true, nil, // Not testing activity handler here + nil, // Not testing nexus operation handler here nil, quotas.NoopRequestRateLimiter, ) From 59b192d3659cff238b008b47752e8d1782db6b01 Mon Sep 17 00:00:00 2001 From: Stephan Behnke Date: Thu, 19 Mar 2026 08:53:24 -0700 Subject: [PATCH 02/11] Nexus Standalone: Feature flag (#9567) Add Nexus Standalone feature flag. Tests will be added to respective API impl. --- chasm/lib/nexusoperation/config.go | 8 +++++ chasm/lib/nexusoperation/frontend.go | 47 +++++++++++++++++++++++----- cmd/tools/getproto/files.go | 2 ++ 3 files changed, 49 insertions(+), 8 deletions(-) diff --git a/chasm/lib/nexusoperation/config.go b/chasm/lib/nexusoperation/config.go index fdd51628c8..918b64773a 100644 --- a/chasm/lib/nexusoperation/config.go +++ b/chasm/lib/nexusoperation/config.go @@ -13,6 +13,12 @@ import ( "go.temporal.io/server/common/rpc/interceptor" ) +var Enabled = dynamicconfig.NewNamespaceBoolSetting( + "nexusoperation.enableStandalone", + false, + `Toggles standalone Nexus operation functionality on the server.`, +) + var EnableChasmNexus = dynamicconfig.NewNamespaceBoolSetting( "nexusoperation.enableChasm", false, @@ -188,6 +194,7 @@ Added for safety. Defaults to true. Likely to be removed in future server versio ) type Config struct { + Enabled dynamicconfig.BoolPropertyFnWithNamespaceFilter EnableChasm dynamicconfig.BoolPropertyFnWithNamespaceFilter EnableChasmNexus dynamicconfig.BoolPropertyFnWithNamespaceFilter NumHistoryShards int32 @@ -210,6 +217,7 @@ type Config struct { func configProvider(dc *dynamicconfig.Collection, cfg *config.Persistence) *Config { return &Config{ + Enabled: Enabled.Get(dc), EnableChasm: dynamicconfig.EnableChasm.Get(dc), EnableChasmNexus: EnableChasmNexus.Get(dc), NumHistoryShards: cfg.NumHistoryShards, diff --git a/chasm/lib/nexusoperation/frontend.go b/chasm/lib/nexusoperation/frontend.go index 35e41a558c..7be22f8062 100644 --- a/chasm/lib/nexusoperation/frontend.go +++ b/chasm/lib/nexusoperation/frontend.go @@ -21,6 +21,8 @@ type FrontendHandler interface { DeleteNexusOperationExecution(context.Context, *workflowservice.DeleteNexusOperationExecutionRequest) (*workflowservice.DeleteNexusOperationExecutionResponse, error) } +var ErrStandaloneNexusOperationDisabled = serviceerror.NewUnimplemented("Standalone Nexus operation is disabled") + type frontendHandler struct { config *Config logger log.Logger @@ -39,34 +41,63 @@ func NewFrontendHandler( } } -func (h *frontendHandler) StartNexusOperationExecution(context.Context, *workflowservice.StartNexusOperationExecutionRequest) (*workflowservice.StartNexusOperationExecutionResponse, error) { +// isStandaloneNexusOperationEnabled checks if standalone Nexus operations are enabled for the given namespace. +func (h *frontendHandler) isStandaloneNexusOperationEnabled(namespaceName string) bool { + return h.config.EnableChasm(namespaceName) && h.config.Enabled(namespaceName) +} + +func (h *frontendHandler) StartNexusOperationExecution(_ context.Context, req *workflowservice.StartNexusOperationExecutionRequest) (*workflowservice.StartNexusOperationExecutionResponse, error) { + if !h.isStandaloneNexusOperationEnabled(req.GetNamespace()) { + return nil, ErrStandaloneNexusOperationDisabled + } return nil, serviceerror.NewUnimplemented("StartNexusOperationExecution not implemented") } -func (h *frontendHandler) DescribeNexusOperationExecution(context.Context, *workflowservice.DescribeNexusOperationExecutionRequest) (*workflowservice.DescribeNexusOperationExecutionResponse, error) { +func (h *frontendHandler) DescribeNexusOperationExecution(_ context.Context, req *workflowservice.DescribeNexusOperationExecutionRequest) (*workflowservice.DescribeNexusOperationExecutionResponse, error) { + if !h.isStandaloneNexusOperationEnabled(req.GetNamespace()) { + return nil, ErrStandaloneNexusOperationDisabled + } return nil, serviceerror.NewUnimplemented("DescribeNexusOperationExecution not implemented") } -func (h *frontendHandler) PollNexusOperationExecution(context.Context, *workflowservice.PollNexusOperationExecutionRequest) (*workflowservice.PollNexusOperationExecutionResponse, error) { +func (h *frontendHandler) PollNexusOperationExecution(_ context.Context, req *workflowservice.PollNexusOperationExecutionRequest) (*workflowservice.PollNexusOperationExecutionResponse, error) { + if !h.isStandaloneNexusOperationEnabled(req.GetNamespace()) { + return nil, ErrStandaloneNexusOperationDisabled + } return nil, serviceerror.NewUnimplemented("PollNexusOperationExecution not implemented") } -func (h *frontendHandler) ListNexusOperationExecutions(context.Context, *workflowservice.ListNexusOperationExecutionsRequest) (*workflowservice.ListNexusOperationExecutionsResponse, error) { +func (h *frontendHandler) ListNexusOperationExecutions(_ context.Context, req *workflowservice.ListNexusOperationExecutionsRequest) (*workflowservice.ListNexusOperationExecutionsResponse, error) { + if !h.isStandaloneNexusOperationEnabled(req.GetNamespace()) { + return nil, ErrStandaloneNexusOperationDisabled + } return nil, serviceerror.NewUnimplemented("ListNexusOperationExecutions not implemented") } -func (h *frontendHandler) CountNexusOperationExecutions(context.Context, *workflowservice.CountNexusOperationExecutionsRequest) (*workflowservice.CountNexusOperationExecutionsResponse, error) { +func (h *frontendHandler) CountNexusOperationExecutions(_ context.Context, req *workflowservice.CountNexusOperationExecutionsRequest) (*workflowservice.CountNexusOperationExecutionsResponse, error) { + if !h.isStandaloneNexusOperationEnabled(req.GetNamespace()) { + return nil, ErrStandaloneNexusOperationDisabled + } return nil, serviceerror.NewUnimplemented("CountNexusOperationExecutions not implemented") } -func (h *frontendHandler) RequestCancelNexusOperationExecution(context.Context, *workflowservice.RequestCancelNexusOperationExecutionRequest) (*workflowservice.RequestCancelNexusOperationExecutionResponse, error) { +func (h *frontendHandler) RequestCancelNexusOperationExecution(_ context.Context, req *workflowservice.RequestCancelNexusOperationExecutionRequest) (*workflowservice.RequestCancelNexusOperationExecutionResponse, error) { + if !h.isStandaloneNexusOperationEnabled(req.GetNamespace()) { + return nil, ErrStandaloneNexusOperationDisabled + } return nil, serviceerror.NewUnimplemented("RequestCancelNexusOperationExecution not implemented") } -func (h *frontendHandler) TerminateNexusOperationExecution(context.Context, *workflowservice.TerminateNexusOperationExecutionRequest) (*workflowservice.TerminateNexusOperationExecutionResponse, error) { +func (h *frontendHandler) TerminateNexusOperationExecution(_ context.Context, req *workflowservice.TerminateNexusOperationExecutionRequest) (*workflowservice.TerminateNexusOperationExecutionResponse, error) { + if !h.isStandaloneNexusOperationEnabled(req.GetNamespace()) { + return nil, ErrStandaloneNexusOperationDisabled + } return nil, serviceerror.NewUnimplemented("TerminateNexusOperationExecution not implemented") } -func (h *frontendHandler) DeleteNexusOperationExecution(context.Context, *workflowservice.DeleteNexusOperationExecutionRequest) (*workflowservice.DeleteNexusOperationExecutionResponse, error) { +func (h *frontendHandler) DeleteNexusOperationExecution(_ context.Context, req *workflowservice.DeleteNexusOperationExecutionRequest) (*workflowservice.DeleteNexusOperationExecutionResponse, error) { + if !h.isStandaloneNexusOperationEnabled(req.GetNamespace()) { + return nil, ErrStandaloneNexusOperationDisabled + } return nil, serviceerror.NewUnimplemented("DeleteNexusOperationExecution not implemented") } diff --git a/cmd/tools/getproto/files.go b/cmd/tools/getproto/files.go index 6ef97ed180..9b0a671ecc 100644 --- a/cmd/tools/getproto/files.go +++ b/cmd/tools/getproto/files.go @@ -9,6 +9,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" compute "go.temporal.io/api/compute/v1" @@ -49,6 +50,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/compute/v1/config.proto"] = compute.File_temporal_api_compute_v1_config_proto From 62b1f3b07a7689ed8d458b73c334689c2049e437 Mon Sep 17 00:00:00 2001 From: Stephan Behnke Date: Thu, 19 Mar 2026 10:04:05 -0700 Subject: [PATCH 03/11] Nexus Standalone: Start + Describe (#9487) Add Nexus Standalone Describe and Start handlers. - [ ] built - [ ] run locally and tested manually - [ ] covered by existing tests - [x] added new unit test(s) - [x] added new functional test(s) --------- Co-authored-by: Claude Opus 4.6 (1M context) --- chasm/lib/nexusoperation/config.go | 6 +- chasm/lib/nexusoperation/frontend.go | 62 +- chasm/lib/nexusoperation/fx.go | 4 +- .../v1/operation.go-helpers.pb.go | 112 +++ .../gen/nexusoperationpb/v1/operation.pb.go | 458 +++++++++++- .../v1/request_response.go-helpers.pb.go | 450 ++++++++++++ .../v1/request_response.pb.go | 686 ++++++++++++++++++ .../gen/nexusoperationpb/v1/service.pb.go | 95 +++ .../nexusoperationpb/v1/service_client.pb.go | 318 ++++++++ .../nexusoperationpb/v1/service_grpc.pb.go | 295 ++++++++ chasm/lib/nexusoperation/handler.go | 96 +++ chasm/lib/nexusoperation/library.go | 47 +- chasm/lib/nexusoperation/operation.go | 243 +++++-- .../lib/nexusoperation/operation_executors.go | 192 +++++ chasm/lib/nexusoperation/operation_tasks.go | 309 ++++---- .../nexusoperation/operation_tasks_test.go | 34 +- .../nexusoperation/proto/v1/operation.proto | 58 +- .../proto/v1/request_response.proto | 61 ++ .../lib/nexusoperation/proto/v1/service.proto | 41 ++ .../nexusoperation/task_handler_helpers.go | 192 +++++ chasm/lib/nexusoperation/validator.go | 182 +++++ chasm/lib/nexusoperation/validator_test.go | 299 ++++++++ tests/nexus_standalone_test.go | 228 ++++++ 23 files changed, 4159 insertions(+), 309 deletions(-) create mode 100644 chasm/lib/nexusoperation/gen/nexusoperationpb/v1/request_response.go-helpers.pb.go create mode 100644 chasm/lib/nexusoperation/gen/nexusoperationpb/v1/request_response.pb.go create mode 100644 chasm/lib/nexusoperation/gen/nexusoperationpb/v1/service.pb.go create mode 100644 chasm/lib/nexusoperation/gen/nexusoperationpb/v1/service_client.pb.go create mode 100644 chasm/lib/nexusoperation/gen/nexusoperationpb/v1/service_grpc.pb.go create mode 100644 chasm/lib/nexusoperation/handler.go create mode 100644 chasm/lib/nexusoperation/operation_executors.go create mode 100644 chasm/lib/nexusoperation/proto/v1/request_response.proto create mode 100644 chasm/lib/nexusoperation/proto/v1/service.proto create mode 100644 chasm/lib/nexusoperation/validator.go create mode 100644 chasm/lib/nexusoperation/validator_test.go create mode 100644 tests/nexus_standalone_test.go diff --git a/chasm/lib/nexusoperation/config.go b/chasm/lib/nexusoperation/config.go index 918b64773a..37b9a67fcd 100644 --- a/chasm/lib/nexusoperation/config.go +++ b/chasm/lib/nexusoperation/config.go @@ -212,7 +212,9 @@ type Config struct { UseSystemCallbackURL dynamicconfig.BoolPropertyFn UseNewFailureWireFormat dynamicconfig.BoolPropertyFnWithNamespaceFilter RecordCancelRequestCompletionEvents dynamicconfig.BoolPropertyFn - RetryPolicy dynamicconfig.TypedPropertyFn[backoff.RetryPolicy] + VisibilityMaxPageSize dynamicconfig.IntPropertyFnWithNamespaceFilter + MaxIDLengthLimit dynamicconfig.IntPropertyFn + RetryPolicy func() backoff.RetryPolicy } func configProvider(dc *dynamicconfig.Collection, cfg *config.Persistence) *Config { @@ -234,6 +236,8 @@ func configProvider(dc *dynamicconfig.Collection, cfg *config.Persistence) *Conf CallbackURLTemplate: CallbackURLTemplate.Get(dc), UseSystemCallbackURL: UseSystemCallbackURL.Get(dc), UseNewFailureWireFormat: UseNewFailureWireFormat.Get(dc), + VisibilityMaxPageSize: dynamicconfig.FrontendVisibilityMaxPageSize.Get(dc), + MaxIDLengthLimit: dynamicconfig.MaxIDLengthLimit.Get(dc), RetryPolicy: RetryPolicy.Get(dc), } } diff --git a/chasm/lib/nexusoperation/frontend.go b/chasm/lib/nexusoperation/frontend.go index 7be22f8062..c74057929f 100644 --- a/chasm/lib/nexusoperation/frontend.go +++ b/chasm/lib/nexusoperation/frontend.go @@ -5,8 +5,11 @@ import ( "go.temporal.io/api/serviceerror" "go.temporal.io/api/workflowservice/v1" + nexusoperationpb "go.temporal.io/server/chasm/lib/nexusoperation/gen/nexusoperationpb/v1" "go.temporal.io/server/common/log" "go.temporal.io/server/common/namespace" + commonnexus "go.temporal.io/server/common/nexus" + "go.temporal.io/server/common/searchattribute" ) // FrontendHandler provides the frontend-facing API for standalone Nexus operations. @@ -24,20 +27,32 @@ type FrontendHandler interface { var ErrStandaloneNexusOperationDisabled = serviceerror.NewUnimplemented("Standalone Nexus operation is disabled") type frontendHandler struct { + client nexusoperationpb.NexusOperationServiceClient config *Config logger log.Logger namespaceRegistry namespace.Registry + endpointRegistry commonnexus.EndpointRegistry + saMapperProvider searchattribute.MapperProvider + saValidator *searchattribute.Validator } func NewFrontendHandler( + client nexusoperationpb.NexusOperationServiceClient, config *Config, logger log.Logger, namespaceRegistry namespace.Registry, + endpointRegistry commonnexus.EndpointRegistry, + saMapperProvider searchattribute.MapperProvider, + saValidator *searchattribute.Validator, ) FrontendHandler { return &frontendHandler{ + client: client, config: config, logger: logger, namespaceRegistry: namespaceRegistry, + endpointRegistry: endpointRegistry, + saMapperProvider: saMapperProvider, + saValidator: saValidator, } } @@ -46,18 +61,57 @@ func (h *frontendHandler) isStandaloneNexusOperationEnabled(namespaceName string return h.config.EnableChasm(namespaceName) && h.config.Enabled(namespaceName) } -func (h *frontendHandler) StartNexusOperationExecution(_ context.Context, req *workflowservice.StartNexusOperationExecutionRequest) (*workflowservice.StartNexusOperationExecutionResponse, error) { +func (h *frontendHandler) StartNexusOperationExecution( + ctx context.Context, + req *workflowservice.StartNexusOperationExecutionRequest, +) (*workflowservice.StartNexusOperationExecutionResponse, error) { if !h.isStandaloneNexusOperationEnabled(req.GetNamespace()) { return nil, ErrStandaloneNexusOperationDisabled } - return nil, serviceerror.NewUnimplemented("StartNexusOperationExecution not implemented") + + namespaceID, err := h.namespaceRegistry.GetNamespaceID(namespace.Name(req.GetNamespace())) + if err != nil { + return nil, err + } + + if err := validateAndNormalizeStartRequest(req, h.config, h.saMapperProvider, h.saValidator); err != nil { + return nil, err + } + + // Verify the endpoint exists before creating the operation. + if _, err := h.endpointRegistry.GetByName(ctx, namespaceID, req.GetEndpoint()); err != nil { + return nil, err + } + + resp, err := h.client.StartNexusOperation(ctx, &nexusoperationpb.StartNexusOperationRequest{ + NamespaceId: namespaceID.String(), + FrontendRequest: req, + }) + return resp.GetFrontendResponse(), err } -func (h *frontendHandler) DescribeNexusOperationExecution(_ context.Context, req *workflowservice.DescribeNexusOperationExecutionRequest) (*workflowservice.DescribeNexusOperationExecutionResponse, error) { +func (h *frontendHandler) DescribeNexusOperationExecution( + ctx context.Context, + req *workflowservice.DescribeNexusOperationExecutionRequest, +) (*workflowservice.DescribeNexusOperationExecutionResponse, error) { if !h.isStandaloneNexusOperationEnabled(req.GetNamespace()) { return nil, ErrStandaloneNexusOperationDisabled } - return nil, serviceerror.NewUnimplemented("DescribeNexusOperationExecution not implemented") + + namespaceID, err := h.namespaceRegistry.GetNamespaceID(namespace.Name(req.GetNamespace())) + if err != nil { + return nil, err + } + + if err := validateDescribeNexusOperationExecutionRequest(req, h.config); err != nil { + return nil, err + } + + resp, err := h.client.DescribeNexusOperation(ctx, &nexusoperationpb.DescribeNexusOperationRequest{ + NamespaceId: namespaceID.String(), + FrontendRequest: req, + }) + return resp.GetFrontendResponse(), err } func (h *frontendHandler) PollNexusOperationExecution(_ context.Context, req *workflowservice.PollNexusOperationExecutionRequest) (*workflowservice.PollNexusOperationExecutionResponse, error) { diff --git a/chasm/lib/nexusoperation/fx.go b/chasm/lib/nexusoperation/fx.go index 804802799c..678558989c 100644 --- a/chasm/lib/nexusoperation/fx.go +++ b/chasm/lib/nexusoperation/fx.go @@ -8,6 +8,7 @@ import ( "go.temporal.io/api/serviceerror" persistencespb "go.temporal.io/server/api/persistence/v1" "go.temporal.io/server/chasm" + nexusoperationpb "go.temporal.io/server/chasm/lib/nexusoperation/gen/nexusoperationpb/v1" "go.temporal.io/server/common" "go.temporal.io/server/common/cluster" "go.temporal.io/server/common/collection" @@ -32,6 +33,7 @@ var Module = fx.Module( fx.Invoke(endpointRegistryLifetimeHooks), fx.Provide(defaultNexusTransportProvider), fx.Provide(clientProviderFactory), + fx.Provide(newHandler), fx.Provide(newCancellationBackoffTaskHandler), fx.Provide(newCancellationInvocationTaskHandler), fx.Provide(newOperationBackoffTaskHandler), @@ -46,6 +48,7 @@ var Module = fx.Module( var FrontendModule = fx.Module( "chasm.lib.nexusoperation.frontend", fx.Provide(configProvider), + fx.Provide(nexusoperationpb.NewNexusOperationServiceLayeredClient), fx.Provide(NewFrontendHandler), ) @@ -109,7 +112,6 @@ func clientProviderFactory( httpTransportProvider NexusTransportProvider, clusterMetadata cluster.Metadata, rpcFactory common.RPCFactory, - config *Config, ) (ClientProvider, error) { cl, err := rpcFactory.CreateLocalFrontendHTTPClient() if err != nil { diff --git a/chasm/lib/nexusoperation/gen/nexusoperationpb/v1/operation.go-helpers.pb.go b/chasm/lib/nexusoperation/gen/nexusoperationpb/v1/operation.go-helpers.pb.go index ea15136ec8..f08af02357 100644 --- a/chasm/lib/nexusoperation/gen/nexusoperationpb/v1/operation.go-helpers.pb.go +++ b/chasm/lib/nexusoperation/gen/nexusoperationpb/v1/operation.go-helpers.pb.go @@ -44,6 +44,80 @@ func (this *OperationState) Equal(that interface{}) bool { return proto.Equal(this, that1) } +// Marshal an object of type NexusOperationTerminateState to the protobuf v3 wire format +func (val *NexusOperationTerminateState) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type NexusOperationTerminateState from the protobuf v3 wire format +func (val *NexusOperationTerminateState) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *NexusOperationTerminateState) Size() int { + return proto.Size(val) +} + +// Equal returns whether two NexusOperationTerminateState 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 *NexusOperationTerminateState) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *NexusOperationTerminateState + switch t := that.(type) { + case *NexusOperationTerminateState: + that1 = t + case NexusOperationTerminateState: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type OperationOutcome to the protobuf v3 wire format +func (val *OperationOutcome) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type OperationOutcome from the protobuf v3 wire format +func (val *OperationOutcome) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *OperationOutcome) Size() int { + return proto.Size(val) +} + +// Equal returns whether two OperationOutcome 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 *OperationOutcome) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *OperationOutcome + switch t := that.(type) { + case *OperationOutcome: + that1 = t + case OperationOutcome: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + // Marshal an object of type CancellationState to the protobuf v3 wire format func (val *CancellationState) Marshal() ([]byte, error) { return proto.Marshal(val) @@ -81,6 +155,43 @@ func (this *CancellationState) Equal(that interface{}) bool { return proto.Equal(this, that1) } +// Marshal an object of type OperationRequestData to the protobuf v3 wire format +func (val *OperationRequestData) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type OperationRequestData from the protobuf v3 wire format +func (val *OperationRequestData) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *OperationRequestData) Size() int { + return proto.Size(val) +} + +// Equal returns whether two OperationRequestData 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 *OperationRequestData) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *OperationRequestData + switch t := that.(type) { + case *OperationRequestData: + that1 = t + case OperationRequestData: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + var ( OperationStatus_shorthandValue = map[string]int32{ "Unspecified": 0, @@ -91,6 +202,7 @@ var ( "Failed": 5, "Canceled": 6, "TimedOut": 7, + "Terminated": 8, } ) diff --git a/chasm/lib/nexusoperation/gen/nexusoperationpb/v1/operation.pb.go b/chasm/lib/nexusoperation/gen/nexusoperationpb/v1/operation.pb.go index 8b7578c848..71dcc7e567 100644 --- a/chasm/lib/nexusoperation/gen/nexusoperationpb/v1/operation.pb.go +++ b/chasm/lib/nexusoperation/gen/nexusoperationpb/v1/operation.pb.go @@ -12,7 +12,9 @@ import ( sync "sync" unsafe "unsafe" + v11 "go.temporal.io/api/common/v1" v1 "go.temporal.io/api/failure/v1" + v12 "go.temporal.io/api/sdk/v1" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" anypb "google.golang.org/protobuf/types/known/anypb" @@ -48,7 +50,8 @@ const ( OPERATION_STATUS_CANCELED OperationStatus = 6 // Operation timed out - exceeded the user supplied schedule-to-close timeout. // Any attempts to complete the operation in this status will be ignored. - OPERATION_STATUS_TIMED_OUT OperationStatus = 7 + OPERATION_STATUS_TIMED_OUT OperationStatus = 7 + OPERATION_STATUS_TERMINATED OperationStatus = 8 ) // Enum value maps for OperationStatus. @@ -62,6 +65,7 @@ var ( 5: "OPERATION_STATUS_FAILED", 6: "OPERATION_STATUS_CANCELED", 7: "OPERATION_STATUS_TIMED_OUT", + 8: "OPERATION_STATUS_TERMINATED", } OperationStatus_value = map[string]int32{ "OPERATION_STATUS_UNSPECIFIED": 0, @@ -72,6 +76,7 @@ var ( "OPERATION_STATUS_FAILED": 5, "OPERATION_STATUS_CANCELED": 6, "OPERATION_STATUS_TIMED_OUT": 7, + "OPERATION_STATUS_TERMINATED": 8, } ) @@ -99,6 +104,8 @@ func (x OperationStatus) String() string { return "Canceled" case OPERATION_STATUS_TIMED_OUT: return "TimedOut" + case OPERATION_STATUS_TERMINATED: + return "Terminated" default: return strconv.Itoa(int(x)) } @@ -252,6 +259,8 @@ type OperationState struct { NextAttemptScheduleTime *timestamppb.Timestamp `protobuf:"bytes,17,opt,name=next_attempt_schedule_time,json=nextAttemptScheduleTime,proto3" json:"next_attempt_schedule_time,omitempty"` // Operation token - only set for asynchronous operations after a successful StartOperation call. OperationToken string `protobuf:"bytes,18,opt,name=operation_token,json=operationToken,proto3" json:"operation_token,omitempty"` + // Explicit terminate request state for standalone operations. + TerminateState *NexusOperationTerminateState `protobuf:"bytes,19,opt,name=terminate_state,json=terminateState,proto3" json:"terminate_state,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -412,6 +421,147 @@ func (x *OperationState) GetOperationToken() string { return "" } +func (x *OperationState) GetTerminateState() *NexusOperationTerminateState { + if x != nil { + return x.TerminateState + } + return nil +} + +type NexusOperationTerminateState struct { + state protoimpl.MessageState `protogen:"open.v1"` + RequestId string `protobuf:"bytes,1,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + Identity string `protobuf:"bytes,2,opt,name=identity,proto3" json:"identity,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *NexusOperationTerminateState) Reset() { + *x = NexusOperationTerminateState{} + mi := &file_temporal_server_chasm_lib_nexusoperation_proto_v1_operation_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *NexusOperationTerminateState) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NexusOperationTerminateState) ProtoMessage() {} + +func (x *NexusOperationTerminateState) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_nexusoperation_proto_v1_operation_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 NexusOperationTerminateState.ProtoReflect.Descriptor instead. +func (*NexusOperationTerminateState) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_nexusoperation_proto_v1_operation_proto_rawDescGZIP(), []int{1} +} + +func (x *NexusOperationTerminateState) GetRequestId() string { + if x != nil { + return x.RequestId + } + return "" +} + +func (x *NexusOperationTerminateState) GetIdentity() string { + if x != nil { + return x.Identity + } + return "" +} + +type OperationOutcome struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Variant: + // + // *OperationOutcome_Successful_ + // *OperationOutcome_Failed_ + Variant isOperationOutcome_Variant `protobuf_oneof:"variant"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OperationOutcome) Reset() { + *x = OperationOutcome{} + mi := &file_temporal_server_chasm_lib_nexusoperation_proto_v1_operation_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OperationOutcome) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OperationOutcome) ProtoMessage() {} + +func (x *OperationOutcome) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_nexusoperation_proto_v1_operation_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 OperationOutcome.ProtoReflect.Descriptor instead. +func (*OperationOutcome) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_nexusoperation_proto_v1_operation_proto_rawDescGZIP(), []int{2} +} + +func (x *OperationOutcome) GetVariant() isOperationOutcome_Variant { + if x != nil { + return x.Variant + } + return nil +} + +func (x *OperationOutcome) GetSuccessful() *OperationOutcome_Successful { + if x != nil { + if x, ok := x.Variant.(*OperationOutcome_Successful_); ok { + return x.Successful + } + } + return nil +} + +func (x *OperationOutcome) GetFailed() *OperationOutcome_Failed { + if x != nil { + if x, ok := x.Variant.(*OperationOutcome_Failed_); ok { + return x.Failed + } + } + return nil +} + +type isOperationOutcome_Variant interface { + isOperationOutcome_Variant() +} + +type OperationOutcome_Successful_ struct { + Successful *OperationOutcome_Successful `protobuf:"bytes,1,opt,name=successful,proto3,oneof"` +} + +type OperationOutcome_Failed_ struct { + Failed *OperationOutcome_Failed `protobuf:"bytes,2,opt,name=failed,proto3,oneof"` +} + +func (*OperationOutcome_Successful_) isOperationOutcome_Variant() {} + +func (*OperationOutcome_Failed_) isOperationOutcome_Variant() {} + type CancellationState struct { state protoimpl.MessageState `protogen:"open.v1"` // Current status of the cancellation request. @@ -430,13 +580,16 @@ type CancellationState struct { // Opaque data injected by the parent (e.g. workflow) for its own bookkeeping. // The cancellation component itself does not interpret this field. ParentData *anypb.Any `protobuf:"bytes,7,opt,name=parent_data,json=parentData,proto3" json:"parent_data,omitempty"` + RequestId string `protobuf:"bytes,8,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + Identity string `protobuf:"bytes,9,opt,name=identity,proto3" json:"identity,omitempty"` + Reason string `protobuf:"bytes,10,opt,name=reason,proto3" json:"reason,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *CancellationState) Reset() { *x = CancellationState{} - mi := &file_temporal_server_chasm_lib_nexusoperation_proto_v1_operation_proto_msgTypes[1] + mi := &file_temporal_server_chasm_lib_nexusoperation_proto_v1_operation_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -448,7 +601,7 @@ func (x *CancellationState) String() string { func (*CancellationState) ProtoMessage() {} func (x *CancellationState) ProtoReflect() protoreflect.Message { - mi := &file_temporal_server_chasm_lib_nexusoperation_proto_v1_operation_proto_msgTypes[1] + mi := &file_temporal_server_chasm_lib_nexusoperation_proto_v1_operation_proto_msgTypes[3] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -461,7 +614,7 @@ func (x *CancellationState) ProtoReflect() protoreflect.Message { // Deprecated: Use CancellationState.ProtoReflect.Descriptor instead. func (*CancellationState) Descriptor() ([]byte, []int) { - return file_temporal_server_chasm_lib_nexusoperation_proto_v1_operation_proto_rawDescGZIP(), []int{1} + return file_temporal_server_chasm_lib_nexusoperation_proto_v1_operation_proto_rawDescGZIP(), []int{3} } func (x *CancellationState) GetStatus() CancellationStatus { @@ -513,11 +666,188 @@ func (x *CancellationState) GetParentData() *anypb.Any { return nil } +func (x *CancellationState) GetRequestId() string { + if x != nil { + return x.RequestId + } + return "" +} + +func (x *CancellationState) GetIdentity() string { + if x != nil { + return x.Identity + } + return "" +} + +func (x *CancellationState) GetReason() string { + if x != nil { + return x.Reason + } + return "" +} + +type OperationRequestData struct { + state protoimpl.MessageState `protogen:"open.v1"` + Input *v11.Payload `protobuf:"bytes,1,opt,name=input,proto3" json:"input,omitempty"` + NexusHeader map[string]string `protobuf:"bytes,2,rep,name=nexus_header,json=nexusHeader,proto3" json:"nexus_header,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + UserMetadata *v12.UserMetadata `protobuf:"bytes,3,opt,name=user_metadata,json=userMetadata,proto3" json:"user_metadata,omitempty"` + Identity string `protobuf:"bytes,4,opt,name=identity,proto3" json:"identity,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OperationRequestData) Reset() { + *x = OperationRequestData{} + mi := &file_temporal_server_chasm_lib_nexusoperation_proto_v1_operation_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OperationRequestData) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OperationRequestData) ProtoMessage() {} + +func (x *OperationRequestData) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_nexusoperation_proto_v1_operation_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 OperationRequestData.ProtoReflect.Descriptor instead. +func (*OperationRequestData) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_nexusoperation_proto_v1_operation_proto_rawDescGZIP(), []int{4} +} + +func (x *OperationRequestData) GetInput() *v11.Payload { + if x != nil { + return x.Input + } + return nil +} + +func (x *OperationRequestData) GetNexusHeader() map[string]string { + if x != nil { + return x.NexusHeader + } + return nil +} + +func (x *OperationRequestData) GetUserMetadata() *v12.UserMetadata { + if x != nil { + return x.UserMetadata + } + return nil +} + +func (x *OperationRequestData) GetIdentity() string { + if x != nil { + return x.Identity + } + return "" +} + +type OperationOutcome_Successful struct { + state protoimpl.MessageState `protogen:"open.v1"` + Result *v11.Payload `protobuf:"bytes,1,opt,name=result,proto3" json:"result,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OperationOutcome_Successful) Reset() { + *x = OperationOutcome_Successful{} + mi := &file_temporal_server_chasm_lib_nexusoperation_proto_v1_operation_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OperationOutcome_Successful) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OperationOutcome_Successful) ProtoMessage() {} + +func (x *OperationOutcome_Successful) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_nexusoperation_proto_v1_operation_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 OperationOutcome_Successful.ProtoReflect.Descriptor instead. +func (*OperationOutcome_Successful) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_nexusoperation_proto_v1_operation_proto_rawDescGZIP(), []int{2, 0} +} + +func (x *OperationOutcome_Successful) GetResult() *v11.Payload { + if x != nil { + return x.Result + } + return nil +} + +type OperationOutcome_Failed struct { + state protoimpl.MessageState `protogen:"open.v1"` + Failure *v1.Failure `protobuf:"bytes,1,opt,name=failure,proto3" json:"failure,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OperationOutcome_Failed) Reset() { + *x = OperationOutcome_Failed{} + mi := &file_temporal_server_chasm_lib_nexusoperation_proto_v1_operation_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OperationOutcome_Failed) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OperationOutcome_Failed) ProtoMessage() {} + +func (x *OperationOutcome_Failed) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_nexusoperation_proto_v1_operation_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 OperationOutcome_Failed.ProtoReflect.Descriptor instead. +func (*OperationOutcome_Failed) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_nexusoperation_proto_v1_operation_proto_rawDescGZIP(), []int{2, 1} +} + +func (x *OperationOutcome_Failed) GetFailure() *v1.Failure { + if x != nil { + return x.Failure + } + return nil +} + var File_temporal_server_chasm_lib_nexusoperation_proto_v1_operation_proto protoreflect.FileDescriptor const file_temporal_server_chasm_lib_nexusoperation_proto_v1_operation_proto_rawDesc = "" + "\n" + - "Atemporal/server/chasm/lib/nexusoperation/proto/v1/operation.proto\x121temporal.server.chasm.lib.nexusoperation.proto.v1\x1a\x19google/protobuf/any.proto\x1a\x1egoogle/protobuf/duration.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a%temporal/api/failure/v1/message.proto\"\xbb\b\n" + + "Atemporal/server/chasm/lib/nexusoperation/proto/v1/operation.proto\x121temporal.server.chasm.lib.nexusoperation.proto.v1\x1a\x19google/protobuf/any.proto\x1a\x1egoogle/protobuf/duration.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a$temporal/api/common/v1/message.proto\x1a%temporal/api/failure/v1/message.proto\x1a'temporal/api/sdk/v1/user_metadata.proto\"\xb5\t\n" + "\x0eOperationState\x12Z\n" + "\x06status\x18\x01 \x01(\x0e2B.temporal.server.chasm.lib.nexusoperation.proto.v1.OperationStatusR\x06status\x12\x1f\n" + "\vendpoint_id\x18\x02 \x01(\tR\n" + @@ -541,7 +871,23 @@ const file_temporal_server_chasm_lib_nexusoperation_proto_v1_operation_proto_raw "\x1alast_attempt_complete_time\x18\x0f \x01(\v2\x1a.google.protobuf.TimestampR\x17lastAttemptCompleteTime\x12R\n" + "\x14last_attempt_failure\x18\x10 \x01(\v2 .temporal.api.failure.v1.FailureR\x12lastAttemptFailure\x12W\n" + "\x1anext_attempt_schedule_time\x18\x11 \x01(\v2\x1a.google.protobuf.TimestampR\x17nextAttemptScheduleTime\x12'\n" + - "\x0foperation_token\x18\x12 \x01(\tR\x0eoperationToken\"\x8c\x04\n" + + "\x0foperation_token\x18\x12 \x01(\tR\x0eoperationToken\x12x\n" + + "\x0fterminate_state\x18\x13 \x01(\v2O.temporal.server.chasm.lib.nexusoperation.proto.v1.NexusOperationTerminateStateR\x0eterminateState\"Y\n" + + "\x1cNexusOperationTerminateState\x12\x1d\n" + + "\n" + + "request_id\x18\x01 \x01(\tR\trequestId\x12\x1a\n" + + "\bidentity\x18\x02 \x01(\tR\bidentity\"\x82\x03\n" + + "\x10OperationOutcome\x12p\n" + + "\n" + + "successful\x18\x01 \x01(\v2N.temporal.server.chasm.lib.nexusoperation.proto.v1.OperationOutcome.SuccessfulH\x00R\n" + + "successful\x12d\n" + + "\x06failed\x18\x02 \x01(\v2J.temporal.server.chasm.lib.nexusoperation.proto.v1.OperationOutcome.FailedH\x00R\x06failed\x1aE\n" + + "\n" + + "Successful\x127\n" + + "\x06result\x18\x01 \x01(\v2\x1f.temporal.api.common.v1.PayloadR\x06result\x1aD\n" + + "\x06Failed\x12:\n" + + "\afailure\x18\x01 \x01(\v2 .temporal.api.failure.v1.FailureR\afailureB\t\n" + + "\avariant\"\xdf\x04\n" + "\x11CancellationState\x12]\n" + "\x06status\x18\x01 \x01(\x0e2E.temporal.server.chasm.lib.nexusoperation.proto.v1.CancellationStatusR\x06status\x12A\n" + "\x0erequested_time\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\rrequestedTime\x12\x18\n" + @@ -550,7 +896,20 @@ const file_temporal_server_chasm_lib_nexusoperation_proto_v1_operation_proto_raw "\x14last_attempt_failure\x18\x05 \x01(\v2 .temporal.api.failure.v1.FailureR\x12lastAttemptFailure\x12W\n" + "\x1anext_attempt_schedule_time\x18\x06 \x01(\v2\x1a.google.protobuf.TimestampR\x17nextAttemptScheduleTime\x125\n" + "\vparent_data\x18\a \x01(\v2\x14.google.protobuf.AnyR\n" + - "parentData*\x8f\x02\n" + + "parentData\x12\x1d\n" + + "\n" + + "request_id\x18\b \x01(\tR\trequestId\x12\x1a\n" + + "\bidentity\x18\t \x01(\tR\bidentity\x12\x16\n" + + "\x06reason\x18\n" + + " \x01(\tR\x06reason\"\xee\x02\n" + + "\x14OperationRequestData\x125\n" + + "\x05input\x18\x01 \x01(\v2\x1f.temporal.api.common.v1.PayloadR\x05input\x12{\n" + + "\fnexus_header\x18\x02 \x03(\v2X.temporal.server.chasm.lib.nexusoperation.proto.v1.OperationRequestData.NexusHeaderEntryR\vnexusHeader\x12F\n" + + "\ruser_metadata\x18\x03 \x01(\v2!.temporal.api.sdk.v1.UserMetadataR\fuserMetadata\x12\x1a\n" + + "\bidentity\x18\x04 \x01(\tR\bidentity\x1a>\n" + + "\x10NexusHeaderEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01*\xb0\x02\n" + "\x0fOperationStatus\x12 \n" + "\x1cOPERATION_STATUS_UNSPECIFIED\x10\x00\x12\x1e\n" + "\x1aOPERATION_STATUS_SCHEDULED\x10\x01\x12 \n" + @@ -559,7 +918,8 @@ const file_temporal_server_chasm_lib_nexusoperation_proto_v1_operation_proto_raw "\x1aOPERATION_STATUS_SUCCEEDED\x10\x04\x12\x1b\n" + "\x17OPERATION_STATUS_FAILED\x10\x05\x12\x1d\n" + "\x19OPERATION_STATUS_CANCELED\x10\x06\x12\x1e\n" + - "\x1aOPERATION_STATUS_TIMED_OUT\x10\a*\x88\x02\n" + + "\x1aOPERATION_STATUS_TIMED_OUT\x10\a\x12\x1f\n" + + "\x1bOPERATION_STATUS_TERMINATED\x10\b*\x88\x02\n" + "\x12CancellationStatus\x12#\n" + "\x1fCANCELLATION_STATUS_UNSPECIFIED\x10\x00\x12!\n" + "\x1dCANCELLATION_STATUS_SCHEDULED\x10\x01\x12#\n" + @@ -582,40 +942,56 @@ func file_temporal_server_chasm_lib_nexusoperation_proto_v1_operation_proto_rawD } var file_temporal_server_chasm_lib_nexusoperation_proto_v1_operation_proto_enumTypes = make([]protoimpl.EnumInfo, 2) -var file_temporal_server_chasm_lib_nexusoperation_proto_v1_operation_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_temporal_server_chasm_lib_nexusoperation_proto_v1_operation_proto_msgTypes = make([]protoimpl.MessageInfo, 8) var file_temporal_server_chasm_lib_nexusoperation_proto_v1_operation_proto_goTypes = []any{ - (OperationStatus)(0), // 0: temporal.server.chasm.lib.nexusoperation.proto.v1.OperationStatus - (CancellationStatus)(0), // 1: temporal.server.chasm.lib.nexusoperation.proto.v1.CancellationStatus - (*OperationState)(nil), // 2: temporal.server.chasm.lib.nexusoperation.proto.v1.OperationState - (*CancellationState)(nil), // 3: temporal.server.chasm.lib.nexusoperation.proto.v1.CancellationState - (*timestamppb.Timestamp)(nil), // 4: google.protobuf.Timestamp - (*durationpb.Duration)(nil), // 5: google.protobuf.Duration - (*anypb.Any)(nil), // 6: google.protobuf.Any - (*v1.Failure)(nil), // 7: temporal.api.failure.v1.Failure + (OperationStatus)(0), // 0: temporal.server.chasm.lib.nexusoperation.proto.v1.OperationStatus + (CancellationStatus)(0), // 1: temporal.server.chasm.lib.nexusoperation.proto.v1.CancellationStatus + (*OperationState)(nil), // 2: temporal.server.chasm.lib.nexusoperation.proto.v1.OperationState + (*NexusOperationTerminateState)(nil), // 3: temporal.server.chasm.lib.nexusoperation.proto.v1.NexusOperationTerminateState + (*OperationOutcome)(nil), // 4: temporal.server.chasm.lib.nexusoperation.proto.v1.OperationOutcome + (*CancellationState)(nil), // 5: temporal.server.chasm.lib.nexusoperation.proto.v1.CancellationState + (*OperationRequestData)(nil), // 6: temporal.server.chasm.lib.nexusoperation.proto.v1.OperationRequestData + (*OperationOutcome_Successful)(nil), // 7: temporal.server.chasm.lib.nexusoperation.proto.v1.OperationOutcome.Successful + (*OperationOutcome_Failed)(nil), // 8: temporal.server.chasm.lib.nexusoperation.proto.v1.OperationOutcome.Failed + nil, // 9: temporal.server.chasm.lib.nexusoperation.proto.v1.OperationRequestData.NexusHeaderEntry + (*timestamppb.Timestamp)(nil), // 10: google.protobuf.Timestamp + (*durationpb.Duration)(nil), // 11: google.protobuf.Duration + (*anypb.Any)(nil), // 12: google.protobuf.Any + (*v1.Failure)(nil), // 13: temporal.api.failure.v1.Failure + (*v11.Payload)(nil), // 14: temporal.api.common.v1.Payload + (*v12.UserMetadata)(nil), // 15: temporal.api.sdk.v1.UserMetadata } var file_temporal_server_chasm_lib_nexusoperation_proto_v1_operation_proto_depIdxs = []int32{ 0, // 0: temporal.server.chasm.lib.nexusoperation.proto.v1.OperationState.status:type_name -> temporal.server.chasm.lib.nexusoperation.proto.v1.OperationStatus - 4, // 1: temporal.server.chasm.lib.nexusoperation.proto.v1.OperationState.scheduled_time:type_name -> google.protobuf.Timestamp - 4, // 2: temporal.server.chasm.lib.nexusoperation.proto.v1.OperationState.started_time:type_name -> google.protobuf.Timestamp - 4, // 3: temporal.server.chasm.lib.nexusoperation.proto.v1.OperationState.closed_time:type_name -> google.protobuf.Timestamp - 5, // 4: temporal.server.chasm.lib.nexusoperation.proto.v1.OperationState.schedule_to_start_timeout:type_name -> google.protobuf.Duration - 5, // 5: temporal.server.chasm.lib.nexusoperation.proto.v1.OperationState.start_to_close_timeout:type_name -> google.protobuf.Duration - 5, // 6: temporal.server.chasm.lib.nexusoperation.proto.v1.OperationState.schedule_to_close_timeout:type_name -> google.protobuf.Duration - 6, // 7: temporal.server.chasm.lib.nexusoperation.proto.v1.OperationState.parent_data:type_name -> google.protobuf.Any - 4, // 8: temporal.server.chasm.lib.nexusoperation.proto.v1.OperationState.last_attempt_complete_time:type_name -> google.protobuf.Timestamp - 7, // 9: temporal.server.chasm.lib.nexusoperation.proto.v1.OperationState.last_attempt_failure:type_name -> temporal.api.failure.v1.Failure - 4, // 10: temporal.server.chasm.lib.nexusoperation.proto.v1.OperationState.next_attempt_schedule_time:type_name -> google.protobuf.Timestamp - 1, // 11: temporal.server.chasm.lib.nexusoperation.proto.v1.CancellationState.status:type_name -> temporal.server.chasm.lib.nexusoperation.proto.v1.CancellationStatus - 4, // 12: temporal.server.chasm.lib.nexusoperation.proto.v1.CancellationState.requested_time:type_name -> google.protobuf.Timestamp - 4, // 13: temporal.server.chasm.lib.nexusoperation.proto.v1.CancellationState.last_attempt_complete_time:type_name -> google.protobuf.Timestamp - 7, // 14: temporal.server.chasm.lib.nexusoperation.proto.v1.CancellationState.last_attempt_failure:type_name -> temporal.api.failure.v1.Failure - 4, // 15: temporal.server.chasm.lib.nexusoperation.proto.v1.CancellationState.next_attempt_schedule_time:type_name -> google.protobuf.Timestamp - 6, // 16: temporal.server.chasm.lib.nexusoperation.proto.v1.CancellationState.parent_data:type_name -> google.protobuf.Any - 17, // [17:17] is the sub-list for method output_type - 17, // [17:17] is the sub-list for method input_type - 17, // [17:17] is the sub-list for extension type_name - 17, // [17:17] is the sub-list for extension extendee - 0, // [0:17] is the sub-list for field type_name + 10, // 1: temporal.server.chasm.lib.nexusoperation.proto.v1.OperationState.scheduled_time:type_name -> google.protobuf.Timestamp + 10, // 2: temporal.server.chasm.lib.nexusoperation.proto.v1.OperationState.started_time:type_name -> google.protobuf.Timestamp + 10, // 3: temporal.server.chasm.lib.nexusoperation.proto.v1.OperationState.closed_time:type_name -> google.protobuf.Timestamp + 11, // 4: temporal.server.chasm.lib.nexusoperation.proto.v1.OperationState.schedule_to_start_timeout:type_name -> google.protobuf.Duration + 11, // 5: temporal.server.chasm.lib.nexusoperation.proto.v1.OperationState.start_to_close_timeout:type_name -> google.protobuf.Duration + 11, // 6: temporal.server.chasm.lib.nexusoperation.proto.v1.OperationState.schedule_to_close_timeout:type_name -> google.protobuf.Duration + 12, // 7: temporal.server.chasm.lib.nexusoperation.proto.v1.OperationState.parent_data:type_name -> google.protobuf.Any + 10, // 8: temporal.server.chasm.lib.nexusoperation.proto.v1.OperationState.last_attempt_complete_time:type_name -> google.protobuf.Timestamp + 13, // 9: temporal.server.chasm.lib.nexusoperation.proto.v1.OperationState.last_attempt_failure:type_name -> temporal.api.failure.v1.Failure + 10, // 10: temporal.server.chasm.lib.nexusoperation.proto.v1.OperationState.next_attempt_schedule_time:type_name -> google.protobuf.Timestamp + 3, // 11: temporal.server.chasm.lib.nexusoperation.proto.v1.OperationState.terminate_state:type_name -> temporal.server.chasm.lib.nexusoperation.proto.v1.NexusOperationTerminateState + 7, // 12: temporal.server.chasm.lib.nexusoperation.proto.v1.OperationOutcome.successful:type_name -> temporal.server.chasm.lib.nexusoperation.proto.v1.OperationOutcome.Successful + 8, // 13: temporal.server.chasm.lib.nexusoperation.proto.v1.OperationOutcome.failed:type_name -> temporal.server.chasm.lib.nexusoperation.proto.v1.OperationOutcome.Failed + 1, // 14: temporal.server.chasm.lib.nexusoperation.proto.v1.CancellationState.status:type_name -> temporal.server.chasm.lib.nexusoperation.proto.v1.CancellationStatus + 10, // 15: temporal.server.chasm.lib.nexusoperation.proto.v1.CancellationState.requested_time:type_name -> google.protobuf.Timestamp + 10, // 16: temporal.server.chasm.lib.nexusoperation.proto.v1.CancellationState.last_attempt_complete_time:type_name -> google.protobuf.Timestamp + 13, // 17: temporal.server.chasm.lib.nexusoperation.proto.v1.CancellationState.last_attempt_failure:type_name -> temporal.api.failure.v1.Failure + 10, // 18: temporal.server.chasm.lib.nexusoperation.proto.v1.CancellationState.next_attempt_schedule_time:type_name -> google.protobuf.Timestamp + 12, // 19: temporal.server.chasm.lib.nexusoperation.proto.v1.CancellationState.parent_data:type_name -> google.protobuf.Any + 14, // 20: temporal.server.chasm.lib.nexusoperation.proto.v1.OperationRequestData.input:type_name -> temporal.api.common.v1.Payload + 9, // 21: temporal.server.chasm.lib.nexusoperation.proto.v1.OperationRequestData.nexus_header:type_name -> temporal.server.chasm.lib.nexusoperation.proto.v1.OperationRequestData.NexusHeaderEntry + 15, // 22: temporal.server.chasm.lib.nexusoperation.proto.v1.OperationRequestData.user_metadata:type_name -> temporal.api.sdk.v1.UserMetadata + 14, // 23: temporal.server.chasm.lib.nexusoperation.proto.v1.OperationOutcome.Successful.result:type_name -> temporal.api.common.v1.Payload + 13, // 24: temporal.server.chasm.lib.nexusoperation.proto.v1.OperationOutcome.Failed.failure:type_name -> temporal.api.failure.v1.Failure + 25, // [25:25] is the sub-list for method output_type + 25, // [25:25] is the sub-list for method input_type + 25, // [25:25] is the sub-list for extension type_name + 25, // [25:25] is the sub-list for extension extendee + 0, // [0:25] is the sub-list for field type_name } func init() { file_temporal_server_chasm_lib_nexusoperation_proto_v1_operation_proto_init() } @@ -623,13 +999,17 @@ func file_temporal_server_chasm_lib_nexusoperation_proto_v1_operation_proto_init if File_temporal_server_chasm_lib_nexusoperation_proto_v1_operation_proto != nil { return } + file_temporal_server_chasm_lib_nexusoperation_proto_v1_operation_proto_msgTypes[2].OneofWrappers = []any{ + (*OperationOutcome_Successful_)(nil), + (*OperationOutcome_Failed_)(nil), + } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_temporal_server_chasm_lib_nexusoperation_proto_v1_operation_proto_rawDesc), len(file_temporal_server_chasm_lib_nexusoperation_proto_v1_operation_proto_rawDesc)), NumEnums: 2, - NumMessages: 2, + NumMessages: 8, NumExtensions: 0, NumServices: 0, }, diff --git a/chasm/lib/nexusoperation/gen/nexusoperationpb/v1/request_response.go-helpers.pb.go b/chasm/lib/nexusoperation/gen/nexusoperationpb/v1/request_response.go-helpers.pb.go new file mode 100644 index 0000000000..42bc21bf6d --- /dev/null +++ b/chasm/lib/nexusoperation/gen/nexusoperationpb/v1/request_response.go-helpers.pb.go @@ -0,0 +1,450 @@ +// Code generated by protoc-gen-go-helpers. DO NOT EDIT. +package nexusoperationpb + +import ( + "google.golang.org/protobuf/proto" +) + +// Marshal an object of type StartNexusOperationRequest to the protobuf v3 wire format +func (val *StartNexusOperationRequest) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type StartNexusOperationRequest from the protobuf v3 wire format +func (val *StartNexusOperationRequest) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *StartNexusOperationRequest) Size() int { + return proto.Size(val) +} + +// Equal returns whether two StartNexusOperationRequest 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 *StartNexusOperationRequest) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *StartNexusOperationRequest + switch t := that.(type) { + case *StartNexusOperationRequest: + that1 = t + case StartNexusOperationRequest: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type StartNexusOperationResponse to the protobuf v3 wire format +func (val *StartNexusOperationResponse) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type StartNexusOperationResponse from the protobuf v3 wire format +func (val *StartNexusOperationResponse) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *StartNexusOperationResponse) Size() int { + return proto.Size(val) +} + +// Equal returns whether two StartNexusOperationResponse 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 *StartNexusOperationResponse) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *StartNexusOperationResponse + switch t := that.(type) { + case *StartNexusOperationResponse: + that1 = t + case StartNexusOperationResponse: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type DescribeNexusOperationRequest to the protobuf v3 wire format +func (val *DescribeNexusOperationRequest) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type DescribeNexusOperationRequest from the protobuf v3 wire format +func (val *DescribeNexusOperationRequest) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *DescribeNexusOperationRequest) Size() int { + return proto.Size(val) +} + +// Equal returns whether two DescribeNexusOperationRequest 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 *DescribeNexusOperationRequest) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *DescribeNexusOperationRequest + switch t := that.(type) { + case *DescribeNexusOperationRequest: + that1 = t + case DescribeNexusOperationRequest: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type DescribeNexusOperationResponse to the protobuf v3 wire format +func (val *DescribeNexusOperationResponse) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type DescribeNexusOperationResponse from the protobuf v3 wire format +func (val *DescribeNexusOperationResponse) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *DescribeNexusOperationResponse) Size() int { + return proto.Size(val) +} + +// Equal returns whether two DescribeNexusOperationResponse 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 *DescribeNexusOperationResponse) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *DescribeNexusOperationResponse + switch t := that.(type) { + case *DescribeNexusOperationResponse: + that1 = t + case DescribeNexusOperationResponse: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type RequestCancelNexusOperationRequest to the protobuf v3 wire format +func (val *RequestCancelNexusOperationRequest) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type RequestCancelNexusOperationRequest from the protobuf v3 wire format +func (val *RequestCancelNexusOperationRequest) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *RequestCancelNexusOperationRequest) Size() int { + return proto.Size(val) +} + +// Equal returns whether two RequestCancelNexusOperationRequest 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 *RequestCancelNexusOperationRequest) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *RequestCancelNexusOperationRequest + switch t := that.(type) { + case *RequestCancelNexusOperationRequest: + that1 = t + case RequestCancelNexusOperationRequest: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type RequestCancelNexusOperationResponse to the protobuf v3 wire format +func (val *RequestCancelNexusOperationResponse) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type RequestCancelNexusOperationResponse from the protobuf v3 wire format +func (val *RequestCancelNexusOperationResponse) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *RequestCancelNexusOperationResponse) Size() int { + return proto.Size(val) +} + +// Equal returns whether two RequestCancelNexusOperationResponse 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 *RequestCancelNexusOperationResponse) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *RequestCancelNexusOperationResponse + switch t := that.(type) { + case *RequestCancelNexusOperationResponse: + that1 = t + case RequestCancelNexusOperationResponse: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type TerminateNexusOperationRequest to the protobuf v3 wire format +func (val *TerminateNexusOperationRequest) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type TerminateNexusOperationRequest from the protobuf v3 wire format +func (val *TerminateNexusOperationRequest) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *TerminateNexusOperationRequest) Size() int { + return proto.Size(val) +} + +// Equal returns whether two TerminateNexusOperationRequest 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 *TerminateNexusOperationRequest) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *TerminateNexusOperationRequest + switch t := that.(type) { + case *TerminateNexusOperationRequest: + that1 = t + case TerminateNexusOperationRequest: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type TerminateNexusOperationResponse to the protobuf v3 wire format +func (val *TerminateNexusOperationResponse) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type TerminateNexusOperationResponse from the protobuf v3 wire format +func (val *TerminateNexusOperationResponse) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *TerminateNexusOperationResponse) Size() int { + return proto.Size(val) +} + +// Equal returns whether two TerminateNexusOperationResponse 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 *TerminateNexusOperationResponse) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *TerminateNexusOperationResponse + switch t := that.(type) { + case *TerminateNexusOperationResponse: + that1 = t + case TerminateNexusOperationResponse: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type DeleteNexusOperationRequest to the protobuf v3 wire format +func (val *DeleteNexusOperationRequest) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type DeleteNexusOperationRequest from the protobuf v3 wire format +func (val *DeleteNexusOperationRequest) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *DeleteNexusOperationRequest) Size() int { + return proto.Size(val) +} + +// Equal returns whether two DeleteNexusOperationRequest 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 *DeleteNexusOperationRequest) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *DeleteNexusOperationRequest + switch t := that.(type) { + case *DeleteNexusOperationRequest: + that1 = t + case DeleteNexusOperationRequest: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type DeleteNexusOperationResponse to the protobuf v3 wire format +func (val *DeleteNexusOperationResponse) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type DeleteNexusOperationResponse from the protobuf v3 wire format +func (val *DeleteNexusOperationResponse) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *DeleteNexusOperationResponse) Size() int { + return proto.Size(val) +} + +// Equal returns whether two DeleteNexusOperationResponse 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 *DeleteNexusOperationResponse) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *DeleteNexusOperationResponse + switch t := that.(type) { + case *DeleteNexusOperationResponse: + that1 = t + case DeleteNexusOperationResponse: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type PollNexusOperationRequest to the protobuf v3 wire format +func (val *PollNexusOperationRequest) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type PollNexusOperationRequest from the protobuf v3 wire format +func (val *PollNexusOperationRequest) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *PollNexusOperationRequest) Size() int { + return proto.Size(val) +} + +// Equal returns whether two PollNexusOperationRequest 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 *PollNexusOperationRequest) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *PollNexusOperationRequest + switch t := that.(type) { + case *PollNexusOperationRequest: + that1 = t + case PollNexusOperationRequest: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} + +// Marshal an object of type PollNexusOperationResponse to the protobuf v3 wire format +func (val *PollNexusOperationResponse) Marshal() ([]byte, error) { + return proto.Marshal(val) +} + +// Unmarshal an object of type PollNexusOperationResponse from the protobuf v3 wire format +func (val *PollNexusOperationResponse) Unmarshal(buf []byte) error { + return proto.Unmarshal(buf, val) +} + +// Size returns the size of the object, in bytes, once serialized +func (val *PollNexusOperationResponse) Size() int { + return proto.Size(val) +} + +// Equal returns whether two PollNexusOperationResponse 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 *PollNexusOperationResponse) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + var that1 *PollNexusOperationResponse + switch t := that.(type) { + case *PollNexusOperationResponse: + that1 = t + case PollNexusOperationResponse: + that1 = &t + default: + return false + } + + return proto.Equal(this, that1) +} diff --git a/chasm/lib/nexusoperation/gen/nexusoperationpb/v1/request_response.pb.go b/chasm/lib/nexusoperation/gen/nexusoperationpb/v1/request_response.pb.go new file mode 100644 index 0000000000..34561fe4e0 --- /dev/null +++ b/chasm/lib/nexusoperation/gen/nexusoperationpb/v1/request_response.pb.go @@ -0,0 +1,686 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// plugins: +// protoc-gen-go +// protoc +// source: temporal/server/chasm/lib/nexusoperation/proto/v1/request_response.proto + +package nexusoperationpb + +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 StartNexusOperationRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + NamespaceId string `protobuf:"bytes,1,opt,name=namespace_id,json=namespaceId,proto3" json:"namespace_id,omitempty"` + FrontendRequest *v1.StartNexusOperationExecutionRequest `protobuf:"bytes,2,opt,name=frontend_request,json=frontendRequest,proto3" json:"frontend_request,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StartNexusOperationRequest) Reset() { + *x = StartNexusOperationRequest{} + mi := &file_temporal_server_chasm_lib_nexusoperation_proto_v1_request_response_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StartNexusOperationRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StartNexusOperationRequest) ProtoMessage() {} + +func (x *StartNexusOperationRequest) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_nexusoperation_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 StartNexusOperationRequest.ProtoReflect.Descriptor instead. +func (*StartNexusOperationRequest) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_nexusoperation_proto_v1_request_response_proto_rawDescGZIP(), []int{0} +} + +func (x *StartNexusOperationRequest) GetNamespaceId() string { + if x != nil { + return x.NamespaceId + } + return "" +} + +func (x *StartNexusOperationRequest) GetFrontendRequest() *v1.StartNexusOperationExecutionRequest { + if x != nil { + return x.FrontendRequest + } + return nil +} + +type StartNexusOperationResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + FrontendResponse *v1.StartNexusOperationExecutionResponse `protobuf:"bytes,1,opt,name=frontend_response,json=frontendResponse,proto3" json:"frontend_response,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StartNexusOperationResponse) Reset() { + *x = StartNexusOperationResponse{} + mi := &file_temporal_server_chasm_lib_nexusoperation_proto_v1_request_response_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StartNexusOperationResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StartNexusOperationResponse) ProtoMessage() {} + +func (x *StartNexusOperationResponse) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_nexusoperation_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 StartNexusOperationResponse.ProtoReflect.Descriptor instead. +func (*StartNexusOperationResponse) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_nexusoperation_proto_v1_request_response_proto_rawDescGZIP(), []int{1} +} + +func (x *StartNexusOperationResponse) GetFrontendResponse() *v1.StartNexusOperationExecutionResponse { + if x != nil { + return x.FrontendResponse + } + return nil +} + +type DescribeNexusOperationRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + NamespaceId string `protobuf:"bytes,1,opt,name=namespace_id,json=namespaceId,proto3" json:"namespace_id,omitempty"` + FrontendRequest *v1.DescribeNexusOperationExecutionRequest `protobuf:"bytes,2,opt,name=frontend_request,json=frontendRequest,proto3" json:"frontend_request,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DescribeNexusOperationRequest) Reset() { + *x = DescribeNexusOperationRequest{} + mi := &file_temporal_server_chasm_lib_nexusoperation_proto_v1_request_response_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DescribeNexusOperationRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DescribeNexusOperationRequest) ProtoMessage() {} + +func (x *DescribeNexusOperationRequest) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_nexusoperation_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 DescribeNexusOperationRequest.ProtoReflect.Descriptor instead. +func (*DescribeNexusOperationRequest) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_nexusoperation_proto_v1_request_response_proto_rawDescGZIP(), []int{2} +} + +func (x *DescribeNexusOperationRequest) GetNamespaceId() string { + if x != nil { + return x.NamespaceId + } + return "" +} + +func (x *DescribeNexusOperationRequest) GetFrontendRequest() *v1.DescribeNexusOperationExecutionRequest { + if x != nil { + return x.FrontendRequest + } + return nil +} + +type DescribeNexusOperationResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + FrontendResponse *v1.DescribeNexusOperationExecutionResponse `protobuf:"bytes,1,opt,name=frontend_response,json=frontendResponse,proto3" json:"frontend_response,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DescribeNexusOperationResponse) Reset() { + *x = DescribeNexusOperationResponse{} + mi := &file_temporal_server_chasm_lib_nexusoperation_proto_v1_request_response_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DescribeNexusOperationResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DescribeNexusOperationResponse) ProtoMessage() {} + +func (x *DescribeNexusOperationResponse) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_nexusoperation_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 DescribeNexusOperationResponse.ProtoReflect.Descriptor instead. +func (*DescribeNexusOperationResponse) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_nexusoperation_proto_v1_request_response_proto_rawDescGZIP(), []int{3} +} + +func (x *DescribeNexusOperationResponse) GetFrontendResponse() *v1.DescribeNexusOperationExecutionResponse { + if x != nil { + return x.FrontendResponse + } + return nil +} + +type RequestCancelNexusOperationRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + NamespaceId string `protobuf:"bytes,1,opt,name=namespace_id,json=namespaceId,proto3" json:"namespace_id,omitempty"` + FrontendRequest *v1.RequestCancelNexusOperationExecutionRequest `protobuf:"bytes,2,opt,name=frontend_request,json=frontendRequest,proto3" json:"frontend_request,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RequestCancelNexusOperationRequest) Reset() { + *x = RequestCancelNexusOperationRequest{} + mi := &file_temporal_server_chasm_lib_nexusoperation_proto_v1_request_response_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RequestCancelNexusOperationRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RequestCancelNexusOperationRequest) ProtoMessage() {} + +func (x *RequestCancelNexusOperationRequest) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_nexusoperation_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 RequestCancelNexusOperationRequest.ProtoReflect.Descriptor instead. +func (*RequestCancelNexusOperationRequest) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_nexusoperation_proto_v1_request_response_proto_rawDescGZIP(), []int{4} +} + +func (x *RequestCancelNexusOperationRequest) GetNamespaceId() string { + if x != nil { + return x.NamespaceId + } + return "" +} + +func (x *RequestCancelNexusOperationRequest) GetFrontendRequest() *v1.RequestCancelNexusOperationExecutionRequest { + if x != nil { + return x.FrontendRequest + } + return nil +} + +type RequestCancelNexusOperationResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RequestCancelNexusOperationResponse) Reset() { + *x = RequestCancelNexusOperationResponse{} + mi := &file_temporal_server_chasm_lib_nexusoperation_proto_v1_request_response_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RequestCancelNexusOperationResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RequestCancelNexusOperationResponse) ProtoMessage() {} + +func (x *RequestCancelNexusOperationResponse) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_nexusoperation_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 RequestCancelNexusOperationResponse.ProtoReflect.Descriptor instead. +func (*RequestCancelNexusOperationResponse) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_nexusoperation_proto_v1_request_response_proto_rawDescGZIP(), []int{5} +} + +type TerminateNexusOperationRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + NamespaceId string `protobuf:"bytes,1,opt,name=namespace_id,json=namespaceId,proto3" json:"namespace_id,omitempty"` + FrontendRequest *v1.TerminateNexusOperationExecutionRequest `protobuf:"bytes,2,opt,name=frontend_request,json=frontendRequest,proto3" json:"frontend_request,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TerminateNexusOperationRequest) Reset() { + *x = TerminateNexusOperationRequest{} + mi := &file_temporal_server_chasm_lib_nexusoperation_proto_v1_request_response_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TerminateNexusOperationRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TerminateNexusOperationRequest) ProtoMessage() {} + +func (x *TerminateNexusOperationRequest) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_nexusoperation_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 TerminateNexusOperationRequest.ProtoReflect.Descriptor instead. +func (*TerminateNexusOperationRequest) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_nexusoperation_proto_v1_request_response_proto_rawDescGZIP(), []int{6} +} + +func (x *TerminateNexusOperationRequest) GetNamespaceId() string { + if x != nil { + return x.NamespaceId + } + return "" +} + +func (x *TerminateNexusOperationRequest) GetFrontendRequest() *v1.TerminateNexusOperationExecutionRequest { + if x != nil { + return x.FrontendRequest + } + return nil +} + +type TerminateNexusOperationResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TerminateNexusOperationResponse) Reset() { + *x = TerminateNexusOperationResponse{} + mi := &file_temporal_server_chasm_lib_nexusoperation_proto_v1_request_response_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TerminateNexusOperationResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TerminateNexusOperationResponse) ProtoMessage() {} + +func (x *TerminateNexusOperationResponse) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_nexusoperation_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 TerminateNexusOperationResponse.ProtoReflect.Descriptor instead. +func (*TerminateNexusOperationResponse) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_nexusoperation_proto_v1_request_response_proto_rawDescGZIP(), []int{7} +} + +type DeleteNexusOperationRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + NamespaceId string `protobuf:"bytes,1,opt,name=namespace_id,json=namespaceId,proto3" json:"namespace_id,omitempty"` + FrontendRequest *v1.DeleteNexusOperationExecutionRequest `protobuf:"bytes,2,opt,name=frontend_request,json=frontendRequest,proto3" json:"frontend_request,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteNexusOperationRequest) Reset() { + *x = DeleteNexusOperationRequest{} + mi := &file_temporal_server_chasm_lib_nexusoperation_proto_v1_request_response_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteNexusOperationRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteNexusOperationRequest) ProtoMessage() {} + +func (x *DeleteNexusOperationRequest) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_nexusoperation_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 DeleteNexusOperationRequest.ProtoReflect.Descriptor instead. +func (*DeleteNexusOperationRequest) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_nexusoperation_proto_v1_request_response_proto_rawDescGZIP(), []int{8} +} + +func (x *DeleteNexusOperationRequest) GetNamespaceId() string { + if x != nil { + return x.NamespaceId + } + return "" +} + +func (x *DeleteNexusOperationRequest) GetFrontendRequest() *v1.DeleteNexusOperationExecutionRequest { + if x != nil { + return x.FrontendRequest + } + return nil +} + +type DeleteNexusOperationResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteNexusOperationResponse) Reset() { + *x = DeleteNexusOperationResponse{} + mi := &file_temporal_server_chasm_lib_nexusoperation_proto_v1_request_response_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteNexusOperationResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteNexusOperationResponse) ProtoMessage() {} + +func (x *DeleteNexusOperationResponse) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_nexusoperation_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 DeleteNexusOperationResponse.ProtoReflect.Descriptor instead. +func (*DeleteNexusOperationResponse) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_nexusoperation_proto_v1_request_response_proto_rawDescGZIP(), []int{9} +} + +type PollNexusOperationRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + NamespaceId string `protobuf:"bytes,1,opt,name=namespace_id,json=namespaceId,proto3" json:"namespace_id,omitempty"` + FrontendRequest *v1.PollNexusOperationExecutionRequest `protobuf:"bytes,2,opt,name=frontend_request,json=frontendRequest,proto3" json:"frontend_request,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PollNexusOperationRequest) Reset() { + *x = PollNexusOperationRequest{} + mi := &file_temporal_server_chasm_lib_nexusoperation_proto_v1_request_response_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PollNexusOperationRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PollNexusOperationRequest) ProtoMessage() {} + +func (x *PollNexusOperationRequest) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_nexusoperation_proto_v1_request_response_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PollNexusOperationRequest.ProtoReflect.Descriptor instead. +func (*PollNexusOperationRequest) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_nexusoperation_proto_v1_request_response_proto_rawDescGZIP(), []int{10} +} + +func (x *PollNexusOperationRequest) GetNamespaceId() string { + if x != nil { + return x.NamespaceId + } + return "" +} + +func (x *PollNexusOperationRequest) GetFrontendRequest() *v1.PollNexusOperationExecutionRequest { + if x != nil { + return x.FrontendRequest + } + return nil +} + +type PollNexusOperationResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + FrontendResponse *v1.PollNexusOperationExecutionResponse `protobuf:"bytes,1,opt,name=frontend_response,json=frontendResponse,proto3" json:"frontend_response,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PollNexusOperationResponse) Reset() { + *x = PollNexusOperationResponse{} + mi := &file_temporal_server_chasm_lib_nexusoperation_proto_v1_request_response_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PollNexusOperationResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PollNexusOperationResponse) ProtoMessage() {} + +func (x *PollNexusOperationResponse) ProtoReflect() protoreflect.Message { + mi := &file_temporal_server_chasm_lib_nexusoperation_proto_v1_request_response_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PollNexusOperationResponse.ProtoReflect.Descriptor instead. +func (*PollNexusOperationResponse) Descriptor() ([]byte, []int) { + return file_temporal_server_chasm_lib_nexusoperation_proto_v1_request_response_proto_rawDescGZIP(), []int{11} +} + +func (x *PollNexusOperationResponse) GetFrontendResponse() *v1.PollNexusOperationExecutionResponse { + if x != nil { + return x.FrontendResponse + } + return nil +} + +var File_temporal_server_chasm_lib_nexusoperation_proto_v1_request_response_proto protoreflect.FileDescriptor + +const file_temporal_server_chasm_lib_nexusoperation_proto_v1_request_response_proto_rawDesc = "" + + "\n" + + "Htemporal/server/chasm/lib/nexusoperation/proto/v1/request_response.proto\x121temporal.server.chasm.lib.nexusoperation.proto.v1\x1a6temporal/api/workflowservice/v1/request_response.proto\"\xb0\x01\n" + + "\x1aStartNexusOperationRequest\x12!\n" + + "\fnamespace_id\x18\x01 \x01(\tR\vnamespaceId\x12o\n" + + "\x10frontend_request\x18\x02 \x01(\v2D.temporal.api.workflowservice.v1.StartNexusOperationExecutionRequestR\x0ffrontendRequest\"\x91\x01\n" + + "\x1bStartNexusOperationResponse\x12r\n" + + "\x11frontend_response\x18\x01 \x01(\v2E.temporal.api.workflowservice.v1.StartNexusOperationExecutionResponseR\x10frontendResponse\"\xb6\x01\n" + + "\x1dDescribeNexusOperationRequest\x12!\n" + + "\fnamespace_id\x18\x01 \x01(\tR\vnamespaceId\x12r\n" + + "\x10frontend_request\x18\x02 \x01(\v2G.temporal.api.workflowservice.v1.DescribeNexusOperationExecutionRequestR\x0ffrontendRequest\"\x97\x01\n" + + "\x1eDescribeNexusOperationResponse\x12u\n" + + "\x11frontend_response\x18\x01 \x01(\v2H.temporal.api.workflowservice.v1.DescribeNexusOperationExecutionResponseR\x10frontendResponse\"\xc0\x01\n" + + "\"RequestCancelNexusOperationRequest\x12!\n" + + "\fnamespace_id\x18\x01 \x01(\tR\vnamespaceId\x12w\n" + + "\x10frontend_request\x18\x02 \x01(\v2L.temporal.api.workflowservice.v1.RequestCancelNexusOperationExecutionRequestR\x0ffrontendRequest\"%\n" + + "#RequestCancelNexusOperationResponse\"\xb8\x01\n" + + "\x1eTerminateNexusOperationRequest\x12!\n" + + "\fnamespace_id\x18\x01 \x01(\tR\vnamespaceId\x12s\n" + + "\x10frontend_request\x18\x02 \x01(\v2H.temporal.api.workflowservice.v1.TerminateNexusOperationExecutionRequestR\x0ffrontendRequest\"!\n" + + "\x1fTerminateNexusOperationResponse\"\xb2\x01\n" + + "\x1bDeleteNexusOperationRequest\x12!\n" + + "\fnamespace_id\x18\x01 \x01(\tR\vnamespaceId\x12p\n" + + "\x10frontend_request\x18\x02 \x01(\v2E.temporal.api.workflowservice.v1.DeleteNexusOperationExecutionRequestR\x0ffrontendRequest\"\x1e\n" + + "\x1cDeleteNexusOperationResponse\"\xae\x01\n" + + "\x19PollNexusOperationRequest\x12!\n" + + "\fnamespace_id\x18\x01 \x01(\tR\vnamespaceId\x12n\n" + + "\x10frontend_request\x18\x02 \x01(\v2C.temporal.api.workflowservice.v1.PollNexusOperationExecutionRequestR\x0ffrontendRequest\"\x8f\x01\n" + + "\x1aPollNexusOperationResponse\x12q\n" + + "\x11frontend_response\x18\x01 \x01(\v2D.temporal.api.workflowservice.v1.PollNexusOperationExecutionResponseR\x10frontendResponseBVZTgo.temporal.io/server/chasm/lib/nexusoperation/gen/nexusoperationpb;nexusoperationpbb\x06proto3" + +var ( + file_temporal_server_chasm_lib_nexusoperation_proto_v1_request_response_proto_rawDescOnce sync.Once + file_temporal_server_chasm_lib_nexusoperation_proto_v1_request_response_proto_rawDescData []byte +) + +func file_temporal_server_chasm_lib_nexusoperation_proto_v1_request_response_proto_rawDescGZIP() []byte { + file_temporal_server_chasm_lib_nexusoperation_proto_v1_request_response_proto_rawDescOnce.Do(func() { + file_temporal_server_chasm_lib_nexusoperation_proto_v1_request_response_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_temporal_server_chasm_lib_nexusoperation_proto_v1_request_response_proto_rawDesc), len(file_temporal_server_chasm_lib_nexusoperation_proto_v1_request_response_proto_rawDesc))) + }) + return file_temporal_server_chasm_lib_nexusoperation_proto_v1_request_response_proto_rawDescData +} + +var file_temporal_server_chasm_lib_nexusoperation_proto_v1_request_response_proto_msgTypes = make([]protoimpl.MessageInfo, 12) +var file_temporal_server_chasm_lib_nexusoperation_proto_v1_request_response_proto_goTypes = []any{ + (*StartNexusOperationRequest)(nil), // 0: temporal.server.chasm.lib.nexusoperation.proto.v1.StartNexusOperationRequest + (*StartNexusOperationResponse)(nil), // 1: temporal.server.chasm.lib.nexusoperation.proto.v1.StartNexusOperationResponse + (*DescribeNexusOperationRequest)(nil), // 2: temporal.server.chasm.lib.nexusoperation.proto.v1.DescribeNexusOperationRequest + (*DescribeNexusOperationResponse)(nil), // 3: temporal.server.chasm.lib.nexusoperation.proto.v1.DescribeNexusOperationResponse + (*RequestCancelNexusOperationRequest)(nil), // 4: temporal.server.chasm.lib.nexusoperation.proto.v1.RequestCancelNexusOperationRequest + (*RequestCancelNexusOperationResponse)(nil), // 5: temporal.server.chasm.lib.nexusoperation.proto.v1.RequestCancelNexusOperationResponse + (*TerminateNexusOperationRequest)(nil), // 6: temporal.server.chasm.lib.nexusoperation.proto.v1.TerminateNexusOperationRequest + (*TerminateNexusOperationResponse)(nil), // 7: temporal.server.chasm.lib.nexusoperation.proto.v1.TerminateNexusOperationResponse + (*DeleteNexusOperationRequest)(nil), // 8: temporal.server.chasm.lib.nexusoperation.proto.v1.DeleteNexusOperationRequest + (*DeleteNexusOperationResponse)(nil), // 9: temporal.server.chasm.lib.nexusoperation.proto.v1.DeleteNexusOperationResponse + (*PollNexusOperationRequest)(nil), // 10: temporal.server.chasm.lib.nexusoperation.proto.v1.PollNexusOperationRequest + (*PollNexusOperationResponse)(nil), // 11: temporal.server.chasm.lib.nexusoperation.proto.v1.PollNexusOperationResponse + (*v1.StartNexusOperationExecutionRequest)(nil), // 12: temporal.api.workflowservice.v1.StartNexusOperationExecutionRequest + (*v1.StartNexusOperationExecutionResponse)(nil), // 13: temporal.api.workflowservice.v1.StartNexusOperationExecutionResponse + (*v1.DescribeNexusOperationExecutionRequest)(nil), // 14: temporal.api.workflowservice.v1.DescribeNexusOperationExecutionRequest + (*v1.DescribeNexusOperationExecutionResponse)(nil), // 15: temporal.api.workflowservice.v1.DescribeNexusOperationExecutionResponse + (*v1.RequestCancelNexusOperationExecutionRequest)(nil), // 16: temporal.api.workflowservice.v1.RequestCancelNexusOperationExecutionRequest + (*v1.TerminateNexusOperationExecutionRequest)(nil), // 17: temporal.api.workflowservice.v1.TerminateNexusOperationExecutionRequest + (*v1.DeleteNexusOperationExecutionRequest)(nil), // 18: temporal.api.workflowservice.v1.DeleteNexusOperationExecutionRequest + (*v1.PollNexusOperationExecutionRequest)(nil), // 19: temporal.api.workflowservice.v1.PollNexusOperationExecutionRequest + (*v1.PollNexusOperationExecutionResponse)(nil), // 20: temporal.api.workflowservice.v1.PollNexusOperationExecutionResponse +} +var file_temporal_server_chasm_lib_nexusoperation_proto_v1_request_response_proto_depIdxs = []int32{ + 12, // 0: temporal.server.chasm.lib.nexusoperation.proto.v1.StartNexusOperationRequest.frontend_request:type_name -> temporal.api.workflowservice.v1.StartNexusOperationExecutionRequest + 13, // 1: temporal.server.chasm.lib.nexusoperation.proto.v1.StartNexusOperationResponse.frontend_response:type_name -> temporal.api.workflowservice.v1.StartNexusOperationExecutionResponse + 14, // 2: temporal.server.chasm.lib.nexusoperation.proto.v1.DescribeNexusOperationRequest.frontend_request:type_name -> temporal.api.workflowservice.v1.DescribeNexusOperationExecutionRequest + 15, // 3: temporal.server.chasm.lib.nexusoperation.proto.v1.DescribeNexusOperationResponse.frontend_response:type_name -> temporal.api.workflowservice.v1.DescribeNexusOperationExecutionResponse + 16, // 4: temporal.server.chasm.lib.nexusoperation.proto.v1.RequestCancelNexusOperationRequest.frontend_request:type_name -> temporal.api.workflowservice.v1.RequestCancelNexusOperationExecutionRequest + 17, // 5: temporal.server.chasm.lib.nexusoperation.proto.v1.TerminateNexusOperationRequest.frontend_request:type_name -> temporal.api.workflowservice.v1.TerminateNexusOperationExecutionRequest + 18, // 6: temporal.server.chasm.lib.nexusoperation.proto.v1.DeleteNexusOperationRequest.frontend_request:type_name -> temporal.api.workflowservice.v1.DeleteNexusOperationExecutionRequest + 19, // 7: temporal.server.chasm.lib.nexusoperation.proto.v1.PollNexusOperationRequest.frontend_request:type_name -> temporal.api.workflowservice.v1.PollNexusOperationExecutionRequest + 20, // 8: temporal.server.chasm.lib.nexusoperation.proto.v1.PollNexusOperationResponse.frontend_response:type_name -> temporal.api.workflowservice.v1.PollNexusOperationExecutionResponse + 9, // [9:9] is the sub-list for method output_type + 9, // [9:9] is the sub-list for method input_type + 9, // [9:9] is the sub-list for extension type_name + 9, // [9:9] is the sub-list for extension extendee + 0, // [0:9] is the sub-list for field type_name +} + +func init() { file_temporal_server_chasm_lib_nexusoperation_proto_v1_request_response_proto_init() } +func file_temporal_server_chasm_lib_nexusoperation_proto_v1_request_response_proto_init() { + if File_temporal_server_chasm_lib_nexusoperation_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_nexusoperation_proto_v1_request_response_proto_rawDesc), len(file_temporal_server_chasm_lib_nexusoperation_proto_v1_request_response_proto_rawDesc)), + NumEnums: 0, + NumMessages: 12, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_temporal_server_chasm_lib_nexusoperation_proto_v1_request_response_proto_goTypes, + DependencyIndexes: file_temporal_server_chasm_lib_nexusoperation_proto_v1_request_response_proto_depIdxs, + MessageInfos: file_temporal_server_chasm_lib_nexusoperation_proto_v1_request_response_proto_msgTypes, + }.Build() + File_temporal_server_chasm_lib_nexusoperation_proto_v1_request_response_proto = out.File + file_temporal_server_chasm_lib_nexusoperation_proto_v1_request_response_proto_goTypes = nil + file_temporal_server_chasm_lib_nexusoperation_proto_v1_request_response_proto_depIdxs = nil +} diff --git a/chasm/lib/nexusoperation/gen/nexusoperationpb/v1/service.pb.go b/chasm/lib/nexusoperation/gen/nexusoperationpb/v1/service.pb.go new file mode 100644 index 0000000000..115d4ecd86 --- /dev/null +++ b/chasm/lib/nexusoperation/gen/nexusoperationpb/v1/service.pb.go @@ -0,0 +1,95 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// plugins: +// protoc-gen-go +// protoc +// source: temporal/server/chasm/lib/nexusoperation/proto/v1/service.proto + +package nexusoperationpb + +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_nexusoperation_proto_v1_service_proto protoreflect.FileDescriptor + +const file_temporal_server_chasm_lib_nexusoperation_proto_v1_service_proto_rawDesc = "" + + "\n" + + "?temporal/server/chasm/lib/nexusoperation/proto/v1/service.proto\x121temporal.server.chasm.lib.nexusoperation.proto.v1\x1aHtemporal/server/chasm/lib/nexusoperation/proto/v1/request_response.proto\x1a0temporal/server/api/common/v1/api_category.proto\x1a.temporal/server/api/routing/v1/extension.proto2\x90\v\n" + + "\x15NexusOperationService\x12\xdf\x01\n" + + "\x13StartNexusOperation\x12M.temporal.server.chasm.lib.nexusoperation.proto.v1.StartNexusOperationRequest\x1aN.temporal.server.chasm.lib.nexusoperation.proto.v1.StartNexusOperationResponse\")\x8a\xb5\x18\x02\b\x01\xd2\xc3\x18\x1f\x1a\x1dfrontend_request.operation_id\x12\xe8\x01\n" + + "\x16DescribeNexusOperation\x12P.temporal.server.chasm.lib.nexusoperation.proto.v1.DescribeNexusOperationRequest\x1aQ.temporal.server.chasm.lib.nexusoperation.proto.v1.DescribeNexusOperationResponse\")\x8a\xb5\x18\x02\b\x01\xd2\xc3\x18\x1f\x1a\x1dfrontend_request.operation_id\x12\xf7\x01\n" + + "\x1bRequestCancelNexusOperation\x12U.temporal.server.chasm.lib.nexusoperation.proto.v1.RequestCancelNexusOperationRequest\x1aV.temporal.server.chasm.lib.nexusoperation.proto.v1.RequestCancelNexusOperationResponse\")\x8a\xb5\x18\x02\b\x01\xd2\xc3\x18\x1f\x1a\x1dfrontend_request.operation_id\x12\xeb\x01\n" + + "\x17TerminateNexusOperation\x12Q.temporal.server.chasm.lib.nexusoperation.proto.v1.TerminateNexusOperationRequest\x1aR.temporal.server.chasm.lib.nexusoperation.proto.v1.TerminateNexusOperationResponse\")\x8a\xb5\x18\x02\b\x01\xd2\xc3\x18\x1f\x1a\x1dfrontend_request.operation_id\x12\xe2\x01\n" + + "\x14DeleteNexusOperation\x12N.temporal.server.chasm.lib.nexusoperation.proto.v1.DeleteNexusOperationRequest\x1aO.temporal.server.chasm.lib.nexusoperation.proto.v1.DeleteNexusOperationResponse\")\x8a\xb5\x18\x02\b\x01\xd2\xc3\x18\x1f\x1a\x1dfrontend_request.operation_id\x12\xdc\x01\n" + + "\x12PollNexusOperation\x12L.temporal.server.chasm.lib.nexusoperation.proto.v1.PollNexusOperationRequest\x1aM.temporal.server.chasm.lib.nexusoperation.proto.v1.PollNexusOperationResponse\")\x8a\xb5\x18\x02\b\x02\xd2\xc3\x18\x1f\x1a\x1dfrontend_request.operation_idBVZTgo.temporal.io/server/chasm/lib/nexusoperation/gen/nexusoperationpb;nexusoperationpbb\x06proto3" + +var file_temporal_server_chasm_lib_nexusoperation_proto_v1_service_proto_goTypes = []any{ + (*StartNexusOperationRequest)(nil), // 0: temporal.server.chasm.lib.nexusoperation.proto.v1.StartNexusOperationRequest + (*DescribeNexusOperationRequest)(nil), // 1: temporal.server.chasm.lib.nexusoperation.proto.v1.DescribeNexusOperationRequest + (*RequestCancelNexusOperationRequest)(nil), // 2: temporal.server.chasm.lib.nexusoperation.proto.v1.RequestCancelNexusOperationRequest + (*TerminateNexusOperationRequest)(nil), // 3: temporal.server.chasm.lib.nexusoperation.proto.v1.TerminateNexusOperationRequest + (*DeleteNexusOperationRequest)(nil), // 4: temporal.server.chasm.lib.nexusoperation.proto.v1.DeleteNexusOperationRequest + (*PollNexusOperationRequest)(nil), // 5: temporal.server.chasm.lib.nexusoperation.proto.v1.PollNexusOperationRequest + (*StartNexusOperationResponse)(nil), // 6: temporal.server.chasm.lib.nexusoperation.proto.v1.StartNexusOperationResponse + (*DescribeNexusOperationResponse)(nil), // 7: temporal.server.chasm.lib.nexusoperation.proto.v1.DescribeNexusOperationResponse + (*RequestCancelNexusOperationResponse)(nil), // 8: temporal.server.chasm.lib.nexusoperation.proto.v1.RequestCancelNexusOperationResponse + (*TerminateNexusOperationResponse)(nil), // 9: temporal.server.chasm.lib.nexusoperation.proto.v1.TerminateNexusOperationResponse + (*DeleteNexusOperationResponse)(nil), // 10: temporal.server.chasm.lib.nexusoperation.proto.v1.DeleteNexusOperationResponse + (*PollNexusOperationResponse)(nil), // 11: temporal.server.chasm.lib.nexusoperation.proto.v1.PollNexusOperationResponse +} +var file_temporal_server_chasm_lib_nexusoperation_proto_v1_service_proto_depIdxs = []int32{ + 0, // 0: temporal.server.chasm.lib.nexusoperation.proto.v1.NexusOperationService.StartNexusOperation:input_type -> temporal.server.chasm.lib.nexusoperation.proto.v1.StartNexusOperationRequest + 1, // 1: temporal.server.chasm.lib.nexusoperation.proto.v1.NexusOperationService.DescribeNexusOperation:input_type -> temporal.server.chasm.lib.nexusoperation.proto.v1.DescribeNexusOperationRequest + 2, // 2: temporal.server.chasm.lib.nexusoperation.proto.v1.NexusOperationService.RequestCancelNexusOperation:input_type -> temporal.server.chasm.lib.nexusoperation.proto.v1.RequestCancelNexusOperationRequest + 3, // 3: temporal.server.chasm.lib.nexusoperation.proto.v1.NexusOperationService.TerminateNexusOperation:input_type -> temporal.server.chasm.lib.nexusoperation.proto.v1.TerminateNexusOperationRequest + 4, // 4: temporal.server.chasm.lib.nexusoperation.proto.v1.NexusOperationService.DeleteNexusOperation:input_type -> temporal.server.chasm.lib.nexusoperation.proto.v1.DeleteNexusOperationRequest + 5, // 5: temporal.server.chasm.lib.nexusoperation.proto.v1.NexusOperationService.PollNexusOperation:input_type -> temporal.server.chasm.lib.nexusoperation.proto.v1.PollNexusOperationRequest + 6, // 6: temporal.server.chasm.lib.nexusoperation.proto.v1.NexusOperationService.StartNexusOperation:output_type -> temporal.server.chasm.lib.nexusoperation.proto.v1.StartNexusOperationResponse + 7, // 7: temporal.server.chasm.lib.nexusoperation.proto.v1.NexusOperationService.DescribeNexusOperation:output_type -> temporal.server.chasm.lib.nexusoperation.proto.v1.DescribeNexusOperationResponse + 8, // 8: temporal.server.chasm.lib.nexusoperation.proto.v1.NexusOperationService.RequestCancelNexusOperation:output_type -> temporal.server.chasm.lib.nexusoperation.proto.v1.RequestCancelNexusOperationResponse + 9, // 9: temporal.server.chasm.lib.nexusoperation.proto.v1.NexusOperationService.TerminateNexusOperation:output_type -> temporal.server.chasm.lib.nexusoperation.proto.v1.TerminateNexusOperationResponse + 10, // 10: temporal.server.chasm.lib.nexusoperation.proto.v1.NexusOperationService.DeleteNexusOperation:output_type -> temporal.server.chasm.lib.nexusoperation.proto.v1.DeleteNexusOperationResponse + 11, // 11: temporal.server.chasm.lib.nexusoperation.proto.v1.NexusOperationService.PollNexusOperation:output_type -> temporal.server.chasm.lib.nexusoperation.proto.v1.PollNexusOperationResponse + 6, // [6:12] is the sub-list for method output_type + 0, // [0:6] 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_nexusoperation_proto_v1_service_proto_init() } +func file_temporal_server_chasm_lib_nexusoperation_proto_v1_service_proto_init() { + if File_temporal_server_chasm_lib_nexusoperation_proto_v1_service_proto != nil { + return + } + file_temporal_server_chasm_lib_nexusoperation_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_nexusoperation_proto_v1_service_proto_rawDesc), len(file_temporal_server_chasm_lib_nexusoperation_proto_v1_service_proto_rawDesc)), + NumEnums: 0, + NumMessages: 0, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_temporal_server_chasm_lib_nexusoperation_proto_v1_service_proto_goTypes, + DependencyIndexes: file_temporal_server_chasm_lib_nexusoperation_proto_v1_service_proto_depIdxs, + }.Build() + File_temporal_server_chasm_lib_nexusoperation_proto_v1_service_proto = out.File + file_temporal_server_chasm_lib_nexusoperation_proto_v1_service_proto_goTypes = nil + file_temporal_server_chasm_lib_nexusoperation_proto_v1_service_proto_depIdxs = nil +} diff --git a/chasm/lib/nexusoperation/gen/nexusoperationpb/v1/service_client.pb.go b/chasm/lib/nexusoperation/gen/nexusoperationpb/v1/service_client.pb.go new file mode 100644 index 0000000000..df6f364db4 --- /dev/null +++ b/chasm/lib/nexusoperation/gen/nexusoperationpb/v1/service_client.pb.go @@ -0,0 +1,318 @@ +// Code generated by protoc-gen-go-chasm. DO NOT EDIT. +package nexusoperationpb + +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" +) + +// NexusOperationServiceLayeredClient is a client for NexusOperationService. +type NexusOperationServiceLayeredClient struct { + metricsHandler metrics.Handler + numShards int32 + redirector history.Redirector[NexusOperationServiceClient] + retryPolicy backoff.RetryPolicy +} + +// NewNexusOperationServiceLayeredClient initializes a new NexusOperationServiceLayeredClient. +func NewNexusOperationServiceLayeredClient( + dc *dynamicconfig.Collection, + rpcFactory common.RPCFactory, + monitor membership.Monitor, + config *config.Persistence, + logger log.Logger, + metricsHandler metrics.Handler, +) (NexusOperationServiceClient, error) { + resolver, err := monitor.GetResolver(primitives.HistoryService) + if err != nil { + return nil, err + } + connections := history.NewConnectionPool(resolver, rpcFactory, NewNexusOperationServiceClient) + var redirector history.Redirector[NexusOperationServiceClient] + if dynamicconfig.HistoryClientOwnershipCachingEnabled.Get(dc)() { + redirector = history.NewCachingRedirector( + connections, + resolver, + logger, + dynamicconfig.HistoryClientOwnershipCachingStaleTTL.Get(dc), + ) + } else { + redirector = history.NewBasicRedirector(connections, resolver) + } + return &NexusOperationServiceLayeredClient{ + metricsHandler: metricsHandler, + redirector: redirector, + numShards: config.NumHistoryShards, + retryPolicy: common.CreateHistoryClientRetryPolicy(), + }, nil +} +func (c *NexusOperationServiceLayeredClient) callStartNexusOperationNoRetry( + ctx context.Context, + request *StartNexusOperationRequest, + opts ...grpc.CallOption, +) (*StartNexusOperationResponse, error) { + var response *StartNexusOperationResponse + 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("NexusOperationService.StartNexusOperation"), + 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().GetOperationId(), c.numShards) + op := func(ctx context.Context, client NexusOperationServiceClient) error { + var err error + ctx, cancel := context.WithTimeout(ctx, history.DefaultTimeout) + defer cancel() + response, err = client.StartNexusOperation(ctx, request, opts...) + return err + } + err = c.redirector.Execute(ctx, shardID, op) + return response, err +} +func (c *NexusOperationServiceLayeredClient) StartNexusOperation( + ctx context.Context, + request *StartNexusOperationRequest, + opts ...grpc.CallOption, +) (*StartNexusOperationResponse, error) { + call := func(ctx context.Context) (*StartNexusOperationResponse, error) { + return c.callStartNexusOperationNoRetry(ctx, request, opts...) + } + return backoff.ThrottleRetryContextWithReturn(ctx, call, c.retryPolicy, common.IsServiceClientTransientError) +} +func (c *NexusOperationServiceLayeredClient) callDescribeNexusOperationNoRetry( + ctx context.Context, + request *DescribeNexusOperationRequest, + opts ...grpc.CallOption, +) (*DescribeNexusOperationResponse, error) { + var response *DescribeNexusOperationResponse + 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("NexusOperationService.DescribeNexusOperation"), + 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().GetOperationId(), c.numShards) + op := func(ctx context.Context, client NexusOperationServiceClient) error { + var err error + ctx, cancel := context.WithTimeout(ctx, history.DefaultTimeout) + defer cancel() + response, err = client.DescribeNexusOperation(ctx, request, opts...) + return err + } + err = c.redirector.Execute(ctx, shardID, op) + return response, err +} +func (c *NexusOperationServiceLayeredClient) DescribeNexusOperation( + ctx context.Context, + request *DescribeNexusOperationRequest, + opts ...grpc.CallOption, +) (*DescribeNexusOperationResponse, error) { + call := func(ctx context.Context) (*DescribeNexusOperationResponse, error) { + return c.callDescribeNexusOperationNoRetry(ctx, request, opts...) + } + return backoff.ThrottleRetryContextWithReturn(ctx, call, c.retryPolicy, common.IsServiceClientTransientError) +} +func (c *NexusOperationServiceLayeredClient) callRequestCancelNexusOperationNoRetry( + ctx context.Context, + request *RequestCancelNexusOperationRequest, + opts ...grpc.CallOption, +) (*RequestCancelNexusOperationResponse, error) { + var response *RequestCancelNexusOperationResponse + 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("NexusOperationService.RequestCancelNexusOperation"), + 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().GetOperationId(), c.numShards) + op := func(ctx context.Context, client NexusOperationServiceClient) error { + var err error + ctx, cancel := context.WithTimeout(ctx, history.DefaultTimeout) + defer cancel() + response, err = client.RequestCancelNexusOperation(ctx, request, opts...) + return err + } + err = c.redirector.Execute(ctx, shardID, op) + return response, err +} +func (c *NexusOperationServiceLayeredClient) RequestCancelNexusOperation( + ctx context.Context, + request *RequestCancelNexusOperationRequest, + opts ...grpc.CallOption, +) (*RequestCancelNexusOperationResponse, error) { + call := func(ctx context.Context) (*RequestCancelNexusOperationResponse, error) { + return c.callRequestCancelNexusOperationNoRetry(ctx, request, opts...) + } + return backoff.ThrottleRetryContextWithReturn(ctx, call, c.retryPolicy, common.IsServiceClientTransientError) +} +func (c *NexusOperationServiceLayeredClient) callTerminateNexusOperationNoRetry( + ctx context.Context, + request *TerminateNexusOperationRequest, + opts ...grpc.CallOption, +) (*TerminateNexusOperationResponse, error) { + var response *TerminateNexusOperationResponse + 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("NexusOperationService.TerminateNexusOperation"), + 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().GetOperationId(), c.numShards) + op := func(ctx context.Context, client NexusOperationServiceClient) error { + var err error + ctx, cancel := context.WithTimeout(ctx, history.DefaultTimeout) + defer cancel() + response, err = client.TerminateNexusOperation(ctx, request, opts...) + return err + } + err = c.redirector.Execute(ctx, shardID, op) + return response, err +} +func (c *NexusOperationServiceLayeredClient) TerminateNexusOperation( + ctx context.Context, + request *TerminateNexusOperationRequest, + opts ...grpc.CallOption, +) (*TerminateNexusOperationResponse, error) { + call := func(ctx context.Context) (*TerminateNexusOperationResponse, error) { + return c.callTerminateNexusOperationNoRetry(ctx, request, opts...) + } + return backoff.ThrottleRetryContextWithReturn(ctx, call, c.retryPolicy, common.IsServiceClientTransientError) +} +func (c *NexusOperationServiceLayeredClient) callDeleteNexusOperationNoRetry( + ctx context.Context, + request *DeleteNexusOperationRequest, + opts ...grpc.CallOption, +) (*DeleteNexusOperationResponse, error) { + var response *DeleteNexusOperationResponse + 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("NexusOperationService.DeleteNexusOperation"), + 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().GetOperationId(), c.numShards) + op := func(ctx context.Context, client NexusOperationServiceClient) error { + var err error + ctx, cancel := context.WithTimeout(ctx, history.DefaultTimeout) + defer cancel() + response, err = client.DeleteNexusOperation(ctx, request, opts...) + return err + } + err = c.redirector.Execute(ctx, shardID, op) + return response, err +} +func (c *NexusOperationServiceLayeredClient) DeleteNexusOperation( + ctx context.Context, + request *DeleteNexusOperationRequest, + opts ...grpc.CallOption, +) (*DeleteNexusOperationResponse, error) { + call := func(ctx context.Context) (*DeleteNexusOperationResponse, error) { + return c.callDeleteNexusOperationNoRetry(ctx, request, opts...) + } + return backoff.ThrottleRetryContextWithReturn(ctx, call, c.retryPolicy, common.IsServiceClientTransientError) +} +func (c *NexusOperationServiceLayeredClient) callPollNexusOperationNoRetry( + ctx context.Context, + request *PollNexusOperationRequest, + opts ...grpc.CallOption, +) (*PollNexusOperationResponse, error) { + var response *PollNexusOperationResponse + 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("NexusOperationService.PollNexusOperation"), + 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().GetOperationId(), c.numShards) + op := func(ctx context.Context, client NexusOperationServiceClient) error { + var err error + ctx, cancel := context.WithTimeout(ctx, history.DefaultTimeout) + defer cancel() + response, err = client.PollNexusOperation(ctx, request, opts...) + return err + } + err = c.redirector.Execute(ctx, shardID, op) + return response, err +} +func (c *NexusOperationServiceLayeredClient) PollNexusOperation( + ctx context.Context, + request *PollNexusOperationRequest, + opts ...grpc.CallOption, +) (*PollNexusOperationResponse, error) { + call := func(ctx context.Context) (*PollNexusOperationResponse, error) { + return c.callPollNexusOperationNoRetry(ctx, request, opts...) + } + return backoff.ThrottleRetryContextWithReturn(ctx, call, c.retryPolicy, common.IsServiceClientTransientError) +} diff --git a/chasm/lib/nexusoperation/gen/nexusoperationpb/v1/service_grpc.pb.go b/chasm/lib/nexusoperation/gen/nexusoperationpb/v1/service_grpc.pb.go new file mode 100644 index 0000000000..39b61ecc69 --- /dev/null +++ b/chasm/lib/nexusoperation/gen/nexusoperationpb/v1/service_grpc.pb.go @@ -0,0 +1,295 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// plugins: +// - protoc-gen-go-grpc +// - protoc +// source: temporal/server/chasm/lib/nexusoperation/proto/v1/service.proto + +package nexusoperationpb + +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 ( + NexusOperationService_StartNexusOperation_FullMethodName = "/temporal.server.chasm.lib.nexusoperation.proto.v1.NexusOperationService/StartNexusOperation" + NexusOperationService_DescribeNexusOperation_FullMethodName = "/temporal.server.chasm.lib.nexusoperation.proto.v1.NexusOperationService/DescribeNexusOperation" + NexusOperationService_RequestCancelNexusOperation_FullMethodName = "/temporal.server.chasm.lib.nexusoperation.proto.v1.NexusOperationService/RequestCancelNexusOperation" + NexusOperationService_TerminateNexusOperation_FullMethodName = "/temporal.server.chasm.lib.nexusoperation.proto.v1.NexusOperationService/TerminateNexusOperation" + NexusOperationService_DeleteNexusOperation_FullMethodName = "/temporal.server.chasm.lib.nexusoperation.proto.v1.NexusOperationService/DeleteNexusOperation" + NexusOperationService_PollNexusOperation_FullMethodName = "/temporal.server.chasm.lib.nexusoperation.proto.v1.NexusOperationService/PollNexusOperation" +) + +// NexusOperationServiceClient is the client API for NexusOperationService 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 NexusOperationServiceClient interface { + StartNexusOperation(ctx context.Context, in *StartNexusOperationRequest, opts ...grpc.CallOption) (*StartNexusOperationResponse, error) + DescribeNexusOperation(ctx context.Context, in *DescribeNexusOperationRequest, opts ...grpc.CallOption) (*DescribeNexusOperationResponse, error) + RequestCancelNexusOperation(ctx context.Context, in *RequestCancelNexusOperationRequest, opts ...grpc.CallOption) (*RequestCancelNexusOperationResponse, error) + TerminateNexusOperation(ctx context.Context, in *TerminateNexusOperationRequest, opts ...grpc.CallOption) (*TerminateNexusOperationResponse, error) + DeleteNexusOperation(ctx context.Context, in *DeleteNexusOperationRequest, opts ...grpc.CallOption) (*DeleteNexusOperationResponse, error) + PollNexusOperation(ctx context.Context, in *PollNexusOperationRequest, opts ...grpc.CallOption) (*PollNexusOperationResponse, error) +} + +type nexusOperationServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewNexusOperationServiceClient(cc grpc.ClientConnInterface) NexusOperationServiceClient { + return &nexusOperationServiceClient{cc} +} + +func (c *nexusOperationServiceClient) StartNexusOperation(ctx context.Context, in *StartNexusOperationRequest, opts ...grpc.CallOption) (*StartNexusOperationResponse, error) { + out := new(StartNexusOperationResponse) + err := c.cc.Invoke(ctx, NexusOperationService_StartNexusOperation_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *nexusOperationServiceClient) DescribeNexusOperation(ctx context.Context, in *DescribeNexusOperationRequest, opts ...grpc.CallOption) (*DescribeNexusOperationResponse, error) { + out := new(DescribeNexusOperationResponse) + err := c.cc.Invoke(ctx, NexusOperationService_DescribeNexusOperation_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *nexusOperationServiceClient) RequestCancelNexusOperation(ctx context.Context, in *RequestCancelNexusOperationRequest, opts ...grpc.CallOption) (*RequestCancelNexusOperationResponse, error) { + out := new(RequestCancelNexusOperationResponse) + err := c.cc.Invoke(ctx, NexusOperationService_RequestCancelNexusOperation_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *nexusOperationServiceClient) TerminateNexusOperation(ctx context.Context, in *TerminateNexusOperationRequest, opts ...grpc.CallOption) (*TerminateNexusOperationResponse, error) { + out := new(TerminateNexusOperationResponse) + err := c.cc.Invoke(ctx, NexusOperationService_TerminateNexusOperation_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *nexusOperationServiceClient) DeleteNexusOperation(ctx context.Context, in *DeleteNexusOperationRequest, opts ...grpc.CallOption) (*DeleteNexusOperationResponse, error) { + out := new(DeleteNexusOperationResponse) + err := c.cc.Invoke(ctx, NexusOperationService_DeleteNexusOperation_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *nexusOperationServiceClient) PollNexusOperation(ctx context.Context, in *PollNexusOperationRequest, opts ...grpc.CallOption) (*PollNexusOperationResponse, error) { + out := new(PollNexusOperationResponse) + err := c.cc.Invoke(ctx, NexusOperationService_PollNexusOperation_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// NexusOperationServiceServer is the server API for NexusOperationService service. +// All implementations must embed UnimplementedNexusOperationServiceServer +// for forward compatibility +type NexusOperationServiceServer interface { + StartNexusOperation(context.Context, *StartNexusOperationRequest) (*StartNexusOperationResponse, error) + DescribeNexusOperation(context.Context, *DescribeNexusOperationRequest) (*DescribeNexusOperationResponse, error) + RequestCancelNexusOperation(context.Context, *RequestCancelNexusOperationRequest) (*RequestCancelNexusOperationResponse, error) + TerminateNexusOperation(context.Context, *TerminateNexusOperationRequest) (*TerminateNexusOperationResponse, error) + DeleteNexusOperation(context.Context, *DeleteNexusOperationRequest) (*DeleteNexusOperationResponse, error) + PollNexusOperation(context.Context, *PollNexusOperationRequest) (*PollNexusOperationResponse, error) + mustEmbedUnimplementedNexusOperationServiceServer() +} + +// UnimplementedNexusOperationServiceServer must be embedded to have forward compatible implementations. +type UnimplementedNexusOperationServiceServer struct { +} + +func (UnimplementedNexusOperationServiceServer) StartNexusOperation(context.Context, *StartNexusOperationRequest) (*StartNexusOperationResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method StartNexusOperation not implemented") +} +func (UnimplementedNexusOperationServiceServer) DescribeNexusOperation(context.Context, *DescribeNexusOperationRequest) (*DescribeNexusOperationResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method DescribeNexusOperation not implemented") +} +func (UnimplementedNexusOperationServiceServer) RequestCancelNexusOperation(context.Context, *RequestCancelNexusOperationRequest) (*RequestCancelNexusOperationResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method RequestCancelNexusOperation not implemented") +} +func (UnimplementedNexusOperationServiceServer) TerminateNexusOperation(context.Context, *TerminateNexusOperationRequest) (*TerminateNexusOperationResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method TerminateNexusOperation not implemented") +} +func (UnimplementedNexusOperationServiceServer) DeleteNexusOperation(context.Context, *DeleteNexusOperationRequest) (*DeleteNexusOperationResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method DeleteNexusOperation not implemented") +} +func (UnimplementedNexusOperationServiceServer) PollNexusOperation(context.Context, *PollNexusOperationRequest) (*PollNexusOperationResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method PollNexusOperation not implemented") +} +func (UnimplementedNexusOperationServiceServer) mustEmbedUnimplementedNexusOperationServiceServer() {} + +// UnsafeNexusOperationServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to NexusOperationServiceServer will +// result in compilation errors. +type UnsafeNexusOperationServiceServer interface { + mustEmbedUnimplementedNexusOperationServiceServer() +} + +func RegisterNexusOperationServiceServer(s grpc.ServiceRegistrar, srv NexusOperationServiceServer) { + s.RegisterService(&NexusOperationService_ServiceDesc, srv) +} + +func _NexusOperationService_StartNexusOperation_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(StartNexusOperationRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(NexusOperationServiceServer).StartNexusOperation(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: NexusOperationService_StartNexusOperation_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(NexusOperationServiceServer).StartNexusOperation(ctx, req.(*StartNexusOperationRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _NexusOperationService_DescribeNexusOperation_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DescribeNexusOperationRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(NexusOperationServiceServer).DescribeNexusOperation(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: NexusOperationService_DescribeNexusOperation_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(NexusOperationServiceServer).DescribeNexusOperation(ctx, req.(*DescribeNexusOperationRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _NexusOperationService_RequestCancelNexusOperation_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RequestCancelNexusOperationRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(NexusOperationServiceServer).RequestCancelNexusOperation(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: NexusOperationService_RequestCancelNexusOperation_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(NexusOperationServiceServer).RequestCancelNexusOperation(ctx, req.(*RequestCancelNexusOperationRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _NexusOperationService_TerminateNexusOperation_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(TerminateNexusOperationRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(NexusOperationServiceServer).TerminateNexusOperation(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: NexusOperationService_TerminateNexusOperation_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(NexusOperationServiceServer).TerminateNexusOperation(ctx, req.(*TerminateNexusOperationRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _NexusOperationService_DeleteNexusOperation_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DeleteNexusOperationRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(NexusOperationServiceServer).DeleteNexusOperation(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: NexusOperationService_DeleteNexusOperation_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(NexusOperationServiceServer).DeleteNexusOperation(ctx, req.(*DeleteNexusOperationRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _NexusOperationService_PollNexusOperation_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(PollNexusOperationRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(NexusOperationServiceServer).PollNexusOperation(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: NexusOperationService_PollNexusOperation_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(NexusOperationServiceServer).PollNexusOperation(ctx, req.(*PollNexusOperationRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// NexusOperationService_ServiceDesc is the grpc.ServiceDesc for NexusOperationService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var NexusOperationService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "temporal.server.chasm.lib.nexusoperation.proto.v1.NexusOperationService", + HandlerType: (*NexusOperationServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "StartNexusOperation", + Handler: _NexusOperationService_StartNexusOperation_Handler, + }, + { + MethodName: "DescribeNexusOperation", + Handler: _NexusOperationService_DescribeNexusOperation_Handler, + }, + { + MethodName: "RequestCancelNexusOperation", + Handler: _NexusOperationService_RequestCancelNexusOperation_Handler, + }, + { + MethodName: "TerminateNexusOperation", + Handler: _NexusOperationService_TerminateNexusOperation_Handler, + }, + { + MethodName: "DeleteNexusOperation", + Handler: _NexusOperationService_DeleteNexusOperation_Handler, + }, + { + MethodName: "PollNexusOperation", + Handler: _NexusOperationService_PollNexusOperation_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "temporal/server/chasm/lib/nexusoperation/proto/v1/service.proto", +} diff --git a/chasm/lib/nexusoperation/handler.go b/chasm/lib/nexusoperation/handler.go new file mode 100644 index 0000000000..87629e2dab --- /dev/null +++ b/chasm/lib/nexusoperation/handler.go @@ -0,0 +1,96 @@ +package nexusoperation + +import ( + "context" + + enumspb "go.temporal.io/api/enums/v1" + "go.temporal.io/api/workflowservice/v1" + "go.temporal.io/server/chasm" + nexusoperationpb "go.temporal.io/server/chasm/lib/nexusoperation/gen/nexusoperationpb/v1" + "go.temporal.io/server/common/log" +) + +type handler struct { + nexusoperationpb.UnimplementedNexusOperationServiceServer + + config *Config + logger log.Logger +} + +func newHandler(config *Config, logger log.Logger) *handler { + return &handler{ + config: config, + logger: logger, + } +} + +// StartNexusOperation creates a new standalone Nexus operation execution via CHASM. +func (h *handler) StartNexusOperation( + ctx context.Context, + req *nexusoperationpb.StartNexusOperationRequest, +) (response *nexusoperationpb.StartNexusOperationResponse, err error) { + defer log.CapturePanic(h.logger, &err) + + frontendReq := req.GetFrontendRequest() + + result, err := chasm.StartExecution[*Operation]( + ctx, + chasm.ExecutionKey{ + NamespaceID: req.GetNamespaceId(), + BusinessID: frontendReq.GetOperationId(), + }, + newStandaloneOperation, + req, + chasm.WithRequestID(frontendReq.GetRequestId()), + chasm.WithBusinessIDPolicy( + idReusePolicyFromProto(frontendReq.GetIdReusePolicy()), + idConflictPolicyFromProto(frontendReq.GetIdConflictPolicy()), + ), + ) + if err != nil { + return nil, err + } + + return &nexusoperationpb.StartNexusOperationResponse{ + FrontendResponse: &workflowservice.StartNexusOperationExecutionResponse{ + RunId: result.ExecutionKey.RunID, + Started: result.Created, + }, + }, nil +} + +// TODO: Add long-poll support. +func (h *handler) DescribeNexusOperation( + ctx context.Context, + req *nexusoperationpb.DescribeNexusOperationRequest, +) (response *nexusoperationpb.DescribeNexusOperationResponse, err error) { + defer log.CapturePanic(h.logger, &err) + + ref := chasm.NewComponentRef[*Operation](chasm.ExecutionKey{ + NamespaceID: req.GetNamespaceId(), + BusinessID: req.GetFrontendRequest().GetOperationId(), + RunID: req.GetFrontendRequest().GetRunId(), + }) + + return chasm.ReadComponent(ctx, ref, (*Operation).buildDescribeResponse, req, nil) +} + +func idReusePolicyFromProto(p enumspb.NexusOperationIdReusePolicy) chasm.BusinessIDReusePolicy { + switch p { + case enumspb.NEXUS_OPERATION_ID_REUSE_POLICY_ALLOW_DUPLICATE_FAILED_ONLY: + return chasm.BusinessIDReusePolicyAllowDuplicateFailedOnly + case enumspb.NEXUS_OPERATION_ID_REUSE_POLICY_REJECT_DUPLICATE: + return chasm.BusinessIDReusePolicyRejectDuplicate + default: + return chasm.BusinessIDReusePolicyAllowDuplicate + } +} + +func idConflictPolicyFromProto(p enumspb.NexusOperationIdConflictPolicy) chasm.BusinessIDConflictPolicy { + switch p { + case enumspb.NEXUS_OPERATION_ID_CONFLICT_POLICY_USE_EXISTING: + return chasm.BusinessIDConflictPolicyUseExisting + default: + return chasm.BusinessIDConflictPolicyFail + } +} diff --git a/chasm/lib/nexusoperation/library.go b/chasm/lib/nexusoperation/library.go index 7c9873a925..002c2368eb 100644 --- a/chasm/lib/nexusoperation/library.go +++ b/chasm/lib/nexusoperation/library.go @@ -2,11 +2,40 @@ package nexusoperation import ( "go.temporal.io/server/chasm" + nexusoperationpb "go.temporal.io/server/chasm/lib/nexusoperation/gen/nexusoperationpb/v1" "google.golang.org/grpc" ) -type Library struct { +// componentOnlyLibrary registers just the components without task executors or gRPC handlers. +// Used in the frontend to enable component ref serialization. +type componentOnlyLibrary struct { chasm.UnimplementedLibrary +} + +func (l *componentOnlyLibrary) Name() string { + return "nexusoperation" +} + +func (l *componentOnlyLibrary) Components() []*chasm.RegistrableComponent { + return []*chasm.RegistrableComponent{ + chasm.NewRegistrableComponent[*Operation]( + "operation", + chasm.WithSearchAttributes( + EndpointSearchAttribute, + ServiceSearchAttribute, + OperationSearchAttribute, + StatusSearchAttribute, + ), + chasm.WithBusinessIDAlias("OperationId"), + ), + chasm.NewRegistrableComponent[*Cancellation]("cancellation"), + } +} + +type Library struct { + componentOnlyLibrary + + handler *handler operationBackoffTaskHandler *operationBackoffTaskHandler operationInvocationTaskHandler *operationInvocationTaskHandler @@ -19,6 +48,7 @@ type Library struct { } func newLibrary( + handler *handler, operationBackoffTaskHandler *operationBackoffTaskHandler, operationInvocationTaskHandler *operationInvocationTaskHandler, operationScheduleToCloseTimeoutTaskHandler *operationScheduleToCloseTimeoutTaskHandler, @@ -28,6 +58,7 @@ func newLibrary( cancellationBackoffTaskHandler *cancellationBackoffTaskHandler, ) *Library { return &Library{ + handler: handler, operationBackoffTaskHandler: operationBackoffTaskHandler, operationInvocationTaskHandler: operationInvocationTaskHandler, operationScheduleToCloseTimeoutTaskHandler: operationScheduleToCloseTimeoutTaskHandler, @@ -38,17 +69,6 @@ func newLibrary( } } -func (l *Library) Name() string { - return "nexusoperation" -} - -func (l *Library) Components() []*chasm.RegistrableComponent { - return []*chasm.RegistrableComponent{ - chasm.NewRegistrableComponent[*Operation]("operation"), - chasm.NewRegistrableComponent[*Cancellation]("cancellation"), - } -} - func (l *Library) Tasks() []*chasm.RegistrableTask { return []*chasm.RegistrableTask{ chasm.NewRegistrableSideEffectTask("invocation", l.operationInvocationTaskHandler), @@ -61,5 +81,6 @@ func (l *Library) Tasks() []*chasm.RegistrableTask { } } -func (l *Library) RegisterServices(_ *grpc.Server) { +func (l *Library) RegisterServices(server *grpc.Server) { + server.RegisterService(&nexusoperationpb.NexusOperationService_ServiceDesc, l.handler) } diff --git a/chasm/lib/nexusoperation/operation.go b/chasm/lib/nexusoperation/operation.go index 8c969e659b..2d08c1434d 100644 --- a/chasm/lib/nexusoperation/operation.go +++ b/chasm/lib/nexusoperation/operation.go @@ -3,18 +3,33 @@ package nexusoperation import ( "time" + "github.com/google/uuid" "github.com/nexus-rpc/sdk-go/nexus" commonpb "go.temporal.io/api/common/v1" + enumspb "go.temporal.io/api/enums/v1" failurepb "go.temporal.io/api/failure/v1" + nexuspb "go.temporal.io/api/nexus/v1" "go.temporal.io/api/serviceerror" + "go.temporal.io/api/workflowservice/v1" "go.temporal.io/server/chasm" nexusoperationpb "go.temporal.io/server/chasm/lib/nexusoperation/gen/nexusoperationpb/v1" "go.temporal.io/server/common/backoff" "google.golang.org/protobuf/types/known/anypb" + "google.golang.org/protobuf/types/known/durationpb" "google.golang.org/protobuf/types/known/timestamppb" ) +var ( + EndpointSearchAttribute = chasm.NewSearchAttributeKeyword("Endpoint", chasm.SearchAttributeFieldKeyword01) + ServiceSearchAttribute = chasm.NewSearchAttributeKeyword("Service", chasm.SearchAttributeFieldKeyword02) + OperationSearchAttribute = chasm.NewSearchAttributeKeyword("Operation", chasm.SearchAttributeFieldKeyword03) + StatusSearchAttribute = chasm.NewSearchAttributeKeyword("ExecutionStatus", chasm.SearchAttributeFieldLowCardinalityKeyword01) +) + +var _ chasm.Component = (*Operation)(nil) +var _ chasm.RootComponent = (*Operation)(nil) var _ chasm.StateMachine[nexusoperationpb.OperationStatus] = (*Operation)(nil) +var _ chasm.VisibilitySearchAttributesProvider = (*Operation)(nil) // ErrCancellationAlreadyRequested is returned when a cancellation has already been requested for an operation. var ErrCancellationAlreadyRequested = serviceerror.NewFailedPrecondition("cancellation already requested") @@ -24,16 +39,12 @@ var ErrOperationAlreadyCompleted = serviceerror.NewFailedPrecondition("operation // InvocationData contains data needed to invoke a Nexus operation. type InvocationData struct { - // Input is the operation input payload. - Input *commonpb.Payload - // Header contains the Nexus headers for the operation. - Header map[string]string - // NexusLink is the link to the caller that scheduled this operation. + Input *commonpb.Payload + Header map[string]string NexusLink nexus.Link } // OperationStore defines the interface that must be implemented by any parent component that wants to manage Nexus operations. -// It's the responsibility of the parrent component to apply the appropriate state transitions to the operation. type OperationStore interface { OnNexusOperationStarted(ctx chasm.MutableContext, operation *Operation, operationToken string, links []*commonpb.Link) error OnNexusOperationCanceled(ctx chasm.MutableContext, operation *Operation, cause *failurepb.Failure) error @@ -49,25 +60,50 @@ type OperationStore interface { // Operation is a CHASM component that represents a Nexus operation. type Operation struct { chasm.UnimplementedComponent - - // Persisted internal state *nexusoperationpb.OperationState - // Pointer to an implementation of the "store". For a workflow-based Nexus operation - // this is a parent pointer back to the workflow. For a standalone Nexus operation this is nil. Store chasm.ParentPtr[OperationStore] - // Cancellation is a child component that manages sending the cancel request to the Nexus endpoint. - // Created when cancelation is requested, nil otherwise. + // Only used for standalone Nexus operations. Workflow operations keep request data in history. + RequestData chasm.Field[*nexusoperationpb.OperationRequestData] Cancellation chasm.Field[*Cancellation] + Visibility chasm.Field[*chasm.Visibility] } -// NewOperation creates a new Operation component with the given persisted state. func NewOperation(state *nexusoperationpb.OperationState) *Operation { return &Operation{OperationState: state} } -// LifecycleState maps the operation's status to a CHASM lifecycle state. +func newStandaloneOperation( + ctx chasm.MutableContext, + req *nexusoperationpb.StartNexusOperationRequest, +) (*Operation, error) { + frontendReq := req.GetFrontendRequest() + op := NewOperation(&nexusoperationpb.OperationState{ + Endpoint: frontendReq.GetEndpoint(), + Service: frontendReq.GetService(), + Operation: frontendReq.GetOperation(), + ScheduleToCloseTimeout: frontendReq.GetScheduleToCloseTimeout(), + ScheduledTime: timestamppb.New(ctx.Now(nil)), + RequestId: uuid.NewString(), + }) + op.RequestData = chasm.NewDataField(ctx, &nexusoperationpb.OperationRequestData{ + Input: frontendReq.GetInput(), + NexusHeader: frontendReq.GetNexusHeader(), + UserMetadata: frontendReq.GetUserMetadata(), + Identity: frontendReq.GetIdentity(), + }) + op.Visibility = chasm.NewComponentField(ctx, chasm.NewVisibilityWithData( + ctx, + frontendReq.GetSearchAttributes().GetIndexedFields(), + nil, + )) + if err := TransitionScheduled.Apply(op, ctx, EventScheduled{}); err != nil { + return nil, err + } + return op, nil +} + func (o *Operation) LifecycleState(_ chasm.Context) chasm.LifecycleState { switch o.Status { case nexusoperationpb.OPERATION_STATUS_SUCCEEDED: @@ -81,19 +117,18 @@ func (o *Operation) LifecycleState(_ chasm.Context) chasm.LifecycleState { } } -// StateMachineState returns the current operation status. +func (o *Operation) ContextMetadata(_ chasm.Context) map[string]string { + return nil +} + func (o *Operation) StateMachineState() nexusoperationpb.OperationStatus { return o.Status } -// SetStateMachineState sets the operation status. func (o *Operation) SetStateMachineState(status nexusoperationpb.OperationStatus) { o.Status = status } -// Cancel requests cancellation of the operation. It creates a Cancellation child component and, if the -// operation has already started, schedules the cancellation request to be sent to the Nexus endpoint. -// parentData is opaque data injected by the parent (e.g. workflow) for its own bookkeeping. func (o *Operation) Cancel(ctx chasm.MutableContext, parentData *anypb.Any) error { if !TransitionCanceled.Possible(o) { return ErrOperationAlreadyCompleted @@ -108,40 +143,30 @@ func (o *Operation) Cancel(ctx chasm.MutableContext, parentData *anypb.Any) erro }) o.Cancellation = chasm.NewComponentField(ctx, cancellation) - // Once started, the handler returns a token that can be used in the cancelation request. - // Until then, no need to schedule the cancelation. if o.Status == nexusoperationpb.OPERATION_STATUS_STARTED { return TransitionCancellationScheduled.Apply(cancellation, ctx, EventCancellationScheduled{ Destination: o.GetEndpoint(), }) } - return nil } -// onStarted applies the started transition or delegates to the store if one is present. func (o *Operation) onStarted(ctx chasm.MutableContext, operationToken string, links []*commonpb.Link) error { store, ok := o.Store.TryGet(ctx) if ok { return store.OnNexusOperationStarted(ctx, o, operationToken, links) } - // TODO(stephan): for standalone, store links - return TransitionStarted.Apply(o, ctx, EventStarted{ - OperationToken: operationToken, - }) + return TransitionStarted.Apply(o, ctx, EventStarted{OperationToken: operationToken}) } -// onCompleted applies the succeeded transition or delegates to the store if one is present. func (o *Operation) onCompleted(ctx chasm.MutableContext, result *commonpb.Payload, links []*commonpb.Link) error { store, ok := o.Store.TryGet(ctx) if ok { return store.OnNexusOperationCompleted(ctx, o, result, links) } - // TODO(stephan): for standalone, store result and links return TransitionSucceeded.Apply(o, ctx, EventSucceeded{}) } -// onFailed applies the failed transition or delegates to the store if one is present. func (o *Operation) onFailed(ctx chasm.MutableContext, cause *failurepb.Failure) error { store, ok := o.Store.TryGet(ctx) if ok { @@ -150,7 +175,6 @@ func (o *Operation) onFailed(ctx chasm.MutableContext, cause *failurepb.Failure) return TransitionFailed.Apply(o, ctx, EventFailed{Failure: cause}) } -// onCanceled applies the canceled transition or delegates to the store if one is present. func (o *Operation) onCanceled(ctx chasm.MutableContext, cause *failurepb.Failure) error { store, ok := o.Store.TryGet(ctx) if ok { @@ -159,31 +183,35 @@ func (o *Operation) onCanceled(ctx chasm.MutableContext, cause *failurepb.Failur return TransitionCanceled.Apply(o, ctx, EventCanceled{Failure: cause}) } -// onTimedOut applies the timed out transition or delegates to the store if one is present. func (o *Operation) onTimedOut(ctx chasm.MutableContext, cause *failurepb.Failure) error { store, ok := o.Store.TryGet(ctx) if ok { return store.OnNexusOperationTimedOut(ctx, o, cause) } - // TODO(stephan): for standalone, store failure + _ = cause return TransitionTimedOut.Apply(o, ctx, EventTimedOut{}) } -// loadStartArgs is a ReadComponent callback that loads the start arguments from the operation. func (o *Operation) loadStartArgs( ctx chasm.Context, _ chasm.NoValue, ) (startArgs, error) { - var invocationData InvocationData - + var ( + invocationData InvocationData + err error + ) if store, ok := o.Store.TryGet(ctx); ok { - var err error invocationData, err = store.NexusOperationInvocationData(ctx, o) if err != nil { return startArgs{}, err } + } else { + requestData := o.RequestData.Get(ctx) + invocationData = InvocationData{ + Input: requestData.GetInput(), + Header: requestData.GetNexusHeader(), + } } - // TODO(stephan): for standalone, populate the invocationData fields. serializedRef, err := ctx.Ref(o) if err != nil { @@ -208,9 +236,8 @@ func (o *Operation) loadStartArgs( }, nil } -// saveInvocationResultInput is the input to the Operation.saveResult method used in UpdateComponent. type saveInvocationResultInput struct { - result startResult + result invocationResult retryPolicy backoff.RetryPolicy } @@ -219,19 +246,19 @@ func (o *Operation) saveInvocationResult( input saveInvocationResultInput, ) (chasm.NoValue, error) { switch r := input.result.(type) { - case startResultOK: + case invocationResultOK: links := convertResponseLinks(r.response.Links, ctx.Logger()) if r.response.Pending != nil { return nil, o.onStarted(ctx, r.response.Pending.Token, links) } return nil, o.onCompleted(ctx, r.response.Successful, links) - case startResultCancel: + case invocationResultCancel: return nil, o.onCanceled(ctx, r.failure) - case startResultFail: + case invocationResultFail: return nil, o.onFailed(ctx, r.failure) - case startResultTimeout: + case invocationResultTimeout: return nil, o.onTimedOut(ctx, r.failure) - case startResultRetry: + case invocationResultRetry: return nil, transitionAttemptFailed.Apply(o, ctx, EventAttemptFailed{ Failure: r.failure, RetryPolicy: input.retryPolicy, @@ -242,21 +269,127 @@ func (o *Operation) saveInvocationResult( } func (o *Operation) resolveUnsuccessfully(ctx chasm.MutableContext, failure *failurepb.Failure, closeTime time.Time) error { - // When we transition from scheduled to failed it is always due to the attempt failing with a non - // retryable failure. The failure should be recorded in the state for standalone Nexus operations. - // Workflow operations will be deleted immediately after this transition leaving no trace of the failure - // object. if o.GetStatus() == nexusoperationpb.OPERATION_STATUS_SCHEDULED { o.LastAttemptCompleteTime = timestamppb.New(ctx.Now(o)) o.LastAttemptFailure = failure } - // TODO(stephan): store failure when unsuccessful outside of an attempt. - o.ClosedTime = timestamppb.New(closeTime) - - // Clear the next attempt schedule time when leaving BACKING_OFF state. This field is only valid in - // BACKING_OFF state. o.NextAttemptScheduleTime = nil - // Terminal state - no tasks to emit. return nil } + +func (o *Operation) Terminate(_ chasm.MutableContext, _ chasm.TerminateComponentRequest) (chasm.TerminateComponentResponse, error) { + return chasm.TerminateComponentResponse{}, serviceerror.NewUnimplemented("not implemented") +} + +func (o *Operation) SearchAttributes(_ chasm.Context) []chasm.SearchAttributeKeyValue { + return []chasm.SearchAttributeKeyValue{ + EndpointSearchAttribute.Value(o.Endpoint), + ServiceSearchAttribute.Value(o.Service), + OperationSearchAttribute.Value(o.Operation), + StatusSearchAttribute.Value(operationExecutionStatus(o.Status).String()), + } +} + +func (o *Operation) buildDescribeResponse( + ctx chasm.Context, + req *nexusoperationpb.DescribeNexusOperationRequest, +) (*nexusoperationpb.DescribeNexusOperationResponse, error) { + var input *commonpb.Payload + if req.GetFrontendRequest().GetIncludeInput() { + input = o.RequestData.Get(ctx).GetInput() + } + + return &nexusoperationpb.DescribeNexusOperationResponse{ + FrontendResponse: &workflowservice.DescribeNexusOperationExecutionResponse{ + RunId: ctx.ExecutionKey().RunID, + Info: o.buildExecutionInfo(ctx), + Input: input, + }, + }, nil +} + +func (o *Operation) buildExecutionInfo(ctx chasm.Context) *nexuspb.NexusOperationExecutionInfo { + requestData := o.RequestData.Get(ctx) + key := ctx.ExecutionKey() + info := &nexuspb.NexusOperationExecutionInfo{ + OperationId: key.BusinessID, + RunId: key.RunID, + Endpoint: o.Endpoint, + Service: o.Service, + Operation: o.Operation, + Status: operationExecutionStatus(o.Status), + State: pendingOperationState(o.Status), + ScheduleToCloseTimeout: o.ScheduleToCloseTimeout, + ScheduleToStartTimeout: o.ScheduleToStartTimeout, + StartToCloseTimeout: o.StartToCloseTimeout, + Attempt: o.Attempt, + ScheduleTime: o.ScheduledTime, + LastAttemptCompleteTime: o.LastAttemptCompleteTime, + LastAttemptFailure: o.LastAttemptFailure, + NextAttemptScheduleTime: o.NextAttemptScheduleTime, + RequestId: o.RequestId, + OperationToken: o.OperationToken, + StateTransitionCount: ctx.ExecutionInfo().StateTransitionCount, + SearchAttributes: &commonpb.SearchAttributes{ + IndexedFields: o.Visibility.Get(ctx).CustomSearchAttributes(ctx), + }, + NexusHeader: requestData.GetNexusHeader(), + UserMetadata: requestData.GetUserMetadata(), + Identity: requestData.GetIdentity(), + } + + if o.ScheduledTime != nil { + if o.ScheduleToCloseTimeout != nil { + info.ExpirationTime = timestamppb.New(o.ScheduledTime.AsTime().Add(o.ScheduleToCloseTimeout.AsDuration())) + } + + if closeTime := o.closeTime(ctx); closeTime != nil { + info.CloseTime = closeTime + info.ExecutionDuration = durationpb.New(closeTime.AsTime().Sub(o.ScheduledTime.AsTime())) + } else { + info.ExecutionDuration = durationpb.New(ctx.Now(o).Sub(o.ScheduledTime.AsTime())) + } + } + + return info +} + +func (o *Operation) closeTime(ctx chasm.Context) *timestamppb.Timestamp { + if !o.LifecycleState(ctx).IsClosed() { + return nil + } + return o.LastAttemptCompleteTime +} + +func operationExecutionStatus(status nexusoperationpb.OperationStatus) enumspb.NexusOperationExecutionStatus { + switch status { + case nexusoperationpb.OPERATION_STATUS_SCHEDULED, + nexusoperationpb.OPERATION_STATUS_BACKING_OFF, + nexusoperationpb.OPERATION_STATUS_STARTED: + return enumspb.NEXUS_OPERATION_EXECUTION_STATUS_RUNNING + case nexusoperationpb.OPERATION_STATUS_SUCCEEDED: + return enumspb.NEXUS_OPERATION_EXECUTION_STATUS_COMPLETED + case nexusoperationpb.OPERATION_STATUS_FAILED: + return enumspb.NEXUS_OPERATION_EXECUTION_STATUS_FAILED + case nexusoperationpb.OPERATION_STATUS_CANCELED: + return enumspb.NEXUS_OPERATION_EXECUTION_STATUS_CANCELED + case nexusoperationpb.OPERATION_STATUS_TIMED_OUT: + return enumspb.NEXUS_OPERATION_EXECUTION_STATUS_TIMED_OUT + default: + return enumspb.NEXUS_OPERATION_EXECUTION_STATUS_UNSPECIFIED + } +} + +func pendingOperationState(status nexusoperationpb.OperationStatus) enumspb.PendingNexusOperationState { + switch status { + case nexusoperationpb.OPERATION_STATUS_SCHEDULED: + return enumspb.PENDING_NEXUS_OPERATION_STATE_SCHEDULED + case nexusoperationpb.OPERATION_STATUS_BACKING_OFF: + return enumspb.PENDING_NEXUS_OPERATION_STATE_BACKING_OFF + case nexusoperationpb.OPERATION_STATUS_STARTED: + return enumspb.PENDING_NEXUS_OPERATION_STATE_STARTED + default: + return enumspb.PENDING_NEXUS_OPERATION_STATE_UNSPECIFIED + } +} diff --git a/chasm/lib/nexusoperation/operation_executors.go b/chasm/lib/nexusoperation/operation_executors.go new file mode 100644 index 0000000000..ad3c2e9d2b --- /dev/null +++ b/chasm/lib/nexusoperation/operation_executors.go @@ -0,0 +1,192 @@ +package nexusoperation + +import ( + "context" + + "go.temporal.io/api/serviceerror" + "go.temporal.io/server/chasm" + nexusoperationpb "go.temporal.io/server/chasm/lib/nexusoperation/gen/nexusoperationpb/v1" + "go.temporal.io/server/common/log" + "go.temporal.io/server/common/metrics" + "go.uber.org/fx" +) + +// OperationTaskExecutorOptions is the fx parameter object for common options supplied to all operation task executors. +type OperationTaskExecutorOptions struct { + fx.In + + Config *Config + + MetricsHandler metrics.Handler + Logger log.Logger +} + +type OperationInvocationTaskExecutor struct { + config *Config + + metricsHandler metrics.Handler + logger log.Logger +} + +func NewOperationInvocationTaskExecutor(opts OperationTaskExecutorOptions) *OperationInvocationTaskExecutor { + return &OperationInvocationTaskExecutor{ + config: opts.Config, + metricsHandler: opts.MetricsHandler, + logger: opts.Logger, + } +} + +func (e *OperationInvocationTaskExecutor) Validate( + ctx chasm.Context, + op *Operation, + attrs chasm.TaskAttributes, + task *nexusoperationpb.InvocationTask, +) (bool, error) { + // TODO: implement + return false, nil +} + +func (e *OperationInvocationTaskExecutor) Execute( + ctx context.Context, + opRef chasm.ComponentRef, + attrs chasm.TaskAttributes, + task *nexusoperationpb.InvocationTask, +) error { + return serviceerror.NewUnimplemented("unimplemented") +} + +type OperationBackoffTaskExecutor struct { + config *Config + + metricsHandler metrics.Handler + logger log.Logger +} + +func NewOperationBackoffTaskExecutor(opts OperationTaskExecutorOptions) *OperationBackoffTaskExecutor { + return &OperationBackoffTaskExecutor{ + config: opts.Config, + metricsHandler: opts.MetricsHandler, + logger: opts.Logger, + } +} + +func (e *OperationBackoffTaskExecutor) Validate( + ctx chasm.Context, + op *Operation, + attrs chasm.TaskAttributes, + task *nexusoperationpb.InvocationBackoffTask, +) (bool, error) { + // TODO: implement + return false, nil +} + +func (e *OperationBackoffTaskExecutor) Execute( + ctx chasm.MutableContext, + op *Operation, + attrs chasm.TaskAttributes, + task *nexusoperationpb.InvocationBackoffTask, +) error { + return serviceerror.NewUnimplemented("unimplemented") +} + +type OperationScheduleToStartTimeoutTaskExecutor struct { + config *Config + + metricsHandler metrics.Handler + logger log.Logger +} + +func NewOperationScheduleToStartTimeoutTaskExecutor(opts OperationTaskExecutorOptions) *OperationScheduleToStartTimeoutTaskExecutor { + return &OperationScheduleToStartTimeoutTaskExecutor{ + config: opts.Config, + metricsHandler: opts.MetricsHandler, + logger: opts.Logger, + } +} + +func (e *OperationScheduleToStartTimeoutTaskExecutor) Validate( + ctx chasm.Context, + op *Operation, + attrs chasm.TaskAttributes, + task *nexusoperationpb.ScheduleToStartTimeoutTask, +) (bool, error) { + // TODO: implement + return false, nil +} + +func (e *OperationScheduleToStartTimeoutTaskExecutor) Execute( + ctx chasm.MutableContext, + op *Operation, + attrs chasm.TaskAttributes, + task *nexusoperationpb.ScheduleToStartTimeoutTask, +) error { + return serviceerror.NewUnimplemented("unimplemented") +} + +type OperationStartToCloseTimeoutTaskExecutor struct { + config *Config + + metricsHandler metrics.Handler + logger log.Logger +} + +func NewOperationStartToCloseTimeoutTaskExecutor(opts OperationTaskExecutorOptions) *OperationStartToCloseTimeoutTaskExecutor { + return &OperationStartToCloseTimeoutTaskExecutor{ + config: opts.Config, + metricsHandler: opts.MetricsHandler, + logger: opts.Logger, + } +} + +func (e *OperationStartToCloseTimeoutTaskExecutor) Validate( + ctx chasm.Context, + op *Operation, + attrs chasm.TaskAttributes, + task *nexusoperationpb.StartToCloseTimeoutTask, +) (bool, error) { + // TODO: implement + return false, nil +} + +func (e *OperationStartToCloseTimeoutTaskExecutor) Execute( + ctx chasm.MutableContext, + op *Operation, + attrs chasm.TaskAttributes, + task *nexusoperationpb.StartToCloseTimeoutTask, +) error { + return serviceerror.NewUnimplemented("unimplemented") +} + +type OperationScheduleToCloseTimeoutTaskExecutor struct { + config *Config + + metricsHandler metrics.Handler + logger log.Logger +} + +func NewOperationScheduleToCloseTimeoutTaskExecutor(opts OperationTaskExecutorOptions) *OperationScheduleToCloseTimeoutTaskExecutor { + return &OperationScheduleToCloseTimeoutTaskExecutor{ + config: opts.Config, + metricsHandler: opts.MetricsHandler, + logger: opts.Logger, + } +} + +func (e *OperationScheduleToCloseTimeoutTaskExecutor) Validate( + ctx chasm.Context, + op *Operation, + attrs chasm.TaskAttributes, + task *nexusoperationpb.ScheduleToCloseTimeoutTask, +) (bool, error) { + // TODO: implement + return false, nil +} + +func (e *OperationScheduleToCloseTimeoutTaskExecutor) Execute( + ctx chasm.MutableContext, + op *Operation, + attrs chasm.TaskAttributes, + task *nexusoperationpb.ScheduleToCloseTimeoutTask, +) error { + return serviceerror.NewUnimplemented("unimplemented") +} diff --git a/chasm/lib/nexusoperation/operation_tasks.go b/chasm/lib/nexusoperation/operation_tasks.go index d35f296f01..5032932e74 100644 --- a/chasm/lib/nexusoperation/operation_tasks.go +++ b/chasm/lib/nexusoperation/operation_tasks.go @@ -4,6 +4,8 @@ import ( "context" "errors" "fmt" + "maps" + "sync/atomic" "time" "github.com/nexus-rpc/sdk-go/nexus" @@ -11,7 +13,7 @@ import ( enumspb "go.temporal.io/api/enums/v1" failurepb "go.temporal.io/api/failure/v1" "go.temporal.io/api/serviceerror" - tokenspb "go.temporal.io/server/api/token/v1" + persistencespb "go.temporal.io/server/api/persistence/v1" "go.temporal.io/server/chasm" nexusoperationpb "go.temporal.io/server/chasm/lib/nexusoperation/gen/nexusoperationpb/v1" "go.temporal.io/server/common/log" @@ -20,135 +22,64 @@ 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/common/resource" queueserrors "go.temporal.io/server/service/history/queues/errors" "go.uber.org/fx" ) -// startResult is a marker interface for the outcome of a Nexus start operation call. -type startResult interface { - mustImplementStartResult() -} - -// startResultOK indicates the operation completed synchronously or started asynchronously. -type startResultOK struct { - response *nexusrpc.ClientStartOperationResponse[*commonpb.Payload] - links []*commonpb.Link -} - -func (startResultOK) mustImplementStartResult() {} - -// startResultFail indicates a non-retryable failure. -type startResultFail struct { - failure *failurepb.Failure -} - -func (startResultFail) mustImplementStartResult() {} - -// startResultRetry indicates a retryable failure. -type startResultRetry struct { - failure *failurepb.Failure -} - -func (startResultRetry) mustImplementStartResult() {} - -// startResultCancel indicates the operation completed as canceled. -type startResultCancel struct { - failure *failurepb.Failure -} - -func (startResultCancel) mustImplementStartResult() {} - -// startResultTimeout indicates the operation timed out while attempting to invoke. -type startResultTimeout struct { - failure *failurepb.Failure -} - -func (startResultTimeout) mustImplementStartResult() {} - -func newStartResult( - response *nexusrpc.ClientStartOperationResponse[*commonpb.Payload], - callErr error, -) (startResult, error) { - if callErr == nil { - return startResultOK{response: response}, nil - } - - if opErr, ok := errors.AsType[*nexus.OperationError](callErr); ok { - failure, err := operationErrorToFailure(opErr) - if err != nil { - return nil, err - } - if opErr.State == nexus.OperationStateCanceled { - return startResultCancel{failure: failure}, nil - } - return startResultFail{failure: failure}, nil - } - - if opTimeoutBelowMinErr, ok := errors.AsType[*operationTimeoutBelowMinError](callErr); ok { - failure := &failurepb.Failure{ - Message: "operation timed out", - FailureInfo: &failurepb.Failure_TimeoutFailureInfo{ - TimeoutFailureInfo: &failurepb.TimeoutFailureInfo{ - TimeoutType: opTimeoutBelowMinErr.timeoutType, - }, - }, - } - return startResultTimeout{failure: failure}, nil - } - - failure, retryable, err := callErrorToFailure(callErr) - if err != nil { - return nil, err - } - if retryable { - return startResultRetry{failure: failure}, nil - } - return startResultFail{failure: failure}, nil -} +// operationTaskHandlerOptions is the fx parameter object for common options supplied to all operation task handlers. +type operationTaskHandlerOptions struct { + fx.In -// operationErrorToFailure converts a Nexus OperationError to the appropriate failure. -func operationErrorToFailure(opErr *nexus.OperationError) (*failurepb.Failure, error) { - var nf nexus.Failure - if opErr.OriginalFailure != nil { - nf = *opErr.OriginalFailure - } else { - var err error - nf, err = nexusrpc.DefaultFailureConverter().ErrorToFailure(opErr) - if err != nil { - return nil, err - } - } - // Special marker for Temporal->Temporal calls to indicate that the original failure should be unwrapped. - // Temporal uses a wrapper operation error with no additional information to transmit the OperationError over the network. - // The meaningful information is in the operation error's cause. - unwrapError := nf.Metadata["unwrap-error"] == "true" + Config *Config - if unwrapError && nf.Cause != nil { - return commonnexus.NexusFailureToTemporalFailure(*nf.Cause) - } - // Transform the OperationError to either ApplicationFailure or CanceledFailure based on the operation error state. - return commonnexus.NexusFailureToTemporalFailure(nf) + MetricsHandler metrics.Handler + Logger log.Logger } // operationInvocationTaskHandlerOptions is the fx parameter object for the invocation task executor. type operationInvocationTaskHandlerOptions struct { fx.In - InvocationTaskHandlerOptions + Config *Config + NamespaceRegistry namespace.Registry + MetricsHandler metrics.Handler + Logger log.Logger CallbackTokenGenerator *commonnexus.CallbackTokenGenerator + ClientProvider ClientProvider + EndpointRegistry commonnexus.EndpointRegistry + HTTPTraceProvider commonnexus.HTTPClientTraceProvider + HistoryClient resource.HistoryClient + ChasmRegistry *chasm.Registry } type operationInvocationTaskHandler struct { chasm.SideEffectTaskHandlerBase[*nexusoperationpb.InvocationTask] - nexusTaskHandlerBase + config *Config + namespaceRegistry namespace.Registry + metricsHandler metrics.Handler + logger log.Logger callbackTokenGenerator *commonnexus.CallbackTokenGenerator + clientProvider ClientProvider + endpointRegistry commonnexus.EndpointRegistry + httpTraceProvider commonnexus.HTTPClientTraceProvider + historyClient resource.HistoryClient + chasmRegistry *chasm.Registry } func newOperationInvocationTaskHandler(opts operationInvocationTaskHandlerOptions) *operationInvocationTaskHandler { return &operationInvocationTaskHandler{ - nexusTaskHandlerBase: opts.toBase(), + config: opts.Config, + namespaceRegistry: opts.NamespaceRegistry, + metricsHandler: opts.MetricsHandler, + logger: opts.Logger, callbackTokenGenerator: opts.CallbackTokenGenerator, + clientProvider: opts.ClientProvider, + endpointRegistry: opts.EndpointRegistry, + httpTraceProvider: opts.HTTPTraceProvider, + historyClient: opts.HistoryClient, + chasmRegistry: opts.ChasmRegistry, } } @@ -178,12 +109,12 @@ func (h *operationInvocationTaskHandler) Execute( return err } - endpoint, err := h.lookupEndpoint(ctx, ns.ID(), args.endpointID, args.endpointName) + endpoint, err := h.resolveEndpoint(ctx, ns, args) if err != nil { if _, ok := errors.AsType[*serviceerror.NotFound](err); ok { h.logger.Error("endpoint not found while processing invocation task", tag.Error(err)) handlerErr := nexus.NewHandlerErrorf(nexus.HandlerErrorTypeNotFound, "endpoint not registered") - result, err := newStartResult(nil, handlerErr) + result, err := newInvocationResult(nil, handlerErr) if err != nil { return fmt.Errorf("failed to construct invocation result: %w", err) } @@ -196,7 +127,7 @@ func (h *operationInvocationTaskHandler) Execute( return err } - callbackURL, err := h.buildCallbackURL(ns, endpoint) + callbackURL, err := buildCallbackURL(h.config.UseSystemCallbackURL(), h.config.CallbackURLTemplate(), ns, endpoint) if err != nil { return fmt.Errorf("failed to build callback URL: %w", err) } @@ -212,15 +143,11 @@ func (h *operationInvocationTaskHandler) Execute( // Adjust timeout based on remaining operation timeouts. // ScheduleToStart takes precedence over ScheduleToClose since it is already capped by it. if args.scheduleToStartTimeout > 0 { - if t := args.scheduleToStartTimeout - elapsed; t < callTimeout { - callTimeout = t - timeoutType = enumspb.TIMEOUT_TYPE_SCHEDULE_TO_START - } + callTimeout = min(callTimeout, args.scheduleToStartTimeout-elapsed) + timeoutType = enumspb.TIMEOUT_TYPE_SCHEDULE_TO_START } else if args.scheduleToCloseTimeout > 0 { - if t := args.scheduleToCloseTimeout - elapsed; t < callTimeout { - callTimeout = t - timeoutType = enumspb.TIMEOUT_TYPE_SCHEDULE_TO_CLOSE - } + callTimeout = min(callTimeout, args.scheduleToCloseTimeout-elapsed) + timeoutType = enumspb.TIMEOUT_TYPE_SCHEDULE_TO_CLOSE } // Inform the handler of the operation timeout via header. @@ -232,10 +159,7 @@ func (h *operationInvocationTaskHandler) Execute( if args.scheduleToCloseTimeout > 0 { opTimeout = min(args.scheduleToCloseTimeout-elapsed, opTimeout) } - header := nexus.Header(args.header) - if header == nil { - header = make(nexus.Header, 2) // To set the failure support and timeout headers. - } + header := buildRequestHeader(args.header) // Set the operation timeout header if not already set. if opTimeoutHeader := header.Get(nexus.HeaderOperationTimeout); opTimeout != maxDuration && opTimeoutHeader == "" { header.Set(nexus.HeaderOperationTimeout, commonnexus.FormatDuration(opTimeout)) @@ -245,8 +169,11 @@ func (h *operationInvocationTaskHandler) Execute( header.Set(nexusrpc.HeaderTemporalNexusFailureSupport, "true") } - callCtx, cancel := h.setupCallContext(ctx, callTimeout) + callCtx, cancel := context.WithTimeout(ctx, callTimeout) defer cancel() + // Set this value on the parent context so that our custom HTTP caller can mutate it since we cannot + // access response headers directly. + callCtx = context.WithValue(callCtx, commonnexus.FailureSourceContextKey, &atomic.Value{}) options := nexus.StartOperationOptions{ Header: header, @@ -258,21 +185,7 @@ func (h *operationInvocationTaskHandler) Execute( Links: []nexus.Link{args.nexusLink}, } - invocation, err := h.newInvocation( - callCtx, ns, endpoint, args.endpointName, args.service, - callTimeout, timeoutType, - invocationTraceContext{ - operationTag: "StartOperation", - namespaceName: ns.Name().String(), - requestID: args.requestID, - operation: args.operation, - endpointName: args.endpointName, - workflowID: opRef.BusinessID, - runID: opRef.RunID, - attemptStart: args.currentTime.UTC(), - attempt: task.GetAttempt(), - }, - ) + invocation, err := h.newInvocation(callCtx, ns, endpoint, opRef, args, task, callTimeout, timeoutType) if err != nil { return fmt.Errorf("failed to construct invocation: %w", err) } @@ -284,9 +197,9 @@ func (h *operationInvocationTaskHandler) Execute( } failureSource := failureSourceFromContext(callCtx) - h.recordCallOutcome(ns, endpoint, args.endpointName, "StartOperation", startCallOutcomeTag(callCtx, response, callErr), callErr, callDuration, failureSource) + h.recordStartCallOutcome(callCtx, ns, endpoint, args, response, callErr, callDuration, failureSource) - result, err := newStartResult(response, callErr) + result, err := newInvocationResult(response, callErr) if err != nil { return fmt.Errorf("failed to construct invocation result: %w", err) } @@ -302,6 +215,74 @@ func (h *operationInvocationTaskHandler) Execute( return saveErr } +func buildRequestHeader(header map[string]string) nexus.Header { + if header == nil { + return make(nexus.Header, 2) // To set the failure support and timeout headers. + } + return nexus.Header(maps.Clone(header)) +} + +func (h *operationInvocationTaskHandler) resolveEndpoint( + ctx context.Context, + ns *namespace.Namespace, + args startArgs, +) (*persistencespb.NexusEndpointEntry, error) { + // Skip endpoint lookup for system-internal operations. + if args.endpointName == commonnexus.SystemEndpoint { + return nil, nil + } + // This happens when we accept the ScheduleNexusOperation command when the endpoint is not found in the + // registry as indicated by the EndpointNotFoundAlwaysNonRetryable dynamic config. + // The config has been removed but we keep this check for backward compatibility. + if args.endpointID == "" { + return nil, nexus.NewHandlerErrorf(nexus.HandlerErrorTypeNotFound, "endpoint not registered") + } + return lookupEndpoint(ctx, h.endpointRegistry, ns.ID(), args.endpointID, args.endpointName) +} + +func (h *operationInvocationTaskHandler) newInvocation( + ctx context.Context, + ns *namespace.Namespace, + endpoint *persistencespb.NexusEndpointEntry, + opRef chasm.ComponentRef, + args startArgs, + task *nexusoperationpb.InvocationTask, + callTimeout time.Duration, + timeoutType enumspb.TimeoutType, +) (invocation, error) { + base := nexusTaskHandlerBase{ + config: h.config, + namespaceRegistry: h.namespaceRegistry, + metricsHandler: h.metricsHandler, + logger: h.logger, + clientProvider: h.clientProvider, + endpointRegistry: h.endpointRegistry, + httpTraceProvider: h.httpTraceProvider, + historyClient: h.historyClient, + chasmRegistry: h.chasmRegistry, + } + return base.newInvocation( + ctx, + ns, + endpoint, + args.endpointName, + args.service, + callTimeout, + timeoutType, + invocationTraceContext{ + operationTag: "StartOperation", + namespaceName: ns.Name().String(), + requestID: args.requestID, + operation: args.operation, + endpointName: args.endpointName, + workflowID: opRef.BusinessID, + runID: opRef.RunID, + attemptStart: args.currentTime.UTC(), + attempt: task.GetAttempt(), + }, + ) +} + func (h *operationInvocationTaskHandler) validateStartResult( ns *namespace.Namespace, result *nexusrpc.ClientStartOperationResponse[*commonpb.Payload], @@ -319,19 +300,37 @@ func (h *operationInvocationTaskHandler) validateStartResult( return nil } -// generateCallbackToken creates a callback token for the given operation reference. -func (h *operationInvocationTaskHandler) generateCallbackToken( - serializedRef []byte, - requestID string, -) (string, error) { - token, err := h.callbackTokenGenerator.Tokenize(&tokenspb.NexusOperationCompletion{ - ComponentRef: serializedRef, - RequestId: requestID, - }) - if err != nil { - return "", fmt.Errorf("%w: %w", queueserrors.NewUnprocessableTaskError("failed to generate a callback token"), err) +func (h *operationInvocationTaskHandler) recordStartCallOutcome( + callCtx context.Context, + ns *namespace.Namespace, + endpoint *persistencespb.NexusEndpointEntry, + args startArgs, + response *nexusrpc.ClientStartOperationResponse[*commonpb.Payload], + callErr error, + callDuration time.Duration, + failureSource string, +) { + methodTag := metrics.NexusMethodTag("StartOperation") + namespaceTag := metrics.NamespaceTag(ns.Name().String()) + var destTag metrics.Tag + if endpoint != nil { + destTag = metrics.DestinationTag(endpoint.Endpoint.Spec.GetName()) + } else { + destTag = metrics.DestinationTag(args.endpointName) + } + outcomeTag := metrics.OutcomeTag(startCallOutcomeTag(callCtx, response, callErr)) + failureSourceTag := metrics.FailureSourceTag(failureSource) + OutboundRequestCounter.With(h.metricsHandler).Record(1, namespaceTag, destTag, methodTag, outcomeTag, failureSourceTag) + OutboundRequestLatency.With(h.metricsHandler).Record(callDuration, namespaceTag, destTag, methodTag, outcomeTag, failureSourceTag) + + if callErr != nil { + _, isTimeoutBelowMin := errors.AsType[*operationTimeoutBelowMinError](callErr) + if failureSource == commonnexus.FailureSourceWorker || isTimeoutBelowMin { + h.logger.Debug("Nexus StartOperation request failed", tag.Error(callErr)) + } else { + h.logger.Error("Nexus StartOperation request failed", tag.Error(callErr)) + } } - return token, nil } type operationBackoffTaskHandler struct { @@ -342,7 +341,7 @@ type operationBackoffTaskHandler struct { logger log.Logger } -func newOperationBackoffTaskHandler(opts commonTaskHandlerOptions) *operationBackoffTaskHandler { +func newOperationBackoffTaskHandler(opts operationTaskHandlerOptions) *operationBackoffTaskHandler { return &operationBackoffTaskHandler{ config: opts.Config, metricsHandler: opts.MetricsHandler, @@ -376,7 +375,7 @@ type operationScheduleToStartTimeoutTaskHandler struct { logger log.Logger } -func newOperationScheduleToStartTimeoutTaskHandler(opts commonTaskHandlerOptions) *operationScheduleToStartTimeoutTaskHandler { +func newOperationScheduleToStartTimeoutTaskHandler(opts operationTaskHandlerOptions) *operationScheduleToStartTimeoutTaskHandler { return &operationScheduleToStartTimeoutTaskHandler{ config: opts.Config, metricsHandler: opts.MetricsHandler, @@ -417,7 +416,7 @@ type operationStartToCloseTimeoutTaskHandler struct { logger log.Logger } -func newOperationStartToCloseTimeoutTaskHandler(opts commonTaskHandlerOptions) *operationStartToCloseTimeoutTaskHandler { +func newOperationStartToCloseTimeoutTaskHandler(opts operationTaskHandlerOptions) *operationStartToCloseTimeoutTaskHandler { return &operationStartToCloseTimeoutTaskHandler{ config: opts.Config, metricsHandler: opts.MetricsHandler, @@ -458,7 +457,7 @@ type operationScheduleToCloseTimeoutTaskHandler struct { logger log.Logger } -func newOperationScheduleToCloseTimeoutTaskHandler(opts commonTaskHandlerOptions) *operationScheduleToCloseTimeoutTaskHandler { +func newOperationScheduleToCloseTimeoutTaskHandler(opts operationTaskHandlerOptions) *operationScheduleToCloseTimeoutTaskHandler { return &operationScheduleToCloseTimeoutTaskHandler{ config: opts.Config, metricsHandler: opts.MetricsHandler, diff --git a/chasm/lib/nexusoperation/operation_tasks_test.go b/chasm/lib/nexusoperation/operation_tasks_test.go index 192a236d3b..4556c43f02 100644 --- a/chasm/lib/nexusoperation/operation_tasks_test.go +++ b/chasm/lib/nexusoperation/operation_tasks_test.go @@ -74,25 +74,23 @@ func newInvocationTaskTestEnv( require.NoError(t, err) handler := &operationInvocationTaskHandler{ - nexusTaskHandlerBase: nexusTaskHandlerBase{ - config: &Config{ - RequestTimeout: dynamicconfig.GetDurationPropertyFnFilteredByDestination(requestTimeout), - MaxOperationTokenLength: dynamicconfig.GetIntPropertyFnFilteredByNamespace(10), - MinRequestTimeout: dynamicconfig.GetDurationPropertyFnFilteredByNamespace(time.Millisecond), - PayloadSizeLimit: dynamicconfig.GetIntPropertyFnFilteredByNamespace(2 * 1024 * 1024), - CallbackURLTemplate: dynamicconfig.GetTypedPropertyFn(callbackTmpl), - UseSystemCallbackURL: dynamicconfig.GetBoolPropertyFn(false), - UseNewFailureWireFormat: dynamicconfig.GetBoolPropertyFnFilteredByNamespace(true), - RetryPolicy: dynamicconfig.GetTypedPropertyFn[backoff.RetryPolicy]( - backoff.NewExponentialRetryPolicy(time.Second), - ), - }, - namespaceRegistry: nsRegistry, - metricsHandler: metricsHandler, - logger: log.NewNoopLogger(), - clientProvider: clientProvider, - endpointRegistry: endpointReg, + config: &Config{ + RequestTimeout: dynamicconfig.GetDurationPropertyFnFilteredByDestination(requestTimeout), + MaxOperationTokenLength: dynamicconfig.GetIntPropertyFnFilteredByNamespace(10), + MinRequestTimeout: dynamicconfig.GetDurationPropertyFnFilteredByNamespace(time.Millisecond), + PayloadSizeLimit: dynamicconfig.GetIntPropertyFnFilteredByNamespace(2 * 1024 * 1024), + CallbackURLTemplate: dynamicconfig.GetTypedPropertyFn(callbackTmpl), + UseSystemCallbackURL: dynamicconfig.GetBoolPropertyFn(false), + UseNewFailureWireFormat: dynamicconfig.GetBoolPropertyFnFilteredByNamespace(true), + RetryPolicy: dynamicconfig.GetTypedPropertyFn[backoff.RetryPolicy]( + backoff.NewExponentialRetryPolicy(time.Second), + ), }, + namespaceRegistry: nsRegistry, + metricsHandler: metricsHandler, + logger: log.NewNoopLogger(), + clientProvider: clientProvider, + endpointRegistry: endpointReg, callbackTokenGenerator: commonnexus.NewCallbackTokenGenerator(), } diff --git a/chasm/lib/nexusoperation/proto/v1/operation.proto b/chasm/lib/nexusoperation/proto/v1/operation.proto index 860f729688..f736b141e5 100644 --- a/chasm/lib/nexusoperation/proto/v1/operation.proto +++ b/chasm/lib/nexusoperation/proto/v1/operation.proto @@ -5,70 +5,77 @@ package temporal.server.chasm.lib.nexusoperation.proto.v1; import "google/protobuf/any.proto"; import "google/protobuf/duration.proto"; import "google/protobuf/timestamp.proto"; +import "temporal/api/common/v1/message.proto"; import "temporal/api/failure/v1/message.proto"; +import "temporal/api/sdk/v1/user_metadata.proto"; option go_package = "go.temporal.io/server/chasm/lib/nexusoperation/gen/nexusoperationpb;nexusoperationpb"; message OperationState { // Current status of the operation. OperationStatus status = 1; - // Endpoint ID - used internally to avoid failing requests when endpoint is renamed. string endpoint_id = 2; - // Endpoint name - resolved from the endpoint registry for this workflow's namespace. string endpoint = 3; - // Service name. string service = 4; - // Operation name. string operation = 5; - // The time when the operation was scheduled. google.protobuf.Timestamp scheduled_time = 6; - // The time when the operation was started. Only set for asynchronous operations after a successful StartOperation // call. Taken from the component time or the time reported in an async completion request, whichever happens first. google.protobuf.Timestamp started_time = 7; - // The time when the operation reached a terminal state. Taken from the component time or the time reported in an // async completion request, whichever happens first. google.protobuf.Timestamp closed_time = 8; - // Schedule-to-start timeout for this operation. google.protobuf.Duration schedule_to_start_timeout = 9; - // Start-to-close timeout for this operation. google.protobuf.Duration start_to_close_timeout = 10; - // Schedule-to-close timeout for this operation. google.protobuf.Duration schedule_to_close_timeout = 11; - // Unique request ID allocated for all retry attempts of the StartOperation request. string request_id = 12; - // Opaque data injected by the parent (e.g. workflow) for its own bookkeeping. // The operation component itself does not interpret this field. google.protobuf.Any parent_data = 13; - // The number of attempts made to deliver the start operation request. // This number is approximate, it is incremented when a task is added to the history queue. // In practice, there could be more attempts if a task is executed but fails to commit, or less attempts if a task was // never executed. int32 attempt = 14; - // The time when the last attempt completed. google.protobuf.Timestamp last_attempt_complete_time = 15; - // The last attempt's failure, if any. temporal.api.failure.v1.Failure last_attempt_failure = 16; - // The time when the next attempt is scheduled (only set when in BACKING_OFF state). google.protobuf.Timestamp next_attempt_schedule_time = 17; - // Operation token - only set for asynchronous operations after a successful StartOperation call. string operation_token = 18; + // Explicit terminate request state for standalone operations. + NexusOperationTerminateState terminate_state = 19; +} + +message NexusOperationTerminateState { + string request_id = 1; + string identity = 2; +} + +message OperationOutcome { + message Successful { + temporal.api.common.v1.Payload result = 1; + } + + message Failed { + temporal.api.failure.v1.Failure failure = 1; + } + + oneof variant { + Successful successful = 1; + Failed failed = 2; + } } enum OperationStatus { @@ -91,31 +98,29 @@ enum OperationStatus { // Operation timed out - exceeded the user supplied schedule-to-close timeout. // Any attempts to complete the operation in this status will be ignored. OPERATION_STATUS_TIMED_OUT = 7; + OPERATION_STATUS_TERMINATED = 8; } message CancellationState { // Current status of the cancellation request. CancellationStatus status = 1; - // The time when cancellation was requested. google.protobuf.Timestamp requested_time = 2; - // The number of attempts made to deliver the cancel operation request. // This number represents a minimum bound since the attempt is incremented after the request completes. int32 attempt = 3; - // The time when the last attempt completed. google.protobuf.Timestamp last_attempt_complete_time = 4; - // The last attempt's failure, if any. temporal.api.failure.v1.Failure last_attempt_failure = 5; - // The time when the next attempt is scheduled (only set when in BACKING_OFF state). google.protobuf.Timestamp next_attempt_schedule_time = 6; - // Opaque data injected by the parent (e.g. workflow) for its own bookkeeping. // The cancellation component itself does not interpret this field. google.protobuf.Any parent_data = 7; + string request_id = 8; + string identity = 9; + string reason = 10; } enum CancellationStatus { @@ -134,3 +139,10 @@ enum CancellationStatus { // Cancellation request is blocked (eg: by circuit breaker). CANCELLATION_STATUS_BLOCKED = 6; } + +message OperationRequestData { + temporal.api.common.v1.Payload input = 1; + map nexus_header = 2; + temporal.api.sdk.v1.UserMetadata user_metadata = 3; + string identity = 4; +} diff --git a/chasm/lib/nexusoperation/proto/v1/request_response.proto b/chasm/lib/nexusoperation/proto/v1/request_response.proto new file mode 100644 index 0000000000..19508cc24c --- /dev/null +++ b/chasm/lib/nexusoperation/proto/v1/request_response.proto @@ -0,0 +1,61 @@ +syntax = "proto3"; + +package temporal.server.chasm.lib.nexusoperation.proto.v1; + +import "temporal/api/workflowservice/v1/request_response.proto"; + +option go_package = "go.temporal.io/server/chasm/lib/nexusoperation/gen/nexusoperationpb;nexusoperationpb"; + +message StartNexusOperationRequest { + string namespace_id = 1; + + temporal.api.workflowservice.v1.StartNexusOperationExecutionRequest frontend_request = 2; +} + +message StartNexusOperationResponse { + temporal.api.workflowservice.v1.StartNexusOperationExecutionResponse frontend_response = 1; +} + +message DescribeNexusOperationRequest { + string namespace_id = 1; + + temporal.api.workflowservice.v1.DescribeNexusOperationExecutionRequest frontend_request = 2; +} + +message DescribeNexusOperationResponse { + temporal.api.workflowservice.v1.DescribeNexusOperationExecutionResponse frontend_response = 1; +} + +message RequestCancelNexusOperationRequest { + string namespace_id = 1; + + temporal.api.workflowservice.v1.RequestCancelNexusOperationExecutionRequest frontend_request = 2; +} + +message RequestCancelNexusOperationResponse {} + +message TerminateNexusOperationRequest { + string namespace_id = 1; + + temporal.api.workflowservice.v1.TerminateNexusOperationExecutionRequest frontend_request = 2; +} + +message TerminateNexusOperationResponse {} + +message DeleteNexusOperationRequest { + string namespace_id = 1; + + temporal.api.workflowservice.v1.DeleteNexusOperationExecutionRequest frontend_request = 2; +} + +message DeleteNexusOperationResponse {} + +message PollNexusOperationRequest { + string namespace_id = 1; + + temporal.api.workflowservice.v1.PollNexusOperationExecutionRequest frontend_request = 2; +} + +message PollNexusOperationResponse { + temporal.api.workflowservice.v1.PollNexusOperationExecutionResponse frontend_response = 1; +} diff --git a/chasm/lib/nexusoperation/proto/v1/service.proto b/chasm/lib/nexusoperation/proto/v1/service.proto new file mode 100644 index 0000000000..c71599abdf --- /dev/null +++ b/chasm/lib/nexusoperation/proto/v1/service.proto @@ -0,0 +1,41 @@ +syntax = "proto3"; + +package temporal.server.chasm.lib.nexusoperation.proto.v1; + +import "chasm/lib/nexusoperation/proto/v1/request_response.proto"; +import "temporal/server/api/common/v1/api_category.proto"; +import "temporal/server/api/routing/v1/extension.proto"; + +option go_package = "go.temporal.io/server/chasm/lib/nexusoperation/gen/nexusoperationpb;nexusoperationpb"; + +service NexusOperationService { + rpc StartNexusOperation(StartNexusOperationRequest) returns (StartNexusOperationResponse) { + option (temporal.server.api.routing.v1.routing).business_id = "frontend_request.operation_id"; + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + rpc DescribeNexusOperation(DescribeNexusOperationRequest) returns (DescribeNexusOperationResponse) { + option (temporal.server.api.routing.v1.routing).business_id = "frontend_request.operation_id"; + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + rpc RequestCancelNexusOperation(RequestCancelNexusOperationRequest) returns (RequestCancelNexusOperationResponse) { + option (temporal.server.api.routing.v1.routing).business_id = "frontend_request.operation_id"; + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + rpc TerminateNexusOperation(TerminateNexusOperationRequest) returns (TerminateNexusOperationResponse) { + option (temporal.server.api.routing.v1.routing).business_id = "frontend_request.operation_id"; + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + rpc DeleteNexusOperation(DeleteNexusOperationRequest) returns (DeleteNexusOperationResponse) { + option (temporal.server.api.routing.v1.routing).business_id = "frontend_request.operation_id"; + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_STANDARD; + } + + rpc PollNexusOperation(PollNexusOperationRequest) returns (PollNexusOperationResponse) { + option (temporal.server.api.routing.v1.routing).business_id = "frontend_request.operation_id"; + option (temporal.server.api.common.v1.api_category).category = API_CATEGORY_LONG_POLL; + } +} diff --git a/chasm/lib/nexusoperation/task_handler_helpers.go b/chasm/lib/nexusoperation/task_handler_helpers.go index 9d96229ccf..e4ce572b62 100644 --- a/chasm/lib/nexusoperation/task_handler_helpers.go +++ b/chasm/lib/nexusoperation/task_handler_helpers.go @@ -6,6 +6,7 @@ import ( "fmt" "strings" "sync/atomic" + "text/template" "time" "github.com/nexus-rpc/sdk-go/nexus" @@ -13,11 +14,15 @@ import ( enumspb "go.temporal.io/api/enums/v1" failurepb "go.temporal.io/api/failure/v1" "go.temporal.io/api/serviceerror" + persistencespb "go.temporal.io/server/api/persistence/v1" + tokenspb "go.temporal.io/server/api/token/v1" "go.temporal.io/server/common" "go.temporal.io/server/common/log" "go.temporal.io/server/common/log/tag" + "go.temporal.io/server/common/namespace" commonnexus "go.temporal.io/server/common/nexus" "go.temporal.io/server/common/nexus/nexusrpc" + queueserrors "go.temporal.io/server/service/history/queues/errors" ) var ( @@ -210,3 +215,190 @@ func callErrorToFailure(callErr error) (*failurepb.Failure, bool, error) { } return failure, retryable, nil } + +// invocationResult is a marker interface for the outcome of a Nexus start operation call. +type invocationResult interface { + mustImplementInvocationResult() +} + +type invocationResultOK struct { + response *nexusrpc.ClientStartOperationResponse[*commonpb.Payload] + links []*commonpb.Link +} + +func (invocationResultOK) mustImplementInvocationResult() {} + +type invocationResultFail struct { + failure *failurepb.Failure +} + +func (invocationResultFail) mustImplementInvocationResult() {} + +type invocationResultRetry struct { + failure *failurepb.Failure +} + +func (invocationResultRetry) mustImplementInvocationResult() {} + +type invocationResultCancel struct { + failure *failurepb.Failure +} + +func (invocationResultCancel) mustImplementInvocationResult() {} + +type invocationResultTimeout struct { + failure *failurepb.Failure +} + +func (invocationResultTimeout) mustImplementInvocationResult() {} + +func newInvocationResult( + response *nexusrpc.ClientStartOperationResponse[*commonpb.Payload], + callErr error, +) (invocationResult, error) { + if callErr == nil { + return invocationResultOK{response: response}, nil + } + + if serviceErr, ok := errors.AsType[serviceerror.ServiceError](callErr); ok { + retryable := common.IsRetryableRPCError(callErr) + failure := &failurepb.Failure{ + Message: fmt.Sprintf("%s: %s", strings.Replace(fmt.Sprintf("%T", serviceErr), "*serviceerror.", "", 1), serviceErr.Error()), + FailureInfo: &failurepb.Failure_ServerFailureInfo{ + ServerFailureInfo: &failurepb.ServerFailureInfo{ + NonRetryable: !retryable, + }, + }, + } + if retryable { + return invocationResultRetry{failure: failure}, nil + } + return invocationResultFail{failure: failure}, nil + } + + if handlerErr, ok := errors.AsType[*nexus.HandlerError](callErr); ok { + var nf nexus.Failure + if handlerErr.OriginalFailure != nil { + nf = *handlerErr.OriginalFailure + } else { + var err error + nf, err = nexusrpc.DefaultFailureConverter().ErrorToFailure(handlerErr) + if err != nil { + return nil, err + } + } + failure, err := commonnexus.NexusFailureToTemporalFailure(nf) + if err != nil { + return nil, err + } + if handlerErr.Retryable() { + return invocationResultRetry{failure: failure}, nil + } + return invocationResultFail{failure: failure}, nil + } + + if opErr, ok := errors.AsType[*nexus.OperationError](callErr); ok { + failure, err := operationErrorToFailure(opErr) + if err != nil { + return nil, err + } + if opErr.State == nexus.OperationStateCanceled { + return invocationResultCancel{failure: failure}, nil + } + return invocationResultFail{failure: failure}, nil + } + + if opTimeoutBelowMinErr, ok := errors.AsType[*operationTimeoutBelowMinError](callErr); ok { + failure := &failurepb.Failure{ + Message: "operation timed out", + FailureInfo: &failurepb.Failure_TimeoutFailureInfo{ + TimeoutFailureInfo: &failurepb.TimeoutFailureInfo{ + TimeoutType: opTimeoutBelowMinErr.timeoutType, + }, + }, + } + return invocationResultTimeout{failure: failure}, nil + } + + if errors.Is(callErr, context.DeadlineExceeded) || errors.Is(callErr, context.Canceled) { + callErr = errRequestTimedOut + } + + failure := &failurepb.Failure{ + Message: callErr.Error(), + FailureInfo: &failurepb.Failure_ServerFailureInfo{ + ServerFailureInfo: &failurepb.ServerFailureInfo{}, + }, + } + if errors.Is(callErr, ErrResponseBodyTooLarge) || errors.Is(callErr, ErrInvalidOperationToken) { + failure.GetServerFailureInfo().NonRetryable = true + return invocationResultFail{failure: failure}, nil + } + return invocationResultRetry{failure: failure}, nil +} + +func operationErrorToFailure(opErr *nexus.OperationError) (*failurepb.Failure, error) { + var nf nexus.Failure + if opErr.OriginalFailure != nil { + nf = *opErr.OriginalFailure + } else { + var err error + nf, err = nexusrpc.DefaultFailureConverter().ErrorToFailure(opErr) + if err != nil { + return nil, err + } + } + unwrapError := nf.Metadata["unwrap-error"] == "true" + if unwrapError && nf.Cause != nil { + return commonnexus.NexusFailureToTemporalFailure(*nf.Cause) + } + return commonnexus.NexusFailureToTemporalFailure(nf) +} + +func buildCallbackURL( + useSystemCallback bool, + callbackTemplate *template.Template, + ns *namespace.Namespace, + endpoint *persistencespb.NexusEndpointEntry, +) (string, error) { + if endpoint == nil { + return commonnexus.SystemCallbackURL, nil + } + target := endpoint.GetEndpoint().GetSpec().GetTarget().GetVariant() + if !useSystemCallback { + return buildCallbackFromTemplate(callbackTemplate, ns) + } + switch target.(type) { + case *persistencespb.NexusEndpointTarget_Worker_: + return commonnexus.SystemCallbackURL, nil + case *persistencespb.NexusEndpointTarget_External_: + return buildCallbackFromTemplate(callbackTemplate, ns) + default: + return "", fmt.Errorf("unknown endpoint target type: %T", target) + } +} + +// lookupEndpoint gets an endpoint from the registry, preferring to look up by ID and falling back to name lookup. +// The fallback is needed because endpoints may be deleted and recreated with the same name but a different ID. +// In that case, the ID stored in the operation state becomes stale, but the name-based lookup still resolves correctly. +func lookupEndpoint(ctx context.Context, registry commonnexus.EndpointRegistry, namespaceID namespace.ID, endpointID, endpointName string) (*persistencespb.NexusEndpointEntry, error) { + entry, err := registry.GetByID(ctx, endpointID) + if err != nil { + if _, ok := errors.AsType[*serviceerror.NotFound](err); ok { + return registry.GetByName(ctx, namespaceID, endpointName) + } + return nil, err + } + return entry, nil +} + +func (h *operationInvocationTaskHandler) generateCallbackToken(serializedRef []byte, requestID string) (string, error) { + token, err := h.callbackTokenGenerator.Tokenize(&tokenspb.NexusOperationCompletion{ + ComponentRef: serializedRef, + RequestId: requestID, + }) + if err != nil { + return "", fmt.Errorf("%w: %w", queueserrors.NewUnprocessableTaskError("failed to generate a callback token"), err) + } + return token, nil +} diff --git a/chasm/lib/nexusoperation/validator.go b/chasm/lib/nexusoperation/validator.go new file mode 100644 index 0000000000..bb2b3f4ed6 --- /dev/null +++ b/chasm/lib/nexusoperation/validator.go @@ -0,0 +1,182 @@ +package nexusoperation + +import ( + "errors" + "fmt" + "slices" + "strings" + "time" + + "github.com/google/uuid" + commonpb "go.temporal.io/api/common/v1" + enumspb "go.temporal.io/api/enums/v1" + "go.temporal.io/api/serviceerror" + "go.temporal.io/api/workflowservice/v1" + "go.temporal.io/server/common/primitives/timestamp" + "go.temporal.io/server/common/searchattribute" + "google.golang.org/protobuf/types/known/durationpb" +) + +// ValidateServiceName checks that the service name does not exceed the configured limit. +func ValidateServiceName(service string, limit int) error { + if len(service) > limit { + return fmt.Errorf("service exceeds length limit. Length=%d Limit=%d", len(service), limit) + } + return nil +} + +// ValidateOperationName checks that the operation name does not exceed the configured limit. +func ValidateOperationName(operation string, limit int) error { + if len(operation) > limit { + return fmt.Errorf("operation exceeds length limit. Length=%d Limit=%d", len(operation), limit) + } + return nil +} + +// ValidateAndLowercaseNexusHeaders validates headers and returns a new map with lower-cased keys. +func ValidateAndLowercaseNexusHeaders(headers map[string]string, disallowed []string, sizeLimit int) (map[string]string, error) { + headerLength := 0 + lowered := make(map[string]string, len(headers)) + for k, v := range headers { + lowerK := strings.ToLower(k) + headerLength += len(lowerK) + len(v) + if slices.Contains(disallowed, lowerK) { + return nil, fmt.Errorf("nexus_header contains a disallowed key: %q", k) + } + lowered[lowerK] = v + } + if headerLength > sizeLimit { + return nil, errors.New("nexus_header exceeds size limit") + } + return lowered, nil +} + +// ValidateAndCapScheduleToCloseTimeout validates and caps the schedule-to-close timeout. +// It returns the (possibly capped) duration. +func ValidateAndCapScheduleToCloseTimeout(timeout *durationpb.Duration, maxTimeout time.Duration) (*durationpb.Duration, error) { + if err := timestamp.ValidateAndCapProtoDuration(timeout); err != nil { + return timeout, fmt.Errorf("schedule_to_close_timeout is invalid: %v", err) + } + if maxTimeout > 0 { + if t := timeout.AsDuration(); t == 0 || t > maxTimeout { + timeout = durationpb.New(maxTimeout) + } + } + return timeout, nil +} + +// ValidatePayloadSize checks that the payload does not exceed the size limit. +func ValidatePayloadSize(input *commonpb.Payload, limit int) error { + if input.Size() > limit { + return errors.New("input exceeds size limit") + } + return nil +} + +func validateAndNormalizeStartRequest( + req *workflowservice.StartNexusOperationExecutionRequest, + config *Config, + saMapperProvider searchattribute.MapperProvider, + saValidator *searchattribute.Validator, +) error { + ns := req.GetNamespace() + if req.GetOperationId() == "" { + return serviceerror.NewInvalidArgument("operation_id is required") + } + if len(req.GetOperationId()) > config.MaxIDLengthLimit() { + return serviceerror.NewInvalidArgumentf("operation_id exceeds length limit. Length=%d Limit=%d", + len(req.GetOperationId()), config.MaxIDLengthLimit()) + } + if req.GetRequestId() == "" { + req.RequestId = uuid.NewString() + } + if len(req.GetRequestId()) > config.MaxIDLengthLimit() { + return serviceerror.NewInvalidArgumentf("request_id exceeds length limit. Length=%d Limit=%d", + len(req.GetRequestId()), config.MaxIDLengthLimit()) + } + if len(req.GetIdentity()) > config.MaxIDLengthLimit() { + return serviceerror.NewInvalidArgumentf("identity exceeds length limit. Length=%d Limit=%d", + len(req.GetIdentity()), config.MaxIDLengthLimit()) + } + if req.GetEndpoint() == "" { + return serviceerror.NewInvalidArgument("endpoint is required") + } + if req.GetService() == "" { + return serviceerror.NewInvalidArgument("service is required") + } + if err := ValidateServiceName(req.GetService(), config.MaxServiceNameLength(ns)); err != nil { + return serviceerror.NewInvalidArgument(err.Error()) + } + if req.GetOperation() == "" { + return serviceerror.NewInvalidArgument("operation is required") + } + if err := ValidateOperationName(req.GetOperation(), config.MaxOperationNameLength(ns)); err != nil { + return serviceerror.NewInvalidArgument(err.Error()) + } + var err error + if req.ScheduleToCloseTimeout, err = ValidateAndCapScheduleToCloseTimeout( + req.GetScheduleToCloseTimeout(), + config.MaxOperationScheduleToCloseTimeout(ns), + ); err != nil { + return serviceerror.NewInvalidArgument(err.Error()) + } + if err := ValidatePayloadSize(req.GetInput(), config.PayloadSizeLimit(ns)); err != nil { + return serviceerror.NewInvalidArgument(err.Error()) + } + loweredHeaders, err := ValidateAndLowercaseNexusHeaders(req.GetNexusHeader(), config.DisallowedOperationHeaders(), config.MaxOperationHeaderSize(ns)) + if err != nil { + return serviceerror.NewInvalidArgument(err.Error()) + } + req.NexusHeader = loweredHeaders + if err := validateAndNormalizeSearchAttributes(req, saMapperProvider, saValidator); err != nil { + // SA validator already returns properly typed gRPC status errors; no need to re-wrap. + return err + } + if req.GetIdReusePolicy() == enumspb.NEXUS_OPERATION_ID_REUSE_POLICY_UNSPECIFIED { + req.IdReusePolicy = enumspb.NEXUS_OPERATION_ID_REUSE_POLICY_ALLOW_DUPLICATE + } + if req.GetIdConflictPolicy() == enumspb.NEXUS_OPERATION_ID_CONFLICT_POLICY_UNSPECIFIED { + req.IdConflictPolicy = enumspb.NEXUS_OPERATION_ID_CONFLICT_POLICY_FAIL + } + return nil +} + +func validateDescribeNexusOperationExecutionRequest(req *workflowservice.DescribeNexusOperationExecutionRequest, config *Config) error { + if req.GetOperationId() == "" { + return serviceerror.NewInvalidArgument("operation_id is required") + } + if len(req.GetOperationId()) > config.MaxIDLengthLimit() { + return serviceerror.NewInvalidArgumentf("operation_id exceeds length limit. Length=%d Limit=%d", + len(req.GetOperationId()), config.MaxIDLengthLimit()) + } + if len(req.GetRunId()) > config.MaxIDLengthLimit() { + return serviceerror.NewInvalidArgumentf("run_id exceeds length limit. Length=%d Limit=%d", + len(req.GetRunId()), config.MaxIDLengthLimit()) + } + // TODO: Add long-poll validation (run_id required when long_poll_token is set). + return nil +} + +func validateAndNormalizeSearchAttributes( + req *workflowservice.StartNexusOperationExecutionRequest, + saMapperProvider searchattribute.MapperProvider, + saValidator *searchattribute.Validator, +) error { + namespaceName := req.GetNamespace() + + // Unalias search attributes for validation. + saToValidate := req.SearchAttributes + if saMapperProvider != nil && saToValidate != nil { + var err error + saToValidate, err = searchattribute.UnaliasFields(saMapperProvider, saToValidate, namespaceName) + if err != nil { + return err + } + } + + if err := saValidator.Validate(saToValidate, namespaceName); err != nil { + return err + } + + return saValidator.ValidateSize(saToValidate, namespaceName) +} diff --git a/chasm/lib/nexusoperation/validator_test.go b/chasm/lib/nexusoperation/validator_test.go new file mode 100644 index 0000000000..4ad22d4026 --- /dev/null +++ b/chasm/lib/nexusoperation/validator_test.go @@ -0,0 +1,299 @@ +package nexusoperation + +import ( + "strings" + "testing" + "time" + + "github.com/stretchr/testify/require" + commonpb "go.temporal.io/api/common/v1" + enumspb "go.temporal.io/api/enums/v1" + "go.temporal.io/api/serviceerror" + "go.temporal.io/api/workflowservice/v1" + "go.temporal.io/server/common/dynamicconfig" + "go.temporal.io/server/common/payload" + "go.temporal.io/server/common/persistence/visibility/manager" + "go.temporal.io/server/common/searchattribute" + "go.uber.org/mock/gomock" + "google.golang.org/protobuf/types/known/durationpb" +) + +func TestValidateStartNexusOperationExecutionRequest(t *testing.T) { + ctrl := gomock.NewController(t) + mockVisibilityManager := manager.NewMockVisibilityManager(ctrl) + mockVisibilityManager.EXPECT().GetIndexName().Return("index-name").AnyTimes() + mockVisibilityManager.EXPECT().ValidateCustomSearchAttributes(gomock.Any()).Return(nil, nil).AnyTimes() + + saValidator := searchattribute.NewValidator( + searchattribute.NewTestEsProvider(), + searchattribute.NewTestMapperProvider(nil), + func(string) int { return 2 }, // max number of keys + func(string) int { return 20 }, // max size of value + func(string) int { return 100 }, // max total size + mockVisibilityManager, + dynamicconfig.GetBoolPropertyFnFilteredByNamespace(false), + dynamicconfig.GetBoolPropertyFnFilteredByNamespace(false), + ) + + config := &Config{ + MaxIDLengthLimit: func() int { return 50 }, + MaxServiceNameLength: func(string) int { return 10 }, + MaxOperationNameLength: func(string) int { return 10 }, + PayloadSizeLimit: func(string) int { return 10 }, + MaxOperationHeaderSize: func(string) int { return 10 }, + DisallowedOperationHeaders: func() []string { return []string{"disallowed-header"} }, + MaxOperationScheduleToCloseTimeout: func(string) time.Duration { return time.Hour }, + } + + for _, tc := range []struct { + name string + mutate func(*workflowservice.StartNexusOperationExecutionRequest) + errMsg string + check func(*testing.T, *workflowservice.StartNexusOperationExecutionRequest) + }{ + { + name: "valid request", + }, + { + name: "operation_id - required", + mutate: func(r *workflowservice.StartNexusOperationExecutionRequest) { r.OperationId = "" }, + errMsg: "operation_id is required", + }, + { + name: "operation_id - exceeds length limit", + mutate: func(r *workflowservice.StartNexusOperationExecutionRequest) { + r.OperationId = strings.Repeat("x", 51) + }, + errMsg: "operation_id exceeds length limit", + }, + { + name: "request_id - defaults empty to UUID", + mutate: func(r *workflowservice.StartNexusOperationExecutionRequest) { + r.RequestId = "" + }, + check: func(t *testing.T, r *workflowservice.StartNexusOperationExecutionRequest) { + require.Len(t, r.RequestId, 36) // UUID length + }, + }, + { + name: "request_id - exceeds length limit", + mutate: func(r *workflowservice.StartNexusOperationExecutionRequest) { + r.RequestId = strings.Repeat("x", 51) + }, + errMsg: "request_id exceeds length limit", + }, + { + name: "identity - exceeds length limit", + mutate: func(r *workflowservice.StartNexusOperationExecutionRequest) { + r.Identity = strings.Repeat("x", 51) + }, + errMsg: "identity exceeds length limit", + }, + { + name: "endpoint - required", + mutate: func(r *workflowservice.StartNexusOperationExecutionRequest) { r.Endpoint = "" }, + errMsg: "endpoint is required", + }, + { + name: "service - required", + mutate: func(r *workflowservice.StartNexusOperationExecutionRequest) { r.Service = "" }, + errMsg: "service is required", + }, + { + name: "service - exceeds length limit", + mutate: func(r *workflowservice.StartNexusOperationExecutionRequest) { + r.Service = "too-long-svc" + }, + errMsg: "service exceeds length limit", + }, + { + name: "operation - required", + mutate: func(r *workflowservice.StartNexusOperationExecutionRequest) { r.Operation = "" }, + errMsg: "operation is required", + }, + { + name: "operation - exceeds length limit", + mutate: func(r *workflowservice.StartNexusOperationExecutionRequest) { + r.Operation = "too-long-op!" + }, + errMsg: "operation exceeds length limit", + }, + { + name: "schedule_to_close_timeout - invalid", + mutate: func(r *workflowservice.StartNexusOperationExecutionRequest) { + r.ScheduleToCloseTimeout = &durationpb.Duration{Seconds: -1} + }, + errMsg: "schedule_to_close_timeout is invalid", + }, + { + name: "schedule_to_close_timeout - caps exceeding max", + mutate: func(r *workflowservice.StartNexusOperationExecutionRequest) { + r.ScheduleToCloseTimeout = durationpb.New(2 * time.Hour) + }, + check: func(t *testing.T, r *workflowservice.StartNexusOperationExecutionRequest) { + require.Equal(t, time.Hour, r.ScheduleToCloseTimeout.AsDuration()) + }, + }, + { + name: "schedule_to_close_timeout - caps unset to max", + check: func(t *testing.T, r *workflowservice.StartNexusOperationExecutionRequest) { + require.Equal(t, time.Hour, r.ScheduleToCloseTimeout.AsDuration()) + }, + }, + { + name: "schedule_to_close_timeout - preserves within max", + mutate: func(r *workflowservice.StartNexusOperationExecutionRequest) { + r.ScheduleToCloseTimeout = durationpb.New(30 * time.Minute) + }, + check: func(t *testing.T, r *workflowservice.StartNexusOperationExecutionRequest) { + require.Equal(t, 30*time.Minute, r.ScheduleToCloseTimeout.AsDuration()) + }, + }, + { + name: "input - exceeds size limit", + mutate: func(r *workflowservice.StartNexusOperationExecutionRequest) { + r.Input = &commonpb.Payload{Data: []byte("too-long-input")} + }, + errMsg: "input exceeds size limit", + }, + { + name: "nexus_header - disallowed key", + mutate: func(r *workflowservice.StartNexusOperationExecutionRequest) { + r.NexusHeader = map[string]string{"Disallowed-Header": "value"} + }, + errMsg: "nexus_header contains a disallowed key", + }, + { + name: "nexus_header - exceeds size limit", + mutate: func(r *workflowservice.StartNexusOperationExecutionRequest) { + r.NexusHeader = map[string]string{"key": "too-long-val"} + }, + errMsg: "nexus_header exceeds size limit", + }, + { + name: "id_policies - defaults unspecified", + check: func(t *testing.T, r *workflowservice.StartNexusOperationExecutionRequest) { + require.Equal(t, enumspb.NEXUS_OPERATION_ID_REUSE_POLICY_ALLOW_DUPLICATE, r.IdReusePolicy) + require.Equal(t, enumspb.NEXUS_OPERATION_ID_CONFLICT_POLICY_FAIL, r.IdConflictPolicy) + }, + }, + { + name: "id_policies - preserves explicit values", + mutate: func(r *workflowservice.StartNexusOperationExecutionRequest) { + r.IdReusePolicy = enumspb.NEXUS_OPERATION_ID_REUSE_POLICY_REJECT_DUPLICATE + r.IdConflictPolicy = enumspb.NEXUS_OPERATION_ID_CONFLICT_POLICY_USE_EXISTING + }, + check: func(t *testing.T, r *workflowservice.StartNexusOperationExecutionRequest) { + require.Equal(t, enumspb.NEXUS_OPERATION_ID_REUSE_POLICY_REJECT_DUPLICATE, r.IdReusePolicy) + require.Equal(t, enumspb.NEXUS_OPERATION_ID_CONFLICT_POLICY_USE_EXISTING, r.IdConflictPolicy) + }, + }, + { + name: "search_attributes - too many keys", + mutate: func(r *workflowservice.StartNexusOperationExecutionRequest) { + r.SearchAttributes = &commonpb.SearchAttributes{ + IndexedFields: map[string]*commonpb.Payload{ + "CustomKeywordField": payload.EncodeString("v1"), + "CustomTextField": payload.EncodeString("v2"), + "CustomIntField": payload.EncodeString("3"), + }, + } + }, + errMsg: "number of search attributes", + }, + { + name: "search_attributes - value exceeds size limit", + mutate: func(r *workflowservice.StartNexusOperationExecutionRequest) { + r.SearchAttributes = &commonpb.SearchAttributes{ + IndexedFields: map[string]*commonpb.Payload{ + "CustomKeywordField": payload.EncodeString(strings.Repeat("x", 100)), + }, + } + }, + errMsg: "exceeds size limit", + }, + } { + t.Run(tc.name, func(t *testing.T) { + req := &workflowservice.StartNexusOperationExecutionRequest{ + Namespace: "default", + OperationId: "op-id", + RequestId: "request-id", + Endpoint: "endpoint", + Service: "service", + Operation: "operation", + SearchAttributes: &commonpb.SearchAttributes{ + IndexedFields: map[string]*commonpb.Payload{ + "CustomKeywordField": payload.EncodeString("val"), + }, + }, + } + if tc.mutate != nil { + tc.mutate(req) + } + err := validateAndNormalizeStartRequest(req, config, nil, saValidator) + if tc.errMsg != "" { + var invalidArgErr *serviceerror.InvalidArgument + require.ErrorAs(t, err, &invalidArgErr) + require.Contains(t, err.Error(), tc.errMsg) + } else { + require.NoError(t, err) + } + if tc.check != nil { + tc.check(t, req) + } + }) + } +} + +func TestValidateDescribeNexusOperationExecutionRequest(t *testing.T) { + config := &Config{ + MaxIDLengthLimit: func() int { return 20 }, + } + + for _, tc := range []struct { + name string + mutate func(*workflowservice.DescribeNexusOperationExecutionRequest) + errMsg string + }{ + { + name: "valid request", + }, + { + name: "operation_id - required", + mutate: func(r *workflowservice.DescribeNexusOperationExecutionRequest) { r.OperationId = "" }, + errMsg: "operation_id is required", + }, + { + name: "operation_id - exceeds length limit", + mutate: func(r *workflowservice.DescribeNexusOperationExecutionRequest) { + r.OperationId = "this-operation-id-is-too-long" + }, + errMsg: "operation_id exceeds length limit", + }, + { + name: "run_id - exceeds length limit", + mutate: func(r *workflowservice.DescribeNexusOperationExecutionRequest) { + r.RunId = "this-run-id-is-too-long!!" + }, + errMsg: "run_id exceeds length limit", + }, + } { + t.Run(tc.name, func(t *testing.T) { + validReq := &workflowservice.DescribeNexusOperationExecutionRequest{ + Namespace: "default", + OperationId: "operation-id", + } + if tc.mutate != nil { + tc.mutate(validReq) + } + err := validateDescribeNexusOperationExecutionRequest(validReq, config) + if tc.errMsg != "" { + var invalidArgErr *serviceerror.InvalidArgument + require.ErrorAs(t, err, &invalidArgErr) + require.Contains(t, err.Error(), tc.errMsg) + } else { + require.NoError(t, err) + } + }) + } +} diff --git a/tests/nexus_standalone_test.go b/tests/nexus_standalone_test.go new file mode 100644 index 0000000000..7429facded --- /dev/null +++ b/tests/nexus_standalone_test.go @@ -0,0 +1,228 @@ +package tests + +import ( + "cmp" + "testing" + "time" + + commonpb "go.temporal.io/api/common/v1" + enumspb "go.temporal.io/api/enums/v1" + nexuspb "go.temporal.io/api/nexus/v1" + "go.temporal.io/api/operatorservice/v1" + sdkpb "go.temporal.io/api/sdk/v1" + "go.temporal.io/api/serviceerror" + "go.temporal.io/api/workflowservice/v1" + "go.temporal.io/server/chasm/lib/nexusoperation" + "go.temporal.io/server/common/dynamicconfig" + "go.temporal.io/server/common/payload" + "go.temporal.io/server/common/testing/protorequire" + "go.temporal.io/server/tests/testcore" + "google.golang.org/protobuf/types/known/durationpb" +) + +var nexusStandaloneOpts = []testcore.TestOption{ + testcore.WithDedicatedCluster(), + testcore.WithDynamicConfig(dynamicconfig.EnableChasm, true), + testcore.WithDynamicConfig(nexusoperation.Enabled, true), +} + +func TestStandaloneNexusOperation(t *testing.T) { + t.Parallel() + + t.Run("StartAndDescribe", func(t *testing.T) { + s := testcore.NewEnv(t, nexusStandaloneOpts...) + endpointName := createNexusEndpoint(s) + + testInput := payload.EncodeString("test-input") + testHeader := map[string]string{"test-key": "test-value"} + testUserMetadata := &sdkpb.UserMetadata{ + Summary: payload.EncodeString("test-summary"), + Details: payload.EncodeString("test-details"), + } + testSearchAttributes := &commonpb.SearchAttributes{ + IndexedFields: map[string]*commonpb.Payload{ + "CustomKeywordField": payload.EncodeString("test-value"), + }, + } + startResp, err := startNexusOperation(s, &workflowservice.StartNexusOperationExecutionRequest{ + OperationId: "test-op", + Endpoint: endpointName, + Input: testInput, + NexusHeader: testHeader, + UserMetadata: testUserMetadata, + SearchAttributes: testSearchAttributes, + }) + s.NoError(err) + s.True(startResp.GetStarted()) + + // Describe without IncludeInput. + descResp, err := s.FrontendClient().DescribeNexusOperationExecution(s.Context(), &workflowservice.DescribeNexusOperationExecutionRequest{ + Namespace: s.Namespace().String(), + OperationId: "test-op", + RunId: startResp.RunId, + }) + s.NoError(err) + s.Equal(startResp.RunId, descResp.RunId) + s.Nil(descResp.GetInput()) // not included by default + + info := descResp.GetInfo() + protorequire.ProtoEqual(t, &nexuspb.NexusOperationExecutionInfo{ + OperationId: "test-op", + RunId: startResp.RunId, + Endpoint: endpointName, + Service: "test-service", + Operation: "test-operation", + Status: enumspb.NEXUS_OPERATION_EXECUTION_STATUS_RUNNING, + State: enumspb.PENDING_NEXUS_OPERATION_STATE_SCHEDULED, + ScheduleToCloseTimeout: durationpb.New(10 * time.Minute), + NexusHeader: testHeader, + UserMetadata: testUserMetadata, + SearchAttributes: testSearchAttributes, + Attempt: 0, + StateTransitionCount: 1, + // Dynamic fields copied from actual response for comparison. + RequestId: info.GetRequestId(), + ScheduleTime: info.GetScheduleTime(), + ExpirationTime: info.GetExpirationTime(), + ExecutionDuration: info.GetExecutionDuration(), + }, info) + s.NotEmpty(info.GetRequestId()) + s.NotNil(info.GetScheduleTime()) + s.NotNil(info.GetExpirationTime()) + s.NotNil(info.GetExecutionDuration()) + + // Describe with IncludeInput. + descResp, err = s.FrontendClient().DescribeNexusOperationExecution(s.Context(), &workflowservice.DescribeNexusOperationExecutionRequest{ + Namespace: s.Namespace().String(), + OperationId: "test-op", + RunId: startResp.RunId, + IncludeInput: true, + }) + s.NoError(err) + protorequire.ProtoEqual(t, testInput, descResp.GetInput()) + }) + + // Validates that request validation is wired up in the frontend. + // Exhaustive validation cases are covered in unit tests. + t.Run("StartValidation", func(t *testing.T) { + s := testcore.NewEnv(t, nexusStandaloneOpts...) + + _, err := startNexusOperation(s, &workflowservice.StartNexusOperationExecutionRequest{ + OperationId: "", // required field + }) + s.Error(err) + s.Contains(err.Error(), "operation_id is required") + }) + + t.Run("DescribeNotFound", func(t *testing.T) { + s := testcore.NewEnv(t, nexusStandaloneOpts...) + + _, err := s.FrontendClient().DescribeNexusOperationExecution(s.Context(), &workflowservice.DescribeNexusOperationExecutionRequest{ + Namespace: s.Namespace().String(), + OperationId: "does-not-exist", + }) + var notFound *serviceerror.NotFound + s.ErrorAs(err, ¬Found) + }) + + t.Run("DescribeWrongRunId", func(t *testing.T) { + s := testcore.NewEnv(t, nexusStandaloneOpts...) + endpointName := createNexusEndpoint(s) + + _, err := startNexusOperation(s, &workflowservice.StartNexusOperationExecutionRequest{ + OperationId: "test-op", + Endpoint: endpointName, + }) + s.NoError(err) + + _, err = s.FrontendClient().DescribeNexusOperationExecution(s.Context(), &workflowservice.DescribeNexusOperationExecutionRequest{ + Namespace: s.Namespace().String(), + OperationId: "test-op", + RunId: "00000000-0000-0000-0000-000000000000", + }) + var notFound *serviceerror.NotFound + s.ErrorAs(err, ¬Found) + }) + + t.Run("IDConflictPolicy_Fail", func(t *testing.T) { + s := testcore.NewEnv(t, nexusStandaloneOpts...) + endpointName := createNexusEndpoint(s) + + resp1, err := startNexusOperation(s, &workflowservice.StartNexusOperationExecutionRequest{ + OperationId: "test-op", + Endpoint: endpointName, + }) + s.NoError(err) + + // Second start with different request ID should fail. + _, err = startNexusOperation(s, &workflowservice.StartNexusOperationExecutionRequest{ + OperationId: "test-op", + Endpoint: endpointName, + RequestId: "different-request-id", + }) + s.Error(err) + + // Second start with same request ID should return existing run. + resp2, err := startNexusOperation(s, &workflowservice.StartNexusOperationExecutionRequest{ + OperationId: "test-op", + Endpoint: endpointName, + }) + s.NoError(err) + s.Equal(resp1.RunId, resp2.RunId) + s.False(resp2.GetStarted()) + }) + + t.Run("IDConflictPolicy_UseExisting", func(t *testing.T) { + s := testcore.NewEnv(t, nexusStandaloneOpts...) + endpointName := createNexusEndpoint(s) + + resp1, err := startNexusOperation(s, &workflowservice.StartNexusOperationExecutionRequest{ + OperationId: "test-op", + Endpoint: endpointName, + }) + s.NoError(err) + + resp2, err := startNexusOperation(s, &workflowservice.StartNexusOperationExecutionRequest{ + OperationId: "test-op", + Endpoint: endpointName, + RequestId: "different-request-id", + IdConflictPolicy: enumspb.NEXUS_OPERATION_ID_CONFLICT_POLICY_USE_EXISTING, + }) + s.NoError(err) + s.Equal(resp1.RunId, resp2.RunId) + s.False(resp2.GetStarted()) + }) +} + +func createNexusEndpoint(s *testcore.TestEnv) string { + name := testcore.RandomizedNexusEndpoint(s.T().Name()) + _, err := s.OperatorClient().CreateNexusEndpoint(s.Context(), &operatorservice.CreateNexusEndpointRequest{ + Spec: &nexuspb.EndpointSpec{ + Name: name, + Target: &nexuspb.EndpointTarget{ + Variant: &nexuspb.EndpointTarget_Worker_{ + Worker: &nexuspb.EndpointTarget_Worker{ + Namespace: s.Namespace().String(), + TaskQueue: "unused-for-test", + }, + }, + }, + }, + }) + s.NoError(err) + return name +} + +func startNexusOperation( + s *testcore.TestEnv, + req *workflowservice.StartNexusOperationExecutionRequest, +) (*workflowservice.StartNexusOperationExecutionResponse, error) { + req.Namespace = cmp.Or(req.Namespace, s.Namespace().String()) + req.Service = cmp.Or(req.Service, "test-service") + req.Operation = cmp.Or(req.Operation, "test-operation") + req.RequestId = cmp.Or(req.RequestId, s.Tv().RequestID()) + if req.ScheduleToCloseTimeout == nil { + req.ScheduleToCloseTimeout = durationpb.New(10 * time.Minute) + } + return s.FrontendClient().StartNexusOperationExecution(s.Context(), req) +} From bd8b806b5f15a3c7b1366db07e35a24329f84652 Mon Sep 17 00:00:00 2001 From: Stephan Behnke Date: Fri, 20 Mar 2026 08:58:58 -0700 Subject: [PATCH 04/11] Nexus Standalone: List + Count (#9559) Add Nexus Standalone List and Count handlers. - [ ] built - [ ] run locally and tested manually - [ ] covered by existing tests - [ ] added new unit test(s) - [x] added new functional test(s) --- chasm/lib/nexusoperation/frontend.go | 103 +++++++- chasm/lib/nexusoperation/fx.go | 6 + chasm/lib/nexusoperation/library.go | 4 + tests/nexus_standalone_test.go | 359 +++++++++++++++++++++++++++ 4 files changed, 466 insertions(+), 6 deletions(-) diff --git a/chasm/lib/nexusoperation/frontend.go b/chasm/lib/nexusoperation/frontend.go index c74057929f..8f9c440e99 100644 --- a/chasm/lib/nexusoperation/frontend.go +++ b/chasm/lib/nexusoperation/frontend.go @@ -3,13 +3,20 @@ package nexusoperation import ( "context" + commonpb "go.temporal.io/api/common/v1" + enumspb "go.temporal.io/api/enums/v1" + nexuspb "go.temporal.io/api/nexus/v1" "go.temporal.io/api/serviceerror" "go.temporal.io/api/workflowservice/v1" + "go.temporal.io/server/chasm" nexusoperationpb "go.temporal.io/server/chasm/lib/nexusoperation/gen/nexusoperationpb/v1" "go.temporal.io/server/common/log" "go.temporal.io/server/common/namespace" commonnexus "go.temporal.io/server/common/nexus" "go.temporal.io/server/common/searchattribute" + "google.golang.org/protobuf/types/known/durationpb" + "google.golang.org/protobuf/types/known/emptypb" + "google.golang.org/protobuf/types/known/timestamppb" ) // FrontendHandler provides the frontend-facing API for standalone Nexus operations. @@ -121,28 +128,112 @@ func (h *frontendHandler) PollNexusOperationExecution(_ context.Context, req *wo return nil, serviceerror.NewUnimplemented("PollNexusOperationExecution not implemented") } -func (h *frontendHandler) ListNexusOperationExecutions(_ context.Context, req *workflowservice.ListNexusOperationExecutionsRequest) (*workflowservice.ListNexusOperationExecutionsResponse, error) { +func (h *frontendHandler) ListNexusOperationExecutions( + ctx context.Context, + req *workflowservice.ListNexusOperationExecutionsRequest, +) (*workflowservice.ListNexusOperationExecutionsResponse, error) { if !h.isStandaloneNexusOperationEnabled(req.GetNamespace()) { return nil, ErrStandaloneNexusOperationDisabled } - return nil, serviceerror.NewUnimplemented("ListNexusOperationExecutions not implemented") + + pageSize := req.GetPageSize() + maxPageSize := int32(h.config.VisibilityMaxPageSize(req.GetNamespace())) + if pageSize <= 0 || pageSize > maxPageSize { + pageSize = maxPageSize + } + + resp, err := chasm.ListExecutions[*Operation, *emptypb.Empty](ctx, &chasm.ListExecutionsRequest{ + NamespaceName: req.GetNamespace(), + PageSize: int(pageSize), + NextPageToken: req.GetNextPageToken(), + Query: req.GetQuery(), + }) + if err != nil { + return nil, err + } + + operations := make([]*nexuspb.NexusOperationExecutionListInfo, 0, len(resp.Executions)) + for _, exec := range resp.Executions { + endpoint, _ := chasm.SearchAttributeValue(exec.ChasmSearchAttributes, EndpointSearchAttribute) + service, _ := chasm.SearchAttributeValue(exec.ChasmSearchAttributes, ServiceSearchAttribute) + operation, _ := chasm.SearchAttributeValue(exec.ChasmSearchAttributes, OperationSearchAttribute) + statusStr, _ := chasm.SearchAttributeValue(exec.ChasmSearchAttributes, StatusSearchAttribute) + status, _ := enumspb.NexusOperationExecutionStatusFromString(statusStr) + + var closeTime *timestamppb.Timestamp + var executionDuration *durationpb.Duration + if !exec.CloseTime.IsZero() { + closeTime = timestamppb.New(exec.CloseTime) + if !exec.StartTime.IsZero() { + executionDuration = durationpb.New(exec.CloseTime.Sub(exec.StartTime)) + } + } + + operations = append(operations, &nexuspb.NexusOperationExecutionListInfo{ + OperationId: exec.BusinessID, + RunId: exec.RunID, + Endpoint: endpoint, + Service: service, + Operation: operation, + Status: status, + ScheduleTime: timestamppb.New(exec.StartTime), + CloseTime: closeTime, + ExecutionDuration: executionDuration, + StateTransitionCount: exec.StateTransitionCount, + SearchAttributes: &commonpb.SearchAttributes{IndexedFields: exec.CustomSearchAttributes}, + }) + } + + return &workflowservice.ListNexusOperationExecutionsResponse{ + Operations: operations, + NextPageToken: resp.NextPageToken, + }, nil } -func (h *frontendHandler) CountNexusOperationExecutions(_ context.Context, req *workflowservice.CountNexusOperationExecutionsRequest) (*workflowservice.CountNexusOperationExecutionsResponse, error) { +func (h *frontendHandler) CountNexusOperationExecutions( + ctx context.Context, + req *workflowservice.CountNexusOperationExecutionsRequest, +) (*workflowservice.CountNexusOperationExecutionsResponse, error) { if !h.isStandaloneNexusOperationEnabled(req.GetNamespace()) { return nil, ErrStandaloneNexusOperationDisabled } - return nil, serviceerror.NewUnimplemented("CountNexusOperationExecutions not implemented") + + resp, err := chasm.CountExecutions[*Operation](ctx, &chasm.CountExecutionsRequest{ + NamespaceName: req.GetNamespace(), + Query: req.GetQuery(), + }) + if err != nil { + return nil, err + } + + groups := make([]*workflowservice.CountNexusOperationExecutionsResponse_AggregationGroup, 0, len(resp.Groups)) + for _, g := range resp.Groups { + groups = append(groups, &workflowservice.CountNexusOperationExecutionsResponse_AggregationGroup{ + GroupValues: g.Values, + Count: g.Count, + }) + } + + return &workflowservice.CountNexusOperationExecutionsResponse{ + Count: resp.Count, + Groups: groups, + }, nil } -func (h *frontendHandler) RequestCancelNexusOperationExecution(_ context.Context, req *workflowservice.RequestCancelNexusOperationExecutionRequest) (*workflowservice.RequestCancelNexusOperationExecutionResponse, error) { +func (h *frontendHandler) RequestCancelNexusOperationExecution( + _ context.Context, + req *workflowservice.RequestCancelNexusOperationExecutionRequest, +) (*workflowservice.RequestCancelNexusOperationExecutionResponse, error) { if !h.isStandaloneNexusOperationEnabled(req.GetNamespace()) { return nil, ErrStandaloneNexusOperationDisabled } return nil, serviceerror.NewUnimplemented("RequestCancelNexusOperationExecution not implemented") } -func (h *frontendHandler) TerminateNexusOperationExecution(_ context.Context, req *workflowservice.TerminateNexusOperationExecutionRequest) (*workflowservice.TerminateNexusOperationExecutionResponse, error) { +func (h *frontendHandler) TerminateNexusOperationExecution( + _ context.Context, + req *workflowservice.TerminateNexusOperationExecutionRequest, +) (*workflowservice.TerminateNexusOperationExecutionResponse, error) { if !h.isStandaloneNexusOperationEnabled(req.GetNamespace()) { return nil, ErrStandaloneNexusOperationDisabled } diff --git a/chasm/lib/nexusoperation/fx.go b/chasm/lib/nexusoperation/fx.go index 678558989c..ec5caeb11a 100644 --- a/chasm/lib/nexusoperation/fx.go +++ b/chasm/lib/nexusoperation/fx.go @@ -50,6 +50,12 @@ var FrontendModule = fx.Module( fx.Provide(configProvider), fx.Provide(nexusoperationpb.NewNexusOperationServiceLayeredClient), fx.Provide(NewFrontendHandler), + fx.Provide(newComponentOnlyLibrary), + fx.Invoke(func(l *componentOnlyLibrary, registry *chasm.Registry) error { + // Frontend needs to register the component in order to serialize ComponentRefs, but doesn't + // need task handlers. + return registry.Register(l) + }), ) func register( diff --git a/chasm/lib/nexusoperation/library.go b/chasm/lib/nexusoperation/library.go index 002c2368eb..c7e0fb8878 100644 --- a/chasm/lib/nexusoperation/library.go +++ b/chasm/lib/nexusoperation/library.go @@ -12,6 +12,10 @@ type componentOnlyLibrary struct { chasm.UnimplementedLibrary } +func newComponentOnlyLibrary() *componentOnlyLibrary { + return &componentOnlyLibrary{} +} + func (l *componentOnlyLibrary) Name() string { return "nexusoperation" } diff --git a/tests/nexus_standalone_test.go b/tests/nexus_standalone_test.go index 7429facded..1be5d00ebf 100644 --- a/tests/nexus_standalone_test.go +++ b/tests/nexus_standalone_test.go @@ -2,9 +2,12 @@ package tests import ( "cmp" + "fmt" "testing" "time" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" commonpb "go.temporal.io/api/common/v1" enumspb "go.temporal.io/api/enums/v1" nexuspb "go.temporal.io/api/nexus/v1" @@ -213,6 +216,362 @@ func createNexusEndpoint(s *testcore.TestEnv) string { return name } +func TestStandaloneNexusOperationList(t *testing.T) { + t.Parallel() + + t.Run("ListAndVerifyFields", func(t *testing.T) { + s := testcore.NewEnv(t, nexusStandaloneOpts...) + endpointName := createNexusEndpoint(s) + + startResp, err := startNexusOperation(s, &workflowservice.StartNexusOperationExecutionRequest{ + OperationId: "list-test-op", + Endpoint: endpointName, + }) + s.NoError(err) + + var listResp *workflowservice.ListNexusOperationExecutionsResponse + s.EventuallyWithT(func(t *assert.CollectT) { + var err error + listResp, err = s.FrontendClient().ListNexusOperationExecutions(s.Context(), &workflowservice.ListNexusOperationExecutionsRequest{ + Namespace: s.Namespace().String(), + Query: "OperationId = 'list-test-op'", + }) + require.NoError(t, err) + require.Len(t, listResp.GetOperations(), 1) + }, testcore.WaitForESToSettle, 100*time.Millisecond) + op := listResp.GetOperations()[0] + protorequire.ProtoEqual(t, &nexuspb.NexusOperationExecutionListInfo{ + OperationId: "list-test-op", + RunId: startResp.RunId, + Endpoint: endpointName, + Service: "test-service", + Operation: "test-operation", + Status: enumspb.NEXUS_OPERATION_EXECUTION_STATUS_RUNNING, + StateTransitionCount: op.GetStateTransitionCount(), + SearchAttributes: op.GetSearchAttributes(), + // Dynamic fields copied from actual response for comparison. + ScheduleTime: op.GetScheduleTime(), + }, op) + require.NotNil(t, op.GetScheduleTime()) + }) + + t.Run("ListWithQueryFilter", func(t *testing.T) { + s := testcore.NewEnv(t, nexusStandaloneOpts...) + endpointA := createNexusEndpoint(s) + endpointB := createNexusEndpoint(s) + + _, err := startNexusOperation(s, &workflowservice.StartNexusOperationExecutionRequest{ + OperationId: "filter-op-1", + Endpoint: endpointA, + }) + s.NoError(err) + + _, err = startNexusOperation(s, &workflowservice.StartNexusOperationExecutionRequest{ + OperationId: "filter-op-2", + Endpoint: endpointB, + }) + s.NoError(err) + + var listResp *workflowservice.ListNexusOperationExecutionsResponse + s.EventuallyWithT(func(t *assert.CollectT) { + var err error + listResp, err = s.FrontendClient().ListNexusOperationExecutions(s.Context(), &workflowservice.ListNexusOperationExecutionsRequest{ + Namespace: s.Namespace().String(), + Query: fmt.Sprintf("Endpoint = '%s'", endpointA), + }) + require.NoError(t, err) + require.Len(t, listResp.GetOperations(), 1) + }, testcore.WaitForESToSettle, 100*time.Millisecond) + require.Equal(t, "filter-op-1", listResp.GetOperations()[0].GetOperationId()) + }) + + t.Run("ListWithCustomSearchAttributes", func(t *testing.T) { + s := testcore.NewEnv(t, nexusStandaloneOpts...) + endpointName := createNexusEndpoint(s) + + testSA := &commonpb.SearchAttributes{ + IndexedFields: map[string]*commonpb.Payload{ + "CustomKeywordField": payload.EncodeString("list-sa-value"), + }, + } + _, err := startNexusOperation(s, &workflowservice.StartNexusOperationExecutionRequest{ + OperationId: "sa-op", + Endpoint: endpointName, + SearchAttributes: testSA, + }) + s.NoError(err) + + var listResp *workflowservice.ListNexusOperationExecutionsResponse + s.EventuallyWithT(func(t *assert.CollectT) { + var err error + listResp, err = s.FrontendClient().ListNexusOperationExecutions(s.Context(), &workflowservice.ListNexusOperationExecutionsRequest{ + Namespace: s.Namespace().String(), + Query: "CustomKeywordField = 'list-sa-value'", + }) + require.NoError(t, err) + require.Len(t, listResp.GetOperations(), 1) + }, testcore.WaitForESToSettle, 100*time.Millisecond) + require.Equal(t, "sa-op", listResp.GetOperations()[0].GetOperationId()) + returnedSA := listResp.GetOperations()[0].GetSearchAttributes().GetIndexedFields()["CustomKeywordField"] + require.NotNil(t, returnedSA) + var returnedValue string + require.NoError(t, payload.Decode(returnedSA, &returnedValue)) + require.Equal(t, "list-sa-value", returnedValue) + }) + + t.Run("QueryByExecutionStatus", func(t *testing.T) { + s := testcore.NewEnv(t, nexusStandaloneOpts...) + endpointName := createNexusEndpoint(s) + + _, err := startNexusOperation(s, &workflowservice.StartNexusOperationExecutionRequest{ + OperationId: "status-op", + Endpoint: endpointName, + }) + s.NoError(err) + + var listResp *workflowservice.ListNexusOperationExecutionsResponse + s.EventuallyWithT(func(t *assert.CollectT) { + var err error + listResp, err = s.FrontendClient().ListNexusOperationExecutions(s.Context(), &workflowservice.ListNexusOperationExecutionsRequest{ + Namespace: s.Namespace().String(), + Query: "ExecutionStatus = 'Running' AND OperationId = 'status-op'", + }) + require.NoError(t, err) + require.Len(t, listResp.GetOperations(), 1) + }, testcore.WaitForESToSettle, 100*time.Millisecond) + require.Equal(t, "status-op", listResp.GetOperations()[0].GetOperationId()) + }) + + t.Run("QueryByMultipleFields", func(t *testing.T) { + s := testcore.NewEnv(t, nexusStandaloneOpts...) + endpointName := createNexusEndpoint(s) + + _, err := startNexusOperation(s, &workflowservice.StartNexusOperationExecutionRequest{ + OperationId: "multi-op", + Endpoint: endpointName, + Service: "multi-service", + }) + s.NoError(err) + + var listResp *workflowservice.ListNexusOperationExecutionsResponse + s.EventuallyWithT(func(t *assert.CollectT) { + var err error + listResp, err = s.FrontendClient().ListNexusOperationExecutions(s.Context(), &workflowservice.ListNexusOperationExecutionsRequest{ + Namespace: s.Namespace().String(), + Query: fmt.Sprintf("Endpoint = '%s' AND Service = 'multi-service'", endpointName), + }) + require.NoError(t, err) + require.Len(t, listResp.GetOperations(), 1) + }, testcore.WaitForESToSettle, 100*time.Millisecond) + require.Equal(t, "multi-op", listResp.GetOperations()[0].GetOperationId()) + }) + + t.Run("PageSizeCapping", func(t *testing.T) { + s := testcore.NewEnv(t, nexusStandaloneOpts...) + endpointName := createNexusEndpoint(s) + + for i := range 2 { + _, err := startNexusOperation(s, &workflowservice.StartNexusOperationExecutionRequest{ + OperationId: fmt.Sprintf("paged-op-%d", i), + Endpoint: endpointName, + }) + s.NoError(err) + } + + // Wait for both to be indexed. + query := fmt.Sprintf("Endpoint = '%s'", endpointName) + s.EventuallyWithT(func(t *assert.CollectT) { + resp, err := s.FrontendClient().ListNexusOperationExecutions(s.Context(), &workflowservice.ListNexusOperationExecutionsRequest{ + Namespace: s.Namespace().String(), + Query: query, + }) + require.NoError(t, err) + require.Len(t, resp.GetOperations(), 2) + }, testcore.WaitForESToSettle, 100*time.Millisecond) + + // Override max page size to 1. + cleanup := s.OverrideDynamicConfig(dynamicconfig.FrontendVisibilityMaxPageSize, 1) + defer cleanup() + + // PageSize 0 should default to max (1), returning only 1 result. + resp, err := s.FrontendClient().ListNexusOperationExecutions(s.Context(), &workflowservice.ListNexusOperationExecutionsRequest{ + Namespace: s.Namespace().String(), + PageSize: 0, + Query: query, + }) + require.NoError(t, err) + require.Len(t, resp.GetOperations(), 1) + + // PageSize > max should also be capped. First page. + resp, err = s.FrontendClient().ListNexusOperationExecutions(s.Context(), &workflowservice.ListNexusOperationExecutionsRequest{ + Namespace: s.Namespace().String(), + PageSize: 2, + Query: query, + }) + require.NoError(t, err) + require.Len(t, resp.GetOperations(), 1) + + // Second page. + resp, err = s.FrontendClient().ListNexusOperationExecutions(s.Context(), &workflowservice.ListNexusOperationExecutionsRequest{ + Namespace: s.Namespace().String(), + PageSize: 2, + Query: query, + NextPageToken: resp.GetNextPageToken(), + }) + require.NoError(t, err) + require.Len(t, resp.GetOperations(), 1) + + // No more results. + resp, err = s.FrontendClient().ListNexusOperationExecutions(s.Context(), &workflowservice.ListNexusOperationExecutionsRequest{ + Namespace: s.Namespace().String(), + PageSize: 2, + Query: query, + NextPageToken: resp.GetNextPageToken(), + }) + require.NoError(t, err) + require.Empty(t, resp.GetOperations()) + require.Nil(t, resp.GetNextPageToken()) + }) + + t.Run("InvalidQuery", func(t *testing.T) { + s := testcore.NewEnv(t, nexusStandaloneOpts...) + + _, err := s.FrontendClient().ListNexusOperationExecutions(s.Context(), &workflowservice.ListNexusOperationExecutionsRequest{ + Namespace: s.Namespace().String(), + Query: "invalid query syntax !!!", + }) + s.ErrorAs(err, new(*serviceerror.InvalidArgument)) + s.ErrorContains(err, "invalid query") + }) + + t.Run("InvalidSearchAttribute", func(t *testing.T) { + s := testcore.NewEnv(t, nexusStandaloneOpts...) + + _, err := s.FrontendClient().ListNexusOperationExecutions(s.Context(), &workflowservice.ListNexusOperationExecutionsRequest{ + Namespace: s.Namespace().String(), + Query: "NonExistentField = 'value'", + }) + s.ErrorAs(err, new(*serviceerror.InvalidArgument)) + s.ErrorContains(err, "NonExistentField") + }) + + t.Run("NamespaceNotFound", func(t *testing.T) { + s := testcore.NewEnv(t, nexusStandaloneOpts...) + + _, err := s.FrontendClient().ListNexusOperationExecutions(s.Context(), &workflowservice.ListNexusOperationExecutionsRequest{ + Namespace: "non-existent-namespace", + }) + s.ErrorAs(err, new(*serviceerror.NamespaceNotFound)) + s.ErrorContains(err, "non-existent-namespace") + }) +} + +func TestStandaloneNexusOperationCount(t *testing.T) { + t.Parallel() + + t.Run("CountByOperationId", func(t *testing.T) { + s := testcore.NewEnv(t, nexusStandaloneOpts...) + endpointName := createNexusEndpoint(s) + + _, err := startNexusOperation(s, &workflowservice.StartNexusOperationExecutionRequest{ + OperationId: "count-op", + Endpoint: endpointName, + }) + s.NoError(err) + + s.EventuallyWithT(func(t *assert.CollectT) { + resp, err := s.FrontendClient().CountNexusOperationExecutions(s.Context(), &workflowservice.CountNexusOperationExecutionsRequest{ + Namespace: s.Namespace().String(), + Query: "OperationId = 'count-op'", + }) + require.NoError(t, err) + require.Equal(t, int64(1), resp.GetCount()) + }, testcore.WaitForESToSettle, 100*time.Millisecond) + }) + + t.Run("CountByEndpoint", func(t *testing.T) { + s := testcore.NewEnv(t, nexusStandaloneOpts...) + endpointName := createNexusEndpoint(s) + + for i := range 3 { + _, err := startNexusOperation(s, &workflowservice.StartNexusOperationExecutionRequest{ + OperationId: fmt.Sprintf("count-ep-op-%d", i), + Endpoint: endpointName, + }) + s.NoError(err) + } + + s.EventuallyWithT(func(t *assert.CollectT) { + resp, err := s.FrontendClient().CountNexusOperationExecutions(s.Context(), &workflowservice.CountNexusOperationExecutionsRequest{ + Namespace: s.Namespace().String(), + Query: fmt.Sprintf("Endpoint = '%s'", endpointName), + }) + require.NoError(t, err) + require.Equal(t, int64(3), resp.GetCount()) + }, testcore.WaitForESToSettle, 100*time.Millisecond) + }) + + t.Run("CountByExecutionStatus", func(t *testing.T) { + s := testcore.NewEnv(t, nexusStandaloneOpts...) + endpointName := createNexusEndpoint(s) + + _, err := startNexusOperation(s, &workflowservice.StartNexusOperationExecutionRequest{ + OperationId: "count-status-op", + Endpoint: endpointName, + }) + s.NoError(err) + + s.EventuallyWithT(func(t *assert.CollectT) { + resp, err := s.FrontendClient().CountNexusOperationExecutions(s.Context(), &workflowservice.CountNexusOperationExecutionsRequest{ + Namespace: s.Namespace().String(), + Query: fmt.Sprintf("ExecutionStatus = 'Running' AND Endpoint = '%s'", endpointName), + }) + require.NoError(t, err) + require.Equal(t, int64(1), resp.GetCount()) + }, testcore.WaitForESToSettle, 100*time.Millisecond) + }) + + t.Run("GroupByExecutionStatus", func(t *testing.T) { + s := testcore.NewEnv(t, nexusStandaloneOpts...) + endpointName := createNexusEndpoint(s) + + for i := range 3 { + _, err := startNexusOperation(s, &workflowservice.StartNexusOperationExecutionRequest{ + OperationId: fmt.Sprintf("group-op-%d", i), + Endpoint: endpointName, + }) + s.NoError(err) + } + + var countResp *workflowservice.CountNexusOperationExecutionsResponse + s.EventuallyWithT(func(t *assert.CollectT) { + var err error + countResp, err = s.FrontendClient().CountNexusOperationExecutions(s.Context(), &workflowservice.CountNexusOperationExecutionsRequest{ + Namespace: s.Namespace().String(), + Query: fmt.Sprintf("Endpoint = '%s' GROUP BY ExecutionStatus", endpointName), + }) + require.NoError(t, err) + require.Equal(t, int64(3), countResp.GetCount()) + }, testcore.WaitForESToSettle, 100*time.Millisecond) + require.Len(t, countResp.GetGroups(), 1) + require.Equal(t, int64(3), countResp.GetGroups()[0].GetCount()) + var groupValue string + require.NoError(t, payload.Decode(countResp.GetGroups()[0].GetGroupValues()[0], &groupValue)) + require.Equal(t, "Running", groupValue) + }) + + t.Run("GroupByUnsupportedField", func(t *testing.T) { + s := testcore.NewEnv(t, nexusStandaloneOpts...) + + _, err := s.FrontendClient().CountNexusOperationExecutions(s.Context(), &workflowservice.CountNexusOperationExecutionsRequest{ + Namespace: s.Namespace().String(), + Query: "GROUP BY Endpoint", + }) + s.ErrorAs(err, new(*serviceerror.InvalidArgument)) + s.ErrorContains(err, "'GROUP BY' clause is only supported for ExecutionStatus") + }) +} + func startNexusOperation( s *testcore.TestEnv, req *workflowservice.StartNexusOperationExecutionRequest, From 449e90d786c3780d901012507d52e7c3c76d75a9 Mon Sep 17 00:00:00 2001 From: Stephan Behnke Date: Mon, 6 Apr 2026 17:06:26 -0700 Subject: [PATCH 05/11] Nexus Standalone: Terminate + Cancel (#9624) Co-Authored-By: Claude Opus 4.6 (1M context) --- chasm/lib/nexusoperation/config.go | 11 + chasm/lib/nexusoperation/frontend.go | 48 ++- chasm/lib/nexusoperation/handler.go | 60 ++++ chasm/lib/nexusoperation/operation.go | 142 ++++++--- .../nexusoperation/operation_statemachine.go | 34 +++ .../operation_statemachine_test.go | 45 ++- chasm/lib/nexusoperation/validator.go | 102 ++++++- chasm/lib/nexusoperation/validator_test.go | 214 +++++++++++++- chasm/lib/nexusoperation/workflow/events.go | 4 +- tests/nexus_standalone_test.go | 279 ++++++++++++++++++ 10 files changed, 862 insertions(+), 77 deletions(-) diff --git a/chasm/lib/nexusoperation/config.go b/chasm/lib/nexusoperation/config.go index 37b9a67fcd..c50da27ed4 100644 --- a/chasm/lib/nexusoperation/config.go +++ b/chasm/lib/nexusoperation/config.go @@ -186,6 +186,13 @@ var UseSystemCallbackURL = dynamicconfig.NewGlobalBoolSetting( When true, uses the fixed system callback URL for all worker targets.`, ) +var MaxReasonLength = dynamicconfig.NewNamespaceIntSetting( + "nexusoperation.limit.reasonLength", + 1000, + `Limits the maximum allowed length for a reason string in Nexus operation requests. +Uses Go's len() function to determine the length.`, +) + var UseNewFailureWireFormat = dynamicconfig.NewNamespaceBoolSetting( "nexusoperation.useNewFailureWireFormat", true, @@ -210,10 +217,12 @@ type Config struct { PayloadSizeLimit dynamicconfig.IntPropertyFnWithNamespaceFilter CallbackURLTemplate dynamicconfig.TypedPropertyFn[*template.Template] UseSystemCallbackURL dynamicconfig.BoolPropertyFn + PayloadSizeLimitWarn dynamicconfig.IntPropertyFnWithNamespaceFilter UseNewFailureWireFormat dynamicconfig.BoolPropertyFnWithNamespaceFilter RecordCancelRequestCompletionEvents dynamicconfig.BoolPropertyFn VisibilityMaxPageSize dynamicconfig.IntPropertyFnWithNamespaceFilter MaxIDLengthLimit dynamicconfig.IntPropertyFn + MaxReasonLength dynamicconfig.IntPropertyFnWithNamespaceFilter RetryPolicy func() backoff.RetryPolicy } @@ -233,11 +242,13 @@ func configProvider(dc *dynamicconfig.Collection, cfg *config.Persistence) *Conf DisallowedOperationHeaders: DisallowedOperationHeaders.Get(dc), MaxOperationScheduleToCloseTimeout: MaxOperationScheduleToCloseTimeout.Get(dc), PayloadSizeLimit: dynamicconfig.BlobSizeLimitError.Get(dc), + PayloadSizeLimitWarn: dynamicconfig.BlobSizeLimitWarn.Get(dc), CallbackURLTemplate: CallbackURLTemplate.Get(dc), UseSystemCallbackURL: UseSystemCallbackURL.Get(dc), UseNewFailureWireFormat: UseNewFailureWireFormat.Get(dc), VisibilityMaxPageSize: dynamicconfig.FrontendVisibilityMaxPageSize.Get(dc), MaxIDLengthLimit: dynamicconfig.MaxIDLengthLimit.Get(dc), + MaxReasonLength: MaxReasonLength.Get(dc), RetryPolicy: RetryPolicy.Get(dc), } } diff --git a/chasm/lib/nexusoperation/frontend.go b/chasm/lib/nexusoperation/frontend.go index 8f9c440e99..c30b56a7a4 100644 --- a/chasm/lib/nexusoperation/frontend.go +++ b/chasm/lib/nexusoperation/frontend.go @@ -81,7 +81,7 @@ func (h *frontendHandler) StartNexusOperationExecution( return nil, err } - if err := validateAndNormalizeStartRequest(req, h.config, h.saMapperProvider, h.saValidator); err != nil { + if err := validateAndNormalizeStartRequest(req, h.config, h.logger, h.saMapperProvider, h.saValidator); err != nil { return nil, err } @@ -110,7 +110,7 @@ func (h *frontendHandler) DescribeNexusOperationExecution( return nil, err } - if err := validateDescribeNexusOperationExecutionRequest(req, h.config); err != nil { + if err := validateAndNormalizeDescribeRequest(req, h.config); err != nil { return nil, err } @@ -221,23 +221,59 @@ func (h *frontendHandler) CountNexusOperationExecutions( } func (h *frontendHandler) RequestCancelNexusOperationExecution( - _ context.Context, + ctx context.Context, req *workflowservice.RequestCancelNexusOperationExecutionRequest, ) (*workflowservice.RequestCancelNexusOperationExecutionResponse, error) { if !h.isStandaloneNexusOperationEnabled(req.GetNamespace()) { return nil, ErrStandaloneNexusOperationDisabled } - return nil, serviceerror.NewUnimplemented("RequestCancelNexusOperationExecution not implemented") + + namespaceID, err := h.namespaceRegistry.GetNamespaceID(namespace.Name(req.GetNamespace())) + if err != nil { + return nil, err + } + + if err := validateAndNormalizeCancelRequest(req, h.config); err != nil { + return nil, err + } + + _, err = h.client.RequestCancelNexusOperation(ctx, &nexusoperationpb.RequestCancelNexusOperationRequest{ + NamespaceId: namespaceID.String(), + FrontendRequest: req, + }) + if err != nil { + return nil, err + } + + return &workflowservice.RequestCancelNexusOperationExecutionResponse{}, nil } func (h *frontendHandler) TerminateNexusOperationExecution( - _ context.Context, + ctx context.Context, req *workflowservice.TerminateNexusOperationExecutionRequest, ) (*workflowservice.TerminateNexusOperationExecutionResponse, error) { if !h.isStandaloneNexusOperationEnabled(req.GetNamespace()) { return nil, ErrStandaloneNexusOperationDisabled } - return nil, serviceerror.NewUnimplemented("TerminateNexusOperationExecution not implemented") + + namespaceID, err := h.namespaceRegistry.GetNamespaceID(namespace.Name(req.GetNamespace())) + if err != nil { + return nil, err + } + + if err := validateAndNormalizeTerminateRequest(req, h.config); err != nil { + return nil, err + } + + _, err = h.client.TerminateNexusOperation(ctx, &nexusoperationpb.TerminateNexusOperationRequest{ + NamespaceId: namespaceID.String(), + FrontendRequest: req, + }) + if err != nil { + return nil, err + } + + return &workflowservice.TerminateNexusOperationExecutionResponse{}, nil } func (h *frontendHandler) DeleteNexusOperationExecution(_ context.Context, req *workflowservice.DeleteNexusOperationExecutionRequest) (*workflowservice.DeleteNexusOperationExecutionResponse, error) { diff --git a/chasm/lib/nexusoperation/handler.go b/chasm/lib/nexusoperation/handler.go index 87629e2dab..598c31d164 100644 --- a/chasm/lib/nexusoperation/handler.go +++ b/chasm/lib/nexusoperation/handler.go @@ -75,6 +75,66 @@ func (h *handler) DescribeNexusOperation( return chasm.ReadComponent(ctx, ref, (*Operation).buildDescribeResponse, req, nil) } +// RequestCancelNexusOperation requests cancellation of a standalone Nexus operation via CHASM. +func (h *handler) RequestCancelNexusOperation( + ctx context.Context, + req *nexusoperationpb.RequestCancelNexusOperationRequest, +) (response *nexusoperationpb.RequestCancelNexusOperationResponse, err error) { + defer log.CapturePanic(h.logger, &err) + + ref := chasm.NewComponentRef[*Operation](chasm.ExecutionKey{ + NamespaceID: req.GetNamespaceId(), + BusinessID: req.GetFrontendRequest().GetOperationId(), + RunID: req.GetFrontendRequest().GetRunId(), + }) + + resp, _, err := chasm.UpdateComponent( + ctx, + ref, + func(o *Operation, ctx chasm.MutableContext, req *nexusoperationpb.RequestCancelNexusOperationRequest) (*nexusoperationpb.RequestCancelNexusOperationResponse, error) { + if err := o.RequestCancel(ctx, &nexusoperationpb.CancellationState{ + RequestId: req.GetFrontendRequest().GetRequestId(), + Identity: req.GetFrontendRequest().GetIdentity(), + Reason: req.GetFrontendRequest().GetReason(), + }); err != nil { + return nil, err + } + return &nexusoperationpb.RequestCancelNexusOperationResponse{}, nil + }, req) + + return resp, err +} + +// TerminateNexusOperation terminates a standalone Nexus operation via CHASM. +func (h *handler) TerminateNexusOperation( + ctx context.Context, + req *nexusoperationpb.TerminateNexusOperationRequest, +) (response *nexusoperationpb.TerminateNexusOperationResponse, err error) { + defer log.CapturePanic(h.logger, &err) + + ref := chasm.NewComponentRef[*Operation](chasm.ExecutionKey{ + NamespaceID: req.GetNamespaceId(), + BusinessID: req.GetFrontendRequest().GetOperationId(), + RunID: req.GetFrontendRequest().GetRunId(), + }) + + resp, _, err := chasm.UpdateComponent( + ctx, + ref, + func(o *Operation, ctx chasm.MutableContext, req *nexusoperationpb.TerminateNexusOperationRequest) (*nexusoperationpb.TerminateNexusOperationResponse, error) { + if _, err := o.Terminate(ctx, chasm.TerminateComponentRequest{ + RequestID: req.GetFrontendRequest().GetRequestId(), + Identity: req.GetFrontendRequest().GetIdentity(), + Reason: req.GetFrontendRequest().GetReason(), + }); err != nil { + return nil, err + } + return &nexusoperationpb.TerminateNexusOperationResponse{}, nil + }, req) + + return resp, err +} + func idReusePolicyFromProto(p enumspb.NexusOperationIdReusePolicy) chasm.BusinessIDReusePolicy { switch p { case enumspb.NEXUS_OPERATION_ID_REUSE_POLICY_ALLOW_DUPLICATE_FAILED_ONLY: diff --git a/chasm/lib/nexusoperation/operation.go b/chasm/lib/nexusoperation/operation.go index 2d08c1434d..a4468217a6 100644 --- a/chasm/lib/nexusoperation/operation.go +++ b/chasm/lib/nexusoperation/operation.go @@ -1,6 +1,7 @@ package nexusoperation import ( + "fmt" "time" "github.com/google/uuid" @@ -14,7 +15,7 @@ import ( "go.temporal.io/server/chasm" nexusoperationpb "go.temporal.io/server/chasm/lib/nexusoperation/gen/nexusoperationpb/v1" "go.temporal.io/server/common/backoff" - "google.golang.org/protobuf/types/known/anypb" + queueserrors "go.temporal.io/server/service/history/queues/errors" "google.golang.org/protobuf/types/known/durationpb" "google.golang.org/protobuf/types/known/timestamppb" ) @@ -31,20 +32,15 @@ var _ chasm.RootComponent = (*Operation)(nil) var _ chasm.StateMachine[nexusoperationpb.OperationStatus] = (*Operation)(nil) var _ chasm.VisibilitySearchAttributesProvider = (*Operation)(nil) -// ErrCancellationAlreadyRequested is returned when a cancellation has already been requested for an operation. var ErrCancellationAlreadyRequested = serviceerror.NewFailedPrecondition("cancellation already requested") - -// ErrOperationAlreadyCompleted is returned when trying to cancel an operation that has already completed. var ErrOperationAlreadyCompleted = serviceerror.NewFailedPrecondition("operation already completed") -// InvocationData contains data needed to invoke a Nexus operation. type InvocationData struct { Input *commonpb.Payload Header map[string]string NexusLink nexus.Link } -// OperationStore defines the interface that must be implemented by any parent component that wants to manage Nexus operations. type OperationStore interface { OnNexusOperationStarted(ctx chasm.MutableContext, operation *Operation, operationToken string, links []*commonpb.Link) error OnNexusOperationCanceled(ctx chasm.MutableContext, operation *Operation, cause *failurepb.Failure) error @@ -57,16 +53,15 @@ type OperationStore interface { NexusOperationInvocationData(ctx chasm.Context, operation *Operation) (InvocationData, error) } -// Operation is a CHASM component that represents a Nexus operation. type Operation struct { chasm.UnimplementedComponent *nexusoperationpb.OperationState Store chasm.ParentPtr[OperationStore] - // Only used for standalone Nexus operations. Workflow operations keep request data in history. RequestData chasm.Field[*nexusoperationpb.OperationRequestData] Cancellation chasm.Field[*Cancellation] + Outcome chasm.Field[*nexusoperationpb.OperationOutcome] Visibility chasm.Field[*chasm.Visibility] } @@ -93,6 +88,7 @@ func newStandaloneOperation( UserMetadata: frontendReq.GetUserMetadata(), Identity: frontendReq.GetIdentity(), }) + op.Outcome = chasm.NewDataField(ctx, &nexusoperationpb.OperationOutcome{}) op.Visibility = chasm.NewComponentField(ctx, chasm.NewVisibilityWithData( ctx, frontendReq.GetSearchAttributes().GetIndexedFields(), @@ -110,7 +106,8 @@ func (o *Operation) LifecycleState(_ chasm.Context) chasm.LifecycleState { return chasm.LifecycleStateCompleted case nexusoperationpb.OPERATION_STATUS_FAILED, nexusoperationpb.OPERATION_STATUS_CANCELED, - nexusoperationpb.OPERATION_STATUS_TIMED_OUT: + nexusoperationpb.OPERATION_STATUS_TIMED_OUT, + nexusoperationpb.OPERATION_STATUS_TERMINATED: return chasm.LifecycleStateFailed default: return chasm.LifecycleStateRunning @@ -129,22 +126,27 @@ func (o *Operation) SetStateMachineState(status nexusoperationpb.OperationStatus o.Status = status } -func (o *Operation) Cancel(ctx chasm.MutableContext, parentData *anypb.Any) error { +func (o *Operation) RequestCancel( + ctx chasm.MutableContext, + req *nexusoperationpb.CancellationState, +) error { if !TransitionCanceled.Possible(o) { return ErrOperationAlreadyCompleted } - if _, ok := o.Cancellation.TryGet(ctx); ok { - return ErrCancellationAlreadyRequested - } - cancellation := newCancellation(&nexusoperationpb.CancellationState{ - RequestedTime: timestamppb.New(ctx.Now(o)), - ParentData: parentData, - }) - o.Cancellation = chasm.NewComponentField(ctx, cancellation) + if existingCancellation, ok := o.Cancellation.TryGet(ctx); ok { + existingReqID := existingCancellation.GetRequestId() + newReqID := req.GetRequestId() + if existingReqID != newReqID { + return fmt.Errorf("%w with request ID %s", ErrCancellationAlreadyRequested, existingReqID) + } + return nil + } + cancel := newCancellation(req) + o.Cancellation = chasm.NewComponentField(ctx, cancel) if o.Status == nexusoperationpb.OPERATION_STATUS_STARTED { - return TransitionCancellationScheduled.Apply(cancellation, ctx, EventCancellationScheduled{ + return TransitionCancellationScheduled.Apply(cancel, ctx, EventCancellationScheduled{ Destination: o.GetEndpoint(), }) } @@ -152,43 +154,56 @@ func (o *Operation) Cancel(ctx chasm.MutableContext, parentData *anypb.Any) erro } func (o *Operation) onStarted(ctx chasm.MutableContext, operationToken string, links []*commonpb.Link) error { - store, ok := o.Store.TryGet(ctx) - if ok { + if store, ok := o.Store.TryGet(ctx); ok { return store.OnNexusOperationStarted(ctx, o, operationToken, links) } return TransitionStarted.Apply(o, ctx, EventStarted{OperationToken: operationToken}) } func (o *Operation) onCompleted(ctx chasm.MutableContext, result *commonpb.Payload, links []*commonpb.Link) error { - store, ok := o.Store.TryGet(ctx) - if ok { + if store, ok := o.Store.TryGet(ctx); ok { return store.OnNexusOperationCompleted(ctx, o, result, links) } + outcome := o.outcome(ctx) + outcome.Variant = &nexusoperationpb.OperationOutcome_Successful_{ + Successful: &nexusoperationpb.OperationOutcome_Successful{Result: result}, + } return TransitionSucceeded.Apply(o, ctx, EventSucceeded{}) } func (o *Operation) onFailed(ctx chasm.MutableContext, cause *failurepb.Failure) error { - store, ok := o.Store.TryGet(ctx) - if ok { + if store, ok := o.Store.TryGet(ctx); ok { return store.OnNexusOperationFailed(ctx, o, cause) } + if cause != nil { + o.outcome(ctx).Variant = &nexusoperationpb.OperationOutcome_Failed_{ + Failed: &nexusoperationpb.OperationOutcome_Failed{Failure: cause}, + } + } return TransitionFailed.Apply(o, ctx, EventFailed{Failure: cause}) } func (o *Operation) onCanceled(ctx chasm.MutableContext, cause *failurepb.Failure) error { - store, ok := o.Store.TryGet(ctx) - if ok { + if store, ok := o.Store.TryGet(ctx); ok { return store.OnNexusOperationCanceled(ctx, o, cause) } + if cause != nil { + o.outcome(ctx).Variant = &nexusoperationpb.OperationOutcome_Failed_{ + Failed: &nexusoperationpb.OperationOutcome_Failed{Failure: cause}, + } + } return TransitionCanceled.Apply(o, ctx, EventCanceled{Failure: cause}) } func (o *Operation) onTimedOut(ctx chasm.MutableContext, cause *failurepb.Failure) error { - store, ok := o.Store.TryGet(ctx) - if ok { + if store, ok := o.Store.TryGet(ctx); ok { return store.OnNexusOperationTimedOut(ctx, o, cause) } - _ = cause + if cause != nil { + o.outcome(ctx).Variant = &nexusoperationpb.OperationOutcome_Failed_{ + Failed: &nexusoperationpb.OperationOutcome_Failed{Failure: cause}, + } + } return TransitionTimedOut.Apply(o, ctx, EventTimedOut{}) } @@ -264,7 +279,7 @@ func (o *Operation) saveInvocationResult( RetryPolicy: input.retryPolicy, }) default: - return nil, serviceerror.NewInternalf("cannot save invocation result of type %T", r) + return nil, queueserrors.NewUnprocessableTaskError(fmt.Sprintf("unrecognized invocation result %T", r)) } } @@ -275,11 +290,35 @@ func (o *Operation) resolveUnsuccessfully(ctx chasm.MutableContext, failure *fai } o.ClosedTime = timestamppb.New(closeTime) o.NextAttemptScheduleTime = nil + if failure != nil { + o.outcome(ctx).Variant = &nexusoperationpb.OperationOutcome_Failed_{ + Failed: &nexusoperationpb.OperationOutcome_Failed{Failure: failure}, + } + } return nil } -func (o *Operation) Terminate(_ chasm.MutableContext, _ chasm.TerminateComponentRequest) (chasm.TerminateComponentResponse, error) { - return chasm.TerminateComponentResponse{}, serviceerror.NewUnimplemented("not implemented") +func (o *Operation) outcome(ctx chasm.MutableContext) *nexusoperationpb.OperationOutcome { + if outcome, ok := o.Outcome.TryGet(ctx); ok { + return outcome + } + outcome := &nexusoperationpb.OperationOutcome{} + o.Outcome = chasm.NewDataField(ctx, outcome) + return outcome +} + +func (o *Operation) Terminate( + ctx chasm.MutableContext, + req chasm.TerminateComponentRequest, +) (chasm.TerminateComponentResponse, error) { + if o.GetTerminateState() != nil { + if existingReqID := o.TerminateState.GetRequestId(); existingReqID != req.RequestID { + return chasm.TerminateComponentResponse{}, + serviceerror.NewFailedPreconditionf("already terminated with request ID %s", existingReqID) + } + return chasm.TerminateComponentResponse{}, nil + } + return chasm.TerminateComponentResponse{}, TransitionTerminated.Apply(o, ctx, EventTerminated{TerminateComponentRequest: req}) } func (o *Operation) SearchAttributes(_ chasm.Context) []chasm.SearchAttributeKeyValue { @@ -295,18 +334,30 @@ func (o *Operation) buildDescribeResponse( ctx chasm.Context, req *nexusoperationpb.DescribeNexusOperationRequest, ) (*nexusoperationpb.DescribeNexusOperationResponse, error) { - var input *commonpb.Payload + resp := &workflowservice.DescribeNexusOperationExecutionResponse{ + RunId: ctx.ExecutionKey().RunID, + Info: o.buildExecutionInfo(ctx), + } if req.GetFrontendRequest().GetIncludeInput() { - input = o.RequestData.Get(ctx).GetInput() + resp.Input = o.RequestData.Get(ctx).GetInput() } - - return &nexusoperationpb.DescribeNexusOperationResponse{ - FrontendResponse: &workflowservice.DescribeNexusOperationExecutionResponse{ - RunId: ctx.ExecutionKey().RunID, - Info: o.buildExecutionInfo(ctx), - Input: input, - }, - }, nil + if req.GetFrontendRequest().GetIncludeOutcome() && o.LifecycleState(ctx).IsClosed() { + outcome := o.Outcome.Get(ctx) + if successful := outcome.GetSuccessful(); successful != nil { + resp.Outcome = &workflowservice.DescribeNexusOperationExecutionResponse_Result{ + Result: successful.GetResult(), + } + } else if failure := outcome.GetFailed().GetFailure(); failure != nil { + resp.Outcome = &workflowservice.DescribeNexusOperationExecutionResponse_Failure{ + Failure: failure, + } + } else if o.LastAttemptFailure != nil { + resp.Outcome = &workflowservice.DescribeNexusOperationExecutionResponse_Failure{ + Failure: o.LastAttemptFailure, + } + } + } + return &nexusoperationpb.DescribeNexusOperationResponse{FrontendResponse: resp}, nil } func (o *Operation) buildExecutionInfo(ctx chasm.Context) *nexuspb.NexusOperationExecutionInfo { @@ -343,7 +394,6 @@ func (o *Operation) buildExecutionInfo(ctx chasm.Context) *nexuspb.NexusOperatio if o.ScheduleToCloseTimeout != nil { info.ExpirationTime = timestamppb.New(o.ScheduledTime.AsTime().Add(o.ScheduleToCloseTimeout.AsDuration())) } - if closeTime := o.closeTime(ctx); closeTime != nil { info.CloseTime = closeTime info.ExecutionDuration = durationpb.New(closeTime.AsTime().Sub(o.ScheduledTime.AsTime())) @@ -359,7 +409,7 @@ func (o *Operation) closeTime(ctx chasm.Context) *timestamppb.Timestamp { if !o.LifecycleState(ctx).IsClosed() { return nil } - return o.LastAttemptCompleteTime + return o.ClosedTime } func operationExecutionStatus(status nexusoperationpb.OperationStatus) enumspb.NexusOperationExecutionStatus { @@ -376,6 +426,8 @@ func operationExecutionStatus(status nexusoperationpb.OperationStatus) enumspb.N return enumspb.NEXUS_OPERATION_EXECUTION_STATUS_CANCELED case nexusoperationpb.OPERATION_STATUS_TIMED_OUT: return enumspb.NEXUS_OPERATION_EXECUTION_STATUS_TIMED_OUT + case nexusoperationpb.OPERATION_STATUS_TERMINATED: + return enumspb.NEXUS_OPERATION_EXECUTION_STATUS_TERMINATED default: return enumspb.NEXUS_OPERATION_EXECUTION_STATUS_UNSPECIFIED } diff --git a/chasm/lib/nexusoperation/operation_statemachine.go b/chasm/lib/nexusoperation/operation_statemachine.go index 36e9c1b4eb..f729c96c9a 100644 --- a/chasm/lib/nexusoperation/operation_statemachine.go +++ b/chasm/lib/nexusoperation/operation_statemachine.go @@ -232,6 +232,40 @@ var TransitionCanceled = chasm.NewTransition( }, ) +// EventTerminated is triggered when the operation is terminated by user request. +type EventTerminated struct { + chasm.TerminateComponentRequest +} + +var TransitionTerminated = chasm.NewTransition( + []nexusoperationpb.OperationStatus{ + nexusoperationpb.OPERATION_STATUS_SCHEDULED, + nexusoperationpb.OPERATION_STATUS_STARTED, + nexusoperationpb.OPERATION_STATUS_BACKING_OFF, + }, + nexusoperationpb.OPERATION_STATUS_TERMINATED, + func(o *Operation, ctx chasm.MutableContext, event EventTerminated) error { + o.TerminateState = &nexusoperationpb.NexusOperationTerminateState{ + RequestId: event.RequestID, + Identity: event.Identity, + } + o.ClosedTime = timestamppb.New(ctx.Now(o)) + o.NextAttemptScheduleTime = nil + outcome := o.Outcome.Get(ctx) + outcome.Variant = &nexusoperationpb.OperationOutcome_Failed_{ + Failed: &nexusoperationpb.OperationOutcome_Failed{ + Failure: &failurepb.Failure{ + Message: event.Reason, + FailureInfo: &failurepb.Failure_TerminatedFailureInfo{ + TerminatedFailureInfo: &failurepb.TerminatedFailureInfo{}, + }, + }, + }, + } + return nil + }, +) + // EventTimedOut is triggered when the schedule-to-close timeout is triggered for an operation. type EventTimedOut struct { } diff --git a/chasm/lib/nexusoperation/operation_statemachine_test.go b/chasm/lib/nexusoperation/operation_statemachine_test.go index 4e2fb8e629..04305b96f5 100644 --- a/chasm/lib/nexusoperation/operation_statemachine_test.go +++ b/chasm/lib/nexusoperation/operation_statemachine_test.go @@ -22,7 +22,8 @@ var ( ) func newTestOperation() *Operation { - return &Operation{ + ctx := &chasm.MockMutableContext{} + op := &Operation{ OperationState: &nexusoperationpb.OperationState{ Status: nexusoperationpb.OPERATION_STATUS_UNSPECIFIED, EndpointId: "endpoint-id", @@ -35,6 +36,8 @@ func newTestOperation() *Operation { Attempt: 0, }, } + op.Outcome = chasm.NewDataField(ctx, &nexusoperationpb.OperationOutcome{}) + return op } func TestTransitionScheduled(t *testing.T) { @@ -509,3 +512,43 @@ func TestTransitionTimedOut(t *testing.T) { require.Equal(t, defaultTime, operation.ClosedTime.AsTime()) require.Empty(t, ctx.Tasks) } + +func TestTransitionTerminated(t *testing.T) { + ctx := &chasm.MockMutableContext{ + MockContext: chasm.MockContext{ + HandleNow: func(chasm.Component) time.Time { return defaultTime }, + }, + } + + operation := newTestOperation() + operation.Status = nexusoperationpb.OPERATION_STATUS_SCHEDULED + + err := TransitionTerminated.Apply(operation, ctx, EventTerminated{ + TerminateComponentRequest: chasm.TerminateComponentRequest{ + RequestID: "terminate-request-id", + Reason: "test reason", + Identity: "test-identity", + }, + }) + require.NoError(t, err) + + require.Equal(t, nexusoperationpb.OPERATION_STATUS_TERMINATED, operation.Status) + require.Equal(t, defaultTime, operation.ClosedTime.AsTime()) + protorequire.ProtoEqual(t, &nexusoperationpb.NexusOperationTerminateState{ + RequestId: "terminate-request-id", + Identity: "test-identity", + }, operation.TerminateState) + protorequire.ProtoEqual(t, &nexusoperationpb.OperationOutcome{ + Variant: &nexusoperationpb.OperationOutcome_Failed_{ + Failed: &nexusoperationpb.OperationOutcome_Failed{ + Failure: &failurepb.Failure{ + Message: "test reason", + FailureInfo: &failurepb.Failure_TerminatedFailureInfo{ + TerminatedFailureInfo: &failurepb.TerminatedFailureInfo{}, + }, + }, + }, + }, + }, operation.Outcome.Get(ctx)) + require.Empty(t, ctx.Tasks) +} diff --git a/chasm/lib/nexusoperation/validator.go b/chasm/lib/nexusoperation/validator.go index bb2b3f4ed6..1e8376faf2 100644 --- a/chasm/lib/nexusoperation/validator.go +++ b/chasm/lib/nexusoperation/validator.go @@ -12,6 +12,8 @@ import ( enumspb "go.temporal.io/api/enums/v1" "go.temporal.io/api/serviceerror" "go.temporal.io/api/workflowservice/v1" + "go.temporal.io/server/common/log" + "go.temporal.io/server/common/log/tag" "go.temporal.io/server/common/primitives/timestamp" "go.temporal.io/server/common/searchattribute" "google.golang.org/protobuf/types/known/durationpb" @@ -76,10 +78,17 @@ func ValidatePayloadSize(input *commonpb.Payload, limit int) error { func validateAndNormalizeStartRequest( req *workflowservice.StartNexusOperationExecutionRequest, config *Config, + logger log.Logger, saMapperProvider searchattribute.MapperProvider, saValidator *searchattribute.Validator, ) error { ns := req.GetNamespace() + if req.GetRequestId() == "" { + req.RequestId = uuid.NewString() + } else if len(req.GetRequestId()) > config.MaxIDLengthLimit() { + return serviceerror.NewInvalidArgumentf("request_id exceeds length limit. Length=%d Limit=%d", + len(req.GetRequestId()), config.MaxIDLengthLimit()) + } if req.GetOperationId() == "" { return serviceerror.NewInvalidArgument("operation_id is required") } @@ -87,13 +96,6 @@ func validateAndNormalizeStartRequest( return serviceerror.NewInvalidArgumentf("operation_id exceeds length limit. Length=%d Limit=%d", len(req.GetOperationId()), config.MaxIDLengthLimit()) } - if req.GetRequestId() == "" { - req.RequestId = uuid.NewString() - } - if len(req.GetRequestId()) > config.MaxIDLengthLimit() { - return serviceerror.NewInvalidArgumentf("request_id exceeds length limit. Length=%d Limit=%d", - len(req.GetRequestId()), config.MaxIDLengthLimit()) - } if len(req.GetIdentity()) > config.MaxIDLengthLimit() { return serviceerror.NewInvalidArgumentf("identity exceeds length limit. Length=%d Limit=%d", len(req.GetIdentity()), config.MaxIDLengthLimit()) @@ -120,9 +122,20 @@ func validateAndNormalizeStartRequest( ); err != nil { return serviceerror.NewInvalidArgument(err.Error()) } - if err := ValidatePayloadSize(req.GetInput(), config.PayloadSizeLimit(ns)); err != nil { - return serviceerror.NewInvalidArgument(err.Error()) + + inputSize := req.GetInput().Size() + if inputSize > config.PayloadSizeLimitWarn(ns) { + logger.Warn("Nexus Start Operation input size exceeds the warning limit.", + tag.WorkflowNamespace(ns), + tag.OperationID(req.GetOperationId()), + tag.BlobSize(int64(inputSize)), + tag.BlobSizeViolationOperation("StartNexusOperationExecution")) + } + if inputSize > config.PayloadSizeLimit(ns) { + return serviceerror.NewInvalidArgumentf("input exceeds size limit. Length=%d Limit=%d", + inputSize, config.PayloadSizeLimit(ns)) } + loweredHeaders, err := ValidateAndLowercaseNexusHeaders(req.GetNexusHeader(), config.DisallowedOperationHeaders(), config.MaxOperationHeaderSize(ns)) if err != nil { return serviceerror.NewInvalidArgument(err.Error()) @@ -141,7 +154,7 @@ func validateAndNormalizeStartRequest( return nil } -func validateDescribeNexusOperationExecutionRequest(req *workflowservice.DescribeNexusOperationExecutionRequest, config *Config) error { +func validateAndNormalizeDescribeRequest(req *workflowservice.DescribeNexusOperationExecutionRequest, config *Config) error { if req.GetOperationId() == "" { return serviceerror.NewInvalidArgument("operation_id is required") } @@ -149,14 +162,77 @@ func validateDescribeNexusOperationExecutionRequest(req *workflowservice.Describ return serviceerror.NewInvalidArgumentf("operation_id exceeds length limit. Length=%d Limit=%d", len(req.GetOperationId()), config.MaxIDLengthLimit()) } - if len(req.GetRunId()) > config.MaxIDLengthLimit() { - return serviceerror.NewInvalidArgumentf("run_id exceeds length limit. Length=%d Limit=%d", - len(req.GetRunId()), config.MaxIDLengthLimit()) + if runID := req.GetRunId(); runID != "" { + if err := uuid.Validate(runID); err != nil { + return serviceerror.NewInvalidArgument("run_id is not a valid UUID") + } } // TODO: Add long-poll validation (run_id required when long_poll_token is set). return nil } +func validateAndNormalizeCancelRequest(req *workflowservice.RequestCancelNexusOperationExecutionRequest, config *Config) error { + if req.GetRequestId() == "" { + req.RequestId = uuid.NewString() + } else if len(req.GetRequestId()) > config.MaxIDLengthLimit() { + return serviceerror.NewInvalidArgumentf("request_id exceeds length limit. Length=%d Limit=%d", + len(req.GetRequestId()), config.MaxIDLengthLimit()) + } + if req.GetOperationId() == "" { + return serviceerror.NewInvalidArgument("operation_id is required") + } + if len(req.GetOperationId()) > config.MaxIDLengthLimit() { + return serviceerror.NewInvalidArgumentf("operation_id exceeds length limit. Length=%d Limit=%d", + len(req.GetOperationId()), config.MaxIDLengthLimit()) + } + if runID := req.GetRunId(); runID != "" { + if err := uuid.Validate(runID); err != nil { + return serviceerror.NewInvalidArgument("run_id is not a valid UUID") + } + } + if len(req.GetIdentity()) > config.MaxIDLengthLimit() { + return serviceerror.NewInvalidArgumentf("identity exceeds length limit. Length=%d Limit=%d", + len(req.GetIdentity()), config.MaxIDLengthLimit()) + } + if len(req.GetReason()) > config.MaxReasonLength(req.GetNamespace()) { + return serviceerror.NewInvalidArgumentf("reason exceeds length limit. Length=%d Limit=%d", + len(req.GetReason()), config.MaxReasonLength(req.GetNamespace())) + } + + return nil +} + +func validateAndNormalizeTerminateRequest(req *workflowservice.TerminateNexusOperationExecutionRequest, config *Config) error { + if req.GetRequestId() == "" { + req.RequestId = uuid.NewString() + } else if len(req.GetRequestId()) > config.MaxIDLengthLimit() { + return serviceerror.NewInvalidArgumentf("request_id exceeds length limit. Length=%d Limit=%d", + len(req.GetRequestId()), config.MaxIDLengthLimit()) + } + if req.GetOperationId() == "" { + return serviceerror.NewInvalidArgument("operation_id is required") + } + if len(req.GetOperationId()) > config.MaxIDLengthLimit() { + return serviceerror.NewInvalidArgumentf("operation_id exceeds length limit. Length=%d Limit=%d", + len(req.GetOperationId()), config.MaxIDLengthLimit()) + } + if runID := req.GetRunId(); runID != "" { + if err := uuid.Validate(runID); err != nil { + return serviceerror.NewInvalidArgument("run_id is not a valid UUID") + } + } + if len(req.GetIdentity()) > config.MaxIDLengthLimit() { + return serviceerror.NewInvalidArgumentf("identity exceeds length limit. Length=%d Limit=%d", + len(req.GetIdentity()), config.MaxIDLengthLimit()) + } + if len(req.GetReason()) > config.MaxReasonLength(req.GetNamespace()) { + return serviceerror.NewInvalidArgumentf("reason exceeds length limit. Length=%d Limit=%d", + len(req.GetReason()), config.MaxReasonLength(req.GetNamespace())) + } + + return nil +} + func validateAndNormalizeSearchAttributes( req *workflowservice.StartNexusOperationExecutionRequest, saMapperProvider searchattribute.MapperProvider, diff --git a/chasm/lib/nexusoperation/validator_test.go b/chasm/lib/nexusoperation/validator_test.go index 4ad22d4026..a7bd13b243 100644 --- a/chasm/lib/nexusoperation/validator_test.go +++ b/chasm/lib/nexusoperation/validator_test.go @@ -11,6 +11,7 @@ import ( "go.temporal.io/api/serviceerror" "go.temporal.io/api/workflowservice/v1" "go.temporal.io/server/common/dynamicconfig" + "go.temporal.io/server/common/log" "go.temporal.io/server/common/payload" "go.temporal.io/server/common/persistence/visibility/manager" "go.temporal.io/server/common/searchattribute" @@ -39,7 +40,8 @@ func TestValidateStartNexusOperationExecutionRequest(t *testing.T) { MaxIDLengthLimit: func() int { return 50 }, MaxServiceNameLength: func(string) int { return 10 }, MaxOperationNameLength: func(string) int { return 10 }, - PayloadSizeLimit: func(string) int { return 10 }, + PayloadSizeLimit: func(string) int { return 20 }, + PayloadSizeLimitWarn: func(string) int { return 10 }, MaxOperationHeaderSize: func(string) int { return 10 }, DisallowedOperationHeaders: func() []string { return []string{"disallowed-header"} }, MaxOperationScheduleToCloseTimeout: func(string) time.Duration { return time.Hour }, @@ -55,8 +57,10 @@ func TestValidateStartNexusOperationExecutionRequest(t *testing.T) { name: "valid request", }, { - name: "operation_id - required", - mutate: func(r *workflowservice.StartNexusOperationExecutionRequest) { r.OperationId = "" }, + name: "operation_id - required", + mutate: func(r *workflowservice.StartNexusOperationExecutionRequest) { + r.OperationId = "" + }, errMsg: "operation_id is required", }, { @@ -149,10 +153,16 @@ func TestValidateStartNexusOperationExecutionRequest(t *testing.T) { require.Equal(t, 30*time.Minute, r.ScheduleToCloseTimeout.AsDuration()) }, }, + { + name: "input - exceeds warning limit but within hard limit", + mutate: func(r *workflowservice.StartNexusOperationExecutionRequest) { + r.Input = &commonpb.Payload{Data: []byte("exceed-warn-limit")} + }, + }, { name: "input - exceeds size limit", mutate: func(r *workflowservice.StartNexusOperationExecutionRequest) { - r.Input = &commonpb.Payload{Data: []byte("too-long-input")} + r.Input = &commonpb.Payload{Data: []byte("this-input-is-longer-than-twenty-characters")} }, errMsg: "input exceeds size limit", }, @@ -230,7 +240,7 @@ func TestValidateStartNexusOperationExecutionRequest(t *testing.T) { if tc.mutate != nil { tc.mutate(req) } - err := validateAndNormalizeStartRequest(req, config, nil, saValidator) + err := validateAndNormalizeStartRequest(req, config, log.NewNoopLogger(), nil, saValidator) if tc.errMsg != "" { var invalidArgErr *serviceerror.InvalidArgument require.ErrorAs(t, err, &invalidArgErr) @@ -259,8 +269,10 @@ func TestValidateDescribeNexusOperationExecutionRequest(t *testing.T) { name: "valid request", }, { - name: "operation_id - required", - mutate: func(r *workflowservice.DescribeNexusOperationExecutionRequest) { r.OperationId = "" }, + name: "operation_id - required", + mutate: func(r *workflowservice.DescribeNexusOperationExecutionRequest) { + r.OperationId = "" + }, errMsg: "operation_id is required", }, { @@ -271,11 +283,11 @@ func TestValidateDescribeNexusOperationExecutionRequest(t *testing.T) { errMsg: "operation_id exceeds length limit", }, { - name: "run_id - exceeds length limit", + name: "run_id - not a valid UUID", mutate: func(r *workflowservice.DescribeNexusOperationExecutionRequest) { - r.RunId = "this-run-id-is-too-long!!" + r.RunId = "not-a-uuid" }, - errMsg: "run_id exceeds length limit", + errMsg: "run_id is not a valid UUID", }, } { t.Run(tc.name, func(t *testing.T) { @@ -286,7 +298,94 @@ func TestValidateDescribeNexusOperationExecutionRequest(t *testing.T) { if tc.mutate != nil { tc.mutate(validReq) } - err := validateDescribeNexusOperationExecutionRequest(validReq, config) + err := validateAndNormalizeDescribeRequest(validReq, config) + if tc.errMsg != "" { + var invalidArgErr *serviceerror.InvalidArgument + require.ErrorAs(t, err, &invalidArgErr) + require.Contains(t, err.Error(), tc.errMsg) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestValidateRequestCancelNexusOperationExecutionRequest(t *testing.T) { + config := &Config{ + MaxIDLengthLimit: func() int { return 20 }, + MaxReasonLength: func(string) int { return 20 }, + } + + for _, tc := range []struct { + name string + mutate func(*workflowservice.RequestCancelNexusOperationExecutionRequest) + errMsg string + check func(*testing.T, *workflowservice.RequestCancelNexusOperationExecutionRequest) + }{ + { + name: "valid request", + }, + { + name: "request_id - defaults empty to UUID", + mutate: func(r *workflowservice.RequestCancelNexusOperationExecutionRequest) { + r.RequestId = "" + }, + check: func(t *testing.T, r *workflowservice.RequestCancelNexusOperationExecutionRequest) { + require.Len(t, r.RequestId, 36) // UUID length + }, + }, + { + name: "operation_id - required", + mutate: func(r *workflowservice.RequestCancelNexusOperationExecutionRequest) { + r.OperationId = "" + }, + errMsg: "operation_id is required", + }, + { + name: "operation_id - exceeds length limit", + mutate: func(r *workflowservice.RequestCancelNexusOperationExecutionRequest) { + r.OperationId = "this-operation-id-is-too-long" + }, + errMsg: "operation_id exceeds length limit", + }, + { + name: "request_id - exceeds length limit", + mutate: func(r *workflowservice.RequestCancelNexusOperationExecutionRequest) { + r.RequestId = "this-request-id-is-too-long" + }, + errMsg: "request_id exceeds length limit", + }, + { + name: "run_id - not a valid UUID", + mutate: func(r *workflowservice.RequestCancelNexusOperationExecutionRequest) { + r.RunId = "not-a-uuid" + }, + errMsg: "run_id is not a valid UUID", + }, + { + name: "identity - exceeds length limit", + mutate: func(r *workflowservice.RequestCancelNexusOperationExecutionRequest) { + r.Identity = "this-identity-is-too-long!!" + }, + errMsg: "identity exceeds length limit", + }, + { + name: "reason - exceeds length limit", + mutate: func(r *workflowservice.RequestCancelNexusOperationExecutionRequest) { + r.Reason = "this-reason-is-longer-than-twenty-characters" + }, + errMsg: "reason exceeds length limit", + }, + } { + t.Run(tc.name, func(t *testing.T) { + validReq := &workflowservice.RequestCancelNexusOperationExecutionRequest{ + Namespace: "default", + OperationId: "operation-id", + } + if tc.mutate != nil { + tc.mutate(validReq) + } + err := validateAndNormalizeCancelRequest(validReq, config) if tc.errMsg != "" { var invalidArgErr *serviceerror.InvalidArgument require.ErrorAs(t, err, &invalidArgErr) @@ -294,6 +393,99 @@ func TestValidateDescribeNexusOperationExecutionRequest(t *testing.T) { } else { require.NoError(t, err) } + if tc.check != nil { + tc.check(t, validReq) + } + }) + } +} + +func TestValidateTerminateNexusOperationExecutionRequest(t *testing.T) { + config := &Config{ + MaxIDLengthLimit: func() int { return 20 }, + MaxReasonLength: func(string) int { return 20 }, + } + + for _, tc := range []struct { + name string + mutate func(*workflowservice.TerminateNexusOperationExecutionRequest) + errMsg string + check func(*testing.T, *workflowservice.TerminateNexusOperationExecutionRequest) + }{ + { + name: "valid request", + }, + { + name: "request_id - defaults empty to UUID", + mutate: func(r *workflowservice.TerminateNexusOperationExecutionRequest) { + r.RequestId = "" + }, + check: func(t *testing.T, r *workflowservice.TerminateNexusOperationExecutionRequest) { + require.Len(t, r.RequestId, 36) // UUID length + }, + }, + { + name: "operation_id - required", + mutate: func(r *workflowservice.TerminateNexusOperationExecutionRequest) { + r.OperationId = "" + }, + errMsg: "operation_id is required", + }, + { + name: "operation_id - exceeds length limit", + mutate: func(r *workflowservice.TerminateNexusOperationExecutionRequest) { + r.OperationId = "this-operation-id-is-too-long" + }, + errMsg: "operation_id exceeds length limit", + }, + { + name: "request_id - exceeds length limit", + mutate: func(r *workflowservice.TerminateNexusOperationExecutionRequest) { + r.RequestId = "this-request-id-is-too-long" + }, + errMsg: "request_id exceeds length limit", + }, + { + name: "run_id - not a valid UUID", + mutate: func(r *workflowservice.TerminateNexusOperationExecutionRequest) { + r.RunId = "not-a-uuid" + }, + errMsg: "run_id is not a valid UUID", + }, + { + name: "identity - exceeds length limit", + mutate: func(r *workflowservice.TerminateNexusOperationExecutionRequest) { + r.Identity = "this-identity-is-too-long!!" + }, + errMsg: "identity exceeds length limit", + }, + { + name: "reason - exceeds length limit", + mutate: func(r *workflowservice.TerminateNexusOperationExecutionRequest) { + r.Reason = "this-reason-is-longer-than-twenty-characters" + }, + errMsg: "reason exceeds length limit", + }, + } { + t.Run(tc.name, func(t *testing.T) { + validReq := &workflowservice.TerminateNexusOperationExecutionRequest{ + Namespace: "default", + OperationId: "operation-id", + } + if tc.mutate != nil { + tc.mutate(validReq) + } + err := validateAndNormalizeTerminateRequest(validReq, config) + if tc.errMsg != "" { + var invalidArgErr *serviceerror.InvalidArgument + require.ErrorAs(t, err, &invalidArgErr) + require.Contains(t, err.Error(), tc.errMsg) + } else { + require.NoError(t, err) + } + if tc.check != nil { + tc.check(t, validReq) + } }) } } diff --git a/chasm/lib/nexusoperation/workflow/events.go b/chasm/lib/nexusoperation/workflow/events.go index 3c5c54cf8c..fd2acbe25f 100644 --- a/chasm/lib/nexusoperation/workflow/events.go +++ b/chasm/lib/nexusoperation/workflow/events.go @@ -90,7 +90,9 @@ func (d CancelRequestedEventDefinition) Apply(ctx chasm.MutableContext, wf *chas return serviceerror.NewInternalf("failed to marshal cancellation parent data: %v", err) } - return op.Cancel(ctx, cancelParentData) + return op.RequestCancel(ctx, &nexusoperationpb.CancellationState{ + ParentData: cancelParentData, + }) } func (d CancelRequestedEventDefinition) CherryPick(_ chasm.MutableContext, _ *chasmworkflow.Workflow, _ *historypb.HistoryEvent, _ map[enumspb.ResetReapplyExcludeType]struct{}) error { diff --git a/tests/nexus_standalone_test.go b/tests/nexus_standalone_test.go index 1be5d00ebf..b4f40c749c 100644 --- a/tests/nexus_standalone_test.go +++ b/tests/nexus_standalone_test.go @@ -197,6 +197,285 @@ func TestStandaloneNexusOperation(t *testing.T) { }) } +func TestRequestCancelNexusOperationExecution(t *testing.T) { + t.Parallel() + + t.Run("RequestCancel", func(t *testing.T) { + s := testcore.NewEnv(t, nexusStandaloneOpts...) + endpointName := createNexusEndpoint(s) + + startResp, err := startNexusOperation(s, &workflowservice.StartNexusOperationExecutionRequest{ + OperationId: "test-op", + Endpoint: endpointName, + }) + s.NoError(err) + s.True(startResp.GetStarted()) + + _, err = s.FrontendClient().RequestCancelNexusOperationExecution(s.Context(), &workflowservice.RequestCancelNexusOperationExecutionRequest{ + Namespace: s.Namespace().String(), + OperationId: "test-op", + RunId: startResp.RunId, + }) + s.NoError(err) + + // Verify state after cancel — operation is still running + descResp, err := s.FrontendClient().DescribeNexusOperationExecution(s.Context(), &workflowservice.DescribeNexusOperationExecutionRequest{ + Namespace: s.Namespace().String(), + OperationId: "test-op", + RunId: startResp.RunId, + }) + s.NoError(err) + s.Equal(enumspb.NEXUS_OPERATION_EXECUTION_STATUS_RUNNING, descResp.GetInfo().GetStatus()) + }) + + // TODO: Enable once cancel is fully implemented. + t.Run("AlreadyCanceled", func(t *testing.T) { + s := testcore.NewEnv(t, nexusStandaloneOpts...) + endpointName := createNexusEndpoint(s) + + startResp, err := startNexusOperation(s, &workflowservice.StartNexusOperationExecutionRequest{ + OperationId: "test-op", + Endpoint: endpointName, + }) + s.NoError(err) + + // Cancel the operation. + _, err = s.FrontendClient().RequestCancelNexusOperationExecution(s.Context(), &workflowservice.RequestCancelNexusOperationExecutionRequest{ + Namespace: s.Namespace().String(), + OperationId: "test-op", + RunId: startResp.RunId, + RequestId: "cancel-request-id", + }) + s.NoError(err) + + // Cancel again with same request ID — should be idempotent. + _, err = s.FrontendClient().RequestCancelNexusOperationExecution(s.Context(), &workflowservice.RequestCancelNexusOperationExecutionRequest{ + Namespace: s.Namespace().String(), + OperationId: "test-op", + RunId: startResp.RunId, + RequestId: "cancel-request-id", + }) + s.NoError(err) + + // Cancel with a different request ID — should error. + _, err = s.FrontendClient().RequestCancelNexusOperationExecution(s.Context(), &workflowservice.RequestCancelNexusOperationExecutionRequest{ + Namespace: s.Namespace().String(), + OperationId: "test-op", + RunId: startResp.RunId, + RequestId: "different-request-id", + }) + s.Error(err) + s.Contains(err.Error(), "cancellation already requested") + }) + + // TODO: Enable once cancel is fully implemented for standalone Nexus operations. + t.Run("AlreadyTerminated", func(t *testing.T) { + t.Skip("Cancel not yet fully implemented for standalone Nexus operations") + + s := testcore.NewEnv(t, nexusStandaloneOpts...) + endpointName := createNexusEndpoint(s) + + startResp, err := startNexusOperation(s, &workflowservice.StartNexusOperationExecutionRequest{ + OperationId: "test-op", + Endpoint: endpointName, + }) + s.NoError(err) + + // Terminate the operation first. + _, err = s.FrontendClient().TerminateNexusOperationExecution(s.Context(), &workflowservice.TerminateNexusOperationExecutionRequest{ + Namespace: s.Namespace().String(), + OperationId: "test-op", + RunId: startResp.RunId, + RequestId: "terminate-request-id", + Reason: "test termination", + }) + s.NoError(err) + + // Cancel a terminated operation — should error. + _, err = s.FrontendClient().RequestCancelNexusOperationExecution(s.Context(), &workflowservice.RequestCancelNexusOperationExecutionRequest{ + Namespace: s.Namespace().String(), + OperationId: "test-op", + RunId: startResp.RunId, + }) + s.Error(err) + s.Contains(err.Error(), "operation already completed") + }) + + // TODO: Enable once cancel is fully implemented for standalone Nexus operations. + t.Run("NotFound", func(t *testing.T) { + t.Skip("Cancel not yet fully implemented for standalone Nexus operations") + + s := testcore.NewEnv(t, nexusStandaloneOpts...) + + _, err := s.FrontendClient().RequestCancelNexusOperationExecution(s.Context(), &workflowservice.RequestCancelNexusOperationExecutionRequest{ + Namespace: s.Namespace().String(), + OperationId: "does-not-exist", + }) + var notFound *serviceerror.NotFound + s.ErrorAs(err, ¬Found) + }) + + // Validates that request validation is wired up in the frontend. + // Exhaustive validation cases are covered in unit tests. + t.Run("Validation", func(t *testing.T) { + s := testcore.NewEnv(t, nexusStandaloneOpts...) + + _, err := s.FrontendClient().RequestCancelNexusOperationExecution(s.Context(), &workflowservice.RequestCancelNexusOperationExecutionRequest{ + Namespace: s.Namespace().String(), + OperationId: "", // required field + }) + s.Error(err) + s.Contains(err.Error(), "operation_id is required") + }) +} + +func TestTerminateNexusOperationExecution(t *testing.T) { + t.Parallel() + + t.Run("Terminate", func(t *testing.T) { + s := testcore.NewEnv(t, nexusStandaloneOpts...) + endpointName := createNexusEndpoint(s) + + startResp, err := startNexusOperation(s, &workflowservice.StartNexusOperationExecutionRequest{ + OperationId: "test-op", + Endpoint: endpointName, + }) + s.NoError(err) + s.True(startResp.GetStarted()) + + _, err = s.FrontendClient().TerminateNexusOperationExecution(s.Context(), &workflowservice.TerminateNexusOperationExecutionRequest{ + Namespace: s.Namespace().String(), + OperationId: "test-op", + RunId: startResp.RunId, + RequestId: "terminate-request-id", + Reason: "test termination", + }) + s.NoError(err) + + // Verify state after terminate. + descResp, err := s.FrontendClient().DescribeNexusOperationExecution(s.Context(), &workflowservice.DescribeNexusOperationExecutionRequest{ + Namespace: s.Namespace().String(), + OperationId: "test-op", + RunId: startResp.RunId, + IncludeOutcome: true, + }) + s.NoError(err) + s.Equal(enumspb.NEXUS_OPERATION_EXECUTION_STATUS_TERMINATED, descResp.GetInfo().GetStatus()) + failure := descResp.GetFailure() + s.NotNil(failure) + s.Equal("test termination", failure.GetMessage()) + s.NotNil(failure.GetTerminatedFailureInfo()) + }) + + t.Run("AlreadyTerminated", func(t *testing.T) { + s := testcore.NewEnv(t, nexusStandaloneOpts...) + endpointName := createNexusEndpoint(s) + + startResp, err := startNexusOperation(s, &workflowservice.StartNexusOperationExecutionRequest{ + OperationId: "test-op", + Endpoint: endpointName, + }) + s.NoError(err) + + // Terminate the operation. + _, err = s.FrontendClient().TerminateNexusOperationExecution(s.Context(), &workflowservice.TerminateNexusOperationExecutionRequest{ + Namespace: s.Namespace().String(), + OperationId: "test-op", + RunId: startResp.RunId, + RequestId: "terminate-request-id", + Reason: "test termination", + }) + s.NoError(err) + + // Terminate again with same request ID — should be idempotent. + _, err = s.FrontendClient().TerminateNexusOperationExecution(s.Context(), &workflowservice.TerminateNexusOperationExecutionRequest{ + Namespace: s.Namespace().String(), + OperationId: "test-op", + RunId: startResp.RunId, + RequestId: "terminate-request-id", + Reason: "test termination again", + }) + s.NoError(err) + + // Terminate with a different request ID — should error. + _, err = s.FrontendClient().TerminateNexusOperationExecution(s.Context(), &workflowservice.TerminateNexusOperationExecutionRequest{ + Namespace: s.Namespace().String(), + OperationId: "test-op", + RunId: startResp.RunId, + RequestId: "different-request-id", + Reason: "test termination different", + }) + s.Error(err) + s.Contains(err.Error(), "already terminated") + }) + + // TODO: Enable once terminate is fully implemented for standalone Nexus operations. + t.Run("AlreadyCanceled", func(t *testing.T) { + t.Skip("Terminate not yet fully implemented for standalone Nexus operations") + + s := testcore.NewEnv(t, nexusStandaloneOpts...) + endpointName := createNexusEndpoint(s) + + startResp, err := startNexusOperation(s, &workflowservice.StartNexusOperationExecutionRequest{ + OperationId: "test-op", + Endpoint: endpointName, + }) + s.NoError(err) + + // Cancel the operation first. + _, err = s.FrontendClient().RequestCancelNexusOperationExecution(s.Context(), &workflowservice.RequestCancelNexusOperationExecutionRequest{ + Namespace: s.Namespace().String(), + OperationId: "test-op", + RunId: startResp.RunId, + }) + s.NoError(err) + + // Terminate a canceled operation — should succeed. + _, err = s.FrontendClient().TerminateNexusOperationExecution(s.Context(), &workflowservice.TerminateNexusOperationExecutionRequest{ + Namespace: s.Namespace().String(), + OperationId: "test-op", + RunId: startResp.RunId, + RequestId: "terminate-request-id", + Reason: "test termination", + }) + s.NoError(err) + + // Verify state changed to terminated (terminate overrides cancel request). + descResp, err := s.FrontendClient().DescribeNexusOperationExecution(s.Context(), &workflowservice.DescribeNexusOperationExecutionRequest{ + Namespace: s.Namespace().String(), + OperationId: "test-op", + RunId: startResp.RunId, + }) + s.NoError(err) + s.Equal(enumspb.NEXUS_OPERATION_EXECUTION_STATUS_TERMINATED, descResp.GetInfo().GetStatus()) + }) + + t.Run("NotFound", func(t *testing.T) { + s := testcore.NewEnv(t, nexusStandaloneOpts...) + + _, err := s.FrontendClient().TerminateNexusOperationExecution(s.Context(), &workflowservice.TerminateNexusOperationExecutionRequest{ + Namespace: s.Namespace().String(), + OperationId: "does-not-exist", + Reason: "test termination", + }) + var notFound *serviceerror.NotFound + s.ErrorAs(err, ¬Found) + }) + + // Validates that request validation is wired up in the frontend. + // Exhaustive validation cases are covered in unit tests. + t.Run("Validation", func(t *testing.T) { + s := testcore.NewEnv(t, nexusStandaloneOpts...) + + _, err := s.FrontendClient().TerminateNexusOperationExecution(s.Context(), &workflowservice.TerminateNexusOperationExecutionRequest{ + Namespace: s.Namespace().String(), + OperationId: "", // required field + }) + s.Error(err) + s.Contains(err.Error(), "operation_id is required") + }) +} + func createNexusEndpoint(s *testcore.TestEnv) string { name := testcore.RandomizedNexusEndpoint(s.T().Name()) _, err := s.OperatorClient().CreateNexusEndpoint(s.Context(), &operatorservice.CreateNexusEndpointRequest{ From 26da83125997b8af4710918f452df445d18303ca Mon Sep 17 00:00:00 2001 From: Stephan Behnke Date: Mon, 6 Apr 2026 17:08:36 -0700 Subject: [PATCH 06/11] Nexus Standalone: Delete (#9654) Co-Authored-By: Claude Opus 4.6 (1M context) --- chasm/lib/nexusoperation/frontend.go | 25 +++- chasm/lib/nexusoperation/handler.go | 25 ++++ .../nexusoperation/operation_statemachine.go | 1 + .../operation_statemachine_test.go | 89 +++++++++----- chasm/lib/nexusoperation/validator.go | 16 +++ chasm/lib/nexusoperation/validator_test.go | 65 +++++++++- tests/nexus_standalone_test.go | 111 +++++++++++++++++- 7 files changed, 296 insertions(+), 36 deletions(-) diff --git a/chasm/lib/nexusoperation/frontend.go b/chasm/lib/nexusoperation/frontend.go index c30b56a7a4..0ec13e716e 100644 --- a/chasm/lib/nexusoperation/frontend.go +++ b/chasm/lib/nexusoperation/frontend.go @@ -276,9 +276,30 @@ func (h *frontendHandler) TerminateNexusOperationExecution( return &workflowservice.TerminateNexusOperationExecutionResponse{}, nil } -func (h *frontendHandler) DeleteNexusOperationExecution(_ context.Context, req *workflowservice.DeleteNexusOperationExecutionRequest) (*workflowservice.DeleteNexusOperationExecutionResponse, error) { +func (h *frontendHandler) DeleteNexusOperationExecution( + ctx context.Context, + req *workflowservice.DeleteNexusOperationExecutionRequest, +) (*workflowservice.DeleteNexusOperationExecutionResponse, error) { if !h.isStandaloneNexusOperationEnabled(req.GetNamespace()) { return nil, ErrStandaloneNexusOperationDisabled } - return nil, serviceerror.NewUnimplemented("DeleteNexusOperationExecution not implemented") + + namespaceID, err := h.namespaceRegistry.GetNamespaceID(namespace.Name(req.GetNamespace())) + if err != nil { + return nil, err + } + + if err := validateAndNormalizeDeleteRequest(req, h.config); err != nil { + return nil, err + } + + _, err = h.client.DeleteNexusOperation(ctx, &nexusoperationpb.DeleteNexusOperationRequest{ + NamespaceId: namespaceID.String(), + FrontendRequest: req, + }) + if err != nil { + return nil, err + } + + return &workflowservice.DeleteNexusOperationExecutionResponse{}, nil } diff --git a/chasm/lib/nexusoperation/handler.go b/chasm/lib/nexusoperation/handler.go index 598c31d164..8fd30f0ef6 100644 --- a/chasm/lib/nexusoperation/handler.go +++ b/chasm/lib/nexusoperation/handler.go @@ -135,6 +135,31 @@ func (h *handler) TerminateNexusOperation( return resp, err } +// DeleteNexusOperation terminates the nexus operation if running, then schedules it for deletion. +func (h *handler) DeleteNexusOperation( + ctx context.Context, + req *nexusoperationpb.DeleteNexusOperationRequest, +) (response *nexusoperationpb.DeleteNexusOperationResponse, err error) { + defer log.CapturePanic(h.logger, &err) + + frontendReq := req.GetFrontendRequest() + + key := chasm.ExecutionKey{ + NamespaceID: req.GetNamespaceId(), + BusinessID: frontendReq.GetOperationId(), + RunID: frontendReq.GetRunId(), + } + + if err := chasm.DeleteExecution[*Operation](ctx, key, chasm.DeleteExecutionRequest{ + TerminateComponentRequest: chasm.TerminateComponentRequest{ + Reason: "Delete nexus operation execution", + }, + }); err != nil { + return nil, err + } + + return &nexusoperationpb.DeleteNexusOperationResponse{}, nil +} func idReusePolicyFromProto(p enumspb.NexusOperationIdReusePolicy) chasm.BusinessIDReusePolicy { switch p { case enumspb.NEXUS_OPERATION_ID_REUSE_POLICY_ALLOW_DUPLICATE_FAILED_ONLY: diff --git a/chasm/lib/nexusoperation/operation_statemachine.go b/chasm/lib/nexusoperation/operation_statemachine.go index f729c96c9a..1e700f79f7 100644 --- a/chasm/lib/nexusoperation/operation_statemachine.go +++ b/chasm/lib/nexusoperation/operation_statemachine.go @@ -242,6 +242,7 @@ var TransitionTerminated = chasm.NewTransition( nexusoperationpb.OPERATION_STATUS_SCHEDULED, nexusoperationpb.OPERATION_STATUS_STARTED, nexusoperationpb.OPERATION_STATUS_BACKING_OFF, + nexusoperationpb.OPERATION_STATUS_CANCELED, }, nexusoperationpb.OPERATION_STATUS_TERMINATED, func(o *Operation, ctx chasm.MutableContext, event EventTerminated) error { diff --git a/chasm/lib/nexusoperation/operation_statemachine_test.go b/chasm/lib/nexusoperation/operation_statemachine_test.go index 04305b96f5..4f203a0965 100644 --- a/chasm/lib/nexusoperation/operation_statemachine_test.go +++ b/chasm/lib/nexusoperation/operation_statemachine_test.go @@ -514,41 +514,70 @@ func TestTransitionTimedOut(t *testing.T) { } func TestTransitionTerminated(t *testing.T) { - ctx := &chasm.MockMutableContext{ - MockContext: chasm.MockContext{ - HandleNow: func(chasm.Component) time.Time { return defaultTime }, + testCases := []struct { + name string + startStatus nexusoperationpb.OperationStatus + }{ + { + name: "terminated from scheduled", + startStatus: nexusoperationpb.OPERATION_STATUS_SCHEDULED, + }, + { + name: "terminated from started", + startStatus: nexusoperationpb.OPERATION_STATUS_STARTED, + }, + { + name: "terminated from backing off", + startStatus: nexusoperationpb.OPERATION_STATUS_BACKING_OFF, + }, + { + name: "terminated from canceled", + startStatus: nexusoperationpb.OPERATION_STATUS_CANCELED, }, } - operation := newTestOperation() - operation.Status = nexusoperationpb.OPERATION_STATUS_SCHEDULED + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ctx := &chasm.MockMutableContext{ + MockContext: chasm.MockContext{ + HandleNow: func(chasm.Component) time.Time { return defaultTime }, + }, + } + operation := newTestOperation() + operation.Status = tc.startStatus - err := TransitionTerminated.Apply(operation, ctx, EventTerminated{ - TerminateComponentRequest: chasm.TerminateComponentRequest{ - RequestID: "terminate-request-id", - Reason: "test reason", - Identity: "test-identity", - }, - }) - require.NoError(t, err) + event := EventTerminated{TerminateComponentRequest: chasm.TerminateComponentRequest{ + RequestID: "terminate-request-id", + Reason: "test reason", + Identity: "test-identity", + }} - require.Equal(t, nexusoperationpb.OPERATION_STATUS_TERMINATED, operation.Status) - require.Equal(t, defaultTime, operation.ClosedTime.AsTime()) - protorequire.ProtoEqual(t, &nexusoperationpb.NexusOperationTerminateState{ - RequestId: "terminate-request-id", - Identity: "test-identity", - }, operation.TerminateState) - protorequire.ProtoEqual(t, &nexusoperationpb.OperationOutcome{ - Variant: &nexusoperationpb.OperationOutcome_Failed_{ - Failed: &nexusoperationpb.OperationOutcome_Failed{ - Failure: &failurepb.Failure{ - Message: "test reason", - FailureInfo: &failurepb.Failure_TerminatedFailureInfo{ - TerminatedFailureInfo: &failurepb.TerminatedFailureInfo{}, + err := TransitionTerminated.Apply(operation, ctx, event) + require.NoError(t, err) + + require.Equal(t, nexusoperationpb.OPERATION_STATUS_TERMINATED, operation.Status) + require.Equal(t, defaultTime, operation.ClosedTime.AsTime()) + protorequire.ProtoEqual(t, &nexusoperationpb.NexusOperationTerminateState{ + RequestId: "terminate-request-id", + Identity: "test-identity", + }, operation.TerminateState) + + // Verify outcome failure is set with terminated info and reason as message. + protorequire.ProtoEqual(t, &nexusoperationpb.OperationOutcome{ + Variant: &nexusoperationpb.OperationOutcome_Failed_{ + Failed: &nexusoperationpb.OperationOutcome_Failed{ + Failure: &failurepb.Failure{ + Message: "test reason", + FailureInfo: &failurepb.Failure_TerminatedFailureInfo{ + TerminatedFailureInfo: &failurepb.TerminatedFailureInfo{}, + }, + }, }, }, - }, - }, - }, operation.Outcome.Get(ctx)) - require.Empty(t, ctx.Tasks) + }, operation.Outcome.Get(ctx)) + + // Terminal state - no tasks should be emitted + require.Empty(t, ctx.Tasks) + }) + } } diff --git a/chasm/lib/nexusoperation/validator.go b/chasm/lib/nexusoperation/validator.go index 1e8376faf2..fe5aaf5b20 100644 --- a/chasm/lib/nexusoperation/validator.go +++ b/chasm/lib/nexusoperation/validator.go @@ -154,6 +154,22 @@ func validateAndNormalizeStartRequest( return nil } +func validateAndNormalizeDeleteRequest(req *workflowservice.DeleteNexusOperationExecutionRequest, config *Config) error { + if req.GetOperationId() == "" { + return serviceerror.NewInvalidArgument("operation_id is required") + } + if len(req.GetOperationId()) > config.MaxIDLengthLimit() { + return serviceerror.NewInvalidArgumentf("operation_id exceeds length limit. Length=%d Limit=%d", + len(req.GetOperationId()), config.MaxIDLengthLimit()) + } + if req.GetRunId() != "" { + if err := uuid.Validate(req.GetRunId()); err != nil { + return serviceerror.NewInvalidArgument("invalid run id: must be a valid UUID") + } + } + return nil +} + func validateAndNormalizeDescribeRequest(req *workflowservice.DescribeNexusOperationExecutionRequest, config *Config) error { if req.GetOperationId() == "" { return serviceerror.NewInvalidArgument("operation_id is required") diff --git a/chasm/lib/nexusoperation/validator_test.go b/chasm/lib/nexusoperation/validator_test.go index a7bd13b243..453e2bb552 100644 --- a/chasm/lib/nexusoperation/validator_test.go +++ b/chasm/lib/nexusoperation/validator_test.go @@ -331,7 +331,7 @@ func TestValidateRequestCancelNexusOperationExecutionRequest(t *testing.T) { r.RequestId = "" }, check: func(t *testing.T, r *workflowservice.RequestCancelNexusOperationExecutionRequest) { - require.Len(t, r.RequestId, 36) // UUID length + require.Len(t, r.RequestId, 36) }, }, { @@ -400,6 +400,67 @@ func TestValidateRequestCancelNexusOperationExecutionRequest(t *testing.T) { } } +func TestValidateDeleteNexusOperationExecutionRequest(t *testing.T) { + config := &Config{ + MaxIDLengthLimit: func() int { return 20 }, + } + + for _, tc := range []struct { + name string + mutate func(*workflowservice.DeleteNexusOperationExecutionRequest) + errMsg string + }{ + { + name: "valid request", + }, + { + name: "valid request - with run_id", + mutate: func(r *workflowservice.DeleteNexusOperationExecutionRequest) { + r.RunId = "550e8400-e29b-41d4-a716-446655440000" + }, + }, + { + name: "operation_id - required", + mutate: func(r *workflowservice.DeleteNexusOperationExecutionRequest) { + r.OperationId = "" + }, + errMsg: "operation_id is required", + }, + { + name: "operation_id - exceeds length limit", + mutate: func(r *workflowservice.DeleteNexusOperationExecutionRequest) { + r.OperationId = "this-operation-id-is-too-long" + }, + errMsg: "operation_id exceeds length limit", + }, + { + name: "run_id - invalid UUID", + mutate: func(r *workflowservice.DeleteNexusOperationExecutionRequest) { + r.RunId = "not-a-valid-uuid" + }, + errMsg: "invalid run id: must be a valid UUID", + }, + } { + t.Run(tc.name, func(t *testing.T) { + validReq := &workflowservice.DeleteNexusOperationExecutionRequest{ + Namespace: "default", + OperationId: "operation-id", + } + if tc.mutate != nil { + tc.mutate(validReq) + } + err := validateAndNormalizeDeleteRequest(validReq, config) + if tc.errMsg != "" { + var invalidArgErr *serviceerror.InvalidArgument + require.ErrorAs(t, err, &invalidArgErr) + require.Contains(t, err.Error(), tc.errMsg) + } else { + require.NoError(t, err) + } + }) + } +} + func TestValidateTerminateNexusOperationExecutionRequest(t *testing.T) { config := &Config{ MaxIDLengthLimit: func() int { return 20 }, @@ -421,7 +482,7 @@ func TestValidateTerminateNexusOperationExecutionRequest(t *testing.T) { r.RequestId = "" }, check: func(t *testing.T, r *workflowservice.TerminateNexusOperationExecutionRequest) { - require.Len(t, r.RequestId, 36) // UUID length + require.Len(t, r.RequestId, 36) }, }, { diff --git a/tests/nexus_standalone_test.go b/tests/nexus_standalone_test.go index b4f40c749c..aaa01a4d57 100644 --- a/tests/nexus_standalone_test.go +++ b/tests/nexus_standalone_test.go @@ -2,6 +2,7 @@ package tests import ( "cmp" + "errors" "fmt" "testing" "time" @@ -268,9 +269,9 @@ func TestRequestCancelNexusOperationExecution(t *testing.T) { s.Contains(err.Error(), "cancellation already requested") }) - // TODO: Enable once cancel is fully implemented for standalone Nexus operations. + // TODO: Enable once cancel/terminate interaction is fully implemented for standalone Nexus operations. t.Run("AlreadyTerminated", func(t *testing.T) { - t.Skip("Cancel not yet fully implemented for standalone Nexus operations") + t.Skip("Cancel/terminate interaction not yet fully implemented for standalone Nexus operations") s := testcore.NewEnv(t, nexusStandaloneOpts...) endpointName := createNexusEndpoint(s) @@ -851,6 +852,99 @@ func TestStandaloneNexusOperationCount(t *testing.T) { }) } +func TestStandaloneNexusOperationDelete(t *testing.T) { + t.Parallel() + + t.Run("DeleteScheduled", func(t *testing.T) { + s := testcore.NewEnv(t, nexusStandaloneOpts...) + endpointName := createNexusEndpoint(s) + + startResp, err := startNexusOperation(s, &workflowservice.StartNexusOperationExecutionRequest{ + OperationId: "test-op", + Endpoint: endpointName, + }) + s.NoError(err) + + _, err = s.FrontendClient().DeleteNexusOperationExecution(s.Context(), &workflowservice.DeleteNexusOperationExecutionRequest{ + Namespace: s.Namespace().String(), + OperationId: "test-op", + RunId: startResp.RunId, + }) + s.NoError(err) + + eventuallyNexusOperationDeleted(s, t, "test-op", startResp.RunId) + }) + + t.Run("DeleteNoRunID", func(t *testing.T) { + s := testcore.NewEnv(t, nexusStandaloneOpts...) + endpointName := createNexusEndpoint(s) + + startResp, err := startNexusOperation(s, &workflowservice.StartNexusOperationExecutionRequest{ + OperationId: "test-op", + Endpoint: endpointName, + // RunId not set + }) + s.NoError(err) + + _, err = s.FrontendClient().DeleteNexusOperationExecution(s.Context(), &workflowservice.DeleteNexusOperationExecutionRequest{ + Namespace: s.Namespace().String(), + OperationId: "test-op", + }) + s.NoError(err) + + eventuallyNexusOperationDeleted(s, t, "test-op", startResp.RunId) + }) + + t.Run("DeleteNonExistent", func(t *testing.T) { + s := testcore.NewEnv(t, nexusStandaloneOpts...) + + _, err := s.FrontendClient().DeleteNexusOperationExecution(s.Context(), &workflowservice.DeleteNexusOperationExecutionRequest{ + Namespace: s.Namespace().String(), + OperationId: "does-not-exist", + }) + s.ErrorAs(err, new(*serviceerror.NotFound)) + }) + + t.Run("DeleteAlreadyDeleted", func(t *testing.T) { + s := testcore.NewEnv(t, nexusStandaloneOpts...) + endpointName := createNexusEndpoint(s) + + startResp, err := startNexusOperation(s, &workflowservice.StartNexusOperationExecutionRequest{ + OperationId: "test-op", + Endpoint: endpointName, + }) + s.NoError(err) + + _, err = s.FrontendClient().DeleteNexusOperationExecution(s.Context(), &workflowservice.DeleteNexusOperationExecutionRequest{ + Namespace: s.Namespace().String(), + OperationId: "test-op", + RunId: startResp.RunId, + }) + s.NoError(err) + + eventuallyNexusOperationDeleted(s, t, "test-op", startResp.RunId) + + _, err = s.FrontendClient().DeleteNexusOperationExecution(s.Context(), &workflowservice.DeleteNexusOperationExecutionRequest{ + Namespace: s.Namespace().String(), + OperationId: "test-op", + RunId: startResp.RunId, + }) + s.ErrorAs(err, new(*serviceerror.NotFound)) + }) + + // Validates that request validation is wired up in the frontend. + // Exhaustive validation cases are covered in unit tests. + t.Run("DeleteValidation", func(t *testing.T) { + s := testcore.NewEnv(t, nexusStandaloneOpts...) + + _, err := s.FrontendClient().DeleteNexusOperationExecution(s.Context(), &workflowservice.DeleteNexusOperationExecutionRequest{ + Namespace: s.Namespace().String(), + }) + s.Error(err) + s.ErrorContains(err, "operation_id is required") + }) +} + func startNexusOperation( s *testcore.TestEnv, req *workflowservice.StartNexusOperationExecutionRequest, @@ -864,3 +958,16 @@ func startNexusOperation( } return s.FrontendClient().StartNexusOperationExecution(s.Context(), req) } + +func eventuallyNexusOperationDeleted(s *testcore.TestEnv, t *testing.T, operationID, runID string) { + t.Helper() + require.Eventually(t, func() bool { + _, err := s.FrontendClient().DescribeNexusOperationExecution(s.Context(), &workflowservice.DescribeNexusOperationExecutionRequest{ + Namespace: s.Namespace().String(), + OperationId: operationID, + RunId: runID, + }) + var notFoundErr *serviceerror.NotFound + return errors.As(err, ¬FoundErr) + }, 5*time.Second, 100*time.Millisecond) +} From 7a7b23200f5bd029537a54dcb3a095ce7e0785e8 Mon Sep 17 00:00:00 2001 From: Stephan Behnke Date: Mon, 6 Apr 2026 17:11:15 -0700 Subject: [PATCH 07/11] Nexus Standalone: Poll (#9780) Co-Authored-By: Claude Opus 4.6 (1M context) --- chasm/lib/nexusoperation/config.go | 19 + chasm/lib/nexusoperation/frontend.go | 24 +- chasm/lib/nexusoperation/handler.go | 109 ++- chasm/lib/nexusoperation/operation.go | 77 +- chasm/lib/nexusoperation/operation_test.go | 90 +++ chasm/lib/nexusoperation/validator.go | 47 +- chasm/lib/nexusoperation/validator_test.go | 134 +++- common/testing/protoutils/enum.go | 18 + tests/nexus_standalone_test.go | 814 ++++++++++++++++++--- 9 files changed, 1209 insertions(+), 123 deletions(-) create mode 100644 chasm/lib/nexusoperation/operation_test.go create mode 100644 common/testing/protoutils/enum.go diff --git a/chasm/lib/nexusoperation/config.go b/chasm/lib/nexusoperation/config.go index c50da27ed4..a13cc5c670 100644 --- a/chasm/lib/nexusoperation/config.go +++ b/chasm/lib/nexusoperation/config.go @@ -13,6 +13,21 @@ import ( "go.temporal.io/server/common/rpc/interceptor" ) +var LongPollTimeout = dynamicconfig.NewNamespaceDurationSetting( + "nexusoperation.longPollTimeout", + 20*time.Second, + `Maximum timeout for nexus operation long-poll requests. Actual wait may be shorter to leave +longPollBuffer before the caller deadline.`, +) + +var LongPollBuffer = dynamicconfig.NewNamespaceDurationSetting( + "nexusoperation.longPollBuffer", + time.Second, + `A buffer used to adjust the nexus operation long-poll timeouts. + Specifically, nexus operation long-poll requests are timed out at a time which leaves at least the buffer's duration + remaining before the caller's deadline, if permitted by the caller's deadline.`, +) + var Enabled = dynamicconfig.NewNamespaceBoolSetting( "nexusoperation.enableStandalone", false, @@ -205,6 +220,8 @@ type Config struct { EnableChasm dynamicconfig.BoolPropertyFnWithNamespaceFilter EnableChasmNexus dynamicconfig.BoolPropertyFnWithNamespaceFilter NumHistoryShards int32 + LongPollBuffer dynamicconfig.DurationPropertyFnWithNamespaceFilter + LongPollTimeout dynamicconfig.DurationPropertyFnWithNamespaceFilter RequestTimeout dynamicconfig.DurationPropertyFnWithDestinationFilter MinRequestTimeout dynamicconfig.DurationPropertyFnWithNamespaceFilter MaxConcurrentOperationsPerWorkflow dynamicconfig.IntPropertyFnWithNamespaceFilter @@ -232,6 +249,8 @@ func configProvider(dc *dynamicconfig.Collection, cfg *config.Persistence) *Conf EnableChasm: dynamicconfig.EnableChasm.Get(dc), EnableChasmNexus: EnableChasmNexus.Get(dc), NumHistoryShards: cfg.NumHistoryShards, + LongPollBuffer: LongPollBuffer.Get(dc), + LongPollTimeout: LongPollTimeout.Get(dc), RequestTimeout: RequestTimeout.Get(dc), MinRequestTimeout: MinRequestTimeout.Get(dc), MaxConcurrentOperationsPerWorkflow: MaxConcurrentOperationsPerWorkflow.Get(dc), diff --git a/chasm/lib/nexusoperation/frontend.go b/chasm/lib/nexusoperation/frontend.go index 0ec13e716e..975a0a9a62 100644 --- a/chasm/lib/nexusoperation/frontend.go +++ b/chasm/lib/nexusoperation/frontend.go @@ -110,7 +110,7 @@ func (h *frontendHandler) DescribeNexusOperationExecution( return nil, err } - if err := validateAndNormalizeDescribeRequest(req, h.config); err != nil { + if err := validateAndNormalizeDescribeRequest(req, namespaceID.String(), h.config); err != nil { return nil, err } @@ -121,11 +121,29 @@ func (h *frontendHandler) DescribeNexusOperationExecution( return resp.GetFrontendResponse(), err } -func (h *frontendHandler) PollNexusOperationExecution(_ context.Context, req *workflowservice.PollNexusOperationExecutionRequest) (*workflowservice.PollNexusOperationExecutionResponse, error) { +// PollNexusOperationExecution long-polls for a Nexus operation to reach a specific stage. +func (h *frontendHandler) PollNexusOperationExecution( + ctx context.Context, + req *workflowservice.PollNexusOperationExecutionRequest, +) (*workflowservice.PollNexusOperationExecutionResponse, error) { if !h.isStandaloneNexusOperationEnabled(req.GetNamespace()) { return nil, ErrStandaloneNexusOperationDisabled } - return nil, serviceerror.NewUnimplemented("PollNexusOperationExecution not implemented") + + if err := validateAndNormalizePollRequest(req, h.config); err != nil { + return nil, err + } + + namespaceID, err := h.namespaceRegistry.GetNamespaceID(namespace.Name(req.GetNamespace())) + if err != nil { + return nil, err + } + + resp, err := h.client.PollNexusOperation(ctx, &nexusoperationpb.PollNexusOperationRequest{ + NamespaceId: namespaceID.String(), + FrontendRequest: req, + }) + return resp.GetFrontendResponse(), err } func (h *frontendHandler) ListNexusOperationExecutions( diff --git a/chasm/lib/nexusoperation/handler.go b/chasm/lib/nexusoperation/handler.go index 8fd30f0ef6..5f6b2cc01c 100644 --- a/chasm/lib/nexusoperation/handler.go +++ b/chasm/lib/nexusoperation/handler.go @@ -2,11 +2,14 @@ package nexusoperation import ( "context" + "errors" enumspb "go.temporal.io/api/enums/v1" + "go.temporal.io/api/serviceerror" "go.temporal.io/api/workflowservice/v1" "go.temporal.io/server/chasm" nexusoperationpb "go.temporal.io/server/chasm/lib/nexusoperation/gen/nexusoperationpb/v1" + "go.temporal.io/server/common/contextutil" "go.temporal.io/server/common/log" ) @@ -59,7 +62,14 @@ func (h *handler) StartNexusOperation( }, nil } -// TODO: Add long-poll support. +// DescribeNexusOperation queries current operation state, optionally as a long-poll that waits +// for any state change. +// +// When used to long-poll, 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. This response is sent before +// the caller's deadline (see nexusoperation.longPollBuffer) so that it is likely that the caller +// does indeed receive the non-error response. func (h *handler) DescribeNexusOperation( ctx context.Context, req *nexusoperationpb.DescribeNexusOperationRequest, @@ -72,7 +82,102 @@ func (h *handler) DescribeNexusOperation( RunID: req.GetFrontendRequest().GetRunId(), }) - return chasm.ReadComponent(ctx, ref, (*Operation).buildDescribeResponse, req, nil) + token := req.GetFrontendRequest().GetLongPollToken() + if len(token) == 0 { + // No long poll. + return chasm.ReadComponent(ctx, ref, (*Operation).buildDescribeResponse, req) + } + + // Determine the long poll timeout and buffer. + ns := req.GetFrontendRequest().GetNamespace() + ctx, cancel := contextutil.WithDeadlineBuffer( + ctx, + h.config.LongPollTimeout(ns), + h.config.LongPollBuffer(ns), + ) + defer cancel() + + // Poll for the operation state to change. + response, _, err = chasm.PollComponent(ctx, ref, func( + o *Operation, + ctx chasm.Context, + req *nexusoperationpb.DescribeNexusOperationRequest, + ) (*nexusoperationpb.DescribeNexusOperationResponse, bool, error) { + changed, err := chasm.ExecutionStateChanged(o, ctx, token) + if err != nil { + if errors.Is(err, chasm.ErrMalformedComponentRef) { + return nil, false, serviceerror.NewInvalidArgument("invalid long poll token") + } + if errors.Is(err, chasm.ErrInvalidComponentRef) { + return nil, false, serviceerror.NewInvalidArgument("long poll token does not match execution") + } + return nil, false, err + } + if changed { + response, err := o.buildDescribeResponse(ctx, req) + return response, true, err + } + return nil, false, nil + }, req) + + if err != nil && ctx.Err() != nil { + // Send empty non-error response on deadline expiry: caller should continue long-polling. + return &nexusoperationpb.DescribeNexusOperationResponse{ + FrontendResponse: &workflowservice.DescribeNexusOperationExecutionResponse{}, + }, nil + } + return response, err +} + +// PollNexusOperation long-polls for a Nexus operation to reach a specific stage. +// +// 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. This response is sent before the caller's +// deadline (see nexusoperation.longPollBuffer) so that it is likely that the caller +// does indeed receive the non-error response. +func (h *handler) PollNexusOperation( + ctx context.Context, + req *nexusoperationpb.PollNexusOperationRequest, +) (response *nexusoperationpb.PollNexusOperationResponse, err error) { + defer log.CapturePanic(h.logger, &err) + + ref := chasm.NewComponentRef[*Operation](chasm.ExecutionKey{ + NamespaceID: req.GetNamespaceId(), + BusinessID: req.GetFrontendRequest().GetOperationId(), + RunID: req.GetFrontendRequest().GetRunId(), + }) + + // Determine the long poll timeout and buffer. + ns := req.GetFrontendRequest().GetNamespace() + ctx, cancel := contextutil.WithDeadlineBuffer( + ctx, + h.config.LongPollTimeout(ns), + h.config.LongPollBuffer(ns), + ) + defer cancel() + + // Poll for the wait stage to be reached. + waitStage := req.GetFrontendRequest().GetWaitStage() + response, _, err = chasm.PollComponent(ctx, ref, func( + o *Operation, + ctx chasm.Context, + req *nexusoperationpb.PollNexusOperationRequest, + ) (*nexusoperationpb.PollNexusOperationResponse, bool, error) { + if o.isWaitStageReached(ctx, waitStage) { + response := o.buildPollResponse(ctx) + return response, true, nil + } + return nil, false, nil + }, req) + + if err != nil && ctx.Err() != nil { + // Send an empty non-error response as an invitation to resubmit the long-poll. + return &nexusoperationpb.PollNexusOperationResponse{ + FrontendResponse: &workflowservice.PollNexusOperationExecutionResponse{}, + }, nil + } + return response, err } // RequestCancelNexusOperation requests cancellation of a standalone Nexus operation via CHASM. diff --git a/chasm/lib/nexusoperation/operation.go b/chasm/lib/nexusoperation/operation.go index a4468217a6..ff48714122 100644 --- a/chasm/lib/nexusoperation/operation.go +++ b/chasm/lib/nexusoperation/operation.go @@ -334,32 +334,87 @@ func (o *Operation) buildDescribeResponse( ctx chasm.Context, req *nexusoperationpb.DescribeNexusOperationRequest, ) (*nexusoperationpb.DescribeNexusOperationResponse, error) { + token, err := ctx.Ref(o) + if err != nil { + return nil, err + } + resp := &workflowservice.DescribeNexusOperationExecutionResponse{ - RunId: ctx.ExecutionKey().RunID, - Info: o.buildExecutionInfo(ctx), + RunId: ctx.ExecutionKey().RunID, + Info: o.buildExecutionInfo(ctx), + LongPollToken: token, } if req.GetFrontendRequest().GetIncludeInput() { resp.Input = o.RequestData.Get(ctx).GetInput() } - if req.GetFrontendRequest().GetIncludeOutcome() && o.LifecycleState(ctx).IsClosed() { - outcome := o.Outcome.Get(ctx) - if successful := outcome.GetSuccessful(); successful != nil { + if req.GetFrontendRequest().GetIncludeOutcome() && o.isClosed() { + if successful, failure := o.describeOutcome(ctx); successful != nil { resp.Outcome = &workflowservice.DescribeNexusOperationExecutionResponse_Result{ - Result: successful.GetResult(), + Result: successful, } - } else if failure := outcome.GetFailed().GetFailure(); failure != nil { + } else if failure != nil { resp.Outcome = &workflowservice.DescribeNexusOperationExecutionResponse_Failure{ Failure: failure, } - } else if o.LastAttemptFailure != nil { - resp.Outcome = &workflowservice.DescribeNexusOperationExecutionResponse_Failure{ - Failure: o.LastAttemptFailure, - } } } return &nexusoperationpb.DescribeNexusOperationResponse{FrontendResponse: resp}, nil } +func (o *Operation) buildPollResponse( + ctx chasm.Context, +) *nexusoperationpb.PollNexusOperationResponse { + resp := &workflowservice.PollNexusOperationExecutionResponse{ + RunId: ctx.ExecutionKey().RunID, + OperationToken: o.OperationToken, + } + + if o.isClosed() { + resp.WaitStage = enumspb.NEXUS_OPERATION_WAIT_STAGE_CLOSED + if successful, failure := o.describeOutcome(ctx); successful != nil { + resp.Outcome = &workflowservice.PollNexusOperationExecutionResponse_Result{ + Result: successful, + } + } else if failure != nil { + resp.Outcome = &workflowservice.PollNexusOperationExecutionResponse_Failure{ + Failure: failure, + } + } + } else { + resp.WaitStage = enumspb.NEXUS_OPERATION_WAIT_STAGE_STARTED + } + + return &nexusoperationpb.PollNexusOperationResponse{ + FrontendResponse: resp, + } +} + +func (o *Operation) describeOutcome(ctx chasm.Context) (*commonpb.Payload, *failurepb.Failure) { + outcome := o.Outcome.Get(ctx) + if successful := outcome.GetSuccessful(); successful != nil { + return successful.GetResult(), nil + } + if failure := outcome.GetFailed().GetFailure(); failure != nil { + return nil, failure + } + return nil, o.LastAttemptFailure +} + +func (o *Operation) isWaitStageReached(_ chasm.Context, waitStage enumspb.NexusOperationWaitStage) bool { + switch waitStage { + case enumspb.NEXUS_OPERATION_WAIT_STAGE_STARTED: + return o.Status == nexusoperationpb.OPERATION_STATUS_STARTED || o.isClosed() + case enumspb.NEXUS_OPERATION_WAIT_STAGE_CLOSED: + return o.isClosed() + default: + return false + } +} + +func (o *Operation) isClosed() bool { + return o.LifecycleState(nil).IsClosed() +} + func (o *Operation) buildExecutionInfo(ctx chasm.Context) *nexuspb.NexusOperationExecutionInfo { requestData := o.RequestData.Get(ctx) key := ctx.ExecutionKey() diff --git a/chasm/lib/nexusoperation/operation_test.go b/chasm/lib/nexusoperation/operation_test.go new file mode 100644 index 0000000000..cedeaf09a3 --- /dev/null +++ b/chasm/lib/nexusoperation/operation_test.go @@ -0,0 +1,90 @@ +package nexusoperation + +import ( + "slices" + "testing" + + "github.com/stretchr/testify/require" + enumspb "go.temporal.io/api/enums/v1" + "go.temporal.io/server/chasm" + "go.temporal.io/server/chasm/lib/nexusoperation/gen/nexusoperationpb/v1" + "go.temporal.io/server/common/testing/protoutils" +) + +func TestIsWaitStageReached(t *testing.T) { + t.Parallel() + + ctx := &chasm.MockContext{} + allStatuses := protoutils.EnumValues[nexusoperationpb.OperationStatus]() + + tests := []struct { + name string + waitStage enumspb.NexusOperationWaitStage + reached []nexusoperationpb.OperationStatus + notReached []nexusoperationpb.OperationStatus + }{ + { + name: "Unspecified", + waitStage: enumspb.NEXUS_OPERATION_WAIT_STAGE_UNSPECIFIED, + notReached: allStatuses, + }, + { + name: "Started", + waitStage: enumspb.NEXUS_OPERATION_WAIT_STAGE_STARTED, + reached: []nexusoperationpb.OperationStatus{ + nexusoperationpb.OPERATION_STATUS_STARTED, + nexusoperationpb.OPERATION_STATUS_SUCCEEDED, + nexusoperationpb.OPERATION_STATUS_FAILED, + nexusoperationpb.OPERATION_STATUS_CANCELED, + nexusoperationpb.OPERATION_STATUS_TERMINATED, + nexusoperationpb.OPERATION_STATUS_TIMED_OUT, + }, + notReached: []nexusoperationpb.OperationStatus{ + nexusoperationpb.OPERATION_STATUS_UNSPECIFIED, + nexusoperationpb.OPERATION_STATUS_SCHEDULED, + nexusoperationpb.OPERATION_STATUS_BACKING_OFF, + }, + }, + { + name: "Closed", + waitStage: enumspb.NEXUS_OPERATION_WAIT_STAGE_CLOSED, + reached: []nexusoperationpb.OperationStatus{ + nexusoperationpb.OPERATION_STATUS_SUCCEEDED, + nexusoperationpb.OPERATION_STATUS_FAILED, + nexusoperationpb.OPERATION_STATUS_CANCELED, + nexusoperationpb.OPERATION_STATUS_TERMINATED, + nexusoperationpb.OPERATION_STATUS_TIMED_OUT, + }, + notReached: []nexusoperationpb.OperationStatus{ + nexusoperationpb.OPERATION_STATUS_UNSPECIFIED, + nexusoperationpb.OPERATION_STATUS_SCHEDULED, + nexusoperationpb.OPERATION_STATUS_BACKING_OFF, + nexusoperationpb.OPERATION_STATUS_STARTED, + }, + }, + } + + coveredWaitStages := []enumspb.NexusOperationWaitStage{} + for _, tt := range tests { + coveredWaitStages = append(coveredWaitStages, tt.waitStage) + t.Run(tt.name, func(t *testing.T) { + op := newTestOperation() + + coveredStatuses := append(slices.Clone(tt.reached), tt.notReached...) + require.ElementsMatch(t, allStatuses, coveredStatuses) + + for _, status := range tt.reached { + op.Status = status + require.Truef(t, op.isWaitStageReached(ctx, tt.waitStage), "expected %s to match %s", status, tt.waitStage) + } + + for _, status := range tt.notReached { + op.Status = status + require.Falsef(t, op.isWaitStageReached(ctx, tt.waitStage), "expected %s not to match %s", status, tt.waitStage) + } + }) + } + + allWaitStages := protoutils.EnumValues[enumspb.NexusOperationWaitStage]() + require.ElementsMatch(t, allWaitStages, coveredWaitStages) +} diff --git a/chasm/lib/nexusoperation/validator.go b/chasm/lib/nexusoperation/validator.go index fe5aaf5b20..88714ac9dc 100644 --- a/chasm/lib/nexusoperation/validator.go +++ b/chasm/lib/nexusoperation/validator.go @@ -12,6 +12,7 @@ import ( enumspb "go.temporal.io/api/enums/v1" "go.temporal.io/api/serviceerror" "go.temporal.io/api/workflowservice/v1" + "go.temporal.io/server/chasm" "go.temporal.io/server/common/log" "go.temporal.io/server/common/log/tag" "go.temporal.io/server/common/primitives/timestamp" @@ -170,7 +171,50 @@ func validateAndNormalizeDeleteRequest(req *workflowservice.DeleteNexusOperation return nil } -func validateAndNormalizeDescribeRequest(req *workflowservice.DescribeNexusOperationExecutionRequest, config *Config) error { +func validateAndNormalizeDescribeRequest( + req *workflowservice.DescribeNexusOperationExecutionRequest, + namespaceID string, + config *Config, +) error { + if req.GetOperationId() == "" { + return serviceerror.NewInvalidArgument("operation_id is required") + } + if len(req.GetOperationId()) > config.MaxIDLengthLimit() { + return serviceerror.NewInvalidArgumentf("operation_id exceeds length limit. Length=%d Limit=%d", + len(req.GetOperationId()), config.MaxIDLengthLimit()) + } + if len(req.GetLongPollToken()) > 0 && req.GetRunId() == "" { + return serviceerror.NewInvalidArgument("run_id is required when long_poll_token is provided") + } + if req.GetRunId() != "" { + if err := uuid.Validate(req.GetRunId()); err != nil { + return serviceerror.NewInvalidArgument("run_id is not a valid UUID") + } + } + if len(req.GetLongPollToken()) > 0 { + ref, err := chasm.DeserializeComponentRef(req.GetLongPollToken()) + if err != nil { + return serviceerror.NewInvalidArgument("invalid long poll token") + } + if ref.NamespaceID != namespaceID { + return serviceerror.NewInvalidArgument("long poll token does not match execution") + } + } + return nil +} + +func validateAndNormalizePollRequest(req *workflowservice.PollNexusOperationExecutionRequest, config *Config) error { + // Normalize wait stage: UNSPECIFIED defaults to CLOSED. + if req.GetWaitStage() == enumspb.NEXUS_OPERATION_WAIT_STAGE_UNSPECIFIED { + req.WaitStage = enumspb.NEXUS_OPERATION_WAIT_STAGE_CLOSED + } else { + switch req.GetWaitStage() { + case enumspb.NEXUS_OPERATION_WAIT_STAGE_STARTED, + enumspb.NEXUS_OPERATION_WAIT_STAGE_CLOSED: + default: + return serviceerror.NewInvalidArgumentf("unsupported wait_stage: %s", req.GetWaitStage()) + } + } if req.GetOperationId() == "" { return serviceerror.NewInvalidArgument("operation_id is required") } @@ -183,7 +227,6 @@ func validateAndNormalizeDescribeRequest(req *workflowservice.DescribeNexusOpera return serviceerror.NewInvalidArgument("run_id is not a valid UUID") } } - // TODO: Add long-poll validation (run_id required when long_poll_token is set). return nil } diff --git a/chasm/lib/nexusoperation/validator_test.go b/chasm/lib/nexusoperation/validator_test.go index 453e2bb552..925fb92d2b 100644 --- a/chasm/lib/nexusoperation/validator_test.go +++ b/chasm/lib/nexusoperation/validator_test.go @@ -10,6 +10,7 @@ import ( enumspb "go.temporal.io/api/enums/v1" "go.temporal.io/api/serviceerror" "go.temporal.io/api/workflowservice/v1" + persistencespb "go.temporal.io/server/api/persistence/v1" "go.temporal.io/server/common/dynamicconfig" "go.temporal.io/server/common/log" "go.temporal.io/server/common/payload" @@ -260,6 +261,21 @@ func TestValidateDescribeNexusOperationExecutionRequest(t *testing.T) { MaxIDLengthLimit: func() int { return 20 }, } + validRunID := "11111111-2222-3333-4444-555555555555" + validToken, err := (&persistencespb.ChasmComponentRef{ + NamespaceId: "test-namespace-id", + BusinessId: "operation-id", + RunId: validRunID, + }).Marshal() + require.NoError(t, err) + + wrongNamespaceToken, err := (&persistencespb.ChasmComponentRef{ + NamespaceId: "other-namespace-id", + BusinessId: "operation-id", + RunId: validRunID, + }).Marshal() + require.NoError(t, err) + for _, tc := range []struct { name string mutate func(*workflowservice.DescribeNexusOperationExecutionRequest) @@ -289,6 +305,29 @@ func TestValidateDescribeNexusOperationExecutionRequest(t *testing.T) { }, errMsg: "run_id is not a valid UUID", }, + { + name: "long_poll_token - requires run_id", + mutate: func(r *workflowservice.DescribeNexusOperationExecutionRequest) { + r.LongPollToken = validToken + }, + errMsg: "run_id is required when long_poll_token is provided", + }, + { + name: "long_poll_token - rejects malformed token", + mutate: func(r *workflowservice.DescribeNexusOperationExecutionRequest) { + r.RunId = validRunID + r.LongPollToken = []byte("not-a-token") + }, + errMsg: "invalid long poll token", + }, + { + name: "long_poll_token - rejects wrong namespace", + mutate: func(r *workflowservice.DescribeNexusOperationExecutionRequest) { + r.RunId = validRunID + r.LongPollToken = wrongNamespaceToken + }, + errMsg: "long poll token does not match execution", + }, } { t.Run(tc.name, func(t *testing.T) { validReq := &workflowservice.DescribeNexusOperationExecutionRequest{ @@ -298,7 +337,7 @@ func TestValidateDescribeNexusOperationExecutionRequest(t *testing.T) { if tc.mutate != nil { tc.mutate(validReq) } - err := validateAndNormalizeDescribeRequest(validReq, config) + err := validateAndNormalizeDescribeRequest(validReq, "test-namespace-id", config) if tc.errMsg != "" { var invalidArgErr *serviceerror.InvalidArgument require.ErrorAs(t, err, &invalidArgErr) @@ -550,3 +589,96 @@ func TestValidateTerminateNexusOperationExecutionRequest(t *testing.T) { }) } } + +func TestValidatePollNexusOperationExecutionRequest(t *testing.T) { + config := &Config{ + MaxIDLengthLimit: func() int { return 20 }, + } + + for _, tc := range []struct { + name string + mutate func(*workflowservice.PollNexusOperationExecutionRequest) + errMsg string + check func(*testing.T, *workflowservice.PollNexusOperationExecutionRequest) + }{ + { + name: "valid request", + }, + { + name: "operation_id - required", + mutate: func(r *workflowservice.PollNexusOperationExecutionRequest) { + r.OperationId = "" + }, + errMsg: "operation_id is required", + }, + { + name: "operation_id - exceeds length limit", + mutate: func(r *workflowservice.PollNexusOperationExecutionRequest) { + r.OperationId = "this-operation-id-is-too-long" + }, + errMsg: "operation_id exceeds length limit", + }, + { + name: "run_id - not a valid UUID", + mutate: func(r *workflowservice.PollNexusOperationExecutionRequest) { + r.RunId = "not-a-uuid" + }, + errMsg: "run_id is not a valid UUID", + }, + { + name: "wait_stage - normalizes UNSPECIFIED to CLOSED", + mutate: func(r *workflowservice.PollNexusOperationExecutionRequest) { + r.WaitStage = enumspb.NEXUS_OPERATION_WAIT_STAGE_UNSPECIFIED + }, + check: func(t *testing.T, r *workflowservice.PollNexusOperationExecutionRequest) { + require.Equal(t, enumspb.NEXUS_OPERATION_WAIT_STAGE_CLOSED, r.WaitStage) + }, + }, + { + name: "wait_stage - preserves STARTED", + mutate: func(r *workflowservice.PollNexusOperationExecutionRequest) { + r.WaitStage = enumspb.NEXUS_OPERATION_WAIT_STAGE_STARTED + }, + check: func(t *testing.T, r *workflowservice.PollNexusOperationExecutionRequest) { + require.Equal(t, enumspb.NEXUS_OPERATION_WAIT_STAGE_STARTED, r.WaitStage) + }, + }, + { + name: "wait_stage - preserves CLOSED", + mutate: func(r *workflowservice.PollNexusOperationExecutionRequest) { + r.WaitStage = enumspb.NEXUS_OPERATION_WAIT_STAGE_CLOSED + }, + check: func(t *testing.T, r *workflowservice.PollNexusOperationExecutionRequest) { + require.Equal(t, enumspb.NEXUS_OPERATION_WAIT_STAGE_CLOSED, r.WaitStage) + }, + }, + { + name: "wait_stage - rejects unsupported value", + mutate: func(r *workflowservice.PollNexusOperationExecutionRequest) { + r.WaitStage = enumspb.NexusOperationWaitStage(99) + }, + errMsg: "unsupported wait_stage", + }, + } { + t.Run(tc.name, func(t *testing.T) { + validReq := &workflowservice.PollNexusOperationExecutionRequest{ + Namespace: "default", + OperationId: "operation-id", + } + if tc.mutate != nil { + tc.mutate(validReq) + } + err := validateAndNormalizePollRequest(validReq, config) + if tc.errMsg != "" { + var invalidArgErr *serviceerror.InvalidArgument + require.ErrorAs(t, err, &invalidArgErr) + require.Contains(t, err.Error(), tc.errMsg) + } else { + require.NoError(t, err) + } + if tc.check != nil { + tc.check(t, validReq) + } + }) + } +} diff --git a/common/testing/protoutils/enum.go b/common/testing/protoutils/enum.go new file mode 100644 index 0000000000..8d62b819de --- /dev/null +++ b/common/testing/protoutils/enum.go @@ -0,0 +1,18 @@ +package protoutils + +import "google.golang.org/protobuf/reflect/protoreflect" + +type ProtoEnum interface { + ~int32 + Descriptor() protoreflect.EnumDescriptor +} + +func EnumValues[T ProtoEnum]() []T { + var zero T + values := zero.Descriptor().Values() + enums := make([]T, values.Len()) + for i := range enums { + enums[i] = T(values.Get(i).Number()) + } + return enums +} diff --git a/tests/nexus_standalone_test.go b/tests/nexus_standalone_test.go index aaa01a4d57..46801e179c 100644 --- a/tests/nexus_standalone_test.go +++ b/tests/nexus_standalone_test.go @@ -2,19 +2,23 @@ package tests import ( "cmp" + "context" "errors" "fmt" "testing" "time" + "github.com/nexus-rpc/sdk-go/nexus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" commonpb "go.temporal.io/api/common/v1" enumspb "go.temporal.io/api/enums/v1" + failurepb "go.temporal.io/api/failure/v1" nexuspb "go.temporal.io/api/nexus/v1" "go.temporal.io/api/operatorservice/v1" sdkpb "go.temporal.io/api/sdk/v1" "go.temporal.io/api/serviceerror" + taskqueuepb "go.temporal.io/api/taskqueue/v1" "go.temporal.io/api/workflowservice/v1" "go.temporal.io/server/chasm/lib/nexusoperation" "go.temporal.io/server/common/dynamicconfig" @@ -30,7 +34,7 @@ var nexusStandaloneOpts = []testcore.TestOption{ testcore.WithDynamicConfig(nexusoperation.Enabled, true), } -func TestStandaloneNexusOperation(t *testing.T) { +func TestStartStandaloneNexusOperation(t *testing.T) { t.Parallel() t.Run("StartAndDescribe", func(t *testing.T) { @@ -59,44 +63,53 @@ func TestStandaloneNexusOperation(t *testing.T) { s.NoError(err) s.True(startResp.GetStarted()) - // Describe without IncludeInput. - descResp, err := s.FrontendClient().DescribeNexusOperationExecution(s.Context(), &workflowservice.DescribeNexusOperationExecutionRequest{ - Namespace: s.Namespace().String(), - OperationId: "test-op", - RunId: startResp.RunId, - }) - s.NoError(err) - s.Equal(startResp.RunId, descResp.RunId) - s.Nil(descResp.GetInput()) // not included by default - - info := descResp.GetInfo() - protorequire.ProtoEqual(t, &nexuspb.NexusOperationExecutionInfo{ - OperationId: "test-op", - RunId: startResp.RunId, - Endpoint: endpointName, - Service: "test-service", - Operation: "test-operation", - Status: enumspb.NEXUS_OPERATION_EXECUTION_STATUS_RUNNING, - State: enumspb.PENDING_NEXUS_OPERATION_STATE_SCHEDULED, - ScheduleToCloseTimeout: durationpb.New(10 * time.Minute), - NexusHeader: testHeader, - UserMetadata: testUserMetadata, - SearchAttributes: testSearchAttributes, - Attempt: 0, - StateTransitionCount: 1, - // Dynamic fields copied from actual response for comparison. - RequestId: info.GetRequestId(), - ScheduleTime: info.GetScheduleTime(), - ExpirationTime: info.GetExpirationTime(), - ExecutionDuration: info.GetExecutionDuration(), - }, info) - s.NotEmpty(info.GetRequestId()) - s.NotNil(info.GetScheduleTime()) - s.NotNil(info.GetExpirationTime()) - s.NotNil(info.GetExecutionDuration()) + for _, tc := range []struct { + name string + runID string + }{ + {name: "WithRunID", runID: startResp.RunId}, + {name: "WithEmptyRunID", runID: ""}, + } { + t.Run(tc.name, func(t *testing.T) { + descResp, err := s.FrontendClient().DescribeNexusOperationExecution(s.Context(), &workflowservice.DescribeNexusOperationExecutionRequest{ + Namespace: s.Namespace().String(), + OperationId: "test-op", + RunId: tc.runID, + }) + s.NoError(err) + s.Equal(startResp.RunId, descResp.RunId) + + info := descResp.GetInfo() + protorequire.ProtoEqual(t, &nexuspb.NexusOperationExecutionInfo{ + OperationId: "test-op", + RunId: startResp.RunId, + Endpoint: endpointName, + Service: "test-service", + Operation: "test-operation", + Status: enumspb.NEXUS_OPERATION_EXECUTION_STATUS_RUNNING, + State: enumspb.PENDING_NEXUS_OPERATION_STATE_SCHEDULED, + ScheduleToCloseTimeout: durationpb.New(10 * time.Minute), + NexusHeader: testHeader, + UserMetadata: testUserMetadata, + SearchAttributes: testSearchAttributes, + Attempt: 1, + StateTransitionCount: 1, + // Dynamic fields copied from actual response for comparison. + RequestId: info.GetRequestId(), + ScheduleTime: info.GetScheduleTime(), + ExpirationTime: info.GetExpirationTime(), + ExecutionDuration: info.GetExecutionDuration(), + }, info) + s.NotEmpty(descResp.GetLongPollToken()) + s.NotEmpty(info.GetRequestId()) + s.NotNil(info.GetScheduleTime()) + s.NotNil(info.GetExpirationTime()) + s.NotNil(info.GetExecutionDuration()) + }) + } // Describe with IncludeInput. - descResp, err = s.FrontendClient().DescribeNexusOperationExecution(s.Context(), &workflowservice.DescribeNexusOperationExecutionRequest{ + descResp, err := s.FrontendClient().DescribeNexusOperationExecution(s.Context(), &workflowservice.DescribeNexusOperationExecutionRequest{ Namespace: s.Namespace().String(), OperationId: "test-op", RunId: startResp.RunId, @@ -108,7 +121,7 @@ func TestStandaloneNexusOperation(t *testing.T) { // Validates that request validation is wired up in the frontend. // Exhaustive validation cases are covered in unit tests. - t.Run("StartValidation", func(t *testing.T) { + t.Run("Validation", func(t *testing.T) { s := testcore.NewEnv(t, nexusStandaloneOpts...) _, err := startNexusOperation(s, &workflowservice.StartNexusOperationExecutionRequest{ @@ -118,37 +131,7 @@ func TestStandaloneNexusOperation(t *testing.T) { s.Contains(err.Error(), "operation_id is required") }) - t.Run("DescribeNotFound", func(t *testing.T) { - s := testcore.NewEnv(t, nexusStandaloneOpts...) - - _, err := s.FrontendClient().DescribeNexusOperationExecution(s.Context(), &workflowservice.DescribeNexusOperationExecutionRequest{ - Namespace: s.Namespace().String(), - OperationId: "does-not-exist", - }) - var notFound *serviceerror.NotFound - s.ErrorAs(err, ¬Found) - }) - - t.Run("DescribeWrongRunId", func(t *testing.T) { - s := testcore.NewEnv(t, nexusStandaloneOpts...) - endpointName := createNexusEndpoint(s) - - _, err := startNexusOperation(s, &workflowservice.StartNexusOperationExecutionRequest{ - OperationId: "test-op", - Endpoint: endpointName, - }) - s.NoError(err) - - _, err = s.FrontendClient().DescribeNexusOperationExecution(s.Context(), &workflowservice.DescribeNexusOperationExecutionRequest{ - Namespace: s.Namespace().String(), - OperationId: "test-op", - RunId: "00000000-0000-0000-0000-000000000000", - }) - var notFound *serviceerror.NotFound - s.ErrorAs(err, ¬Found) - }) - - t.Run("IDConflictPolicy_Fail", func(t *testing.T) { + t.Run("IDConflictPolicyFail", func(t *testing.T) { s := testcore.NewEnv(t, nexusStandaloneOpts...) endpointName := createNexusEndpoint(s) @@ -176,7 +159,7 @@ func TestStandaloneNexusOperation(t *testing.T) { s.False(resp2.GetStarted()) }) - t.Run("IDConflictPolicy_UseExisting", func(t *testing.T) { + t.Run("IDConflictPolicyUseExisting", func(t *testing.T) { s := testcore.NewEnv(t, nexusStandaloneOpts...) endpointName := createNexusEndpoint(s) @@ -198,7 +181,290 @@ func TestStandaloneNexusOperation(t *testing.T) { }) } -func TestRequestCancelNexusOperationExecution(t *testing.T) { +func TestDescribeStandaloneNexusOperation(t *testing.T) { + t.Parallel() + + t.Run("NotFound", func(t *testing.T) { + s := testcore.NewEnv(t, nexusStandaloneOpts...) + + _, err := s.FrontendClient().DescribeNexusOperationExecution(s.Context(), &workflowservice.DescribeNexusOperationExecutionRequest{ + Namespace: s.Namespace().String(), + OperationId: "does-not-exist", + }) + var notFound *serviceerror.NotFound + s.ErrorAs(err, ¬Found) + s.Equal("operation not found for ID: does-not-exist", notFound.Error()) + }) + + t.Run("LongPollStateChange", func(t *testing.T) { + s := testcore.NewEnv(t, nexusStandaloneOpts...) + endpointName := createNexusEndpoint(s) + + startResp, err := startNexusOperation(s, &workflowservice.StartNexusOperationExecutionRequest{ + OperationId: "test-op", + Endpoint: endpointName, + }) + s.NoError(err) + + // Obtain longpoll token. + firstResp, err := s.FrontendClient().DescribeNexusOperationExecution(s.Context(), &workflowservice.DescribeNexusOperationExecutionRequest{ + Namespace: s.Namespace().String(), + OperationId: "test-op", + RunId: startResp.RunId, + }) + s.NoError(err) + s.NotEmpty(firstResp.GetLongPollToken()) + s.Equal(enumspb.NEXUS_OPERATION_EXECUTION_STATUS_RUNNING, firstResp.GetInfo().GetStatus()) + + ctx, cancel := context.WithTimeout(s.Context(), 10*time.Second) + defer cancel() + + // Start polling. + type describeResult struct { + resp *workflowservice.DescribeNexusOperationExecutionResponse + err error + } + describeResultCh := make(chan describeResult, 1) + + go func() { + resp, err := s.FrontendClient().DescribeNexusOperationExecution(ctx, &workflowservice.DescribeNexusOperationExecutionRequest{ + Namespace: s.Namespace().String(), + OperationId: "test-op", + RunId: startResp.RunId, + IncludeOutcome: true, + LongPollToken: firstResp.GetLongPollToken(), + }) + describeResultCh <- describeResult{resp: resp, err: err} + }() + + // Wait 1s to ensure the poll is still active. + select { + case result := <-describeResultCh: + require.NoError(t, result.err) + t.Fatal("DescribeNexusOperationExecution returned before the state changed") + case <-time.After(1 * time.Second): + } + + // Terminate the operation. + terminateErrCh := make(chan error, 1) + go func() { + _, err := s.FrontendClient().TerminateNexusOperationExecution(ctx, &workflowservice.TerminateNexusOperationExecutionRequest{ + Namespace: s.Namespace().String(), + OperationId: "test-op", + RunId: startResp.RunId, + Reason: "test termination", + }) + terminateErrCh <- err + }() + + select { + case err := <-terminateErrCh: + require.NoError(t, err) + case <-ctx.Done(): + t.Fatal("TerminateNexusOperationExecution timed out") + } + + // Verify the longpoll result. + var longPollResp *workflowservice.DescribeNexusOperationExecutionResponse + select { + case result := <-describeResultCh: + require.NoError(t, result.err) + longPollResp = result.resp + case <-ctx.Done(): + t.Fatal("DescribeNexusOperationExecution timed out") + } + + s.Equal(startResp.RunId, longPollResp.GetRunId()) + s.Equal(enumspb.NEXUS_OPERATION_EXECUTION_STATUS_TERMINATED, longPollResp.GetInfo().GetStatus()) + s.Greater(longPollResp.GetInfo().GetStateTransitionCount(), firstResp.GetInfo().GetStateTransitionCount()) + }) + + t.Run("LongPollTimeoutReturnsEmptyResponse", func(t *testing.T) { + s := testcore.NewEnv(t, nexusStandaloneOpts...) + endpointName := createNexusEndpoint(s) + + startResp, err := startNexusOperation(s, &workflowservice.StartNexusOperationExecutionRequest{ + OperationId: "test-op", + Endpoint: endpointName, + }) + s.NoError(err) + + firstResp, err := s.FrontendClient().DescribeNexusOperationExecution(s.Context(), &workflowservice.DescribeNexusOperationExecutionRequest{ + Namespace: s.Namespace().String(), + OperationId: "test-op", + RunId: startResp.RunId, + }) + s.NoError(err) + s.NotEmpty(firstResp.GetLongPollToken()) + + t.Run("CallerDeadlineNotExceeded", func(t *testing.T) { + s.OverrideDynamicConfig(nexusoperation.LongPollBuffer, time.Second) + s.OverrideDynamicConfig(nexusoperation.LongPollTimeout, 10*time.Millisecond) + + ctx, cancel := context.WithTimeout(s.Context(), 5*time.Second) + defer cancel() + + longPollResp, err := s.FrontendClient().DescribeNexusOperationExecution(ctx, &workflowservice.DescribeNexusOperationExecutionRequest{ + Namespace: s.Namespace().String(), + OperationId: "test-op", + RunId: startResp.RunId, + LongPollToken: firstResp.GetLongPollToken(), + }) + s.NoError(err) + protorequire.ProtoEqual(t, &workflowservice.DescribeNexusOperationExecutionResponse{}, longPollResp) + }) + + t.Run("NoCallerDeadline", func(t *testing.T) { + // Frontend still imposes its own deadline upstream, so the buffer must fit within that. + s.OverrideDynamicConfig(nexusoperation.LongPollBuffer, 29*time.Second) + s.OverrideDynamicConfig(nexusoperation.LongPollTimeout, 10*time.Millisecond) + + longPollResp, err := s.FrontendClient().DescribeNexusOperationExecution(context.Background(), &workflowservice.DescribeNexusOperationExecutionRequest{ + Namespace: s.Namespace().String(), + OperationId: "test-op", + RunId: startResp.RunId, + LongPollToken: firstResp.GetLongPollToken(), + }) + s.NoError(err) + protorequire.ProtoEqual(t, &workflowservice.DescribeNexusOperationExecutionResponse{}, longPollResp) + }) + }) + + t.Run("IncludeOutcome_Failure", func(t *testing.T) { + // TODO: Add canceled last-attempt-failure coverage here once standalone cancellation tasks + // can be completed through the public Nexus task APIs. + + testCases := []struct { + name string + setup func(*workflowservice.StartNexusOperationExecutionRequest) + respond func(context.Context, *testcore.TestEnv, *workflowservice.PollNexusTaskQueueResponse) error + expectedStatus enumspb.NexusOperationExecutionStatus + expectedFailureMessage string + }{ + { + name: "TimeoutLastAttemptFailure", + setup: func(req *workflowservice.StartNexusOperationExecutionRequest) { + req.ScheduleToCloseTimeout = durationpb.New(2 * time.Second) + }, + respond: func(ctx context.Context, s *testcore.TestEnv, task *workflowservice.PollNexusTaskQueueResponse) error { + _, err := s.FrontendClient().RespondNexusTaskFailed(ctx, &workflowservice.RespondNexusTaskFailedRequest{ + Namespace: s.Namespace().String(), + Identity: "test-worker", + TaskToken: task.GetTaskToken(), + Error: &nexuspb.HandlerError{ + ErrorType: string(nexus.HandlerErrorTypeInternal), + Failure: &nexuspb.Failure{ + Message: "last attempt failure", + }, + }, + }) + return err + }, + expectedStatus: enumspb.NEXUS_OPERATION_EXECUTION_STATUS_TIMED_OUT, + expectedFailureMessage: "last attempt failure", + }, + { + name: "TerminalFailure", + respond: func(ctx context.Context, s *testcore.TestEnv, task *workflowservice.PollNexusTaskQueueResponse) error { + _, err := s.FrontendClient().RespondNexusTaskCompleted(ctx, &workflowservice.RespondNexusTaskCompletedRequest{ + Namespace: s.Namespace().String(), + Identity: "test-worker", + TaskToken: task.GetTaskToken(), + Response: &nexuspb.Response{ + Variant: &nexuspb.Response_StartOperation{ + StartOperation: &nexuspb.StartOperationResponse{ + Variant: &nexuspb.StartOperationResponse_Failure{ + Failure: &failurepb.Failure{Message: "final failure"}, + }, + }, + }, + }, + }) + return err + }, + expectedStatus: enumspb.NEXUS_OPERATION_EXECUTION_STATUS_FAILED, + expectedFailureMessage: "final failure", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Skip("Enable once standalone Nexus task and cancellation executors are wired through public Nexus task APIs") + + s := testcore.NewEnv(t, nexusStandaloneOpts...) + taskQueue := testcore.RandomizedNexusEndpoint(t.Name()) + endpointName := createNexusEndpointWithTaskQueue(s, taskQueue) + startReq := &workflowservice.StartNexusOperationExecutionRequest{ + OperationId: "test-op", + Endpoint: endpointName, + } + if tc.setup != nil { + tc.setup(startReq) + } + + startResp, err := startNexusOperation(s, startReq) + s.NoError(err) + + ctx, cancel := context.WithTimeout(s.Context(), 10*time.Second) + defer cancel() + + pollerErrCh := make(chan error, 1) + go func() { + task, err := s.FrontendClient().PollNexusTaskQueue(ctx, &workflowservice.PollNexusTaskQueueRequest{ + Namespace: s.Namespace().String(), + Identity: "test-worker", + TaskQueue: &taskqueuepb.TaskQueue{ + Name: taskQueue, + Kind: enumspb.TASK_QUEUE_KIND_NORMAL, + }, + }) + if err != nil { + pollerErrCh <- err + return + } + pollerErrCh <- tc.respond(ctx, s, task) + }() + + require.EventuallyWithT(t, func(t *assert.CollectT) { + descResp, err := s.FrontendClient().DescribeNexusOperationExecution(s.Context(), &workflowservice.DescribeNexusOperationExecutionRequest{ + Namespace: s.Namespace().String(), + OperationId: "test-op", + RunId: startResp.RunId, + IncludeOutcome: true, + }) + require.NoError(t, err) + require.Equal(t, tc.expectedStatus, descResp.GetInfo().GetStatus()) + require.Equal(t, tc.expectedFailureMessage, descResp.GetFailure().GetMessage()) + + pollResp, err := s.FrontendClient().PollNexusOperationExecution(s.Context(), &workflowservice.PollNexusOperationExecutionRequest{ + Namespace: s.Namespace().String(), + OperationId: "test-op", + RunId: startResp.RunId, + WaitStage: enumspb.NEXUS_OPERATION_WAIT_STAGE_CLOSED, + }) + require.NoError(t, err) + require.Equal(t, tc.expectedFailureMessage, pollResp.GetFailure().GetMessage()) + }, 10*time.Second, 100*time.Millisecond) + + s.NoError(<-pollerErrCh) + }) + } + }) + + // Validates that request validation is wired up in the frontend. + // Exhaustive validation cases are covered in unit tests. + t.Run("Validation", func(t *testing.T) { + s := testcore.NewEnv(t, nexusStandaloneOpts...) + + _, err := s.FrontendClient().DescribeNexusOperationExecution(s.Context(), &workflowservice.DescribeNexusOperationExecutionRequest{ + Namespace: s.Namespace().String(), + }) + s.Error(err) + s.ErrorContains(err, "operation_id is required") + }) +} + +func TestStandaloneNexusOperationCancel(t *testing.T) { t.Parallel() t.Run("RequestCancel", func(t *testing.T) { @@ -226,6 +492,25 @@ func TestRequestCancelNexusOperationExecution(t *testing.T) { RunId: startResp.RunId, }) s.NoError(err) + protorequire.ProtoEqual(t, &nexuspb.NexusOperationExecutionInfo{ + OperationId: "test-op", + RunId: startResp.RunId, + Endpoint: endpointName, + Service: "test-service", + Operation: "test-operation", + Status: enumspb.NEXUS_OPERATION_EXECUTION_STATUS_RUNNING, + State: enumspb.PENDING_NEXUS_OPERATION_STATE_SCHEDULED, + ScheduleToCloseTimeout: durationpb.New(10 * time.Minute), + NexusHeader: map[string]string{}, + SearchAttributes: &commonpb.SearchAttributes{}, + Attempt: 1, + StateTransitionCount: descResp.GetInfo().GetStateTransitionCount(), + // Dynamic fields copied from actual response for comparison. + RequestId: descResp.GetInfo().GetRequestId(), + ScheduleTime: descResp.GetInfo().GetScheduleTime(), + ExpirationTime: descResp.GetInfo().GetExpirationTime(), + ExecutionDuration: descResp.GetInfo().GetExecutionDuration(), + }, descResp.GetInfo()) s.Equal(enumspb.NEXUS_OPERATION_EXECUTION_STATUS_RUNNING, descResp.GetInfo().GetStatus()) }) @@ -330,7 +615,7 @@ func TestRequestCancelNexusOperationExecution(t *testing.T) { }) } -func TestTerminateNexusOperationExecution(t *testing.T) { +func TestTerminateStandaloneNexusOperation(t *testing.T) { t.Parallel() t.Run("Terminate", func(t *testing.T) { @@ -353,7 +638,7 @@ func TestTerminateNexusOperationExecution(t *testing.T) { }) s.NoError(err) - // Verify state after terminate. + // Verify outcome after terminate. descResp, err := s.FrontendClient().DescribeNexusOperationExecution(s.Context(), &workflowservice.DescribeNexusOperationExecutionRequest{ Namespace: s.Namespace().String(), OperationId: "test-op", @@ -477,26 +762,7 @@ func TestTerminateNexusOperationExecution(t *testing.T) { }) } -func createNexusEndpoint(s *testcore.TestEnv) string { - name := testcore.RandomizedNexusEndpoint(s.T().Name()) - _, err := s.OperatorClient().CreateNexusEndpoint(s.Context(), &operatorservice.CreateNexusEndpointRequest{ - Spec: &nexuspb.EndpointSpec{ - Name: name, - Target: &nexuspb.EndpointTarget{ - Variant: &nexuspb.EndpointTarget_Worker_{ - Worker: &nexuspb.EndpointTarget_Worker{ - Namespace: s.Namespace().String(), - TaskQueue: "unused-for-test", - }, - }, - }, - }, - }) - s.NoError(err) - return name -} - -func TestStandaloneNexusOperationList(t *testing.T) { +func TestListStandaloneNexusOperation(t *testing.T) { t.Parallel() t.Run("ListAndVerifyFields", func(t *testing.T) { @@ -670,8 +936,7 @@ func TestStandaloneNexusOperationList(t *testing.T) { }, testcore.WaitForESToSettle, 100*time.Millisecond) // Override max page size to 1. - cleanup := s.OverrideDynamicConfig(dynamicconfig.FrontendVisibilityMaxPageSize, 1) - defer cleanup() + s.OverrideDynamicConfig(dynamicconfig.FrontendVisibilityMaxPageSize, 1) // PageSize 0 should default to max (1), returning only 1 result. resp, err := s.FrontendClient().ListNexusOperationExecutions(s.Context(), &workflowservice.ListNexusOperationExecutionsRequest{ @@ -746,10 +1011,10 @@ func TestStandaloneNexusOperationList(t *testing.T) { }) } -func TestStandaloneNexusOperationCount(t *testing.T) { +func TestCountStandaloneNexusOperation(t *testing.T) { t.Parallel() - t.Run("CountByOperationId", func(t *testing.T) { + t.Run("CountByOperationID", func(t *testing.T) { s := testcore.NewEnv(t, nexusStandaloneOpts...) endpointName := createNexusEndpoint(s) @@ -840,6 +1105,33 @@ func TestStandaloneNexusOperationCount(t *testing.T) { require.Equal(t, "Running", groupValue) }) + t.Run("CountByCustomSearchAttribute", func(t *testing.T) { + s := testcore.NewEnv(t, nexusStandaloneOpts...) + endpointName := createNexusEndpoint(s) + + for i := range 2 { + _, err := startNexusOperation(s, &workflowservice.StartNexusOperationExecutionRequest{ + OperationId: fmt.Sprintf("count-sa-op-%d", i), + Endpoint: endpointName, + SearchAttributes: &commonpb.SearchAttributes{ + IndexedFields: map[string]*commonpb.Payload{ + "CustomKeywordField": payload.EncodeString("count-sa-value"), + }, + }, + }) + s.NoError(err) + } + + s.EventuallyWithT(func(t *assert.CollectT) { + resp, err := s.FrontendClient().CountNexusOperationExecutions(s.Context(), &workflowservice.CountNexusOperationExecutionsRequest{ + Namespace: s.Namespace().String(), + Query: "CustomKeywordField = 'count-sa-value'", + }) + require.NoError(t, err) + require.Equal(t, int64(2), resp.GetCount()) + }, testcore.WaitForESToSettle, 100*time.Millisecond) + }) + t.Run("GroupByUnsupportedField", func(t *testing.T) { s := testcore.NewEnv(t, nexusStandaloneOpts...) @@ -850,12 +1142,44 @@ func TestStandaloneNexusOperationCount(t *testing.T) { s.ErrorAs(err, new(*serviceerror.InvalidArgument)) s.ErrorContains(err, "'GROUP BY' clause is only supported for ExecutionStatus") }) + + t.Run("InvalidQuery", func(t *testing.T) { + s := testcore.NewEnv(t, nexusStandaloneOpts...) + + _, err := s.FrontendClient().CountNexusOperationExecutions(s.Context(), &workflowservice.CountNexusOperationExecutionsRequest{ + Namespace: s.Namespace().String(), + Query: "invalid query syntax !!!", + }) + s.ErrorAs(err, new(*serviceerror.InvalidArgument)) + s.ErrorContains(err, "invalid query") + }) + + t.Run("InvalidSearchAttribute", func(t *testing.T) { + s := testcore.NewEnv(t, nexusStandaloneOpts...) + + _, err := s.FrontendClient().CountNexusOperationExecutions(s.Context(), &workflowservice.CountNexusOperationExecutionsRequest{ + Namespace: s.Namespace().String(), + Query: "NonExistentField = 'value'", + }) + s.ErrorAs(err, new(*serviceerror.InvalidArgument)) + s.ErrorContains(err, "NonExistentField") + }) + + t.Run("NamespaceNotFound", func(t *testing.T) { + s := testcore.NewEnv(t, nexusStandaloneOpts...) + + _, err := s.FrontendClient().CountNexusOperationExecutions(s.Context(), &workflowservice.CountNexusOperationExecutionsRequest{ + Namespace: "non-existent-namespace", + }) + s.ErrorAs(err, new(*serviceerror.NamespaceNotFound)) + s.ErrorContains(err, "non-existent-namespace") + }) } -func TestStandaloneNexusOperationDelete(t *testing.T) { +func TestDeleteStandaloneNexusOperation(t *testing.T) { t.Parallel() - t.Run("DeleteScheduled", func(t *testing.T) { + t.Run("Scheduled", func(t *testing.T) { s := testcore.NewEnv(t, nexusStandaloneOpts...) endpointName := createNexusEndpoint(s) @@ -875,7 +1199,7 @@ func TestStandaloneNexusOperationDelete(t *testing.T) { eventuallyNexusOperationDeleted(s, t, "test-op", startResp.RunId) }) - t.Run("DeleteNoRunID", func(t *testing.T) { + t.Run("NoRunID", func(t *testing.T) { s := testcore.NewEnv(t, nexusStandaloneOpts...) endpointName := createNexusEndpoint(s) @@ -895,7 +1219,7 @@ func TestStandaloneNexusOperationDelete(t *testing.T) { eventuallyNexusOperationDeleted(s, t, "test-op", startResp.RunId) }) - t.Run("DeleteNonExistent", func(t *testing.T) { + t.Run("NotFound", func(t *testing.T) { s := testcore.NewEnv(t, nexusStandaloneOpts...) _, err := s.FrontendClient().DeleteNexusOperationExecution(s.Context(), &workflowservice.DeleteNexusOperationExecutionRequest{ @@ -905,7 +1229,7 @@ func TestStandaloneNexusOperationDelete(t *testing.T) { s.ErrorAs(err, new(*serviceerror.NotFound)) }) - t.Run("DeleteAlreadyDeleted", func(t *testing.T) { + t.Run("AlreadyDeleted", func(t *testing.T) { s := testcore.NewEnv(t, nexusStandaloneOpts...) endpointName := createNexusEndpoint(s) @@ -934,7 +1258,7 @@ func TestStandaloneNexusOperationDelete(t *testing.T) { // Validates that request validation is wired up in the frontend. // Exhaustive validation cases are covered in unit tests. - t.Run("DeleteValidation", func(t *testing.T) { + t.Run("Validation", func(t *testing.T) { s := testcore.NewEnv(t, nexusStandaloneOpts...) _, err := s.FrontendClient().DeleteNexusOperationExecution(s.Context(), &workflowservice.DeleteNexusOperationExecutionRequest{ @@ -945,6 +1269,265 @@ func TestStandaloneNexusOperationDelete(t *testing.T) { }) } +func TestStandaloneNexusOperationPoll(t *testing.T) { + t.Parallel() + + t.Run("WaitStageClosed", func(t *testing.T) { + for _, tc := range []struct { + name string + withRunID bool + }{ + {name: "WithEmptyRunID"}, + {name: "WithRunID", withRunID: true}, + } { + t.Run(tc.name, func(t *testing.T) { + s := testcore.NewEnv(t, nexusStandaloneOpts...) + endpointName := createNexusEndpoint(s) + + startResp, err := startNexusOperation(s, &workflowservice.StartNexusOperationExecutionRequest{ + OperationId: "test-op", + Endpoint: endpointName, + }) + require.NoError(t, err) + + ctx, cancel := context.WithTimeout(s.Context(), 10*time.Second) + defer cancel() + + // Start pollling. + type pollResult struct { + resp *workflowservice.PollNexusOperationExecutionResponse + err error + } + pollResultCh := make(chan pollResult, 1) + pollStartedCh := make(chan struct{}, 1) + + go func() { + runID := "" + if tc.withRunID { + runID = startResp.RunId + } + + pollStartedCh <- struct{}{} + resp, err := s.FrontendClient().PollNexusOperationExecution(ctx, &workflowservice.PollNexusOperationExecutionRequest{ + Namespace: s.Namespace().String(), + OperationId: "test-op", + RunId: runID, + WaitStage: enumspb.NEXUS_OPERATION_WAIT_STAGE_CLOSED, + }) + pollResultCh <- pollResult{resp: resp, err: err} + }() + + select { + case <-pollStartedCh: + case <-ctx.Done(): + t.Fatal("PollNexusOperationExecution did not start before timeout") + } + + // PollNexusOperationExecution should not resolve before the operation is closed. + select { + case result := <-pollResultCh: + require.NoError(t, result.err) + t.Fatal("PollNexusOperationExecution returned before the state changed") + default: + } + + // Terminate the operation. + terminateErrCh := make(chan error, 1) + go func() { + _, err := s.FrontendClient().TerminateNexusOperationExecution(ctx, &workflowservice.TerminateNexusOperationExecutionRequest{ + Namespace: s.Namespace().String(), + OperationId: "test-op", + RunId: startResp.RunId, + Reason: "test termination", + }) + terminateErrCh <- err + }() + + select { + case err := <-terminateErrCh: + require.NoError(t, err) + case <-ctx.Done(): + t.Fatal("TerminateNexusOperationExecution timed out") + } + + // Verify the poll result. + var result pollResult + select { + case result = <-pollResultCh: + case <-ctx.Done(): + t.Fatal("PollNexusOperationExecution did not resolve before timeout") + } + require.NoError(t, result.err) + pollResp := result.resp + + protorequire.ProtoEqual(t, &workflowservice.PollNexusOperationExecutionResponse{ + RunId: startResp.RunId, + WaitStage: enumspb.NEXUS_OPERATION_WAIT_STAGE_CLOSED, + OperationToken: pollResp.GetOperationToken(), + Outcome: &workflowservice.PollNexusOperationExecutionResponse_Failure{ + Failure: pollResp.GetFailure(), + }, + }, pollResp) + require.NotNil(t, pollResp.GetFailure().GetTerminatedFailureInfo()) + }) + } + }) + + t.Run("UnspecifiedWaitStageDefaultsToClosed", func(t *testing.T) { + s := testcore.NewEnv(t, nexusStandaloneOpts...) + endpointName := createNexusEndpoint(s) + + startResp, err := startNexusOperation(s, &workflowservice.StartNexusOperationExecutionRequest{ + OperationId: "test-op", + Endpoint: endpointName, + }) + require.NoError(t, err) + + // Terminate the operation. + _, err = s.FrontendClient().TerminateNexusOperationExecution(s.Context(), &workflowservice.TerminateNexusOperationExecutionRequest{ + Namespace: s.Namespace().String(), + OperationId: "test-op", + RunId: startResp.RunId, + Reason: "test termination", + }) + require.NoError(t, err) + + // Poll with UNSPECIFIED WaitStage — should behave the same as CLOSED. + pollResp, err := s.FrontendClient().PollNexusOperationExecution(s.Context(), &workflowservice.PollNexusOperationExecutionRequest{ + Namespace: s.Namespace().String(), + OperationId: "test-op", + RunId: startResp.RunId, + WaitStage: enumspb.NEXUS_OPERATION_WAIT_STAGE_UNSPECIFIED, + }) + require.NoError(t, err) + protorequire.ProtoEqual(t, &workflowservice.PollNexusOperationExecutionResponse{ + RunId: startResp.RunId, + WaitStage: enumspb.NEXUS_OPERATION_WAIT_STAGE_CLOSED, + OperationToken: pollResp.GetOperationToken(), + Outcome: &workflowservice.PollNexusOperationExecutionResponse_Failure{ + Failure: pollResp.GetFailure(), + }, + }, pollResp) + require.NotNil(t, pollResp.GetFailure().GetTerminatedFailureInfo()) + }) + + t.Run("ReturnsLastAttemptFailure", func(t *testing.T) { + t.Skip("Enable once standalone Nexus task and cancellation executors are wired through public Nexus task APIs") + + s := testcore.NewEnv(t, nexusStandaloneOpts...) + taskQueue := testcore.RandomizedNexusEndpoint(t.Name()) + endpointName := createNexusEndpointWithTaskQueue(s, taskQueue) + + startResp, err := startNexusOperation(s, &workflowservice.StartNexusOperationExecutionRequest{ + OperationId: "test-op", + Endpoint: endpointName, + ScheduleToCloseTimeout: durationpb.New(2 * time.Second), + }) + s.NoError(err) + + ctx, cancel := context.WithTimeout(s.Context(), 10*time.Second) + defer cancel() + + pollerErrCh := make(chan error, 1) + go func() { + task, err := s.FrontendClient().PollNexusTaskQueue(ctx, &workflowservice.PollNexusTaskQueueRequest{ + Namespace: s.Namespace().String(), + Identity: "test-worker", + TaskQueue: &taskqueuepb.TaskQueue{ + Name: taskQueue, + Kind: enumspb.TASK_QUEUE_KIND_NORMAL, + }, + }) + if err != nil { + pollerErrCh <- err + return + } + _, err = s.FrontendClient().RespondNexusTaskFailed(ctx, &workflowservice.RespondNexusTaskFailedRequest{ + Namespace: s.Namespace().String(), + Identity: "test-worker", + TaskToken: task.GetTaskToken(), + Error: &nexuspb.HandlerError{ + ErrorType: string(nexus.HandlerErrorTypeInternal), + Failure: &nexuspb.Failure{ + Message: "last attempt failure", + }, + }, + }) + pollerErrCh <- err + }() + + require.EventuallyWithT(t, func(t *assert.CollectT) { + pollResp, err := s.FrontendClient().PollNexusOperationExecution(s.Context(), &workflowservice.PollNexusOperationExecutionRequest{ + Namespace: s.Namespace().String(), + OperationId: "test-op", + RunId: startResp.RunId, + WaitStage: enumspb.NEXUS_OPERATION_WAIT_STAGE_CLOSED, + }) + require.NoError(t, err) + require.Equal(t, enumspb.NEXUS_OPERATION_WAIT_STAGE_CLOSED, pollResp.GetWaitStage()) + require.Equal(t, "last attempt failure", pollResp.GetFailure().GetMessage()) + }, 10*time.Second, 100*time.Millisecond) + + s.NoError(<-pollerErrCh) + }) + + t.Run("NamespaceNotFound", func(t *testing.T) { + s := testcore.NewEnv(t, nexusStandaloneOpts...) + endpointName := createNexusEndpoint(s) + + startResp, err := startNexusOperation(s, &workflowservice.StartNexusOperationExecutionRequest{ + OperationId: "test-op", + Endpoint: endpointName, + }) + require.NoError(t, err) + + _, err = s.FrontendClient().PollNexusOperationExecution(s.Context(), &workflowservice.PollNexusOperationExecutionRequest{ + Namespace: "non-existent-namespace", + OperationId: "test-op", + RunId: startResp.RunId, + WaitStage: enumspb.NEXUS_OPERATION_WAIT_STAGE_CLOSED, + }) + var namespaceNotFoundErr *serviceerror.NamespaceNotFound + require.ErrorAs(t, err, &namespaceNotFoundErr) + require.Contains(t, namespaceNotFoundErr.Error(), "non-existent-namespace") + }) + + t.Run("NotFound", func(t *testing.T) { + s := testcore.NewEnv(t, nexusStandaloneOpts...) + endpointName := createNexusEndpoint(s) + + startResp, err := startNexusOperation(s, &workflowservice.StartNexusOperationExecutionRequest{ + OperationId: "test-op", + Endpoint: endpointName, + }) + require.NoError(t, err) + + _, err = s.FrontendClient().PollNexusOperationExecution(s.Context(), &workflowservice.PollNexusOperationExecutionRequest{ + Namespace: s.Namespace().String(), + OperationId: "non-existent-op", + RunId: startResp.RunId, + WaitStage: enumspb.NEXUS_OPERATION_WAIT_STAGE_CLOSED, + }) + var notFoundErr *serviceerror.NotFound + require.ErrorAs(t, err, ¬FoundErr) + require.Equal(t, "operation not found for ID: non-existent-op", notFoundErr.Error()) + }) + + // Validates that request validation is wired up in the frontend. + // Exhaustive validation cases are covered in unit tests. + t.Run("Validation", func(t *testing.T) { + s := testcore.NewEnv(t, nexusStandaloneOpts...) + + _, err := s.FrontendClient().PollNexusOperationExecution(s.Context(), &workflowservice.PollNexusOperationExecutionRequest{ + Namespace: s.Namespace().String(), + OperationId: "", // required field + WaitStage: enumspb.NEXUS_OPERATION_WAIT_STAGE_CLOSED, + }) + s.Error(err) + s.Contains(err.Error(), "operation_id is required") + }) +} + func startNexusOperation( s *testcore.TestEnv, req *workflowservice.StartNexusOperationExecutionRequest, @@ -959,6 +1542,29 @@ func startNexusOperation( return s.FrontendClient().StartNexusOperationExecution(s.Context(), req) } +func createNexusEndpoint(s *testcore.TestEnv) string { + return createNexusEndpointWithTaskQueue(s, "unused-for-test") +} + +func createNexusEndpointWithTaskQueue(s *testcore.TestEnv, taskQueue string) string { + name := testcore.RandomizedNexusEndpoint(s.T().Name()) + _, err := s.OperatorClient().CreateNexusEndpoint(s.Context(), &operatorservice.CreateNexusEndpointRequest{ + Spec: &nexuspb.EndpointSpec{ + Name: name, + Target: &nexuspb.EndpointTarget{ + Variant: &nexuspb.EndpointTarget_Worker_{ + Worker: &nexuspb.EndpointTarget_Worker{ + Namespace: s.Namespace().String(), + TaskQueue: taskQueue, + }, + }, + }, + }, + }) + s.NoError(err) + return name +} + func eventuallyNexusOperationDeleted(s *testcore.TestEnv, t *testing.T, operationID, runID string) { t.Helper() require.Eventually(t, func() bool { @@ -969,5 +1575,5 @@ func eventuallyNexusOperationDeleted(s *testcore.TestEnv, t *testing.T, operatio }) var notFoundErr *serviceerror.NotFound return errors.As(err, ¬FoundErr) - }, 5*time.Second, 100*time.Millisecond) + }, 10*time.Second, 100*time.Millisecond) } From d13cafb582bd2b6da891fc39639ca73280b1bd48 Mon Sep 17 00:00:00 2001 From: Stephan Behnke Date: Mon, 13 Apr 2026 11:42:28 -0700 Subject: [PATCH 08/11] Fix nexusoperation after main rebase --- chasm/lib/nexusoperation/operation.go | 1 - 1 file changed, 1 deletion(-) diff --git a/chasm/lib/nexusoperation/operation.go b/chasm/lib/nexusoperation/operation.go index ff48714122..385fac754b 100644 --- a/chasm/lib/nexusoperation/operation.go +++ b/chasm/lib/nexusoperation/operation.go @@ -49,7 +49,6 @@ type OperationStore interface { OnNexusOperationCompleted(ctx chasm.MutableContext, operation *Operation, result *commonpb.Payload, links []*commonpb.Link) error OnNexusOperationCancellationCompleted(ctx chasm.MutableContext, operation *Operation) error OnNexusOperationCancellationFailed(ctx chasm.MutableContext, operation *Operation, cause *failurepb.Failure) error - // NexusOperationInvocationData loads invocation data (Input, Header, NexusLink) from the scheduled history event. NexusOperationInvocationData(ctx chasm.Context, operation *Operation) (InvocationData, error) } From c7892084701f465f76b283f1833945cf9268fb19 Mon Sep 17 00:00:00 2001 From: Stephan Behnke Date: Mon, 13 Apr 2026 14:27:26 -0700 Subject: [PATCH 09/11] Clean go.sum after api update --- go.sum | 2 -- 1 file changed, 2 deletions(-) diff --git a/go.sum b/go.sum index 171475c3d2..ce79085eb9 100644 --- a/go.sum +++ b/go.sum @@ -440,8 +440,6 @@ 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.7-0.20260409145211-9f50bee01930 h1:K9Ch2w/vTHQwPVZj9Ft8G+Zb1cdEj7NmlCCCXnR9elI= -go.temporal.io/api v1.62.7-0.20260409145211-9f50bee01930/go.mod h1:iaxoP/9OXMJcQkETTECfwYq4cw/bj4nwov8b3ZLVnXM= go.temporal.io/api v1.62.9-0.20260413170224-bfc609451c3d h1:eYMXtXe15odN5Dut+riDlmbMOZ+QeUlBJRAkUY1lRi8= go.temporal.io/api v1.62.9-0.20260413170224-bfc609451c3d/go.mod h1:iaxoP/9OXMJcQkETTECfwYq4cw/bj4nwov8b3ZLVnXM= go.temporal.io/auto-scaled-workers v0.0.0-20260407181057-edd947d743d2 h1:1hKeH3GyR6YD6LKMHGCZ76t6h1Sgha0hXVQBxWi3dlQ= From 318bd0384e51a42c4dc3c6eb2f17a10a2c93d0b4 Mon Sep 17 00:00:00 2001 From: Stephan Behnke Date: Mon, 13 Apr 2026 15:12:07 -0700 Subject: [PATCH 10/11] Nexus Standalone: translate already started err --- chasm/lib/nexusoperation/handler.go | 7 +++++++ tests/nexus_standalone_test.go | 3 +++ 2 files changed, 10 insertions(+) diff --git a/chasm/lib/nexusoperation/handler.go b/chasm/lib/nexusoperation/handler.go index 5f6b2cc01c..0daf8e8f7e 100644 --- a/chasm/lib/nexusoperation/handler.go +++ b/chasm/lib/nexusoperation/handler.go @@ -51,6 +51,13 @@ func (h *handler) StartNexusOperation( ), ) if err != nil { + if alreadyStartedErr, ok := errors.AsType[*chasm.ExecutionAlreadyStartedError](err); ok { + return nil, serviceerror.NewAlreadyExistsf( + "nexus operation execution already started: request_id=%s, run_id=%s", + alreadyStartedErr.CurrentRequestID, + alreadyStartedErr.CurrentRunID, + ) + } return nil, err } diff --git a/tests/nexus_standalone_test.go b/tests/nexus_standalone_test.go index 46801e179c..01d833bbe8 100644 --- a/tests/nexus_standalone_test.go +++ b/tests/nexus_standalone_test.go @@ -148,6 +148,9 @@ func TestStartStandaloneNexusOperation(t *testing.T) { RequestId: "different-request-id", }) s.Error(err) + var alreadyStartedErr *serviceerror.AlreadyExists + s.ErrorAs(err, &alreadyStartedErr) + s.ErrorContains(err, "nexus operation execution already started") // Second start with same request ID should return existing run. resp2, err := startNexusOperation(s, &workflowservice.StartNexusOperationExecutionRequest{ From 80ec9eec335c34d1d5052db2deca2cfebae78b8d Mon Sep 17 00:00:00 2001 From: Stephan Behnke Date: Wed, 8 Apr 2026 19:32:27 -0700 Subject: [PATCH 11/11] Nexus Standalone: use parallelsuite --- tests/nexus_standalone_test.go | 955 ++++++++++++++++----------------- tests/nexus_test_base.go | 4 + 2 files changed, 474 insertions(+), 485 deletions(-) diff --git a/tests/nexus_standalone_test.go b/tests/nexus_standalone_test.go index 01d833bbe8..a92b49efc9 100644 --- a/tests/nexus_standalone_test.go +++ b/tests/nexus_standalone_test.go @@ -5,6 +5,7 @@ import ( "context" "errors" "fmt" + "strings" "testing" "time" @@ -15,7 +16,6 @@ import ( enumspb "go.temporal.io/api/enums/v1" failurepb "go.temporal.io/api/failure/v1" nexuspb "go.temporal.io/api/nexus/v1" - "go.temporal.io/api/operatorservice/v1" sdkpb "go.temporal.io/api/sdk/v1" "go.temporal.io/api/serviceerror" taskqueuepb "go.temporal.io/api/taskqueue/v1" @@ -23,23 +23,31 @@ import ( "go.temporal.io/server/chasm/lib/nexusoperation" "go.temporal.io/server/common/dynamicconfig" "go.temporal.io/server/common/payload" + "go.temporal.io/server/common/testing/parallelsuite" "go.temporal.io/server/common/testing/protorequire" "go.temporal.io/server/tests/testcore" "google.golang.org/protobuf/types/known/durationpb" ) -var nexusStandaloneOpts = []testcore.TestOption{ - testcore.WithDedicatedCluster(), - testcore.WithDynamicConfig(dynamicconfig.EnableChasm, true), - testcore.WithDynamicConfig(nexusoperation.Enabled, true), +type NexusStandaloneTestSuite struct { + parallelsuite.Suite[*NexusStandaloneTestSuite] } -func TestStartStandaloneNexusOperation(t *testing.T) { - t.Parallel() +func TestNexusStandaloneTestSuite(t *testing.T) { + parallelsuite.Run(t, &NexusStandaloneTestSuite{}) +} + +func (s *NexusStandaloneTestSuite) opts() []testcore.TestOption { + return []testcore.TestOption{ + testcore.WithDynamicConfig(dynamicconfig.EnableChasm, true), + testcore.WithDynamicConfig(nexusoperation.Enabled, true), + } +} - t.Run("StartAndDescribe", func(t *testing.T) { - s := testcore.NewEnv(t, nexusStandaloneOpts...) - endpointName := createNexusEndpoint(s) +func (s *NexusStandaloneTestSuite) TestStartStandaloneNexusOperation() { + s.Run("StartAndDescribe", func(s *NexusStandaloneTestSuite) { + env := newNexusTestEnv(s.T(), false, s.opts()...) + endpointName := env.createRandomNexusEndpoint(s.T()).GetSpec().GetName() testInput := payload.EncodeString("test-input") testHeader := map[string]string{"test-key": "test-value"} @@ -52,7 +60,7 @@ func TestStartStandaloneNexusOperation(t *testing.T) { "CustomKeywordField": payload.EncodeString("test-value"), }, } - startResp, err := startNexusOperation(s, &workflowservice.StartNexusOperationExecutionRequest{ + startResp, err := s.startNexusOperation(env, &workflowservice.StartNexusOperationExecutionRequest{ OperationId: "test-op", Endpoint: endpointName, Input: testInput, @@ -70,79 +78,77 @@ func TestStartStandaloneNexusOperation(t *testing.T) { {name: "WithRunID", runID: startResp.RunId}, {name: "WithEmptyRunID", runID: ""}, } { - t.Run(tc.name, func(t *testing.T) { - descResp, err := s.FrontendClient().DescribeNexusOperationExecution(s.Context(), &workflowservice.DescribeNexusOperationExecutionRequest{ - Namespace: s.Namespace().String(), - OperationId: "test-op", - RunId: tc.runID, - }) - s.NoError(err) - s.Equal(startResp.RunId, descResp.RunId) - - info := descResp.GetInfo() - protorequire.ProtoEqual(t, &nexuspb.NexusOperationExecutionInfo{ - OperationId: "test-op", - RunId: startResp.RunId, - Endpoint: endpointName, - Service: "test-service", - Operation: "test-operation", - Status: enumspb.NEXUS_OPERATION_EXECUTION_STATUS_RUNNING, - State: enumspb.PENDING_NEXUS_OPERATION_STATE_SCHEDULED, - ScheduleToCloseTimeout: durationpb.New(10 * time.Minute), - NexusHeader: testHeader, - UserMetadata: testUserMetadata, - SearchAttributes: testSearchAttributes, - Attempt: 1, - StateTransitionCount: 1, - // Dynamic fields copied from actual response for comparison. - RequestId: info.GetRequestId(), - ScheduleTime: info.GetScheduleTime(), - ExpirationTime: info.GetExpirationTime(), - ExecutionDuration: info.GetExecutionDuration(), - }, info) - s.NotEmpty(descResp.GetLongPollToken()) - s.NotEmpty(info.GetRequestId()) - s.NotNil(info.GetScheduleTime()) - s.NotNil(info.GetExpirationTime()) - s.NotNil(info.GetExecutionDuration()) + descResp, err := env.FrontendClient().DescribeNexusOperationExecution(env.Context(), &workflowservice.DescribeNexusOperationExecutionRequest{ + Namespace: env.Namespace().String(), + OperationId: "test-op", + RunId: tc.runID, }) + s.NoError(err, tc.name) + s.Equal(startResp.RunId, descResp.RunId, tc.name) + + info := descResp.GetInfo() + protorequire.ProtoEqual(s.T(), &nexuspb.NexusOperationExecutionInfo{ + OperationId: "test-op", + RunId: startResp.RunId, + Endpoint: endpointName, + Service: "test-service", + Operation: "test-operation", + Status: enumspb.NEXUS_OPERATION_EXECUTION_STATUS_RUNNING, + State: enumspb.PENDING_NEXUS_OPERATION_STATE_SCHEDULED, + ScheduleToCloseTimeout: durationpb.New(10 * time.Minute), + NexusHeader: testHeader, + UserMetadata: testUserMetadata, + SearchAttributes: testSearchAttributes, + Attempt: 1, + StateTransitionCount: 1, + // Dynamic fields copied from actual response for comparison. + RequestId: info.GetRequestId(), + ScheduleTime: info.GetScheduleTime(), + ExpirationTime: info.GetExpirationTime(), + ExecutionDuration: info.GetExecutionDuration(), + }, info) + s.NotEmpty(descResp.GetLongPollToken(), tc.name) + s.NotEmpty(info.GetRequestId(), tc.name) + s.NotNil(info.GetScheduleTime(), tc.name) + s.NotNil(info.GetExpirationTime(), tc.name) + s.NotNil(info.GetExecutionDuration(), tc.name) } // Describe with IncludeInput. - descResp, err := s.FrontendClient().DescribeNexusOperationExecution(s.Context(), &workflowservice.DescribeNexusOperationExecutionRequest{ - Namespace: s.Namespace().String(), + descResp, err := env.FrontendClient().DescribeNexusOperationExecution(env.Context(), &workflowservice.DescribeNexusOperationExecutionRequest{ + Namespace: env.Namespace().String(), OperationId: "test-op", RunId: startResp.RunId, IncludeInput: true, }) s.NoError(err) - protorequire.ProtoEqual(t, testInput, descResp.GetInput()) + protorequire.ProtoEqual(s.T(), testInput, descResp.GetInput()) }) // Validates that request validation is wired up in the frontend. // Exhaustive validation cases are covered in unit tests. - t.Run("Validation", func(t *testing.T) { - s := testcore.NewEnv(t, nexusStandaloneOpts...) + s.Run("Validation", func(s *NexusStandaloneTestSuite) { + env := newNexusTestEnv(s.T(), false, s.opts()...) - _, err := startNexusOperation(s, &workflowservice.StartNexusOperationExecutionRequest{ + _, err := s.startNexusOperation(env, &workflowservice.StartNexusOperationExecutionRequest{ OperationId: "", // required field }) s.Error(err) s.Contains(err.Error(), "operation_id is required") }) - t.Run("IDConflictPolicyFail", func(t *testing.T) { - s := testcore.NewEnv(t, nexusStandaloneOpts...) - endpointName := createNexusEndpoint(s) + s.Run("IDConflictPolicyFail", func(s *NexusStandaloneTestSuite) { + env := newNexusTestEnv(s.T(), false, s.opts()...) + endpointName := env.createRandomNexusEndpoint(s.T()).GetSpec().GetName() - resp1, err := startNexusOperation(s, &workflowservice.StartNexusOperationExecutionRequest{ + resp1, err := s.startNexusOperation(env, &workflowservice.StartNexusOperationExecutionRequest{ OperationId: "test-op", Endpoint: endpointName, }) s.NoError(err) // Second start with different request ID should fail. - _, err = startNexusOperation(s, &workflowservice.StartNexusOperationExecutionRequest{ + _, err = s.startNexusOperation(env, &workflowservice.StartNexusOperationExecutionRequest{ OperationId: "test-op", Endpoint: endpointName, RequestId: "different-request-id", @@ -153,7 +159,7 @@ func TestStartStandaloneNexusOperation(t *testing.T) { s.ErrorContains(err, "nexus operation execution already started") // Second start with same request ID should return existing run. - resp2, err := startNexusOperation(s, &workflowservice.StartNexusOperationExecutionRequest{ + resp2, err := s.startNexusOperation(env, &workflowservice.StartNexusOperationExecutionRequest{ OperationId: "test-op", Endpoint: endpointName, }) @@ -162,17 +168,17 @@ func TestStartStandaloneNexusOperation(t *testing.T) { s.False(resp2.GetStarted()) }) - t.Run("IDConflictPolicyUseExisting", func(t *testing.T) { - s := testcore.NewEnv(t, nexusStandaloneOpts...) - endpointName := createNexusEndpoint(s) + s.Run("IDConflictPolicyUseExisting", func(s *NexusStandaloneTestSuite) { + env := newNexusTestEnv(s.T(), false, s.opts()...) + endpointName := env.createRandomNexusEndpoint(s.T()).GetSpec().GetName() - resp1, err := startNexusOperation(s, &workflowservice.StartNexusOperationExecutionRequest{ + resp1, err := s.startNexusOperation(env, &workflowservice.StartNexusOperationExecutionRequest{ OperationId: "test-op", Endpoint: endpointName, }) s.NoError(err) - resp2, err := startNexusOperation(s, &workflowservice.StartNexusOperationExecutionRequest{ + resp2, err := s.startNexusOperation(env, &workflowservice.StartNexusOperationExecutionRequest{ OperationId: "test-op", Endpoint: endpointName, RequestId: "different-request-id", @@ -184,14 +190,12 @@ func TestStartStandaloneNexusOperation(t *testing.T) { }) } -func TestDescribeStandaloneNexusOperation(t *testing.T) { - t.Parallel() - - t.Run("NotFound", func(t *testing.T) { - s := testcore.NewEnv(t, nexusStandaloneOpts...) +func (s *NexusStandaloneTestSuite) TestDescribeStandaloneNexusOperation() { + s.Run("NotFound", func(s *NexusStandaloneTestSuite) { + env := newNexusTestEnv(s.T(), false, s.opts()...) - _, err := s.FrontendClient().DescribeNexusOperationExecution(s.Context(), &workflowservice.DescribeNexusOperationExecutionRequest{ - Namespace: s.Namespace().String(), + _, err := env.FrontendClient().DescribeNexusOperationExecution(env.Context(), &workflowservice.DescribeNexusOperationExecutionRequest{ + Namespace: env.Namespace().String(), OperationId: "does-not-exist", }) var notFound *serviceerror.NotFound @@ -199,19 +203,19 @@ func TestDescribeStandaloneNexusOperation(t *testing.T) { s.Equal("operation not found for ID: does-not-exist", notFound.Error()) }) - t.Run("LongPollStateChange", func(t *testing.T) { - s := testcore.NewEnv(t, nexusStandaloneOpts...) - endpointName := createNexusEndpoint(s) + s.Run("LongPollStateChange", func(s *NexusStandaloneTestSuite) { + env := newNexusTestEnv(s.T(), false, s.opts()...) + endpointName := env.createRandomNexusEndpoint(s.T()).GetSpec().GetName() - startResp, err := startNexusOperation(s, &workflowservice.StartNexusOperationExecutionRequest{ + startResp, err := s.startNexusOperation(env, &workflowservice.StartNexusOperationExecutionRequest{ OperationId: "test-op", Endpoint: endpointName, }) s.NoError(err) // Obtain longpoll token. - firstResp, err := s.FrontendClient().DescribeNexusOperationExecution(s.Context(), &workflowservice.DescribeNexusOperationExecutionRequest{ - Namespace: s.Namespace().String(), + firstResp, err := env.FrontendClient().DescribeNexusOperationExecution(env.Context(), &workflowservice.DescribeNexusOperationExecutionRequest{ + Namespace: env.Namespace().String(), OperationId: "test-op", RunId: startResp.RunId, }) @@ -219,7 +223,7 @@ func TestDescribeStandaloneNexusOperation(t *testing.T) { s.NotEmpty(firstResp.GetLongPollToken()) s.Equal(enumspb.NEXUS_OPERATION_EXECUTION_STATUS_RUNNING, firstResp.GetInfo().GetStatus()) - ctx, cancel := context.WithTimeout(s.Context(), 10*time.Second) + ctx, cancel := context.WithTimeout(env.Context(), 10*time.Second) defer cancel() // Start polling. @@ -230,8 +234,8 @@ func TestDescribeStandaloneNexusOperation(t *testing.T) { describeResultCh := make(chan describeResult, 1) go func() { - resp, err := s.FrontendClient().DescribeNexusOperationExecution(ctx, &workflowservice.DescribeNexusOperationExecutionRequest{ - Namespace: s.Namespace().String(), + resp, err := env.FrontendClient().DescribeNexusOperationExecution(ctx, &workflowservice.DescribeNexusOperationExecutionRequest{ + Namespace: env.Namespace().String(), OperationId: "test-op", RunId: startResp.RunId, IncludeOutcome: true, @@ -243,16 +247,16 @@ func TestDescribeStandaloneNexusOperation(t *testing.T) { // Wait 1s to ensure the poll is still active. select { case result := <-describeResultCh: - require.NoError(t, result.err) - t.Fatal("DescribeNexusOperationExecution returned before the state changed") + s.NoError(result.err) + s.T().Fatal("DescribeNexusOperationExecution returned before the state changed") case <-time.After(1 * time.Second): } // Terminate the operation. terminateErrCh := make(chan error, 1) go func() { - _, err := s.FrontendClient().TerminateNexusOperationExecution(ctx, &workflowservice.TerminateNexusOperationExecutionRequest{ - Namespace: s.Namespace().String(), + _, err := env.FrontendClient().TerminateNexusOperationExecution(ctx, &workflowservice.TerminateNexusOperationExecutionRequest{ + Namespace: env.Namespace().String(), OperationId: "test-op", RunId: startResp.RunId, Reason: "test termination", @@ -262,19 +266,19 @@ func TestDescribeStandaloneNexusOperation(t *testing.T) { select { case err := <-terminateErrCh: - require.NoError(t, err) + s.NoError(err) case <-ctx.Done(): - t.Fatal("TerminateNexusOperationExecution timed out") + s.T().Fatal("TerminateNexusOperationExecution timed out") } // Verify the longpoll result. var longPollResp *workflowservice.DescribeNexusOperationExecutionResponse select { case result := <-describeResultCh: - require.NoError(t, result.err) + s.NoError(result.err) longPollResp = result.resp case <-ctx.Done(): - t.Fatal("DescribeNexusOperationExecution timed out") + s.T().Fatal("DescribeNexusOperationExecution timed out") } s.Equal(startResp.RunId, longPollResp.GetRunId()) @@ -282,65 +286,61 @@ func TestDescribeStandaloneNexusOperation(t *testing.T) { s.Greater(longPollResp.GetInfo().GetStateTransitionCount(), firstResp.GetInfo().GetStateTransitionCount()) }) - t.Run("LongPollTimeoutReturnsEmptyResponse", func(t *testing.T) { - s := testcore.NewEnv(t, nexusStandaloneOpts...) - endpointName := createNexusEndpoint(s) + s.Run("LongPollTimeoutReturnsEmptyResponse", func(s *NexusStandaloneTestSuite) { + env := newNexusTestEnv(s.T(), false, s.opts()...) + endpointName := env.createRandomNexusEndpoint(s.T()).GetSpec().GetName() - startResp, err := startNexusOperation(s, &workflowservice.StartNexusOperationExecutionRequest{ + startResp, err := s.startNexusOperation(env, &workflowservice.StartNexusOperationExecutionRequest{ OperationId: "test-op", Endpoint: endpointName, }) s.NoError(err) - firstResp, err := s.FrontendClient().DescribeNexusOperationExecution(s.Context(), &workflowservice.DescribeNexusOperationExecutionRequest{ - Namespace: s.Namespace().String(), + firstResp, err := env.FrontendClient().DescribeNexusOperationExecution(env.Context(), &workflowservice.DescribeNexusOperationExecutionRequest{ + Namespace: env.Namespace().String(), OperationId: "test-op", RunId: startResp.RunId, }) s.NoError(err) s.NotEmpty(firstResp.GetLongPollToken()) - t.Run("CallerDeadlineNotExceeded", func(t *testing.T) { - s.OverrideDynamicConfig(nexusoperation.LongPollBuffer, time.Second) - s.OverrideDynamicConfig(nexusoperation.LongPollTimeout, 10*time.Millisecond) + env.OverrideDynamicConfig(nexusoperation.LongPollBuffer, time.Second) + env.OverrideDynamicConfig(nexusoperation.LongPollTimeout, 10*time.Millisecond) - ctx, cancel := context.WithTimeout(s.Context(), 5*time.Second) - defer cancel() + ctx, cancel := context.WithTimeout(env.Context(), 5*time.Second) + defer cancel() - longPollResp, err := s.FrontendClient().DescribeNexusOperationExecution(ctx, &workflowservice.DescribeNexusOperationExecutionRequest{ - Namespace: s.Namespace().String(), - OperationId: "test-op", - RunId: startResp.RunId, - LongPollToken: firstResp.GetLongPollToken(), - }) - s.NoError(err) - protorequire.ProtoEqual(t, &workflowservice.DescribeNexusOperationExecutionResponse{}, longPollResp) + longPollResp, err := env.FrontendClient().DescribeNexusOperationExecution(ctx, &workflowservice.DescribeNexusOperationExecutionRequest{ + Namespace: env.Namespace().String(), + OperationId: "test-op", + RunId: startResp.RunId, + LongPollToken: firstResp.GetLongPollToken(), }) + s.NoError(err) + protorequire.ProtoEqual(s.T(), &workflowservice.DescribeNexusOperationExecutionResponse{}, longPollResp) - t.Run("NoCallerDeadline", func(t *testing.T) { - // Frontend still imposes its own deadline upstream, so the buffer must fit within that. - s.OverrideDynamicConfig(nexusoperation.LongPollBuffer, 29*time.Second) - s.OverrideDynamicConfig(nexusoperation.LongPollTimeout, 10*time.Millisecond) + // Frontend still imposes its own deadline upstream, so the buffer must fit within that. + env.OverrideDynamicConfig(nexusoperation.LongPollBuffer, 29*time.Second) + env.OverrideDynamicConfig(nexusoperation.LongPollTimeout, 10*time.Millisecond) - longPollResp, err := s.FrontendClient().DescribeNexusOperationExecution(context.Background(), &workflowservice.DescribeNexusOperationExecutionRequest{ - Namespace: s.Namespace().String(), - OperationId: "test-op", - RunId: startResp.RunId, - LongPollToken: firstResp.GetLongPollToken(), - }) - s.NoError(err) - protorequire.ProtoEqual(t, &workflowservice.DescribeNexusOperationExecutionResponse{}, longPollResp) + longPollResp, err = env.FrontendClient().DescribeNexusOperationExecution(context.Background(), &workflowservice.DescribeNexusOperationExecutionRequest{ + Namespace: env.Namespace().String(), + OperationId: "test-op", + RunId: startResp.RunId, + LongPollToken: firstResp.GetLongPollToken(), }) + s.NoError(err) + protorequire.ProtoEqual(s.T(), &workflowservice.DescribeNexusOperationExecutionResponse{}, longPollResp) }) - t.Run("IncludeOutcome_Failure", func(t *testing.T) { + s.Run("IncludeOutcome_Failure", func(s *NexusStandaloneTestSuite) { // TODO: Add canceled last-attempt-failure coverage here once standalone cancellation tasks // can be completed through the public Nexus task APIs. testCases := []struct { name string setup func(*workflowservice.StartNexusOperationExecutionRequest) - respond func(context.Context, *testcore.TestEnv, *workflowservice.PollNexusTaskQueueResponse) error + respond func(context.Context, *NexusTestEnv, *workflowservice.PollNexusTaskQueueResponse) error expectedStatus enumspb.NexusOperationExecutionStatus expectedFailureMessage string }{ @@ -349,7 +349,7 @@ func TestDescribeStandaloneNexusOperation(t *testing.T) { setup: func(req *workflowservice.StartNexusOperationExecutionRequest) { req.ScheduleToCloseTimeout = durationpb.New(2 * time.Second) }, - respond: func(ctx context.Context, s *testcore.TestEnv, task *workflowservice.PollNexusTaskQueueResponse) error { + respond: func(ctx context.Context, s *NexusTestEnv, task *workflowservice.PollNexusTaskQueueResponse) error { _, err := s.FrontendClient().RespondNexusTaskFailed(ctx, &workflowservice.RespondNexusTaskFailedRequest{ Namespace: s.Namespace().String(), Identity: "test-worker", @@ -368,7 +368,7 @@ func TestDescribeStandaloneNexusOperation(t *testing.T) { }, { name: "TerminalFailure", - respond: func(ctx context.Context, s *testcore.TestEnv, task *workflowservice.PollNexusTaskQueueResponse) error { + respond: func(ctx context.Context, s *NexusTestEnv, task *workflowservice.PollNexusTaskQueueResponse) error { _, err := s.FrontendClient().RespondNexusTaskCompleted(ctx, &workflowservice.RespondNexusTaskCompletedRequest{ Namespace: s.Namespace().String(), Identity: "test-worker", @@ -391,12 +391,10 @@ func TestDescribeStandaloneNexusOperation(t *testing.T) { } for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - t.Skip("Enable once standalone Nexus task and cancellation executors are wired through public Nexus task APIs") - - s := testcore.NewEnv(t, nexusStandaloneOpts...) - taskQueue := testcore.RandomizedNexusEndpoint(t.Name()) - endpointName := createNexusEndpointWithTaskQueue(s, taskQueue) + s.Run(tc.name, func(s *NexusStandaloneTestSuite) { + env := newNexusTestEnv(s.T(), false, s.opts()...) + taskQueue := testcore.RandomizedNexusEndpoint(s.T().Name()) + endpointName := env.createNexusEndpoint(s.T(), testcore.RandomizedNexusEndpoint(s.T().Name()), taskQueue).GetSpec().GetName() startReq := &workflowservice.StartNexusOperationExecutionRequest{ OperationId: "test-op", Endpoint: endpointName, @@ -405,16 +403,16 @@ func TestDescribeStandaloneNexusOperation(t *testing.T) { tc.setup(startReq) } - startResp, err := startNexusOperation(s, startReq) + startResp, err := s.startNexusOperation(env, startReq) s.NoError(err) - ctx, cancel := context.WithTimeout(s.Context(), 10*time.Second) + ctx, cancel := context.WithTimeout(env.Context(), 10*time.Second) defer cancel() pollerErrCh := make(chan error, 1) go func() { - task, err := s.FrontendClient().PollNexusTaskQueue(ctx, &workflowservice.PollNexusTaskQueueRequest{ - Namespace: s.Namespace().String(), + task, err := env.FrontendClient().PollNexusTaskQueue(ctx, &workflowservice.PollNexusTaskQueueRequest{ + Namespace: env.Namespace().String(), Identity: "test-worker", TaskQueue: &taskqueuepb.TaskQueue{ Name: taskQueue, @@ -425,12 +423,12 @@ func TestDescribeStandaloneNexusOperation(t *testing.T) { pollerErrCh <- err return } - pollerErrCh <- tc.respond(ctx, s, task) + pollerErrCh <- tc.respond(ctx, env, task) }() - require.EventuallyWithT(t, func(t *assert.CollectT) { - descResp, err := s.FrontendClient().DescribeNexusOperationExecution(s.Context(), &workflowservice.DescribeNexusOperationExecutionRequest{ - Namespace: s.Namespace().String(), + s.EventuallyWithT(func(t *assert.CollectT) { + descResp, err := env.FrontendClient().DescribeNexusOperationExecution(env.Context(), &workflowservice.DescribeNexusOperationExecutionRequest{ + Namespace: env.Namespace().String(), OperationId: "test-op", RunId: startResp.RunId, IncludeOutcome: true, @@ -439,8 +437,8 @@ func TestDescribeStandaloneNexusOperation(t *testing.T) { require.Equal(t, tc.expectedStatus, descResp.GetInfo().GetStatus()) require.Equal(t, tc.expectedFailureMessage, descResp.GetFailure().GetMessage()) - pollResp, err := s.FrontendClient().PollNexusOperationExecution(s.Context(), &workflowservice.PollNexusOperationExecutionRequest{ - Namespace: s.Namespace().String(), + pollResp, err := env.FrontendClient().PollNexusOperationExecution(env.Context(), &workflowservice.PollNexusOperationExecutionRequest{ + Namespace: env.Namespace().String(), OperationId: "test-op", RunId: startResp.RunId, WaitStage: enumspb.NEXUS_OPERATION_WAIT_STAGE_CLOSED, @@ -456,46 +454,44 @@ func TestDescribeStandaloneNexusOperation(t *testing.T) { // Validates that request validation is wired up in the frontend. // Exhaustive validation cases are covered in unit tests. - t.Run("Validation", func(t *testing.T) { - s := testcore.NewEnv(t, nexusStandaloneOpts...) + s.Run("Validation", func(s *NexusStandaloneTestSuite) { + env := newNexusTestEnv(s.T(), false, s.opts()...) - _, err := s.FrontendClient().DescribeNexusOperationExecution(s.Context(), &workflowservice.DescribeNexusOperationExecutionRequest{ - Namespace: s.Namespace().String(), + _, err := env.FrontendClient().DescribeNexusOperationExecution(env.Context(), &workflowservice.DescribeNexusOperationExecutionRequest{ + Namespace: env.Namespace().String(), }) s.Error(err) s.ErrorContains(err, "operation_id is required") }) } -func TestStandaloneNexusOperationCancel(t *testing.T) { - t.Parallel() +func (s *NexusStandaloneTestSuite) TestStandaloneNexusOperationCancel() { + s.Run("RequestCancel", func(s *NexusStandaloneTestSuite) { + env := newNexusTestEnv(s.T(), false, s.opts()...) + endpointName := env.createRandomNexusEndpoint(s.T()).GetSpec().GetName() - t.Run("RequestCancel", func(t *testing.T) { - s := testcore.NewEnv(t, nexusStandaloneOpts...) - endpointName := createNexusEndpoint(s) - - startResp, err := startNexusOperation(s, &workflowservice.StartNexusOperationExecutionRequest{ + startResp, err := s.startNexusOperation(env, &workflowservice.StartNexusOperationExecutionRequest{ OperationId: "test-op", Endpoint: endpointName, }) s.NoError(err) s.True(startResp.GetStarted()) - _, err = s.FrontendClient().RequestCancelNexusOperationExecution(s.Context(), &workflowservice.RequestCancelNexusOperationExecutionRequest{ - Namespace: s.Namespace().String(), + _, err = env.FrontendClient().RequestCancelNexusOperationExecution(env.Context(), &workflowservice.RequestCancelNexusOperationExecutionRequest{ + Namespace: env.Namespace().String(), OperationId: "test-op", RunId: startResp.RunId, }) s.NoError(err) // Verify state after cancel — operation is still running - descResp, err := s.FrontendClient().DescribeNexusOperationExecution(s.Context(), &workflowservice.DescribeNexusOperationExecutionRequest{ - Namespace: s.Namespace().String(), + descResp, err := env.FrontendClient().DescribeNexusOperationExecution(env.Context(), &workflowservice.DescribeNexusOperationExecutionRequest{ + Namespace: env.Namespace().String(), OperationId: "test-op", RunId: startResp.RunId, }) s.NoError(err) - protorequire.ProtoEqual(t, &nexuspb.NexusOperationExecutionInfo{ + protorequire.ProtoEqual(s.T(), &nexuspb.NexusOperationExecutionInfo{ OperationId: "test-op", RunId: startResp.RunId, Endpoint: endpointName, @@ -518,19 +514,19 @@ func TestStandaloneNexusOperationCancel(t *testing.T) { }) // TODO: Enable once cancel is fully implemented. - t.Run("AlreadyCanceled", func(t *testing.T) { - s := testcore.NewEnv(t, nexusStandaloneOpts...) - endpointName := createNexusEndpoint(s) + s.Run("AlreadyCanceled", func(s *NexusStandaloneTestSuite) { + env := newNexusTestEnv(s.T(), false, s.opts()...) + endpointName := env.createRandomNexusEndpoint(s.T()).GetSpec().GetName() - startResp, err := startNexusOperation(s, &workflowservice.StartNexusOperationExecutionRequest{ + startResp, err := s.startNexusOperation(env, &workflowservice.StartNexusOperationExecutionRequest{ OperationId: "test-op", Endpoint: endpointName, }) s.NoError(err) // Cancel the operation. - _, err = s.FrontendClient().RequestCancelNexusOperationExecution(s.Context(), &workflowservice.RequestCancelNexusOperationExecutionRequest{ - Namespace: s.Namespace().String(), + _, err = env.FrontendClient().RequestCancelNexusOperationExecution(env.Context(), &workflowservice.RequestCancelNexusOperationExecutionRequest{ + Namespace: env.Namespace().String(), OperationId: "test-op", RunId: startResp.RunId, RequestId: "cancel-request-id", @@ -538,8 +534,8 @@ func TestStandaloneNexusOperationCancel(t *testing.T) { s.NoError(err) // Cancel again with same request ID — should be idempotent. - _, err = s.FrontendClient().RequestCancelNexusOperationExecution(s.Context(), &workflowservice.RequestCancelNexusOperationExecutionRequest{ - Namespace: s.Namespace().String(), + _, err = env.FrontendClient().RequestCancelNexusOperationExecution(env.Context(), &workflowservice.RequestCancelNexusOperationExecutionRequest{ + Namespace: env.Namespace().String(), OperationId: "test-op", RunId: startResp.RunId, RequestId: "cancel-request-id", @@ -547,8 +543,8 @@ func TestStandaloneNexusOperationCancel(t *testing.T) { s.NoError(err) // Cancel with a different request ID — should error. - _, err = s.FrontendClient().RequestCancelNexusOperationExecution(s.Context(), &workflowservice.RequestCancelNexusOperationExecutionRequest{ - Namespace: s.Namespace().String(), + _, err = env.FrontendClient().RequestCancelNexusOperationExecution(env.Context(), &workflowservice.RequestCancelNexusOperationExecutionRequest{ + Namespace: env.Namespace().String(), OperationId: "test-op", RunId: startResp.RunId, RequestId: "different-request-id", @@ -558,21 +554,22 @@ func TestStandaloneNexusOperationCancel(t *testing.T) { }) // TODO: Enable once cancel/terminate interaction is fully implemented for standalone Nexus operations. - t.Run("AlreadyTerminated", func(t *testing.T) { + s.Run("AlreadyTerminated", func(s *NexusStandaloneTestSuite) { + t := s.T() t.Skip("Cancel/terminate interaction not yet fully implemented for standalone Nexus operations") - s := testcore.NewEnv(t, nexusStandaloneOpts...) - endpointName := createNexusEndpoint(s) + env := newNexusTestEnv(s.T(), false, s.opts()...) + endpointName := env.createRandomNexusEndpoint(s.T()).GetSpec().GetName() - startResp, err := startNexusOperation(s, &workflowservice.StartNexusOperationExecutionRequest{ + startResp, err := s.startNexusOperation(env, &workflowservice.StartNexusOperationExecutionRequest{ OperationId: "test-op", Endpoint: endpointName, }) s.NoError(err) // Terminate the operation first. - _, err = s.FrontendClient().TerminateNexusOperationExecution(s.Context(), &workflowservice.TerminateNexusOperationExecutionRequest{ - Namespace: s.Namespace().String(), + _, err = env.FrontendClient().TerminateNexusOperationExecution(env.Context(), &workflowservice.TerminateNexusOperationExecutionRequest{ + Namespace: env.Namespace().String(), OperationId: "test-op", RunId: startResp.RunId, RequestId: "terminate-request-id", @@ -581,8 +578,8 @@ func TestStandaloneNexusOperationCancel(t *testing.T) { s.NoError(err) // Cancel a terminated operation — should error. - _, err = s.FrontendClient().RequestCancelNexusOperationExecution(s.Context(), &workflowservice.RequestCancelNexusOperationExecutionRequest{ - Namespace: s.Namespace().String(), + _, err = env.FrontendClient().RequestCancelNexusOperationExecution(env.Context(), &workflowservice.RequestCancelNexusOperationExecutionRequest{ + Namespace: env.Namespace().String(), OperationId: "test-op", RunId: startResp.RunId, }) @@ -591,13 +588,14 @@ func TestStandaloneNexusOperationCancel(t *testing.T) { }) // TODO: Enable once cancel is fully implemented for standalone Nexus operations. - t.Run("NotFound", func(t *testing.T) { + s.Run("NotFound", func(s *NexusStandaloneTestSuite) { + t := s.T() t.Skip("Cancel not yet fully implemented for standalone Nexus operations") - s := testcore.NewEnv(t, nexusStandaloneOpts...) + env := newNexusTestEnv(s.T(), false, s.opts()...) - _, err := s.FrontendClient().RequestCancelNexusOperationExecution(s.Context(), &workflowservice.RequestCancelNexusOperationExecutionRequest{ - Namespace: s.Namespace().String(), + _, err := env.FrontendClient().RequestCancelNexusOperationExecution(env.Context(), &workflowservice.RequestCancelNexusOperationExecutionRequest{ + Namespace: env.Namespace().String(), OperationId: "does-not-exist", }) var notFound *serviceerror.NotFound @@ -606,11 +604,11 @@ func TestStandaloneNexusOperationCancel(t *testing.T) { // Validates that request validation is wired up in the frontend. // Exhaustive validation cases are covered in unit tests. - t.Run("Validation", func(t *testing.T) { - s := testcore.NewEnv(t, nexusStandaloneOpts...) + s.Run("Validation", func(s *NexusStandaloneTestSuite) { + env := newNexusTestEnv(s.T(), false, s.opts()...) - _, err := s.FrontendClient().RequestCancelNexusOperationExecution(s.Context(), &workflowservice.RequestCancelNexusOperationExecutionRequest{ - Namespace: s.Namespace().String(), + _, err := env.FrontendClient().RequestCancelNexusOperationExecution(env.Context(), &workflowservice.RequestCancelNexusOperationExecutionRequest{ + Namespace: env.Namespace().String(), OperationId: "", // required field }) s.Error(err) @@ -618,22 +616,20 @@ func TestStandaloneNexusOperationCancel(t *testing.T) { }) } -func TestTerminateStandaloneNexusOperation(t *testing.T) { - t.Parallel() - - t.Run("Terminate", func(t *testing.T) { - s := testcore.NewEnv(t, nexusStandaloneOpts...) - endpointName := createNexusEndpoint(s) +func (s *NexusStandaloneTestSuite) TestTerminateStandaloneNexusOperation() { + s.Run("Terminate", func(s *NexusStandaloneTestSuite) { + env := newNexusTestEnv(s.T(), false, s.opts()...) + endpointName := env.createRandomNexusEndpoint(s.T()).GetSpec().GetName() - startResp, err := startNexusOperation(s, &workflowservice.StartNexusOperationExecutionRequest{ + startResp, err := s.startNexusOperation(env, &workflowservice.StartNexusOperationExecutionRequest{ OperationId: "test-op", Endpoint: endpointName, }) s.NoError(err) s.True(startResp.GetStarted()) - _, err = s.FrontendClient().TerminateNexusOperationExecution(s.Context(), &workflowservice.TerminateNexusOperationExecutionRequest{ - Namespace: s.Namespace().String(), + _, err = env.FrontendClient().TerminateNexusOperationExecution(env.Context(), &workflowservice.TerminateNexusOperationExecutionRequest{ + Namespace: env.Namespace().String(), OperationId: "test-op", RunId: startResp.RunId, RequestId: "terminate-request-id", @@ -642,8 +638,8 @@ func TestTerminateStandaloneNexusOperation(t *testing.T) { s.NoError(err) // Verify outcome after terminate. - descResp, err := s.FrontendClient().DescribeNexusOperationExecution(s.Context(), &workflowservice.DescribeNexusOperationExecutionRequest{ - Namespace: s.Namespace().String(), + descResp, err := env.FrontendClient().DescribeNexusOperationExecution(env.Context(), &workflowservice.DescribeNexusOperationExecutionRequest{ + Namespace: env.Namespace().String(), OperationId: "test-op", RunId: startResp.RunId, IncludeOutcome: true, @@ -656,19 +652,19 @@ func TestTerminateStandaloneNexusOperation(t *testing.T) { s.NotNil(failure.GetTerminatedFailureInfo()) }) - t.Run("AlreadyTerminated", func(t *testing.T) { - s := testcore.NewEnv(t, nexusStandaloneOpts...) - endpointName := createNexusEndpoint(s) + s.Run("AlreadyTerminated", func(s *NexusStandaloneTestSuite) { + env := newNexusTestEnv(s.T(), false, s.opts()...) + endpointName := env.createRandomNexusEndpoint(s.T()).GetSpec().GetName() - startResp, err := startNexusOperation(s, &workflowservice.StartNexusOperationExecutionRequest{ + startResp, err := s.startNexusOperation(env, &workflowservice.StartNexusOperationExecutionRequest{ OperationId: "test-op", Endpoint: endpointName, }) s.NoError(err) // Terminate the operation. - _, err = s.FrontendClient().TerminateNexusOperationExecution(s.Context(), &workflowservice.TerminateNexusOperationExecutionRequest{ - Namespace: s.Namespace().String(), + _, err = env.FrontendClient().TerminateNexusOperationExecution(env.Context(), &workflowservice.TerminateNexusOperationExecutionRequest{ + Namespace: env.Namespace().String(), OperationId: "test-op", RunId: startResp.RunId, RequestId: "terminate-request-id", @@ -677,8 +673,8 @@ func TestTerminateStandaloneNexusOperation(t *testing.T) { s.NoError(err) // Terminate again with same request ID — should be idempotent. - _, err = s.FrontendClient().TerminateNexusOperationExecution(s.Context(), &workflowservice.TerminateNexusOperationExecutionRequest{ - Namespace: s.Namespace().String(), + _, err = env.FrontendClient().TerminateNexusOperationExecution(env.Context(), &workflowservice.TerminateNexusOperationExecutionRequest{ + Namespace: env.Namespace().String(), OperationId: "test-op", RunId: startResp.RunId, RequestId: "terminate-request-id", @@ -687,8 +683,8 @@ func TestTerminateStandaloneNexusOperation(t *testing.T) { s.NoError(err) // Terminate with a different request ID — should error. - _, err = s.FrontendClient().TerminateNexusOperationExecution(s.Context(), &workflowservice.TerminateNexusOperationExecutionRequest{ - Namespace: s.Namespace().String(), + _, err = env.FrontendClient().TerminateNexusOperationExecution(env.Context(), &workflowservice.TerminateNexusOperationExecutionRequest{ + Namespace: env.Namespace().String(), OperationId: "test-op", RunId: startResp.RunId, RequestId: "different-request-id", @@ -699,29 +695,30 @@ func TestTerminateStandaloneNexusOperation(t *testing.T) { }) // TODO: Enable once terminate is fully implemented for standalone Nexus operations. - t.Run("AlreadyCanceled", func(t *testing.T) { + s.Run("AlreadyCanceled", func(s *NexusStandaloneTestSuite) { + t := s.T() t.Skip("Terminate not yet fully implemented for standalone Nexus operations") - s := testcore.NewEnv(t, nexusStandaloneOpts...) - endpointName := createNexusEndpoint(s) + env := newNexusTestEnv(s.T(), false, s.opts()...) + endpointName := env.createRandomNexusEndpoint(s.T()).GetSpec().GetName() - startResp, err := startNexusOperation(s, &workflowservice.StartNexusOperationExecutionRequest{ + startResp, err := s.startNexusOperation(env, &workflowservice.StartNexusOperationExecutionRequest{ OperationId: "test-op", Endpoint: endpointName, }) s.NoError(err) // Cancel the operation first. - _, err = s.FrontendClient().RequestCancelNexusOperationExecution(s.Context(), &workflowservice.RequestCancelNexusOperationExecutionRequest{ - Namespace: s.Namespace().String(), + _, err = env.FrontendClient().RequestCancelNexusOperationExecution(env.Context(), &workflowservice.RequestCancelNexusOperationExecutionRequest{ + Namespace: env.Namespace().String(), OperationId: "test-op", RunId: startResp.RunId, }) s.NoError(err) // Terminate a canceled operation — should succeed. - _, err = s.FrontendClient().TerminateNexusOperationExecution(s.Context(), &workflowservice.TerminateNexusOperationExecutionRequest{ - Namespace: s.Namespace().String(), + _, err = env.FrontendClient().TerminateNexusOperationExecution(env.Context(), &workflowservice.TerminateNexusOperationExecutionRequest{ + Namespace: env.Namespace().String(), OperationId: "test-op", RunId: startResp.RunId, RequestId: "terminate-request-id", @@ -730,8 +727,8 @@ func TestTerminateStandaloneNexusOperation(t *testing.T) { s.NoError(err) // Verify state changed to terminated (terminate overrides cancel request). - descResp, err := s.FrontendClient().DescribeNexusOperationExecution(s.Context(), &workflowservice.DescribeNexusOperationExecutionRequest{ - Namespace: s.Namespace().String(), + descResp, err := env.FrontendClient().DescribeNexusOperationExecution(env.Context(), &workflowservice.DescribeNexusOperationExecutionRequest{ + Namespace: env.Namespace().String(), OperationId: "test-op", RunId: startResp.RunId, }) @@ -739,11 +736,11 @@ func TestTerminateStandaloneNexusOperation(t *testing.T) { s.Equal(enumspb.NEXUS_OPERATION_EXECUTION_STATUS_TERMINATED, descResp.GetInfo().GetStatus()) }) - t.Run("NotFound", func(t *testing.T) { - s := testcore.NewEnv(t, nexusStandaloneOpts...) + s.Run("NotFound", func(s *NexusStandaloneTestSuite) { + env := newNexusTestEnv(s.T(), false, s.opts()...) - _, err := s.FrontendClient().TerminateNexusOperationExecution(s.Context(), &workflowservice.TerminateNexusOperationExecutionRequest{ - Namespace: s.Namespace().String(), + _, err := env.FrontendClient().TerminateNexusOperationExecution(env.Context(), &workflowservice.TerminateNexusOperationExecutionRequest{ + Namespace: env.Namespace().String(), OperationId: "does-not-exist", Reason: "test termination", }) @@ -753,11 +750,11 @@ func TestTerminateStandaloneNexusOperation(t *testing.T) { // Validates that request validation is wired up in the frontend. // Exhaustive validation cases are covered in unit tests. - t.Run("Validation", func(t *testing.T) { - s := testcore.NewEnv(t, nexusStandaloneOpts...) + s.Run("Validation", func(s *NexusStandaloneTestSuite) { + env := newNexusTestEnv(s.T(), false, s.opts()...) - _, err := s.FrontendClient().TerminateNexusOperationExecution(s.Context(), &workflowservice.TerminateNexusOperationExecutionRequest{ - Namespace: s.Namespace().String(), + _, err := env.FrontendClient().TerminateNexusOperationExecution(env.Context(), &workflowservice.TerminateNexusOperationExecutionRequest{ + Namespace: env.Namespace().String(), OperationId: "", // required field }) s.Error(err) @@ -765,14 +762,12 @@ func TestTerminateStandaloneNexusOperation(t *testing.T) { }) } -func TestListStandaloneNexusOperation(t *testing.T) { - t.Parallel() - - t.Run("ListAndVerifyFields", func(t *testing.T) { - s := testcore.NewEnv(t, nexusStandaloneOpts...) - endpointName := createNexusEndpoint(s) +func (s *NexusStandaloneTestSuite) TestListStandaloneNexusOperation() { + s.Run("ListAndVerifyFields", func(s *NexusStandaloneTestSuite) { + env := newNexusTestEnv(s.T(), false, s.opts()...) + endpointName := env.createRandomNexusEndpoint(s.T()).GetSpec().GetName() - startResp, err := startNexusOperation(s, &workflowservice.StartNexusOperationExecutionRequest{ + startResp, err := s.startNexusOperation(env, &workflowservice.StartNexusOperationExecutionRequest{ OperationId: "list-test-op", Endpoint: endpointName, }) @@ -781,15 +776,15 @@ func TestListStandaloneNexusOperation(t *testing.T) { var listResp *workflowservice.ListNexusOperationExecutionsResponse s.EventuallyWithT(func(t *assert.CollectT) { var err error - listResp, err = s.FrontendClient().ListNexusOperationExecutions(s.Context(), &workflowservice.ListNexusOperationExecutionsRequest{ - Namespace: s.Namespace().String(), + listResp, err = env.FrontendClient().ListNexusOperationExecutions(env.Context(), &workflowservice.ListNexusOperationExecutionsRequest{ + Namespace: env.Namespace().String(), Query: "OperationId = 'list-test-op'", }) require.NoError(t, err) require.Len(t, listResp.GetOperations(), 1) }, testcore.WaitForESToSettle, 100*time.Millisecond) op := listResp.GetOperations()[0] - protorequire.ProtoEqual(t, &nexuspb.NexusOperationExecutionListInfo{ + protorequire.ProtoEqual(s.T(), &nexuspb.NexusOperationExecutionListInfo{ OperationId: "list-test-op", RunId: startResp.RunId, Endpoint: endpointName, @@ -801,21 +796,21 @@ func TestListStandaloneNexusOperation(t *testing.T) { // Dynamic fields copied from actual response for comparison. ScheduleTime: op.GetScheduleTime(), }, op) - require.NotNil(t, op.GetScheduleTime()) + s.NotNil(op.GetScheduleTime()) }) - t.Run("ListWithQueryFilter", func(t *testing.T) { - s := testcore.NewEnv(t, nexusStandaloneOpts...) - endpointA := createNexusEndpoint(s) - endpointB := createNexusEndpoint(s) + s.Run("ListWithQueryFilter", func(s *NexusStandaloneTestSuite) { + env := newNexusTestEnv(s.T(), false, s.opts()...) + endpointA := env.createRandomNexusEndpoint(s.T()).GetSpec().GetName() + endpointB := env.createRandomNexusEndpoint(s.T()).GetSpec().GetName() - _, err := startNexusOperation(s, &workflowservice.StartNexusOperationExecutionRequest{ + _, err := s.startNexusOperation(env, &workflowservice.StartNexusOperationExecutionRequest{ OperationId: "filter-op-1", Endpoint: endpointA, }) s.NoError(err) - _, err = startNexusOperation(s, &workflowservice.StartNexusOperationExecutionRequest{ + _, err = s.startNexusOperation(env, &workflowservice.StartNexusOperationExecutionRequest{ OperationId: "filter-op-2", Endpoint: endpointB, }) @@ -824,26 +819,26 @@ func TestListStandaloneNexusOperation(t *testing.T) { var listResp *workflowservice.ListNexusOperationExecutionsResponse s.EventuallyWithT(func(t *assert.CollectT) { var err error - listResp, err = s.FrontendClient().ListNexusOperationExecutions(s.Context(), &workflowservice.ListNexusOperationExecutionsRequest{ - Namespace: s.Namespace().String(), + listResp, err = env.FrontendClient().ListNexusOperationExecutions(env.Context(), &workflowservice.ListNexusOperationExecutionsRequest{ + Namespace: env.Namespace().String(), Query: fmt.Sprintf("Endpoint = '%s'", endpointA), }) require.NoError(t, err) require.Len(t, listResp.GetOperations(), 1) }, testcore.WaitForESToSettle, 100*time.Millisecond) - require.Equal(t, "filter-op-1", listResp.GetOperations()[0].GetOperationId()) + s.Equal("filter-op-1", listResp.GetOperations()[0].GetOperationId()) }) - t.Run("ListWithCustomSearchAttributes", func(t *testing.T) { - s := testcore.NewEnv(t, nexusStandaloneOpts...) - endpointName := createNexusEndpoint(s) + s.Run("ListWithCustomSearchAttributes", func(s *NexusStandaloneTestSuite) { + env := newNexusTestEnv(s.T(), false, s.opts()...) + endpointName := env.createRandomNexusEndpoint(s.T()).GetSpec().GetName() testSA := &commonpb.SearchAttributes{ IndexedFields: map[string]*commonpb.Payload{ "CustomKeywordField": payload.EncodeString("list-sa-value"), }, } - _, err := startNexusOperation(s, &workflowservice.StartNexusOperationExecutionRequest{ + _, err := s.startNexusOperation(env, &workflowservice.StartNexusOperationExecutionRequest{ OperationId: "sa-op", Endpoint: endpointName, SearchAttributes: testSA, @@ -853,26 +848,26 @@ func TestListStandaloneNexusOperation(t *testing.T) { var listResp *workflowservice.ListNexusOperationExecutionsResponse s.EventuallyWithT(func(t *assert.CollectT) { var err error - listResp, err = s.FrontendClient().ListNexusOperationExecutions(s.Context(), &workflowservice.ListNexusOperationExecutionsRequest{ - Namespace: s.Namespace().String(), + listResp, err = env.FrontendClient().ListNexusOperationExecutions(env.Context(), &workflowservice.ListNexusOperationExecutionsRequest{ + Namespace: env.Namespace().String(), Query: "CustomKeywordField = 'list-sa-value'", }) require.NoError(t, err) require.Len(t, listResp.GetOperations(), 1) }, testcore.WaitForESToSettle, 100*time.Millisecond) - require.Equal(t, "sa-op", listResp.GetOperations()[0].GetOperationId()) + s.Equal("sa-op", listResp.GetOperations()[0].GetOperationId()) returnedSA := listResp.GetOperations()[0].GetSearchAttributes().GetIndexedFields()["CustomKeywordField"] - require.NotNil(t, returnedSA) + s.NotNil(returnedSA) var returnedValue string - require.NoError(t, payload.Decode(returnedSA, &returnedValue)) - require.Equal(t, "list-sa-value", returnedValue) + s.NoError(payload.Decode(returnedSA, &returnedValue)) + s.Equal("list-sa-value", returnedValue) }) - t.Run("QueryByExecutionStatus", func(t *testing.T) { - s := testcore.NewEnv(t, nexusStandaloneOpts...) - endpointName := createNexusEndpoint(s) + s.Run("QueryByExecutionStatus", func(s *NexusStandaloneTestSuite) { + env := newNexusTestEnv(s.T(), false, s.opts()...) + endpointName := env.createRandomNexusEndpoint(s.T()).GetSpec().GetName() - _, err := startNexusOperation(s, &workflowservice.StartNexusOperationExecutionRequest{ + _, err := s.startNexusOperation(env, &workflowservice.StartNexusOperationExecutionRequest{ OperationId: "status-op", Endpoint: endpointName, }) @@ -881,21 +876,21 @@ func TestListStandaloneNexusOperation(t *testing.T) { var listResp *workflowservice.ListNexusOperationExecutionsResponse s.EventuallyWithT(func(t *assert.CollectT) { var err error - listResp, err = s.FrontendClient().ListNexusOperationExecutions(s.Context(), &workflowservice.ListNexusOperationExecutionsRequest{ - Namespace: s.Namespace().String(), + listResp, err = env.FrontendClient().ListNexusOperationExecutions(env.Context(), &workflowservice.ListNexusOperationExecutionsRequest{ + Namespace: env.Namespace().String(), Query: "ExecutionStatus = 'Running' AND OperationId = 'status-op'", }) require.NoError(t, err) require.Len(t, listResp.GetOperations(), 1) }, testcore.WaitForESToSettle, 100*time.Millisecond) - require.Equal(t, "status-op", listResp.GetOperations()[0].GetOperationId()) + s.Equal("status-op", listResp.GetOperations()[0].GetOperationId()) }) - t.Run("QueryByMultipleFields", func(t *testing.T) { - s := testcore.NewEnv(t, nexusStandaloneOpts...) - endpointName := createNexusEndpoint(s) + s.Run("QueryByMultipleFields", func(s *NexusStandaloneTestSuite) { + env := newNexusTestEnv(s.T(), false, s.opts()...) + endpointName := env.createRandomNexusEndpoint(s.T()).GetSpec().GetName() - _, err := startNexusOperation(s, &workflowservice.StartNexusOperationExecutionRequest{ + _, err := s.startNexusOperation(env, &workflowservice.StartNexusOperationExecutionRequest{ OperationId: "multi-op", Endpoint: endpointName, Service: "multi-service", @@ -905,22 +900,22 @@ func TestListStandaloneNexusOperation(t *testing.T) { var listResp *workflowservice.ListNexusOperationExecutionsResponse s.EventuallyWithT(func(t *assert.CollectT) { var err error - listResp, err = s.FrontendClient().ListNexusOperationExecutions(s.Context(), &workflowservice.ListNexusOperationExecutionsRequest{ - Namespace: s.Namespace().String(), + listResp, err = env.FrontendClient().ListNexusOperationExecutions(env.Context(), &workflowservice.ListNexusOperationExecutionsRequest{ + Namespace: env.Namespace().String(), Query: fmt.Sprintf("Endpoint = '%s' AND Service = 'multi-service'", endpointName), }) require.NoError(t, err) require.Len(t, listResp.GetOperations(), 1) }, testcore.WaitForESToSettle, 100*time.Millisecond) - require.Equal(t, "multi-op", listResp.GetOperations()[0].GetOperationId()) + s.Equal("multi-op", listResp.GetOperations()[0].GetOperationId()) }) - t.Run("PageSizeCapping", func(t *testing.T) { - s := testcore.NewEnv(t, nexusStandaloneOpts...) - endpointName := createNexusEndpoint(s) + s.Run("PageSizeCapping", func(s *NexusStandaloneTestSuite) { + env := newNexusTestEnv(s.T(), false, s.opts()...) + endpointName := env.createRandomNexusEndpoint(s.T()).GetSpec().GetName() for i := range 2 { - _, err := startNexusOperation(s, &workflowservice.StartNexusOperationExecutionRequest{ + _, err := s.startNexusOperation(env, &workflowservice.StartNexusOperationExecutionRequest{ OperationId: fmt.Sprintf("paged-op-%d", i), Endpoint: endpointName, }) @@ -930,8 +925,8 @@ func TestListStandaloneNexusOperation(t *testing.T) { // Wait for both to be indexed. query := fmt.Sprintf("Endpoint = '%s'", endpointName) s.EventuallyWithT(func(t *assert.CollectT) { - resp, err := s.FrontendClient().ListNexusOperationExecutions(s.Context(), &workflowservice.ListNexusOperationExecutionsRequest{ - Namespace: s.Namespace().String(), + resp, err := env.FrontendClient().ListNexusOperationExecutions(env.Context(), &workflowservice.ListNexusOperationExecutionsRequest{ + Namespace: env.Namespace().String(), Query: query, }) require.NoError(t, err) @@ -939,74 +934,74 @@ func TestListStandaloneNexusOperation(t *testing.T) { }, testcore.WaitForESToSettle, 100*time.Millisecond) // Override max page size to 1. - s.OverrideDynamicConfig(dynamicconfig.FrontendVisibilityMaxPageSize, 1) + env.OverrideDynamicConfig(dynamicconfig.FrontendVisibilityMaxPageSize, 1) // PageSize 0 should default to max (1), returning only 1 result. - resp, err := s.FrontendClient().ListNexusOperationExecutions(s.Context(), &workflowservice.ListNexusOperationExecutionsRequest{ - Namespace: s.Namespace().String(), + resp, err := env.FrontendClient().ListNexusOperationExecutions(env.Context(), &workflowservice.ListNexusOperationExecutionsRequest{ + Namespace: env.Namespace().String(), PageSize: 0, Query: query, }) - require.NoError(t, err) - require.Len(t, resp.GetOperations(), 1) + s.NoError(err) + s.Len(resp.GetOperations(), 1) // PageSize > max should also be capped. First page. - resp, err = s.FrontendClient().ListNexusOperationExecutions(s.Context(), &workflowservice.ListNexusOperationExecutionsRequest{ - Namespace: s.Namespace().String(), + resp, err = env.FrontendClient().ListNexusOperationExecutions(env.Context(), &workflowservice.ListNexusOperationExecutionsRequest{ + Namespace: env.Namespace().String(), PageSize: 2, Query: query, }) - require.NoError(t, err) - require.Len(t, resp.GetOperations(), 1) + s.NoError(err) + s.Len(resp.GetOperations(), 1) // Second page. - resp, err = s.FrontendClient().ListNexusOperationExecutions(s.Context(), &workflowservice.ListNexusOperationExecutionsRequest{ - Namespace: s.Namespace().String(), + resp, err = env.FrontendClient().ListNexusOperationExecutions(env.Context(), &workflowservice.ListNexusOperationExecutionsRequest{ + Namespace: env.Namespace().String(), PageSize: 2, Query: query, NextPageToken: resp.GetNextPageToken(), }) - require.NoError(t, err) - require.Len(t, resp.GetOperations(), 1) + s.NoError(err) + s.Len(resp.GetOperations(), 1) // No more results. - resp, err = s.FrontendClient().ListNexusOperationExecutions(s.Context(), &workflowservice.ListNexusOperationExecutionsRequest{ - Namespace: s.Namespace().String(), + resp, err = env.FrontendClient().ListNexusOperationExecutions(env.Context(), &workflowservice.ListNexusOperationExecutionsRequest{ + Namespace: env.Namespace().String(), PageSize: 2, Query: query, NextPageToken: resp.GetNextPageToken(), }) - require.NoError(t, err) - require.Empty(t, resp.GetOperations()) - require.Nil(t, resp.GetNextPageToken()) + s.NoError(err) + s.Empty(resp.GetOperations()) + s.Nil(resp.GetNextPageToken()) }) - t.Run("InvalidQuery", func(t *testing.T) { - s := testcore.NewEnv(t, nexusStandaloneOpts...) + s.Run("InvalidQuery", func(s *NexusStandaloneTestSuite) { + env := newNexusTestEnv(s.T(), false, s.opts()...) - _, err := s.FrontendClient().ListNexusOperationExecutions(s.Context(), &workflowservice.ListNexusOperationExecutionsRequest{ - Namespace: s.Namespace().String(), + _, err := env.FrontendClient().ListNexusOperationExecutions(env.Context(), &workflowservice.ListNexusOperationExecutionsRequest{ + Namespace: env.Namespace().String(), Query: "invalid query syntax !!!", }) s.ErrorAs(err, new(*serviceerror.InvalidArgument)) s.ErrorContains(err, "invalid query") }) - t.Run("InvalidSearchAttribute", func(t *testing.T) { - s := testcore.NewEnv(t, nexusStandaloneOpts...) + s.Run("InvalidSearchAttribute", func(s *NexusStandaloneTestSuite) { + env := newNexusTestEnv(s.T(), false, s.opts()...) - _, err := s.FrontendClient().ListNexusOperationExecutions(s.Context(), &workflowservice.ListNexusOperationExecutionsRequest{ - Namespace: s.Namespace().String(), + _, err := env.FrontendClient().ListNexusOperationExecutions(env.Context(), &workflowservice.ListNexusOperationExecutionsRequest{ + Namespace: env.Namespace().String(), Query: "NonExistentField = 'value'", }) s.ErrorAs(err, new(*serviceerror.InvalidArgument)) s.ErrorContains(err, "NonExistentField") }) - t.Run("NamespaceNotFound", func(t *testing.T) { - s := testcore.NewEnv(t, nexusStandaloneOpts...) + s.Run("NamespaceNotFound", func(s *NexusStandaloneTestSuite) { + env := newNexusTestEnv(s.T(), false, s.opts()...) - _, err := s.FrontendClient().ListNexusOperationExecutions(s.Context(), &workflowservice.ListNexusOperationExecutionsRequest{ + _, err := env.FrontendClient().ListNexusOperationExecutions(env.Context(), &workflowservice.ListNexusOperationExecutionsRequest{ Namespace: "non-existent-namespace", }) s.ErrorAs(err, new(*serviceerror.NamespaceNotFound)) @@ -1014,22 +1009,20 @@ func TestListStandaloneNexusOperation(t *testing.T) { }) } -func TestCountStandaloneNexusOperation(t *testing.T) { - t.Parallel() - - t.Run("CountByOperationID", func(t *testing.T) { - s := testcore.NewEnv(t, nexusStandaloneOpts...) - endpointName := createNexusEndpoint(s) +func (s *NexusStandaloneTestSuite) TestCountStandaloneNexusOperation() { + s.Run("CountByOperationID", func(s *NexusStandaloneTestSuite) { + env := newNexusTestEnv(s.T(), false, s.opts()...) + endpointName := env.createRandomNexusEndpoint(s.T()).GetSpec().GetName() - _, err := startNexusOperation(s, &workflowservice.StartNexusOperationExecutionRequest{ + _, err := s.startNexusOperation(env, &workflowservice.StartNexusOperationExecutionRequest{ OperationId: "count-op", Endpoint: endpointName, }) s.NoError(err) s.EventuallyWithT(func(t *assert.CollectT) { - resp, err := s.FrontendClient().CountNexusOperationExecutions(s.Context(), &workflowservice.CountNexusOperationExecutionsRequest{ - Namespace: s.Namespace().String(), + resp, err := env.FrontendClient().CountNexusOperationExecutions(env.Context(), &workflowservice.CountNexusOperationExecutionsRequest{ + Namespace: env.Namespace().String(), Query: "OperationId = 'count-op'", }) require.NoError(t, err) @@ -1037,12 +1030,12 @@ func TestCountStandaloneNexusOperation(t *testing.T) { }, testcore.WaitForESToSettle, 100*time.Millisecond) }) - t.Run("CountByEndpoint", func(t *testing.T) { - s := testcore.NewEnv(t, nexusStandaloneOpts...) - endpointName := createNexusEndpoint(s) + s.Run("CountByEndpoint", func(s *NexusStandaloneTestSuite) { + env := newNexusTestEnv(s.T(), false, s.opts()...) + endpointName := env.createRandomNexusEndpoint(s.T()).GetSpec().GetName() for i := range 3 { - _, err := startNexusOperation(s, &workflowservice.StartNexusOperationExecutionRequest{ + _, err := s.startNexusOperation(env, &workflowservice.StartNexusOperationExecutionRequest{ OperationId: fmt.Sprintf("count-ep-op-%d", i), Endpoint: endpointName, }) @@ -1050,8 +1043,8 @@ func TestCountStandaloneNexusOperation(t *testing.T) { } s.EventuallyWithT(func(t *assert.CollectT) { - resp, err := s.FrontendClient().CountNexusOperationExecutions(s.Context(), &workflowservice.CountNexusOperationExecutionsRequest{ - Namespace: s.Namespace().String(), + resp, err := env.FrontendClient().CountNexusOperationExecutions(env.Context(), &workflowservice.CountNexusOperationExecutionsRequest{ + Namespace: env.Namespace().String(), Query: fmt.Sprintf("Endpoint = '%s'", endpointName), }) require.NoError(t, err) @@ -1059,19 +1052,19 @@ func TestCountStandaloneNexusOperation(t *testing.T) { }, testcore.WaitForESToSettle, 100*time.Millisecond) }) - t.Run("CountByExecutionStatus", func(t *testing.T) { - s := testcore.NewEnv(t, nexusStandaloneOpts...) - endpointName := createNexusEndpoint(s) + s.Run("CountByExecutionStatus", func(s *NexusStandaloneTestSuite) { + env := newNexusTestEnv(s.T(), false, s.opts()...) + endpointName := env.createRandomNexusEndpoint(s.T()).GetSpec().GetName() - _, err := startNexusOperation(s, &workflowservice.StartNexusOperationExecutionRequest{ + _, err := s.startNexusOperation(env, &workflowservice.StartNexusOperationExecutionRequest{ OperationId: "count-status-op", Endpoint: endpointName, }) s.NoError(err) s.EventuallyWithT(func(t *assert.CollectT) { - resp, err := s.FrontendClient().CountNexusOperationExecutions(s.Context(), &workflowservice.CountNexusOperationExecutionsRequest{ - Namespace: s.Namespace().String(), + resp, err := env.FrontendClient().CountNexusOperationExecutions(env.Context(), &workflowservice.CountNexusOperationExecutionsRequest{ + Namespace: env.Namespace().String(), Query: fmt.Sprintf("ExecutionStatus = 'Running' AND Endpoint = '%s'", endpointName), }) require.NoError(t, err) @@ -1079,12 +1072,12 @@ func TestCountStandaloneNexusOperation(t *testing.T) { }, testcore.WaitForESToSettle, 100*time.Millisecond) }) - t.Run("GroupByExecutionStatus", func(t *testing.T) { - s := testcore.NewEnv(t, nexusStandaloneOpts...) - endpointName := createNexusEndpoint(s) + s.Run("GroupByExecutionStatus", func(s *NexusStandaloneTestSuite) { + env := newNexusTestEnv(s.T(), false, s.opts()...) + endpointName := env.createRandomNexusEndpoint(s.T()).GetSpec().GetName() for i := range 3 { - _, err := startNexusOperation(s, &workflowservice.StartNexusOperationExecutionRequest{ + _, err := s.startNexusOperation(env, &workflowservice.StartNexusOperationExecutionRequest{ OperationId: fmt.Sprintf("group-op-%d", i), Endpoint: endpointName, }) @@ -1094,26 +1087,26 @@ func TestCountStandaloneNexusOperation(t *testing.T) { var countResp *workflowservice.CountNexusOperationExecutionsResponse s.EventuallyWithT(func(t *assert.CollectT) { var err error - countResp, err = s.FrontendClient().CountNexusOperationExecutions(s.Context(), &workflowservice.CountNexusOperationExecutionsRequest{ - Namespace: s.Namespace().String(), + countResp, err = env.FrontendClient().CountNexusOperationExecutions(env.Context(), &workflowservice.CountNexusOperationExecutionsRequest{ + Namespace: env.Namespace().String(), Query: fmt.Sprintf("Endpoint = '%s' GROUP BY ExecutionStatus", endpointName), }) require.NoError(t, err) require.Equal(t, int64(3), countResp.GetCount()) }, testcore.WaitForESToSettle, 100*time.Millisecond) - require.Len(t, countResp.GetGroups(), 1) - require.Equal(t, int64(3), countResp.GetGroups()[0].GetCount()) + s.Len(countResp.GetGroups(), 1) + s.Equal(int64(3), countResp.GetGroups()[0].GetCount()) var groupValue string - require.NoError(t, payload.Decode(countResp.GetGroups()[0].GetGroupValues()[0], &groupValue)) - require.Equal(t, "Running", groupValue) + s.NoError(payload.Decode(countResp.GetGroups()[0].GetGroupValues()[0], &groupValue)) + s.Equal("Running", groupValue) }) - t.Run("CountByCustomSearchAttribute", func(t *testing.T) { - s := testcore.NewEnv(t, nexusStandaloneOpts...) - endpointName := createNexusEndpoint(s) + s.Run("CountByCustomSearchAttribute", func(s *NexusStandaloneTestSuite) { + env := newNexusTestEnv(s.T(), false, s.opts()...) + endpointName := env.createRandomNexusEndpoint(s.T()).GetSpec().GetName() for i := range 2 { - _, err := startNexusOperation(s, &workflowservice.StartNexusOperationExecutionRequest{ + _, err := s.startNexusOperation(env, &workflowservice.StartNexusOperationExecutionRequest{ OperationId: fmt.Sprintf("count-sa-op-%d", i), Endpoint: endpointName, SearchAttributes: &commonpb.SearchAttributes{ @@ -1126,8 +1119,8 @@ func TestCountStandaloneNexusOperation(t *testing.T) { } s.EventuallyWithT(func(t *assert.CollectT) { - resp, err := s.FrontendClient().CountNexusOperationExecutions(s.Context(), &workflowservice.CountNexusOperationExecutionsRequest{ - Namespace: s.Namespace().String(), + resp, err := env.FrontendClient().CountNexusOperationExecutions(env.Context(), &workflowservice.CountNexusOperationExecutionsRequest{ + Namespace: env.Namespace().String(), Query: "CustomKeywordField = 'count-sa-value'", }) require.NoError(t, err) @@ -1135,43 +1128,43 @@ func TestCountStandaloneNexusOperation(t *testing.T) { }, testcore.WaitForESToSettle, 100*time.Millisecond) }) - t.Run("GroupByUnsupportedField", func(t *testing.T) { - s := testcore.NewEnv(t, nexusStandaloneOpts...) + s.Run("GroupByUnsupportedField", func(s *NexusStandaloneTestSuite) { + env := newNexusTestEnv(s.T(), false, s.opts()...) - _, err := s.FrontendClient().CountNexusOperationExecutions(s.Context(), &workflowservice.CountNexusOperationExecutionsRequest{ - Namespace: s.Namespace().String(), + _, err := env.FrontendClient().CountNexusOperationExecutions(env.Context(), &workflowservice.CountNexusOperationExecutionsRequest{ + Namespace: env.Namespace().String(), Query: "GROUP BY Endpoint", }) s.ErrorAs(err, new(*serviceerror.InvalidArgument)) s.ErrorContains(err, "'GROUP BY' clause is only supported for ExecutionStatus") }) - t.Run("InvalidQuery", func(t *testing.T) { - s := testcore.NewEnv(t, nexusStandaloneOpts...) + s.Run("InvalidQuery", func(s *NexusStandaloneTestSuite) { + env := newNexusTestEnv(s.T(), false, s.opts()...) - _, err := s.FrontendClient().CountNexusOperationExecutions(s.Context(), &workflowservice.CountNexusOperationExecutionsRequest{ - Namespace: s.Namespace().String(), + _, err := env.FrontendClient().CountNexusOperationExecutions(env.Context(), &workflowservice.CountNexusOperationExecutionsRequest{ + Namespace: env.Namespace().String(), Query: "invalid query syntax !!!", }) s.ErrorAs(err, new(*serviceerror.InvalidArgument)) s.ErrorContains(err, "invalid query") }) - t.Run("InvalidSearchAttribute", func(t *testing.T) { - s := testcore.NewEnv(t, nexusStandaloneOpts...) + s.Run("InvalidSearchAttribute", func(s *NexusStandaloneTestSuite) { + env := newNexusTestEnv(s.T(), false, s.opts()...) - _, err := s.FrontendClient().CountNexusOperationExecutions(s.Context(), &workflowservice.CountNexusOperationExecutionsRequest{ - Namespace: s.Namespace().String(), + _, err := env.FrontendClient().CountNexusOperationExecutions(env.Context(), &workflowservice.CountNexusOperationExecutionsRequest{ + Namespace: env.Namespace().String(), Query: "NonExistentField = 'value'", }) s.ErrorAs(err, new(*serviceerror.InvalidArgument)) s.ErrorContains(err, "NonExistentField") }) - t.Run("NamespaceNotFound", func(t *testing.T) { - s := testcore.NewEnv(t, nexusStandaloneOpts...) + s.Run("NamespaceNotFound", func(s *NexusStandaloneTestSuite) { + env := newNexusTestEnv(s.T(), false, s.opts()...) - _, err := s.FrontendClient().CountNexusOperationExecutions(s.Context(), &workflowservice.CountNexusOperationExecutionsRequest{ + _, err := env.FrontendClient().CountNexusOperationExecutions(env.Context(), &workflowservice.CountNexusOperationExecutionsRequest{ Namespace: "non-existent-namespace", }) s.ErrorAs(err, new(*serviceerror.NamespaceNotFound)) @@ -1179,80 +1172,78 @@ func TestCountStandaloneNexusOperation(t *testing.T) { }) } -func TestDeleteStandaloneNexusOperation(t *testing.T) { - t.Parallel() +func (s *NexusStandaloneTestSuite) TestDeleteStandaloneNexusOperation() { + s.Run("Scheduled", func(s *NexusStandaloneTestSuite) { + env := newNexusTestEnv(s.T(), false, s.opts()...) + endpointName := env.createRandomNexusEndpoint(s.T()).GetSpec().GetName() - t.Run("Scheduled", func(t *testing.T) { - s := testcore.NewEnv(t, nexusStandaloneOpts...) - endpointName := createNexusEndpoint(s) - - startResp, err := startNexusOperation(s, &workflowservice.StartNexusOperationExecutionRequest{ + startResp, err := s.startNexusOperation(env, &workflowservice.StartNexusOperationExecutionRequest{ OperationId: "test-op", Endpoint: endpointName, }) s.NoError(err) - _, err = s.FrontendClient().DeleteNexusOperationExecution(s.Context(), &workflowservice.DeleteNexusOperationExecutionRequest{ - Namespace: s.Namespace().String(), + _, err = env.FrontendClient().DeleteNexusOperationExecution(env.Context(), &workflowservice.DeleteNexusOperationExecutionRequest{ + Namespace: env.Namespace().String(), OperationId: "test-op", RunId: startResp.RunId, }) s.NoError(err) - eventuallyNexusOperationDeleted(s, t, "test-op", startResp.RunId) + s.eventuallyDeleted(env, s.T(), "test-op", startResp.RunId) }) - t.Run("NoRunID", func(t *testing.T) { - s := testcore.NewEnv(t, nexusStandaloneOpts...) - endpointName := createNexusEndpoint(s) + s.Run("NoRunID", func(s *NexusStandaloneTestSuite) { + env := newNexusTestEnv(s.T(), false, s.opts()...) + endpointName := env.createRandomNexusEndpoint(s.T()).GetSpec().GetName() - startResp, err := startNexusOperation(s, &workflowservice.StartNexusOperationExecutionRequest{ + startResp, err := s.startNexusOperation(env, &workflowservice.StartNexusOperationExecutionRequest{ OperationId: "test-op", Endpoint: endpointName, // RunId not set }) s.NoError(err) - _, err = s.FrontendClient().DeleteNexusOperationExecution(s.Context(), &workflowservice.DeleteNexusOperationExecutionRequest{ - Namespace: s.Namespace().String(), + _, err = env.FrontendClient().DeleteNexusOperationExecution(env.Context(), &workflowservice.DeleteNexusOperationExecutionRequest{ + Namespace: env.Namespace().String(), OperationId: "test-op", }) s.NoError(err) - eventuallyNexusOperationDeleted(s, t, "test-op", startResp.RunId) + s.eventuallyDeleted(env, s.T(), "test-op", startResp.RunId) }) - t.Run("NotFound", func(t *testing.T) { - s := testcore.NewEnv(t, nexusStandaloneOpts...) + s.Run("NotFound", func(s *NexusStandaloneTestSuite) { + env := newNexusTestEnv(s.T(), false, s.opts()...) - _, err := s.FrontendClient().DeleteNexusOperationExecution(s.Context(), &workflowservice.DeleteNexusOperationExecutionRequest{ - Namespace: s.Namespace().String(), + _, err := env.FrontendClient().DeleteNexusOperationExecution(env.Context(), &workflowservice.DeleteNexusOperationExecutionRequest{ + Namespace: env.Namespace().String(), OperationId: "does-not-exist", }) s.ErrorAs(err, new(*serviceerror.NotFound)) }) - t.Run("AlreadyDeleted", func(t *testing.T) { - s := testcore.NewEnv(t, nexusStandaloneOpts...) - endpointName := createNexusEndpoint(s) + s.Run("AlreadyDeleted", func(s *NexusStandaloneTestSuite) { + env := newNexusTestEnv(s.T(), false, s.opts()...) + endpointName := env.createRandomNexusEndpoint(s.T()).GetSpec().GetName() - startResp, err := startNexusOperation(s, &workflowservice.StartNexusOperationExecutionRequest{ + startResp, err := s.startNexusOperation(env, &workflowservice.StartNexusOperationExecutionRequest{ OperationId: "test-op", Endpoint: endpointName, }) s.NoError(err) - _, err = s.FrontendClient().DeleteNexusOperationExecution(s.Context(), &workflowservice.DeleteNexusOperationExecutionRequest{ - Namespace: s.Namespace().String(), + _, err = env.FrontendClient().DeleteNexusOperationExecution(env.Context(), &workflowservice.DeleteNexusOperationExecutionRequest{ + Namespace: env.Namespace().String(), OperationId: "test-op", RunId: startResp.RunId, }) s.NoError(err) - eventuallyNexusOperationDeleted(s, t, "test-op", startResp.RunId) + s.eventuallyDeleted(env, s.T(), "test-op", startResp.RunId) - _, err = s.FrontendClient().DeleteNexusOperationExecution(s.Context(), &workflowservice.DeleteNexusOperationExecutionRequest{ - Namespace: s.Namespace().String(), + _, err = env.FrontendClient().DeleteNexusOperationExecution(env.Context(), &workflowservice.DeleteNexusOperationExecutionRequest{ + Namespace: env.Namespace().String(), OperationId: "test-op", RunId: startResp.RunId, }) @@ -1261,21 +1252,19 @@ func TestDeleteStandaloneNexusOperation(t *testing.T) { // Validates that request validation is wired up in the frontend. // Exhaustive validation cases are covered in unit tests. - t.Run("Validation", func(t *testing.T) { - s := testcore.NewEnv(t, nexusStandaloneOpts...) + s.Run("Validation", func(s *NexusStandaloneTestSuite) { + env := newNexusTestEnv(s.T(), false, s.opts()...) - _, err := s.FrontendClient().DeleteNexusOperationExecution(s.Context(), &workflowservice.DeleteNexusOperationExecutionRequest{ - Namespace: s.Namespace().String(), + _, err := env.FrontendClient().DeleteNexusOperationExecution(env.Context(), &workflowservice.DeleteNexusOperationExecutionRequest{ + Namespace: env.Namespace().String(), }) s.Error(err) s.ErrorContains(err, "operation_id is required") }) } -func TestStandaloneNexusOperationPoll(t *testing.T) { - t.Parallel() - - t.Run("WaitStageClosed", func(t *testing.T) { +func (s *NexusStandaloneTestSuite) TestStandaloneNexusOperationPoll() { + s.Run("WaitStageClosed", func(s *NexusStandaloneTestSuite) { for _, tc := range []struct { name string withRunID bool @@ -1283,17 +1272,17 @@ func TestStandaloneNexusOperationPoll(t *testing.T) { {name: "WithEmptyRunID"}, {name: "WithRunID", withRunID: true}, } { - t.Run(tc.name, func(t *testing.T) { - s := testcore.NewEnv(t, nexusStandaloneOpts...) - endpointName := createNexusEndpoint(s) + s.Run(tc.name, func(s *NexusStandaloneTestSuite) { + env := newNexusTestEnv(s.T(), false, s.opts()...) + endpointName := env.createRandomNexusEndpoint(s.T()).GetSpec().GetName() - startResp, err := startNexusOperation(s, &workflowservice.StartNexusOperationExecutionRequest{ + startResp, err := s.startNexusOperation(env, &workflowservice.StartNexusOperationExecutionRequest{ OperationId: "test-op", Endpoint: endpointName, }) - require.NoError(t, err) + s.NoError(err) - ctx, cancel := context.WithTimeout(s.Context(), 10*time.Second) + ctx, cancel := context.WithTimeout(env.Context(), 10*time.Second) defer cancel() // Start pollling. @@ -1311,8 +1300,8 @@ func TestStandaloneNexusOperationPoll(t *testing.T) { } pollStartedCh <- struct{}{} - resp, err := s.FrontendClient().PollNexusOperationExecution(ctx, &workflowservice.PollNexusOperationExecutionRequest{ - Namespace: s.Namespace().String(), + resp, err := env.FrontendClient().PollNexusOperationExecution(ctx, &workflowservice.PollNexusOperationExecutionRequest{ + Namespace: env.Namespace().String(), OperationId: "test-op", RunId: runID, WaitStage: enumspb.NEXUS_OPERATION_WAIT_STAGE_CLOSED, @@ -1323,22 +1312,22 @@ func TestStandaloneNexusOperationPoll(t *testing.T) { select { case <-pollStartedCh: case <-ctx.Done(): - t.Fatal("PollNexusOperationExecution did not start before timeout") + s.T().Fatal("PollNexusOperationExecution did not start before timeout") } // PollNexusOperationExecution should not resolve before the operation is closed. select { case result := <-pollResultCh: - require.NoError(t, result.err) - t.Fatal("PollNexusOperationExecution returned before the state changed") + s.NoError(result.err) + s.T().Fatal("PollNexusOperationExecution returned before the state changed") default: } // Terminate the operation. terminateErrCh := make(chan error, 1) go func() { - _, err := s.FrontendClient().TerminateNexusOperationExecution(ctx, &workflowservice.TerminateNexusOperationExecutionRequest{ - Namespace: s.Namespace().String(), + _, err := env.FrontendClient().TerminateNexusOperationExecution(ctx, &workflowservice.TerminateNexusOperationExecutionRequest{ + Namespace: env.Namespace().String(), OperationId: "test-op", RunId: startResp.RunId, Reason: "test termination", @@ -1348,9 +1337,9 @@ func TestStandaloneNexusOperationPoll(t *testing.T) { select { case err := <-terminateErrCh: - require.NoError(t, err) + s.NoError(err) case <-ctx.Done(): - t.Fatal("TerminateNexusOperationExecution timed out") + s.T().Fatal("TerminateNexusOperationExecution timed out") } // Verify the poll result. @@ -1358,12 +1347,12 @@ func TestStandaloneNexusOperationPoll(t *testing.T) { select { case result = <-pollResultCh: case <-ctx.Done(): - t.Fatal("PollNexusOperationExecution did not resolve before timeout") + s.T().Fatal("PollNexusOperationExecution did not resolve before timeout") } - require.NoError(t, result.err) + s.NoError(result.err) pollResp := result.resp - protorequire.ProtoEqual(t, &workflowservice.PollNexusOperationExecutionResponse{ + protorequire.ProtoEqual(s.T(), &workflowservice.PollNexusOperationExecutionResponse{ RunId: startResp.RunId, WaitStage: enumspb.NEXUS_OPERATION_WAIT_STAGE_CLOSED, OperationToken: pollResp.GetOperationToken(), @@ -1371,39 +1360,39 @@ func TestStandaloneNexusOperationPoll(t *testing.T) { Failure: pollResp.GetFailure(), }, }, pollResp) - require.NotNil(t, pollResp.GetFailure().GetTerminatedFailureInfo()) + s.NotNil(pollResp.GetFailure().GetTerminatedFailureInfo()) }) } }) - t.Run("UnspecifiedWaitStageDefaultsToClosed", func(t *testing.T) { - s := testcore.NewEnv(t, nexusStandaloneOpts...) - endpointName := createNexusEndpoint(s) + s.Run("UnspecifiedWaitStageDefaultsToClosed", func(s *NexusStandaloneTestSuite) { + env := newNexusTestEnv(s.T(), false, s.opts()...) + endpointName := env.createRandomNexusEndpoint(s.T()).GetSpec().GetName() - startResp, err := startNexusOperation(s, &workflowservice.StartNexusOperationExecutionRequest{ + startResp, err := s.startNexusOperation(env, &workflowservice.StartNexusOperationExecutionRequest{ OperationId: "test-op", Endpoint: endpointName, }) - require.NoError(t, err) + s.NoError(err) // Terminate the operation. - _, err = s.FrontendClient().TerminateNexusOperationExecution(s.Context(), &workflowservice.TerminateNexusOperationExecutionRequest{ - Namespace: s.Namespace().String(), + _, err = env.FrontendClient().TerminateNexusOperationExecution(env.Context(), &workflowservice.TerminateNexusOperationExecutionRequest{ + Namespace: env.Namespace().String(), OperationId: "test-op", RunId: startResp.RunId, Reason: "test termination", }) - require.NoError(t, err) + s.NoError(err) // Poll with UNSPECIFIED WaitStage — should behave the same as CLOSED. - pollResp, err := s.FrontendClient().PollNexusOperationExecution(s.Context(), &workflowservice.PollNexusOperationExecutionRequest{ - Namespace: s.Namespace().String(), + pollResp, err := env.FrontendClient().PollNexusOperationExecution(env.Context(), &workflowservice.PollNexusOperationExecutionRequest{ + Namespace: env.Namespace().String(), OperationId: "test-op", RunId: startResp.RunId, WaitStage: enumspb.NEXUS_OPERATION_WAIT_STAGE_UNSPECIFIED, }) - require.NoError(t, err) - protorequire.ProtoEqual(t, &workflowservice.PollNexusOperationExecutionResponse{ + s.NoError(err) + protorequire.ProtoEqual(s.T(), &workflowservice.PollNexusOperationExecutionResponse{ RunId: startResp.RunId, WaitStage: enumspb.NEXUS_OPERATION_WAIT_STAGE_CLOSED, OperationToken: pollResp.GetOperationToken(), @@ -1411,30 +1400,28 @@ func TestStandaloneNexusOperationPoll(t *testing.T) { Failure: pollResp.GetFailure(), }, }, pollResp) - require.NotNil(t, pollResp.GetFailure().GetTerminatedFailureInfo()) + s.NotNil(pollResp.GetFailure().GetTerminatedFailureInfo()) }) - t.Run("ReturnsLastAttemptFailure", func(t *testing.T) { - t.Skip("Enable once standalone Nexus task and cancellation executors are wired through public Nexus task APIs") - - s := testcore.NewEnv(t, nexusStandaloneOpts...) - taskQueue := testcore.RandomizedNexusEndpoint(t.Name()) - endpointName := createNexusEndpointWithTaskQueue(s, taskQueue) + s.Run("ReturnsLastAttemptFailure", func(s *NexusStandaloneTestSuite) { + env := newNexusTestEnv(s.T(), false, s.opts()...) + taskQueue := testcore.RandomizedNexusEndpoint(s.T().Name()) + endpointName := env.createNexusEndpoint(s.T(), testcore.RandomizedNexusEndpoint(s.T().Name()), taskQueue).GetSpec().GetName() - startResp, err := startNexusOperation(s, &workflowservice.StartNexusOperationExecutionRequest{ + startResp, err := s.startNexusOperation(env, &workflowservice.StartNexusOperationExecutionRequest{ OperationId: "test-op", Endpoint: endpointName, ScheduleToCloseTimeout: durationpb.New(2 * time.Second), }) s.NoError(err) - ctx, cancel := context.WithTimeout(s.Context(), 10*time.Second) + ctx, cancel := context.WithTimeout(env.Context(), 10*time.Second) defer cancel() pollerErrCh := make(chan error, 1) go func() { - task, err := s.FrontendClient().PollNexusTaskQueue(ctx, &workflowservice.PollNexusTaskQueueRequest{ - Namespace: s.Namespace().String(), + task, err := env.FrontendClient().PollNexusTaskQueue(ctx, &workflowservice.PollNexusTaskQueueRequest{ + Namespace: env.Namespace().String(), Identity: "test-worker", TaskQueue: &taskqueuepb.TaskQueue{ Name: taskQueue, @@ -1445,8 +1432,8 @@ func TestStandaloneNexusOperationPoll(t *testing.T) { pollerErrCh <- err return } - _, err = s.FrontendClient().RespondNexusTaskFailed(ctx, &workflowservice.RespondNexusTaskFailedRequest{ - Namespace: s.Namespace().String(), + _, err = env.FrontendClient().RespondNexusTaskFailed(ctx, &workflowservice.RespondNexusTaskFailedRequest{ + Namespace: env.Namespace().String(), Identity: "test-worker", TaskToken: task.GetTaskToken(), Error: &nexuspb.HandlerError{ @@ -1459,9 +1446,9 @@ func TestStandaloneNexusOperationPoll(t *testing.T) { pollerErrCh <- err }() - require.EventuallyWithT(t, func(t *assert.CollectT) { - pollResp, err := s.FrontendClient().PollNexusOperationExecution(s.Context(), &workflowservice.PollNexusOperationExecutionRequest{ - Namespace: s.Namespace().String(), + s.EventuallyWithT(func(t *assert.CollectT) { + pollResp, err := env.FrontendClient().PollNexusOperationExecution(env.Context(), &workflowservice.PollNexusOperationExecutionRequest{ + Namespace: env.Namespace().String(), OperationId: "test-op", RunId: startResp.RunId, WaitStage: enumspb.NEXUS_OPERATION_WAIT_STAGE_CLOSED, @@ -1474,55 +1461,55 @@ func TestStandaloneNexusOperationPoll(t *testing.T) { s.NoError(<-pollerErrCh) }) - t.Run("NamespaceNotFound", func(t *testing.T) { - s := testcore.NewEnv(t, nexusStandaloneOpts...) - endpointName := createNexusEndpoint(s) + s.Run("NamespaceNotFound", func(s *NexusStandaloneTestSuite) { + env := newNexusTestEnv(s.T(), false, s.opts()...) + endpointName := env.createRandomNexusEndpoint(s.T()).GetSpec().GetName() - startResp, err := startNexusOperation(s, &workflowservice.StartNexusOperationExecutionRequest{ + startResp, err := s.startNexusOperation(env, &workflowservice.StartNexusOperationExecutionRequest{ OperationId: "test-op", Endpoint: endpointName, }) - require.NoError(t, err) + s.NoError(err) - _, err = s.FrontendClient().PollNexusOperationExecution(s.Context(), &workflowservice.PollNexusOperationExecutionRequest{ + _, err = env.FrontendClient().PollNexusOperationExecution(env.Context(), &workflowservice.PollNexusOperationExecutionRequest{ Namespace: "non-existent-namespace", OperationId: "test-op", RunId: startResp.RunId, WaitStage: enumspb.NEXUS_OPERATION_WAIT_STAGE_CLOSED, }) var namespaceNotFoundErr *serviceerror.NamespaceNotFound - require.ErrorAs(t, err, &namespaceNotFoundErr) - require.Contains(t, namespaceNotFoundErr.Error(), "non-existent-namespace") + s.ErrorAs(err, &namespaceNotFoundErr) + s.Contains(namespaceNotFoundErr.Error(), "non-existent-namespace") }) - t.Run("NotFound", func(t *testing.T) { - s := testcore.NewEnv(t, nexusStandaloneOpts...) - endpointName := createNexusEndpoint(s) + s.Run("NotFound", func(s *NexusStandaloneTestSuite) { + env := newNexusTestEnv(s.T(), false, s.opts()...) + endpointName := env.createRandomNexusEndpoint(s.T()).GetSpec().GetName() - startResp, err := startNexusOperation(s, &workflowservice.StartNexusOperationExecutionRequest{ + startResp, err := s.startNexusOperation(env, &workflowservice.StartNexusOperationExecutionRequest{ OperationId: "test-op", Endpoint: endpointName, }) - require.NoError(t, err) + s.NoError(err) - _, err = s.FrontendClient().PollNexusOperationExecution(s.Context(), &workflowservice.PollNexusOperationExecutionRequest{ - Namespace: s.Namespace().String(), + _, err = env.FrontendClient().PollNexusOperationExecution(env.Context(), &workflowservice.PollNexusOperationExecutionRequest{ + Namespace: env.Namespace().String(), OperationId: "non-existent-op", RunId: startResp.RunId, WaitStage: enumspb.NEXUS_OPERATION_WAIT_STAGE_CLOSED, }) var notFoundErr *serviceerror.NotFound - require.ErrorAs(t, err, ¬FoundErr) - require.Equal(t, "operation not found for ID: non-existent-op", notFoundErr.Error()) + s.ErrorAs(err, ¬FoundErr) + s.Equal("operation not found for ID: non-existent-op", notFoundErr.Error()) }) // Validates that request validation is wired up in the frontend. // Exhaustive validation cases are covered in unit tests. - t.Run("Validation", func(t *testing.T) { - s := testcore.NewEnv(t, nexusStandaloneOpts...) + s.Run("Validation", func(s *NexusStandaloneTestSuite) { + env := newNexusTestEnv(s.T(), false, s.opts()...) - _, err := s.FrontendClient().PollNexusOperationExecution(s.Context(), &workflowservice.PollNexusOperationExecutionRequest{ - Namespace: s.Namespace().String(), + _, err := env.FrontendClient().PollNexusOperationExecution(env.Context(), &workflowservice.PollNexusOperationExecutionRequest{ + Namespace: env.Namespace().String(), OperationId: "", // required field WaitStage: enumspb.NEXUS_OPERATION_WAIT_STAGE_CLOSED, }) @@ -1531,48 +1518,46 @@ func TestStandaloneNexusOperationPoll(t *testing.T) { }) } -func startNexusOperation( - s *testcore.TestEnv, +func (s *NexusStandaloneTestSuite) startNexusOperation( + env *NexusTestEnv, req *workflowservice.StartNexusOperationExecutionRequest, ) (*workflowservice.StartNexusOperationExecutionResponse, error) { - req.Namespace = cmp.Or(req.Namespace, s.Namespace().String()) + req.Namespace = cmp.Or(req.Namespace, env.Namespace().String()) req.Service = cmp.Or(req.Service, "test-service") req.Operation = cmp.Or(req.Operation, "test-operation") - req.RequestId = cmp.Or(req.RequestId, s.Tv().RequestID()) + req.RequestId = cmp.Or(req.RequestId, env.Tv().RequestID()) if req.ScheduleToCloseTimeout == nil { req.ScheduleToCloseTimeout = durationpb.New(10 * time.Minute) } - return s.FrontendClient().StartNexusOperationExecution(s.Context(), req) -} -func createNexusEndpoint(s *testcore.TestEnv) string { - return createNexusEndpointWithTaskQueue(s, "unused-for-test") -} + var ( + resp *workflowservice.StartNexusOperationExecutionResponse + err error + ) + s.Eventually(func() bool { + resp, err = env.FrontendClient().StartNexusOperationExecution(env.Context(), req) + if err == nil { + return true + } -func createNexusEndpointWithTaskQueue(s *testcore.TestEnv, taskQueue string) string { - name := testcore.RandomizedNexusEndpoint(s.T().Name()) - _, err := s.OperatorClient().CreateNexusEndpoint(s.Context(), &operatorservice.CreateNexusEndpointRequest{ - Spec: &nexuspb.EndpointSpec{ - Name: name, - Target: &nexuspb.EndpointTarget{ - Variant: &nexuspb.EndpointTarget_Worker_{ - Worker: &nexuspb.EndpointTarget_Worker{ - Namespace: s.Namespace().String(), - TaskQueue: taskQueue, - }, - }, - }, - }, - }) - s.NoError(err) - return name + var notFound *serviceerror.NotFound + if !errors.As(err, ¬Found) { + return true + } + + message := notFound.Error() + return message != "endpoint not registered" && + !strings.HasPrefix(message, "could not find Nexus endpoint by name:") + }, 10*time.Second, 100*time.Millisecond, "start operation should succeed once the endpoint is visible") + + return resp, err } -func eventuallyNexusOperationDeleted(s *testcore.TestEnv, t *testing.T, operationID, runID string) { +func (s *NexusStandaloneTestSuite) eventuallyDeleted(env *NexusTestEnv, t *testing.T, operationID, runID string) { t.Helper() require.Eventually(t, func() bool { - _, err := s.FrontendClient().DescribeNexusOperationExecution(s.Context(), &workflowservice.DescribeNexusOperationExecutionRequest{ - Namespace: s.Namespace().String(), + _, err := env.FrontendClient().DescribeNexusOperationExecution(env.Context(), &workflowservice.DescribeNexusOperationExecutionRequest{ + Namespace: env.Namespace().String(), OperationId: operationID, RunId: runID, }) diff --git a/tests/nexus_test_base.go b/tests/nexus_test_base.go index 74e05b3d81..fe532c5bdd 100644 --- a/tests/nexus_test_base.go +++ b/tests/nexus_test_base.go @@ -57,6 +57,10 @@ func (env *NexusTestEnv) createNexusEndpoint(t *testing.T, name string, taskQueu return resp.Endpoint } +func (env *NexusTestEnv) createRandomNexusEndpoint(t *testing.T) *nexuspb.Endpoint { + return env.createNexusEndpoint(t, testcore.RandomizedNexusEndpoint(t.Name()), "unused") +} + // nexusTaskResponse represents a successful response from a nexus task handler. // A nil response indicates no response should be sent (e.g., handler timed out). type nexusTaskResponse struct {