diff --git a/dockerfiles/dynamicconfig/docker.yaml b/dockerfiles/dynamicconfig/docker.yaml index 4cc5e363..e051c208 100644 --- a/dockerfiles/dynamicconfig/docker.yaml +++ b/dockerfiles/dynamicconfig/docker.yaml @@ -22,3 +22,9 @@ frontend.enableUpdateWorkflowExecutionAsyncAccepted: frontend.workerVersioningWorkflowAPIs: - value: true constraints: {} +nexusoperation.enableStandalone: + - value: true + constraints: {} +history.enableCHASMCallbacks: + - value: true + constraints: {} diff --git a/features/features.go b/features/features.go index 1d760531..c3ec4d85 100644 --- a/features/features.go +++ b/features/features.go @@ -32,6 +32,7 @@ import ( deployment_versioning_routing_with_ramp "github.com/temporalio/features/features/deployment_versioning/routing_with_ramp" eager_activity_non_remote_activities_worker "github.com/temporalio/features/features/eager_activity/non_remote_activities_worker" eager_workflow_successful_start "github.com/temporalio/features/features/eager_workflow/successful_start" + nexus_standalone_workflow_run_success "github.com/temporalio/features/features/nexus/standalone_workflow_run_success" nexus_sync_success "github.com/temporalio/features/features/nexus/sync_success" query_successful_query "github.com/temporalio/features/features/query/successful_query" query_timeout_due_to_no_active_workers "github.com/temporalio/features/features/query/timeout_due_to_no_active_workers" @@ -94,6 +95,7 @@ func init() { deployment_versioning_routing_with_ramp.Feature, eager_activity_non_remote_activities_worker.Feature, eager_workflow_successful_start.Feature, + nexus_standalone_workflow_run_success.Feature, nexus_sync_success.Feature, query_successful_query.Feature, query_timeout_due_to_no_active_workers.Feature, diff --git a/features/nexus/standalone_workflow_run_success/README.md b/features/nexus/standalone_workflow_run_success/README.md new file mode 100644 index 00000000..d8f3dbe9 --- /dev/null +++ b/features/nexus/standalone_workflow_run_success/README.md @@ -0,0 +1,19 @@ +# Standalone Nexus operation succeeds + +A client starts and awaits an async, workflow-backed Nexus operation directly, without a caller +workflow. + +# Detailed spec + +- A Nexus service with an async, workflow-run operation is registered on the worker. +- The test client builds a standalone `NexusClient` from `client.Client.NewNexusClient` and + calls `ExecuteOperation` against a Nexus endpoint. +- The operation starts asynchronously, backed by a handler workflow; the handler workflow's result + is returned via the operation handle. +- The handle's `Get` blocks until the handler workflow completes and returns the operation result + without any caller workflow being involved. + +# Supported SDKs + +- Go: requires SDK v1.44.0 or later (the standalone Nexus operation client API is Go-only at this + time). diff --git a/features/nexus/standalone_workflow_run_success/feature.go b/features/nexus/standalone_workflow_run_success/feature.go new file mode 100644 index 00000000..e3285f3b --- /dev/null +++ b/features/nexus/standalone_workflow_run_success/feature.go @@ -0,0 +1,65 @@ +package standalone_workflow_run_success + +import ( + "context" + "fmt" + "time" + + "github.com/google/uuid" + "github.com/nexus-rpc/sdk-go/nexus" + "github.com/temporalio/features/harness/go/harness" + "go.temporal.io/sdk/client" + "go.temporal.io/sdk/temporalnexus" + "go.temporal.io/sdk/workflow" +) + +const ServiceName = "test-service" + +func HandlerWorkflow(ctx workflow.Context, name string) (string, error) { + return "Hello, " + name + "!", nil +} + +var AsyncWorkflowOperation = temporalnexus.NewWorkflowRunOperation( + "AsyncWorkflowOperation", + HandlerWorkflow, + func(ctx context.Context, input string, opts nexus.StartOperationOptions) (client.StartWorkflowOptions, error) { + return client.StartWorkflowOptions{ID: "nexus-standalone-handler-" + opts.RequestID}, nil + }, +) + +var Service = func() *nexus.Service { + s := nexus.NewService(ServiceName) + s.MustRegister(AsyncWorkflowOperation) + return s +}() + +var Feature = harness.Feature{ + Workflows: HandlerWorkflow, + NexusServices: Service, + Execute: func(ctx context.Context, runner *harness.Runner) (client.WorkflowRun, error) { + // Start a Nexus operation directly from the client, without a caller workflow. + nc, err := runner.Client.NewNexusClient(client.NexusClientOptions{ + Endpoint: runner.NexusEndpoint, + Service: ServiceName, + }) + if err != nil { + return nil, fmt.Errorf("create nexus client: %w", err) + } + handle, err := nc.ExecuteOperation(ctx, AsyncWorkflowOperation, "world", client.StartNexusOperationOptions{ + ID: "standalone-op-" + uuid.NewString(), + ScheduleToCloseTimeout: time.Minute, + }) + if err != nil { + return nil, fmt.Errorf("execute operation: %w", err) + } + var result string + if err := handle.Get(ctx, &result); err != nil { + return nil, fmt.Errorf("get operation result: %w", err) + } + if result != "Hello, world!" { + return nil, fmt.Errorf("expected %q, got %q", "Hello, world!", result) + } + // Return nil run so the harness skips the default workflow-run checks. + return nil, nil + }, +}