Files
tasks/components/nexusoperations/executors_test.go
zeekay 40351cce6f rip: delete temporal fork — native ZAP-only tasksd
Goes from 135 MB binary with 1153 files importing go.temporal.io and 224
importing google.golang.org/grpc to a 10 MB native binary with ZERO of
either.

Deleted:
- tasks/, tasktests/, service/, chasm/, client/, api/, proto/, tools/
- cmd/tools/, components/, docker/
- schema/{cassandra,elasticsearch,mysql,postgresql,sqlite}/
- 40 tainted common/ subdirs + 7 top-level common files
- tests/testutils (no longer referenced after surviving deps drop common)

New cmd/tasksd (~100 lines) boots tasks.Embed (zap.Node) + http.Server.
New pkg/tasks/embed.go registers ZAP handlers for the SDK opcode surface
(0x0060-0x00A5). Workflow handlers return a 501 "not yet implemented"
envelope; native engine lands behind this contract.

Build:  go build ./cmd/tasksd → 10 MB
Vet:    go vet ./... → clean
Boot:   tasksd --zap-port 9999 --http :7243
Probes: /healthz, /v1/tasks/health → 200
UI:     /_/tasks/, / → 200 (embedded Vite bundle)
ZAP:    _tasks._tcp on :9999 (mDNS disabled; direct connect)

go.mod: tidy. No go.temporal.io. No google.golang.org/grpc.
2026-04-26 14:06:21 -07:00

1728 lines
71 KiB
Go

package nexusoperations_test
import (
"context"
"encoding/json"
"errors"
"fmt"
"testing"
"time"
"github.com/google/uuid"
"github.com/nexus-rpc/sdk-go/nexus"
"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"
historypb "go.temporal.io/api/history/v1"
nexuspb "go.temporal.io/api/nexus/v1"
"go.temporal.io/api/serviceerror"
"go.temporal.io/sdk/converter"
enumsspb "go.temporal.io/server/api/enums/v1"
"go.temporal.io/server/api/historyservice/v1"
"go.temporal.io/server/api/historyservicemock/v1"
persistencespb "go.temporal.io/server/api/persistence/v1"
"go.temporal.io/server/chasm"
chasmnexus "go.temporal.io/server/chasm/lib/nexusoperation"
"go.temporal.io/server/common/backoff"
"go.temporal.io/server/common/definition"
"go.temporal.io/server/common/dynamicconfig"
"go.temporal.io/server/common/log"
"go.temporal.io/server/common/metrics"
"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/nexus/nexustest"
"go.temporal.io/server/common/payloads"
"go.temporal.io/server/common/resource"
"go.temporal.io/server/common/testing/protorequire"
"go.temporal.io/server/components/nexusoperations"
"go.temporal.io/server/service/history/consts"
"go.temporal.io/server/service/history/hsm"
"go.temporal.io/server/service/history/hsm/hsmtest"
queueserrors "go.temporal.io/server/service/history/queues/errors"
"go.uber.org/mock/gomock"
"google.golang.org/grpc"
"google.golang.org/protobuf/types/known/durationpb"
)
var endpointEntry = &persistencespb.NexusEndpointEntry{
Id: "enpdoint-id",
Endpoint: &persistencespb.NexusEndpoint{
Spec: &persistencespb.NexusEndpointSpec{
Name: "endpoint",
Target: &persistencespb.NexusEndpointTarget{
Variant: &persistencespb.NexusEndpointTarget_External_{
External: &persistencespb.NexusEndpointTarget_External{
Url: "http://" + uuid.NewString(),
},
},
},
},
},
}
func mustToPayload(t *testing.T, input any) *commonpb.Payload {
conv := converter.GetDefaultDataConverter()
payload, err := conv.ToPayload(input)
require.NoError(t, err)
return payload
}
func TestProcessInvocationTask(t *testing.T) {
handlerLink := &commonpb.Link_WorkflowEvent{
Namespace: "handler-ns",
WorkflowId: "handler-wf-id",
RunId: "handler-run-id",
Reference: &commonpb.Link_WorkflowEvent_EventRef{
EventRef: &commonpb.Link_WorkflowEvent_EventReference{
EventType: enumspb.EVENT_TYPE_WORKFLOW_EXECUTION_STARTED,
},
},
}
handlerNexusLink := commonnexus.ConvertLinkWorkflowEventToNexusLink(handlerLink)
cases := []struct {
name string
endpointNotFound bool
eventHasNoEndpointID bool
cancelBeforeStart bool
header nexus.Header
checkStartOperationOptions func(t *testing.T, options nexus.StartOperationOptions)
onStartOperation func(ctx context.Context, service, operation string, input *nexus.LazyValue, options nexus.StartOperationOptions) (nexus.HandlerStartOperationResult[any], error)
expectedMetricOutcome string
checkOutcome func(t *testing.T, op nexusoperations.Operation, events []*historypb.HistoryEvent)
requestTimeout time.Duration
schedToCloseTimeout time.Duration
startToCloseTimeout time.Duration
schedToStartTimeout time.Duration
destinationDown bool
}{
{
name: "async start",
requestTimeout: time.Hour,
destinationDown: false,
checkStartOperationOptions: func(t *testing.T, options nexus.StartOperationOptions) {
require.Len(t, options.Links, 1)
var links []*commonpb.Link
for _, nexusLink := range options.Links {
link, err := commonnexus.ConvertNexusLinkToLinkWorkflowEvent(nexusLink)
require.NoError(t, err)
links = append(links, &commonpb.Link{
Variant: &commonpb.Link_WorkflowEvent_{
WorkflowEvent: link,
},
})
}
require.NotNil(t, links[0].GetWorkflowEvent())
protorequire.ProtoEqual(t, &commonpb.Link_WorkflowEvent{
Namespace: "ns-name",
WorkflowId: "wf-id",
RunId: "run-id",
Reference: &commonpb.Link_WorkflowEvent_EventRef{
EventRef: &commonpb.Link_WorkflowEvent_EventReference{
EventId: 1,
EventType: enumspb.EVENT_TYPE_NEXUS_OPERATION_SCHEDULED,
},
},
}, links[0].GetWorkflowEvent())
},
onStartOperation: func(
ctx context.Context,
service, operation string,
input *nexus.LazyValue,
options nexus.StartOperationOptions,
) (nexus.HandlerStartOperationResult[any], error) {
nexus.AddHandlerLinks(ctx, handlerNexusLink)
return &nexus.HandlerStartOperationResultAsync{
OperationToken: "op-token",
}, nil
},
expectedMetricOutcome: "pending",
checkOutcome: func(t *testing.T, op nexusoperations.Operation, events []*historypb.HistoryEvent) {
require.Equal(t, enumsspb.NEXUS_OPERATION_STATE_STARTED, op.State())
require.Equal(t, 1, len(events))
require.Equal(t, enumspb.EVENT_TYPE_NEXUS_OPERATION_STARTED, events[0].EventType)
protorequire.ProtoEqual(t, &historypb.NexusOperationStartedEventAttributes{
ScheduledEventId: 1,
OperationToken: "op-token",
OperationId: "op-token",
RequestId: op.RequestId,
}, events[0].GetNexusOperationStartedEventAttributes())
require.Len(t, events[0].Links, 1)
protorequire.ProtoEqual(t, handlerLink, events[0].Links[0].GetWorkflowEvent())
},
},
{
name: "sync start",
requestTimeout: time.Hour,
schedToCloseTimeout: time.Hour,
// To test this value is ignored when ScheduleToCloseTimeout is set (but still set on the header).
header: nexus.Header{nexus.HeaderOperationTimeout: commonnexus.FormatDuration(time.Millisecond)},
destinationDown: false,
onStartOperation: func(ctx context.Context, service, operation string, input *nexus.LazyValue, options nexus.StartOperationOptions) (nexus.HandlerStartOperationResult[any], error) {
// Also use this test case to check the input and options provided.
if service != "service" {
return nil, nexus.NewHandlerErrorf(nexus.HandlerErrorTypeBadRequest, "invalid operation name")
}
if operation != "operation" {
return nil, nexus.NewHandlerErrorf(nexus.HandlerErrorTypeBadRequest, "invalid operation name")
}
if options.CallbackHeader.Get("temporal-callback-token") == "" {
return nil, nexus.NewHandlerErrorf(nexus.HandlerErrorTypeBadRequest, "empty callback token")
}
if options.CallbackURL != "http://localhost/callback" {
return nil, nexus.NewHandlerErrorf(nexus.HandlerErrorTypeBadRequest, "invalid callback URL")
}
if options.Header.Get(nexus.HeaderOperationTimeout) != "1ms" {
return nil, nexus.NewHandlerErrorf(nexus.HandlerErrorTypeBadRequest, "invalid operation timeout header: %s", options.Header.Get(nexus.HeaderOperationTimeout))
}
var v string
if err := input.Consume(&v); err != nil || v != "input" {
return nil, nexus.NewHandlerErrorf(nexus.HandlerErrorTypeBadRequest, "invalid input")
}
return &nexus.HandlerStartOperationResultSync[any]{Value: "result"}, nil
},
expectedMetricOutcome: "successful",
checkOutcome: func(t *testing.T, op nexusoperations.Operation, events []*historypb.HistoryEvent) {
require.Equal(t, enumsspb.NEXUS_OPERATION_STATE_SUCCEEDED, op.State())
require.Equal(t, 1, len(events))
require.Equal(t, enumspb.EVENT_TYPE_NEXUS_OPERATION_COMPLETED, events[0].EventType)
attrs := &historypb.NexusOperationCompletedEventAttributes{
ScheduledEventId: 1,
Result: mustToPayload(t, "result"),
RequestId: op.RequestId,
}
protorequire.ProtoEqual(t, attrs, events[0].GetNexusOperationCompletedEventAttributes())
},
},
{
name: "sync failed",
requestTimeout: time.Hour,
destinationDown: false,
onStartOperation: func(ctx context.Context, service, operation string, input *nexus.LazyValue, options nexus.StartOperationOptions) (nexus.HandlerStartOperationResult[any], error) {
return nil, &nexus.OperationError{
State: nexus.OperationStateFailed,
Message: "operation failed from handler",
Cause: &nexus.FailureError{
Failure: nexus.Failure{Message: "cause", Metadata: map[string]string{"encoding": "json/plain"}, Details: json.RawMessage("\"details\"")},
},
}
},
expectedMetricOutcome: "operation-unsuccessful:failed",
checkOutcome: func(t *testing.T, op nexusoperations.Operation, events []*historypb.HistoryEvent) {
require.Equal(t, enumsspb.NEXUS_OPERATION_STATE_FAILED, op.State())
require.Equal(t, 1, len(events))
require.Equal(t, enumspb.EVENT_TYPE_NEXUS_OPERATION_FAILED, events[0].EventType)
attrs := &historypb.NexusOperationFailedEventAttributes{
ScheduledEventId: 1,
RequestId: op.RequestId,
Failure: &failurepb.Failure{
Message: "nexus operation completed unsuccessfully",
FailureInfo: &failurepb.Failure_NexusOperationExecutionFailureInfo{
NexusOperationExecutionFailureInfo: &failurepb.NexusOperationFailureInfo{
ScheduledEventId: 1,
Endpoint: "endpoint",
Service: "service",
Operation: "operation",
},
},
Cause: &failurepb.Failure{
Message: "operation failed from handler",
FailureInfo: &failurepb.Failure_ApplicationFailureInfo{
ApplicationFailureInfo: &failurepb.ApplicationFailureInfo{
Type: "OperationError",
NonRetryable: true,
},
},
Cause: &failurepb.Failure{
Message: "cause",
FailureInfo: &failurepb.Failure_ApplicationFailureInfo{
ApplicationFailureInfo: &failurepb.ApplicationFailureInfo{
Type: "NexusFailure",
Details: &commonpb.Payloads{
Payloads: []*commonpb.Payload{
mustToPayload(t, nexus.Failure{
Metadata: map[string]string{"encoding": "json/plain"},
Details: json.RawMessage("\"details\""),
}),
},
},
},
},
},
},
},
}
protorequire.ProtoEqual(t, attrs, events[0].GetNexusOperationFailedEventAttributes())
},
},
{
name: "sync canceled",
requestTimeout: time.Hour,
destinationDown: false,
onStartOperation: func(ctx context.Context, service, operation string, input *nexus.LazyValue, options nexus.StartOperationOptions) (nexus.HandlerStartOperationResult[any], error) {
return nil, &nexus.OperationError{
State: nexus.OperationStateCanceled,
Message: "operation canceled from handler",
Cause: &nexus.FailureError{
Failure: nexus.Failure{Message: "cause", Metadata: map[string]string{"encoding": "json/plain"}, Details: json.RawMessage("\"details\"")},
},
}
},
expectedMetricOutcome: "operation-unsuccessful:canceled",
checkOutcome: func(t *testing.T, op nexusoperations.Operation, events []*historypb.HistoryEvent) {
require.Equal(t, enumsspb.NEXUS_OPERATION_STATE_CANCELED, op.State())
require.Equal(t, 1, len(events))
require.Equal(t, enumspb.EVENT_TYPE_NEXUS_OPERATION_CANCELED, events[0].EventType)
attrs := &historypb.NexusOperationCanceledEventAttributes{
ScheduledEventId: 1,
RequestId: op.RequestId,
Failure: &failurepb.Failure{
Message: "nexus operation completed unsuccessfully",
FailureInfo: &failurepb.Failure_NexusOperationExecutionFailureInfo{
NexusOperationExecutionFailureInfo: &failurepb.NexusOperationFailureInfo{
ScheduledEventId: 1,
Endpoint: "endpoint",
Service: "service",
Operation: "operation",
},
},
Cause: &failurepb.Failure{
Message: "operation canceled from handler",
FailureInfo: &failurepb.Failure_CanceledFailureInfo{
CanceledFailureInfo: &failurepb.CanceledFailureInfo{},
},
Cause: &failurepb.Failure{
Message: "cause",
FailureInfo: &failurepb.Failure_ApplicationFailureInfo{
ApplicationFailureInfo: &failurepb.ApplicationFailureInfo{
Type: "NexusFailure",
Details: &commonpb.Payloads{
Payloads: []*commonpb.Payload{
mustToPayload(t, nexus.Failure{
Metadata: map[string]string{"encoding": "json/plain"},
Details: json.RawMessage("\"details\""),
}),
},
},
},
},
},
},
},
}
protorequire.ProtoEqual(t, attrs, events[0].GetNexusOperationCanceledEventAttributes())
},
},
{
name: "transient error",
requestTimeout: time.Hour,
destinationDown: true,
onStartOperation: func(ctx context.Context, service, operation string, input *nexus.LazyValue, options nexus.StartOperationOptions) (nexus.HandlerStartOperationResult[any], error) {
return nil, nexus.NewHandlerErrorf(nexus.HandlerErrorTypeInternal, "internal server error")
},
expectedMetricOutcome: "handler-error:INTERNAL",
checkOutcome: func(t *testing.T, op nexusoperations.Operation, events []*historypb.HistoryEvent) {
require.Equal(t, enumsspb.NEXUS_OPERATION_STATE_BACKING_OFF, op.State())
require.Equal(t, string(nexus.HandlerErrorTypeInternal), op.LastAttemptFailure.GetNexusHandlerFailureInfo().GetType())
require.Equal(t, "internal server error", op.LastAttemptFailure.Message)
require.Equal(t, 0, len(events))
},
},
{
name: "invocation timeout by request timeout",
requestTimeout: 2 * time.Millisecond,
schedToCloseTimeout: time.Hour,
destinationDown: true,
expectedMetricOutcome: "request-timeout",
onStartOperation: func(ctx context.Context, service, operation string, input *nexus.LazyValue, options nexus.StartOperationOptions) (nexus.HandlerStartOperationResult[any], error) {
time.Sleep(time.Millisecond * 100)
return &nexus.HandlerStartOperationResultAsync{OperationToken: "op-token"}, nil
},
checkOutcome: func(t *testing.T, op nexusoperations.Operation, events []*historypb.HistoryEvent) {
require.Equal(t, enumsspb.NEXUS_OPERATION_STATE_BACKING_OFF, op.State())
require.NotNil(t, op.LastAttemptFailure.GetApplicationFailureInfo())
require.Regexp(t, "request timed out", op.LastAttemptFailure.Message)
require.Equal(t, 0, len(events))
},
},
{
name: "invocation timeout by ScheduleToCloseTimeout",
requestTimeout: time.Hour,
schedToCloseTimeout: 10 * time.Millisecond,
destinationDown: true,
expectedMetricOutcome: "request-timeout",
onStartOperation: func(ctx context.Context, service, operation string, input *nexus.LazyValue, options nexus.StartOperationOptions) (nexus.HandlerStartOperationResult[any], error) {
opTimeout, err := time.ParseDuration(options.Header.Get(nexus.HeaderOperationTimeout))
if err != nil || opTimeout > 10*time.Millisecond {
return nil, nexus.NewHandlerErrorf(nexus.HandlerErrorTypeBadRequest, "invalid operation timeout header: %s", options.Header.Get(nexus.HeaderOperationTimeout))
}
time.Sleep(time.Millisecond * 100) //nolint:forbidigo // Allow time.Sleep for timeout tests
return &nexus.HandlerStartOperationResultAsync{OperationToken: "op-token"}, nil
},
checkOutcome: func(t *testing.T, op nexusoperations.Operation, events []*historypb.HistoryEvent) {
require.Equal(t, enumsspb.NEXUS_OPERATION_STATE_BACKING_OFF, op.State())
require.NotNil(t, op.LastAttemptFailure.GetApplicationFailureInfo())
require.Regexp(t, "request timed out", op.LastAttemptFailure.Message)
require.Empty(t, events)
},
},
{
name: "invocation timeout by ScheduleToStartTimeout",
requestTimeout: time.Hour,
schedToStartTimeout: 10 * time.Millisecond,
destinationDown: true,
expectedMetricOutcome: "request-timeout",
onStartOperation: func(ctx context.Context, service, operation string, input *nexus.LazyValue, options nexus.StartOperationOptions) (nexus.HandlerStartOperationResult[any], error) {
if options.Header.Get(nexus.HeaderOperationTimeout) != "" {
return nil, nexus.NewHandlerErrorf(nexus.HandlerErrorTypeBadRequest, "operation timeout header should not be set, got: %s", options.Header.Get(nexus.HeaderOperationTimeout))
}
time.Sleep(time.Millisecond * 100) //nolint:forbidigo // Allow time.Sleep for timeout tests
return &nexus.HandlerStartOperationResultAsync{OperationToken: "op-token"}, nil
},
checkOutcome: func(t *testing.T, op nexusoperations.Operation, events []*historypb.HistoryEvent) {
require.Equal(t, enumsspb.NEXUS_OPERATION_STATE_BACKING_OFF, op.State())
require.NotNil(t, op.LastAttemptFailure.GetApplicationFailureInfo())
require.Regexp(t, "request timed out", op.LastAttemptFailure.Message)
require.Equal(t, 0, len(events))
},
},
{
name: "operation timeout header set by StartToCloseTimeout",
requestTimeout: time.Hour,
startToCloseTimeout: 1 * time.Minute,
destinationDown: false,
expectedMetricOutcome: "pending",
onStartOperation: func(ctx context.Context, service, operation string, input *nexus.LazyValue, options nexus.StartOperationOptions) (nexus.HandlerStartOperationResult[any], error) {
if options.Header.Get(nexus.HeaderOperationTimeout) != "60000ms" {
return nil, nexus.NewHandlerErrorf(nexus.HandlerErrorTypeBadRequest, "invalid operation timeout header: %s", options.Header.Get(nexus.HeaderOperationTimeout))
}
return &nexus.HandlerStartOperationResultAsync{OperationToken: "op-token"}, nil
},
checkOutcome: func(t *testing.T, op nexusoperations.Operation, events []*historypb.HistoryEvent) {
require.Equal(t, enumsspb.NEXUS_OPERATION_STATE_STARTED, op.State())
require.Len(t, events, 1)
},
},
{
name: "ScheduleToCloseTimeout less than MinRequestTimeout",
requestTimeout: time.Hour,
schedToCloseTimeout: time.Microsecond,
destinationDown: false,
expectedMetricOutcome: "operation-timeout",
onStartOperation: nil, // This should not be called if the operation has timed out.
checkOutcome: func(t *testing.T, op nexusoperations.Operation, events []*historypb.HistoryEvent) {
require.Equal(t, enumsspb.NEXUS_OPERATION_STATE_TIMED_OUT, op.State())
require.Equal(t, 1, len(events))
failure := events[0].GetNexusOperationTimedOutEventAttributes().Failure.Cause
require.NotNil(t, failure.GetTimeoutFailureInfo())
require.Equal(t, "operation timed out", failure.Message)
},
},
{
name: "endpoint not found",
endpointNotFound: true,
requestTimeout: time.Hour,
destinationDown: false,
onStartOperation: nil, // This should not be called if the endpoint is not found.
checkOutcome: func(t *testing.T, op nexusoperations.Operation, events []*historypb.HistoryEvent) {
require.Equal(t, enumsspb.NEXUS_OPERATION_STATE_FAILED, op.State())
require.Equal(t, 1, len(events))
failure := events[0].GetNexusOperationFailedEventAttributes().Failure.Cause
require.Equal(t, string(nexus.HandlerErrorTypeNotFound), failure.GetNexusHandlerFailureInfo().GetType())
require.Equal(t, "endpoint not registered", failure.Message)
},
},
{
name: "endpoint not found on command processing",
eventHasNoEndpointID: true,
requestTimeout: time.Hour,
destinationDown: false,
onStartOperation: nil, // This should not be called if the endpoint is not found.
checkOutcome: func(t *testing.T, op nexusoperations.Operation, events []*historypb.HistoryEvent) {
require.Equal(t, enumsspb.NEXUS_OPERATION_STATE_FAILED, op.State())
require.Equal(t, 1, len(events))
failure := events[0].GetNexusOperationFailedEventAttributes().Failure.Cause
require.Equal(t, string(nexus.HandlerErrorTypeNotFound), failure.GetNexusHandlerFailureInfo().GetType())
require.Equal(t, "endpoint not registered", failure.Message)
},
},
{
name: "cancel before start",
cancelBeforeStart: true,
requestTimeout: time.Hour,
destinationDown: false,
onStartOperation: func(
ctx context.Context,
service, operation string,
input *nexus.LazyValue,
options nexus.StartOperationOptions,
) (nexus.HandlerStartOperationResult[any], error) {
nexus.AddHandlerLinks(ctx, handlerNexusLink)
return &nexus.HandlerStartOperationResultAsync{
OperationToken: "op-token",
}, nil
},
expectedMetricOutcome: "pending",
checkOutcome: func(t *testing.T, op nexusoperations.Operation, events []*historypb.HistoryEvent) {
require.Equal(t, enumsspb.NEXUS_OPERATION_STATE_STARTED, op.State())
require.Nil(t, op.LastAttemptFailure)
require.Equal(t, 1, len(events))
},
},
{
name: "token to long",
requestTimeout: time.Hour,
destinationDown: false,
onStartOperation: func(
ctx context.Context,
service, operation string,
input *nexus.LazyValue,
options nexus.StartOperationOptions,
) (nexus.HandlerStartOperationResult[any], error) {
return &nexus.HandlerStartOperationResultAsync{OperationToken: "12345678901"}, nil
},
expectedMetricOutcome: "invalid-operation-token",
checkOutcome: func(t *testing.T, op nexusoperations.Operation, events []*historypb.HistoryEvent) {
require.Equal(t, enumsspb.NEXUS_OPERATION_STATE_FAILED, op.State())
require.Equal(t, 1, len(events))
failure := events[0].GetNexusOperationFailedEventAttributes().Failure.Cause
require.NotNil(t, failure.GetApplicationFailureInfo())
require.Equal(t, "invalid operation token: length exceeds allowed limit (11/10)", failure.Message)
},
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
listenAddr := nexustest.AllocListenAddress()
h := nexustest.Handler{}
h.OnStartOperation = func(
ctx context.Context,
service, operation string,
input *nexus.LazyValue,
options nexus.StartOperationOptions,
) (nexus.HandlerStartOperationResult[any], error) {
if tc.checkStartOperationOptions != nil {
tc.checkStartOperationOptions(t, options)
}
return tc.onStartOperation(ctx, service, operation, input, options)
}
nexustest.NewNexusServer(t, listenAddr, h)
reg := newRegistry(t)
event := mustNewScheduledEvent(time.Now(), &historypb.NexusOperationScheduledEventAttributes{
ScheduleToCloseTimeout: durationpb.New(tc.schedToCloseTimeout),
ScheduleToStartTimeout: durationpb.New(tc.schedToStartTimeout),
StartToCloseTimeout: durationpb.New(tc.startToCloseTimeout),
})
if tc.eventHasNoEndpointID {
event.GetNexusOperationScheduledEventAttributes().EndpointId = ""
}
if tc.header != nil {
event.GetNexusOperationScheduledEventAttributes().NexusHeader = tc.header
}
backend := &hsmtest.NodeBackend{Events: []*historypb.HistoryEvent{event}}
node := newOperationNode(t, backend, backend.Events[0])
env := fakeEnv{node}
if tc.cancelBeforeStart {
op, err := hsm.MachineData[nexusoperations.Operation](node)
require.NoError(t, err)
_, err = op.Cancel(node, time.Now(), 0)
require.NoError(t, err)
c, err := op.Cancelation(node)
require.NoError(t, err)
require.NotNil(t, c)
require.Equal(t, enumspb.NEXUS_OPERATION_CANCELLATION_STATE_UNSPECIFIED, c.State())
}
namespaceRegistry := namespace.NewMockRegistry(ctrl)
namespaceRegistry.EXPECT().GetNamespaceByID(namespace.ID("ns-id")).Return(
namespace.NewNamespaceForTest(&persistencespb.NamespaceInfo{Name: "ns-name"}, nil, false, nil, 0), nil)
metricsHandler := metrics.NewMockHandler(ctrl)
if tc.expectedMetricOutcome != "" {
counter := metrics.NewMockCounterIface(ctrl)
timer := metrics.NewMockTimerIface(ctrl)
metricsHandler.EXPECT().Counter(chasmnexus.OutboundRequestCounter.Name()).Return(counter)
counter.EXPECT().Record(int64(1),
metrics.NamespaceTag("ns-name"),
metrics.DestinationTag("endpoint"),
metrics.NexusMethodTag("StartOperation"),
metrics.OutcomeTag(tc.expectedMetricOutcome),
metrics.FailureSourceTag("_unknown_"))
metricsHandler.EXPECT().Timer(chasmnexus.OutboundRequestLatency.Name()).Return(timer)
timer.EXPECT().Record(gomock.Any(),
metrics.NamespaceTag("ns-name"),
metrics.DestinationTag("endpoint"),
metrics.NexusMethodTag("StartOperation"),
metrics.OutcomeTag(tc.expectedMetricOutcome),
metrics.FailureSourceTag("_unknown_"))
}
endpointReg := nexustest.FakeEndpointRegistry{
OnGetByID: func(ctx context.Context, endpointID string) (*persistencespb.NexusEndpointEntry, error) {
require.Equal(t, "endpoint-id", endpointID)
if tc.endpointNotFound {
return nil, serviceerror.NewNotFound("endpoint not found")
}
return endpointEntry, nil
},
OnGetByName: func(ctx context.Context, namespaceID namespace.ID, endpointName string) (*persistencespb.NexusEndpointEntry, error) {
require.Equal(t, "endpoint", endpointName)
require.Equal(t, "ns-id", namespaceID.String())
if tc.endpointNotFound {
return nil, serviceerror.NewNotFound("endpoint not found")
}
return endpointEntry, nil
},
}
require.NoError(t, nexusoperations.RegisterExecutor(reg, nexusoperations.TaskExecutorOptions{
Config: &nexusoperations.Config{
RequestTimeout: dynamicconfig.GetDurationPropertyFnFilteredByDestination(tc.requestTimeout),
MaxOperationTokenLength: dynamicconfig.GetIntPropertyFnFilteredByNamespace(10),
MinRequestTimeout: dynamicconfig.GetDurationPropertyFnFilteredByNamespace(time.Millisecond),
PayloadSizeLimit: dynamicconfig.GetIntPropertyFnFilteredByNamespace(2 * 1024 * 1024),
CallbackURLTemplate: dynamicconfig.GetStringPropertyFn("http://localhost/callback"),
UseSystemCallbackURL: dynamicconfig.GetBoolPropertyFn(true),
UseNewFailureWireFormat: dynamicconfig.GetBoolPropertyFnFilteredByNamespace(true),
RetryPolicy: func() backoff.RetryPolicy {
return backoff.NewExponentialRetryPolicy(time.Second)
},
},
CallbackTokenGenerator: commonnexus.NewCallbackTokenGenerator(),
NamespaceRegistry: namespaceRegistry,
MetricsHandler: metricsHandler,
Logger: log.NewNoopLogger(),
EndpointRegistry: endpointReg,
ClientProvider: func(ctx context.Context, namespaceID string, entry *persistencespb.NexusEndpointEntry, service string) (*nexusrpc.HTTPClient, error) {
return nexusrpc.NewHTTPClient(nexusrpc.HTTPClientOptions{
BaseURL: "http://" + listenAddr,
Service: service,
Serializer: commonnexus.PayloadSerializer,
})
},
}))
err := reg.ExecuteImmediateTask(
context.Background(),
env,
hsm.Ref{
WorkflowKey: definition.NewWorkflowKey("ns-id", "wf-id", "run-id"),
StateMachineRef: &persistencespb.StateMachineRef{},
},
nexusoperations.InvocationTask{EndpointName: "endpoint-id"},
)
if tc.destinationDown {
var destinationDownErr *queueserrors.DestinationDownError
require.ErrorAs(t, err, &destinationDownErr)
} else {
require.NoError(t, err)
}
op, err := hsm.MachineData[nexusoperations.Operation](node)
require.NoError(t, err)
tc.checkOutcome(t, op, backend.Events[1:]) // Ignore the original scheduled event.
if tc.cancelBeforeStart {
c, err := op.Cancelation(node)
require.NoError(t, err)
require.NotNil(t, c)
require.Equal(t, enumspb.NEXUS_OPERATION_CANCELLATION_STATE_SCHEDULED, c.State())
}
})
}
}
func TestProcessBackoffTask(t *testing.T) {
reg := newRegistry(t)
backend := &hsmtest.NodeBackend{}
node := newOperationNode(t, backend, mustNewScheduledEvent(time.Now(), &historypb.NexusOperationScheduledEventAttributes{
ScheduleToCloseTimeout: durationpb.New(time.Hour),
}))
env := fakeEnv{node}
require.NoError(t, nexusoperations.RegisterExecutor(reg, nexusoperations.TaskExecutorOptions{}))
err := hsm.MachineTransition(node, func(op nexusoperations.Operation) (hsm.TransitionOutput, error) {
return nexusoperations.TransitionAttemptFailed.Apply(op, nexusoperations.EventAttemptFailed{
Node: node,
Time: time.Now(),
Failure: &failurepb.Failure{Message: "test"},
RetryPolicy: backoff.NewExponentialRetryPolicy(time.Second),
})
})
require.NoError(t, err)
err = reg.ExecuteTimerTask(
env,
node,
nexusoperations.BackoffTask{},
)
require.NoError(t, err)
op, err := hsm.MachineData[nexusoperations.Operation](node)
require.NoError(t, err)
require.Equal(t, enumsspb.NEXUS_OPERATION_STATE_SCHEDULED, op.State())
require.Equal(t, 0, len(backend.Events))
}
func TestProcessTimeoutTask(t *testing.T) {
reg := newRegistry(t)
backend := &hsmtest.NodeBackend{}
node := newOperationNode(t, backend, mustNewScheduledEvent(time.Now(), &historypb.NexusOperationScheduledEventAttributes{
ScheduleToCloseTimeout: durationpb.New(time.Hour),
}))
env := fakeEnv{node}
require.NoError(t, nexusoperations.RegisterExecutor(reg, nexusoperations.TaskExecutorOptions{}))
err := reg.ExecuteTimerTask(
env,
node,
nexusoperations.ScheduleToCloseTimeoutTask{},
)
require.NoError(t, err)
op, err := hsm.MachineData[nexusoperations.Operation](node)
require.NoError(t, err)
require.Equal(t, enumsspb.NEXUS_OPERATION_STATE_TIMED_OUT, op.State())
require.Equal(t, 1, len(backend.Events))
require.Equal(t, enumspb.EVENT_TYPE_NEXUS_OPERATION_TIMED_OUT, backend.Events[0].EventType)
protorequire.ProtoEqual(t, &historypb.NexusOperationTimedOutEventAttributes{
ScheduledEventId: 1,
RequestId: op.RequestId,
Failure: &failurepb.Failure{
Message: "nexus operation completed unsuccessfully",
FailureInfo: &failurepb.Failure_NexusOperationExecutionFailureInfo{
NexusOperationExecutionFailureInfo: &failurepb.NexusOperationFailureInfo{
ScheduledEventId: 1,
Endpoint: "endpoint",
Service: "service",
Operation: "operation",
},
},
Cause: &failurepb.Failure{
Message: "operation timed out",
FailureInfo: &failurepb.Failure_TimeoutFailureInfo{
TimeoutFailureInfo: &failurepb.TimeoutFailureInfo{
TimeoutType: enumspb.TIMEOUT_TYPE_SCHEDULE_TO_CLOSE,
},
},
},
},
}, backend.Events[0].GetNexusOperationTimedOutEventAttributes())
}
func TestProcessScheduleToStartTimeoutTask(t *testing.T) {
reg := newRegistry(t)
backend := &hsmtest.NodeBackend{}
node := newOperationNode(t, backend, mustNewScheduledEvent(time.Now(), &historypb.NexusOperationScheduledEventAttributes{
ScheduleToCloseTimeout: durationpb.New(time.Hour),
}))
env := fakeEnv{node}
require.NoError(t, nexusoperations.RegisterExecutor(reg, nexusoperations.TaskExecutorOptions{}))
err := reg.ExecuteTimerTask(
env,
node,
nexusoperations.ScheduleToStartTimeoutTask{},
)
require.NoError(t, err)
op, err := hsm.MachineData[nexusoperations.Operation](node)
require.NoError(t, err)
require.Equal(t, enumsspb.NEXUS_OPERATION_STATE_TIMED_OUT, op.State())
require.Len(t, backend.Events, 1)
require.Equal(t, enumspb.EVENT_TYPE_NEXUS_OPERATION_TIMED_OUT, backend.Events[0].EventType)
protorequire.ProtoEqual(t, &historypb.NexusOperationTimedOutEventAttributes{
ScheduledEventId: 1,
RequestId: op.RequestId,
Failure: &failurepb.Failure{
Message: "nexus operation completed unsuccessfully",
FailureInfo: &failurepb.Failure_NexusOperationExecutionFailureInfo{
NexusOperationExecutionFailureInfo: &failurepb.NexusOperationFailureInfo{
ScheduledEventId: 1,
Endpoint: "endpoint",
Service: "service",
Operation: "operation",
},
},
Cause: &failurepb.Failure{
Message: "operation timed out",
FailureInfo: &failurepb.Failure_TimeoutFailureInfo{
TimeoutFailureInfo: &failurepb.TimeoutFailureInfo{
TimeoutType: enumspb.TIMEOUT_TYPE_SCHEDULE_TO_START,
},
},
},
},
}, backend.Events[0].GetNexusOperationTimedOutEventAttributes())
}
func TestProcessStartToCloseTimeoutTask(t *testing.T) {
reg := newRegistry(t)
backend := &hsmtest.NodeBackend{}
node := newOperationNode(t, backend, mustNewScheduledEvent(time.Now(), &historypb.NexusOperationScheduledEventAttributes{
ScheduleToCloseTimeout: durationpb.New(time.Hour),
}))
env := fakeEnv{node}
require.NoError(t, nexusoperations.RegisterExecutor(reg, nexusoperations.TaskExecutorOptions{}))
// Transition to STARTED state first
err := hsm.MachineTransition(node, func(op nexusoperations.Operation) (hsm.TransitionOutput, error) {
return nexusoperations.TransitionStarted.Apply(op, nexusoperations.EventStarted{
Node: node,
Time: time.Now(),
Attributes: &historypb.NexusOperationStartedEventAttributes{
OperationToken: "test-token",
},
})
})
require.NoError(t, err)
// Now execute the start-to-close timeout
err = reg.ExecuteTimerTask(
env,
node,
nexusoperations.StartToCloseTimeoutTask{},
)
require.NoError(t, err)
op, err := hsm.MachineData[nexusoperations.Operation](node)
require.NoError(t, err)
require.Equal(t, enumsspb.NEXUS_OPERATION_STATE_TIMED_OUT, op.State())
// Should have TIMED_OUT event (STARTED event is not added by the transition)
require.Len(t, backend.Events, 1)
require.Equal(t, enumspb.EVENT_TYPE_NEXUS_OPERATION_TIMED_OUT, backend.Events[0].EventType)
// Verify timeout type and message
timedOutAttrs := backend.Events[0].GetNexusOperationTimedOutEventAttributes()
require.Equal(t, int64(1), timedOutAttrs.ScheduledEventId)
require.Equal(t, op.RequestId, timedOutAttrs.RequestId)
require.NotNil(t, timedOutAttrs.Failure)
require.Equal(t, "nexus operation completed unsuccessfully", timedOutAttrs.Failure.Message)
require.NotNil(t, timedOutAttrs.Failure.Cause)
require.Equal(t, "operation timed out", timedOutAttrs.Failure.Cause.Message)
require.Equal(t, enumspb.TIMEOUT_TYPE_START_TO_CLOSE, timedOutAttrs.Failure.Cause.GetTimeoutFailureInfo().TimeoutType)
// Verify operation token is present in failure info
nexusFailureInfo := timedOutAttrs.Failure.GetNexusOperationExecutionFailureInfo()
require.NotNil(t, nexusFailureInfo)
require.Equal(t, "test-token", nexusFailureInfo.OperationToken)
}
func TestProcessCancelationTask(t *testing.T) {
cases := []struct {
name string
endpointNotFound bool
onCancelOperation func(ctx context.Context, service, operation, token string, options nexus.CancelOperationOptions) error
expectedMetricOutcome string
checkOutcome func(t *testing.T, op nexusoperations.Cancelation)
requestTimeout time.Duration
schedToCloseTimeout time.Duration
startToCloseTimeout time.Duration
destinationDown bool
header map[string]string
}{
{
name: "failure",
requestTimeout: time.Hour,
destinationDown: false,
onCancelOperation: func(ctx context.Context, service, operation, token string, options nexus.CancelOperationOptions) error {
// Check non retryable internal error.
return &nexus.HandlerError{
Type: nexus.HandlerErrorTypeInternal,
Message: "operation not found",
RetryBehavior: nexus.HandlerErrorRetryBehaviorNonRetryable,
}
},
expectedMetricOutcome: "handler-error:INTERNAL",
checkOutcome: func(t *testing.T, c nexusoperations.Cancelation) {
require.Equal(t, enumspb.NEXUS_OPERATION_CANCELLATION_STATE_FAILED, c.State())
require.Equal(t, string(nexus.HandlerErrorTypeInternal), c.LastAttemptFailure.GetNexusHandlerFailureInfo().GetType())
require.Equal(t, "operation not found", c.LastAttemptFailure.Message)
},
},
{
name: "success",
requestTimeout: time.Hour,
destinationDown: false,
onCancelOperation: func(ctx context.Context, service, operation, token string, options nexus.CancelOperationOptions) error {
return nil
},
expectedMetricOutcome: "successful",
checkOutcome: func(t *testing.T, c nexusoperations.Cancelation) {
require.Equal(t, enumspb.NEXUS_OPERATION_CANCELLATION_STATE_SUCCEEDED, c.State())
require.Nil(t, c.LastAttemptFailure.GetApplicationFailureInfo())
},
},
{
name: "success with headers",
requestTimeout: time.Hour,
destinationDown: false,
header: map[string]string{"key": "value"},
onCancelOperation: func(ctx context.Context, service, operation, token string, options nexus.CancelOperationOptions) error {
if options.Header["key"] != "value" {
return nexus.NewHandlerErrorf(nexus.HandlerErrorTypeBadRequest, `"key" header is not equal to "value"`)
}
return nil
},
expectedMetricOutcome: "successful",
checkOutcome: func(t *testing.T, c nexusoperations.Cancelation) {
require.Equal(t, enumspb.NEXUS_OPERATION_CANCELLATION_STATE_SUCCEEDED, c.State())
require.Nil(t, c.LastAttemptFailure.GetApplicationFailureInfo())
},
},
{
name: "transient error",
requestTimeout: time.Hour,
destinationDown: true,
onCancelOperation: func(ctx context.Context, service, operation, token string, options nexus.CancelOperationOptions) error {
return nexus.NewHandlerErrorf(nexus.HandlerErrorTypeInternal, "internal server error")
},
expectedMetricOutcome: "handler-error:INTERNAL",
checkOutcome: func(t *testing.T, c nexusoperations.Cancelation) {
require.Equal(t, enumspb.NEXUS_OPERATION_CANCELLATION_STATE_BACKING_OFF, c.State())
require.NotNil(t, c.LastAttemptFailure.GetNexusHandlerFailureInfo())
require.Equal(t, "internal server error", c.LastAttemptFailure.Message)
},
},
{
name: "invocation timeout",
requestTimeout: 10 * time.Millisecond,
destinationDown: true,
onCancelOperation: func(ctx context.Context, service, operation, token string, options nexus.CancelOperationOptions) error {
time.Sleep(time.Millisecond * 100) //nolint:forbidigo // Allow time.Sleep for timeout tests
return nil
},
expectedMetricOutcome: "request-timeout",
checkOutcome: func(t *testing.T, c nexusoperations.Cancelation) {
require.Equal(t, enumspb.NEXUS_OPERATION_CANCELLATION_STATE_BACKING_OFF, c.State())
require.NotNil(t, c.LastAttemptFailure.GetApplicationFailureInfo())
require.Regexp(t, "Post \"http://localhost:\\d+/service/operation/cancel\": context deadline exceeded", c.LastAttemptFailure.Message)
},
},
{
name: "operation timeout by ScheduleToCloseTimeout",
requestTimeout: time.Hour,
schedToCloseTimeout: time.Microsecond,
destinationDown: false,
onCancelOperation: nil, // This should not be called if the operation has timed out.
expectedMetricOutcome: "operation-timeout",
checkOutcome: func(t *testing.T, c nexusoperations.Cancelation) {
require.Equal(t, enumspb.NEXUS_OPERATION_CANCELLATION_STATE_FAILED, c.State())
require.NotNil(t, c.LastAttemptFailure.GetApplicationFailureInfo())
require.Contains(t, "not enough time to execute another request before ScheduleToClose timeout", c.LastAttemptFailure.Message)
},
},
{
name: "operation timeout by StartToCloseTimeout",
requestTimeout: time.Hour,
startToCloseTimeout: time.Microsecond,
destinationDown: false,
onCancelOperation: nil, // This should not be called if the operation has timed out.
expectedMetricOutcome: "operation-timeout",
checkOutcome: func(t *testing.T, c nexusoperations.Cancelation) {
require.Equal(t, enumspb.NEXUS_OPERATION_CANCELLATION_STATE_FAILED, c.State())
require.NotNil(t, c.LastAttemptFailure.GetApplicationFailureInfo())
require.Contains(t, "not enough time to execute another request before StartToClose timeout", c.LastAttemptFailure.Message)
},
},
{
name: "endpoint not found",
endpointNotFound: true,
requestTimeout: time.Hour,
destinationDown: false,
onCancelOperation: nil, // This should not be called if the endpoint is not found.
checkOutcome: func(t *testing.T, c nexusoperations.Cancelation) {
require.Equal(t, enumspb.NEXUS_OPERATION_CANCELLATION_STATE_FAILED, c.State())
require.Equal(t, string(nexus.HandlerErrorTypeNotFound), c.LastAttemptFailure.GetNexusHandlerFailureInfo().GetType())
require.Equal(t, "endpoint not registered", c.LastAttemptFailure.Message)
},
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
listenAddr := nexustest.AllocListenAddress()
h := nexustest.Handler{}
h.OnCancelOperation = tc.onCancelOperation
nexustest.NewNexusServer(t, listenAddr, h)
reg := newRegistry(t)
event := mustNewScheduledEvent(time.Now(), &historypb.NexusOperationScheduledEventAttributes{
ScheduleToCloseTimeout: durationpb.New(tc.schedToCloseTimeout),
StartToCloseTimeout: durationpb.New(tc.startToCloseTimeout),
})
if tc.header != nil {
event.GetNexusOperationScheduledEventAttributes().NexusHeader = tc.header
}
backend := &hsmtest.NodeBackend{Events: []*historypb.HistoryEvent{event}}
node := newOperationNode(t, backend, backend.Events[0])
op, err := hsm.MachineData[nexusoperations.Operation](node)
require.NoError(t, err)
_, err = nexusoperations.TransitionStarted.Apply(op, nexusoperations.EventStarted{
Time: time.Now(),
Attributes: &historypb.NexusOperationStartedEventAttributes{
OperationToken: "op-token",
},
Node: node,
})
require.NoError(t, err)
_, err = op.Cancel(node, time.Now(), 0)
require.NoError(t, err)
node, err = node.Child([]hsm.Key{nexusoperations.CancelationMachineKey})
require.NoError(t, err)
env := fakeEnv{node}
namespaceRegistry := namespace.NewMockRegistry(ctrl)
namespaceRegistry.EXPECT().GetNamespaceByID(namespace.ID("ns-id")).Return(
namespace.NewNamespaceForTest(&persistencespb.NamespaceInfo{Name: "ns-name"}, nil, false, nil, 0), nil)
metricsHandler := metrics.NewMockHandler(ctrl)
if tc.expectedMetricOutcome != "" {
counter := metrics.NewMockCounterIface(ctrl)
timer := metrics.NewMockTimerIface(ctrl)
metricsHandler.EXPECT().Counter(chasmnexus.OutboundRequestCounter.Name()).Return(counter)
counter.EXPECT().Record(int64(1),
metrics.NamespaceTag("ns-name"),
metrics.DestinationTag("endpoint"),
metrics.NexusMethodTag("CancelOperation"),
metrics.OutcomeTag(tc.expectedMetricOutcome),
metrics.FailureSourceTag("_unknown_"))
metricsHandler.EXPECT().Timer(chasmnexus.OutboundRequestLatency.Name()).Return(timer)
timer.EXPECT().Record(gomock.Any(),
metrics.NamespaceTag("ns-name"),
metrics.DestinationTag("endpoint"),
metrics.NexusMethodTag("CancelOperation"),
metrics.OutcomeTag(tc.expectedMetricOutcome),
metrics.FailureSourceTag("_unknown_"))
}
endpointReg := nexustest.FakeEndpointRegistry{
OnGetByID: func(ctx context.Context, endpointID string) (*persistencespb.NexusEndpointEntry, error) {
require.Equal(t, "endpoint-id", endpointID)
if tc.endpointNotFound {
return nil, serviceerror.NewNotFound("endpoint not found")
}
return endpointEntry, nil
},
OnGetByName: func(ctx context.Context, namespaceID namespace.ID, endpointName string) (*persistencespb.NexusEndpointEntry, error) {
require.Equal(t, "endpoint", endpointName)
require.Equal(t, "ns-id", namespaceID.String())
if tc.endpointNotFound {
return nil, serviceerror.NewNotFound("endpoint not found")
}
return endpointEntry, nil
},
}
require.NoError(t, nexusoperations.RegisterExecutor(reg, nexusoperations.TaskExecutorOptions{
Config: &nexusoperations.Config{
RequestTimeout: dynamicconfig.GetDurationPropertyFnFilteredByDestination(tc.requestTimeout),
MinRequestTimeout: dynamicconfig.GetDurationPropertyFnFilteredByNamespace(time.Millisecond),
RecordCancelRequestCompletionEvents: dynamicconfig.GetBoolPropertyFn(true),
UseNewFailureWireFormat: dynamicconfig.GetBoolPropertyFnFilteredByNamespace(true),
RetryPolicy: func() backoff.RetryPolicy {
return backoff.NewExponentialRetryPolicy(time.Second)
},
},
NamespaceRegistry: namespaceRegistry,
MetricsHandler: metricsHandler,
Logger: log.NewNoopLogger(),
EndpointRegistry: endpointReg,
ClientProvider: func(ctx context.Context, namespaceID string, entry *persistencespb.NexusEndpointEntry, service string) (*nexusrpc.HTTPClient, error) {
return nexusrpc.NewHTTPClient(nexusrpc.HTTPClientOptions{
BaseURL: "http://" + listenAddr,
Service: service,
Serializer: commonnexus.PayloadSerializer,
})
},
}))
err = reg.ExecuteImmediateTask(
context.Background(),
env,
hsm.Ref{
WorkflowKey: definition.NewWorkflowKey("ns-id", "wf-id", "run-id"),
StateMachineRef: &persistencespb.StateMachineRef{},
},
nexusoperations.CancelationTask{EndpointName: "endpoint-id"},
)
if tc.destinationDown {
var destinationDownErr *queueserrors.DestinationDownError
require.ErrorAs(t, err, &destinationDownErr)
} else {
require.NoError(t, err)
}
cancelation, err := hsm.MachineData[nexusoperations.Cancelation](node)
require.NoError(t, err)
tc.checkOutcome(t, cancelation)
})
}
}
func TestProcessCancelationTask_OperationCompleted(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
reg := newRegistry(t)
backend := &hsmtest.NodeBackend{}
node := newOperationNode(t, backend, mustNewScheduledEvent(time.Now(), &historypb.NexusOperationScheduledEventAttributes{
ScheduleToCloseTimeout: durationpb.New(time.Hour),
}))
op, err := hsm.MachineData[nexusoperations.Operation](node)
require.NoError(t, err)
_, err = nexusoperations.TransitionStarted.Apply(op, nexusoperations.EventStarted{
Time: time.Now(),
Attributes: &historypb.NexusOperationStartedEventAttributes{
OperationToken: "op-token",
},
Node: node,
})
require.NoError(t, err)
_, err = op.Cancel(node, time.Now(), 0)
require.NoError(t, err)
_, err = nexusoperations.TransitionSucceeded.Apply(op, nexusoperations.EventSucceeded{
Node: node,
})
require.NoError(t, err)
node, err = node.Child([]hsm.Key{nexusoperations.CancelationMachineKey})
require.NoError(t, err)
env := fakeEnv{node}
namespaceRegistry := namespace.NewMockRegistry(ctrl)
namespaceRegistry.EXPECT().GetNamespaceByID(namespace.ID("ns-id")).Return(
namespace.NewNamespaceForTest(&persistencespb.NamespaceInfo{Name: "ns-name"}, nil, false, nil, 0), nil)
require.NoError(t, nexusoperations.RegisterExecutor(reg, nexusoperations.TaskExecutorOptions{
Config: &nexusoperations.Config{
RequestTimeout: dynamicconfig.GetDurationPropertyFnFilteredByDestination(time.Hour),
UseNewFailureWireFormat: dynamicconfig.GetBoolPropertyFnFilteredByNamespace(true),
RetryPolicy: func() backoff.RetryPolicy {
return backoff.NewExponentialRetryPolicy(time.Second)
},
},
NamespaceRegistry: namespaceRegistry,
EndpointRegistry: nexustest.FakeEndpointRegistry{
OnGetByID: func(ctx context.Context, endpointID string) (*persistencespb.NexusEndpointEntry, error) {
return endpointEntry, nil
},
},
ClientProvider: func(ctx context.Context, namespaceID string, entry *persistencespb.NexusEndpointEntry, service string) (*nexusrpc.HTTPClient, error) {
return nil, serviceerror.NewInternal("shouldn't get here")
},
}))
err = reg.ExecuteImmediateTask(
context.Background(),
env,
hsm.Ref{
WorkflowKey: definition.NewWorkflowKey("ns-id", "wf-id", "run-id"),
StateMachineRef: &persistencespb.StateMachineRef{},
},
nexusoperations.CancelationTask{EndpointName: "endpoint-name"},
)
require.ErrorIs(t, err, consts.ErrStaleReference)
}
func TestProcessCancelationTask_SystemEndpoint(t *testing.T) {
cases := []struct {
name string
setupHistoryClient func(*testing.T, *gomock.Controller) resource.HistoryClient
setupChasmRegistry func(*testing.T) *chasm.Registry
expectedMetricOutcome string
checkOutcome func(t *testing.T, c nexusoperations.Cancelation)
}{
{
name: "success",
setupHistoryClient: func(t *testing.T, ctrl *gomock.Controller) resource.HistoryClient {
mockHistoryClient := historyservicemock.NewMockHistoryServiceClient(ctrl)
mockHistoryClient.EXPECT().CancelNexusOperation(gomock.Any(), gomock.Any(), gomock.Any()).Return(
&historyservice.CancelNexusOperationResponse{}, nil,
)
return mockHistoryClient
},
setupChasmRegistry: func(t *testing.T) *chasm.Registry {
endpointProcessor := createTestNexusEndpointProcessor(
&chasm.NexusOperationProcessorResult{
RoutingKey: mockNexusOperationRoutingKey{shardID: 1},
},
nil,
)
registry := &chasm.Registry{
NexusEndpointProcessor: endpointProcessor,
}
return registry
},
expectedMetricOutcome: "successful",
checkOutcome: func(t *testing.T, c nexusoperations.Cancelation) {
require.Equal(t, enumspb.NEXUS_OPERATION_CANCELLATION_STATE_SUCCEEDED, c.State())
require.Nil(t, c.LastAttemptFailure)
},
},
{
name: "history service error - retryable",
setupHistoryClient: func(t *testing.T, ctrl *gomock.Controller) resource.HistoryClient {
mockHistoryClient := historyservicemock.NewMockHistoryServiceClient(ctrl)
mockHistoryClient.EXPECT().CancelNexusOperation(gomock.Any(), gomock.Any(), gomock.Any()).Return(
nil, serviceerror.NewUnavailable("service unavailable"),
)
return mockHistoryClient
},
setupChasmRegistry: func(t *testing.T) *chasm.Registry {
endpointProcessor := createTestNexusEndpointProcessor(
&chasm.NexusOperationProcessorResult{
RoutingKey: mockNexusOperationRoutingKey{shardID: 1},
},
nil,
)
registry := &chasm.Registry{
NexusEndpointProcessor: endpointProcessor,
}
return registry
},
expectedMetricOutcome: "service-error:Unavailable",
checkOutcome: func(t *testing.T, c nexusoperations.Cancelation) {
require.Equal(t, enumspb.NEXUS_OPERATION_CANCELLATION_STATE_BACKING_OFF, c.State())
require.NotNil(t, c.LastAttemptFailure.GetServerFailureInfo())
require.Contains(t, c.LastAttemptFailure.Message, "Unavailable")
},
},
{
name: "history service error - InvalidArgument (treated as retryable)",
setupHistoryClient: func(t *testing.T, ctrl *gomock.Controller) resource.HistoryClient {
mockHistoryClient := historyservicemock.NewMockHistoryServiceClient(ctrl)
mockHistoryClient.EXPECT().CancelNexusOperation(gomock.Any(), gomock.Any(), gomock.Any()).Return(
nil, serviceerror.NewInvalidArgument("invalid argument"),
)
return mockHistoryClient
},
setupChasmRegistry: func(t *testing.T) *chasm.Registry {
endpointProcessor := createTestNexusEndpointProcessor(
&chasm.NexusOperationProcessorResult{
RoutingKey: mockNexusOperationRoutingKey{shardID: 1},
},
nil,
)
registry := &chasm.Registry{
NexusEndpointProcessor: endpointProcessor,
}
return registry
},
expectedMetricOutcome: "service-error:InvalidArgument",
checkOutcome: func(t *testing.T, c nexusoperations.Cancelation) {
// Note: Service errors in cancelation path are always treated as retryable
require.Equal(t, enumspb.NEXUS_OPERATION_CANCELLATION_STATE_BACKING_OFF, c.State())
require.NotNil(t, c.LastAttemptFailure.GetServerFailureInfo())
require.Contains(t, c.LastAttemptFailure.Message, "InvalidArgument")
},
},
{
name: "chasm processor error",
setupHistoryClient: func(t *testing.T, ctrl *gomock.Controller) resource.HistoryClient {
// Should not be called if processor fails
return historyservicemock.NewMockHistoryServiceClient(ctrl)
},
setupChasmRegistry: func(t *testing.T) *chasm.Registry {
endpointProcessor := createTestNexusEndpointProcessor(
nil,
errors.New("processor failed"),
)
registry := &chasm.Registry{
NexusEndpointProcessor: endpointProcessor,
}
return registry
},
expectedMetricOutcome: "operation-processor-failed",
checkOutcome: func(t *testing.T, c nexusoperations.Cancelation) {
require.Equal(t, enumspb.NEXUS_OPERATION_CANCELLATION_STATE_BACKING_OFF, c.State())
require.Contains(t, c.LastAttemptFailure.Message, "processor failed")
},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
reg := newRegistry(t)
event := mustNewScheduledEvent(time.Now(), &historypb.NexusOperationScheduledEventAttributes{
ScheduleToCloseTimeout: durationpb.New(time.Hour),
})
event.GetNexusOperationScheduledEventAttributes().Input = mustToPayload(t, testOperationProcessorInput{"test"})
// Set endpoint to SystemEndpoint
event.GetNexusOperationScheduledEventAttributes().Endpoint = commonnexus.SystemEndpoint
backend := &hsmtest.NodeBackend{Events: []*historypb.HistoryEvent{event}}
node := newOperationNode(t, backend, backend.Events[0])
// Transition to STARTED state
op, err := hsm.MachineData[nexusoperations.Operation](node)
require.NoError(t, err)
_, err = nexusoperations.TransitionStarted.Apply(op, nexusoperations.EventStarted{
Time: time.Now(),
Attributes: &historypb.NexusOperationStartedEventAttributes{
OperationToken: "system-op-token",
},
Node: node,
})
require.NoError(t, err)
// Cancel the operation
_, err = op.Cancel(node, time.Now(), 0)
require.NoError(t, err)
node, err = node.Child([]hsm.Key{nexusoperations.CancelationMachineKey})
require.NoError(t, err)
env := fakeEnv{node}
namespaceRegistry := namespace.NewMockRegistry(ctrl)
namespaceRegistry.EXPECT().GetNamespaceByID(namespace.ID("ns-id")).Return(
namespace.NewNamespaceForTest(&persistencespb.NamespaceInfo{Name: "ns-name"}, nil, false, nil, 0), nil)
metricsHandler := metrics.NewMockHandler(ctrl)
if tc.expectedMetricOutcome != "" {
counter := metrics.NewMockCounterIface(ctrl)
timer := metrics.NewMockTimerIface(ctrl)
metricsHandler.EXPECT().Counter(chasmnexus.OutboundRequestCounter.Name()).Return(counter)
counter.EXPECT().Record(int64(1),
metrics.NamespaceTag("ns-name"),
metrics.DestinationTag(commonnexus.SystemEndpoint),
metrics.NexusMethodTag("CancelOperation"),
metrics.OutcomeTag(tc.expectedMetricOutcome),
metrics.FailureSourceTag("_unknown_"))
metricsHandler.EXPECT().Timer(chasmnexus.OutboundRequestLatency.Name()).Return(timer)
timer.EXPECT().Record(gomock.Any(),
metrics.NamespaceTag("ns-name"),
metrics.DestinationTag(commonnexus.SystemEndpoint),
metrics.NexusMethodTag("CancelOperation"),
metrics.OutcomeTag(tc.expectedMetricOutcome),
metrics.FailureSourceTag("_unknown_"))
}
historyClient := tc.setupHistoryClient(t, ctrl)
chasmRegistry := tc.setupChasmRegistry(t)
require.NoError(t, nexusoperations.RegisterExecutor(reg, nexusoperations.TaskExecutorOptions{
Config: &nexusoperations.Config{
RequestTimeout: dynamicconfig.GetDurationPropertyFnFilteredByDestination(time.Hour),
MinRequestTimeout: dynamicconfig.GetDurationPropertyFnFilteredByNamespace(time.Millisecond),
RecordCancelRequestCompletionEvents: dynamicconfig.GetBoolPropertyFn(true),
NumHistoryShards: 4,
RetryPolicy: func() backoff.RetryPolicy {
return backoff.NewExponentialRetryPolicy(time.Second)
},
},
NamespaceRegistry: namespaceRegistry,
MetricsHandler: metricsHandler,
Logger: log.NewNoopLogger(),
HistoryClient: historyClient,
ChasmRegistry: chasmRegistry,
// No EndpointRegistry or ClientProvider needed for system endpoint
}))
err = reg.ExecuteImmediateTask(
context.Background(),
env,
hsm.Ref{
WorkflowKey: definition.NewWorkflowKey("ns-id", "wf-id", "run-id"),
StateMachineRef: &persistencespb.StateMachineRef{},
},
nexusoperations.CancelationTask{EndpointName: commonnexus.SystemEndpoint},
)
var destinationDownErr *queueserrors.DestinationDownError
require.NotErrorAs(t, err, &destinationDownErr)
cancelation, err := hsm.MachineData[nexusoperations.Cancelation](node)
require.NoError(t, err)
tc.checkOutcome(t, cancelation)
})
}
}
func TestProcessCancelationBackoffTask(t *testing.T) {
reg := newRegistry(t)
backend := &hsmtest.NodeBackend{}
node := newOperationNode(t, backend, mustNewScheduledEvent(time.Now(), &historypb.NexusOperationScheduledEventAttributes{
ScheduleToCloseTimeout: durationpb.New(time.Hour),
}))
op, err := hsm.MachineData[nexusoperations.Operation](node)
require.NoError(t, err)
_, err = nexusoperations.TransitionStarted.Apply(op, nexusoperations.EventStarted{
Time: time.Now(),
Attributes: &historypb.NexusOperationStartedEventAttributes{
OperationToken: "op-token",
},
Node: node,
})
require.NoError(t, err)
_, err = op.Cancel(node, time.Now(), 0)
require.NoError(t, err)
node, err = node.Child([]hsm.Key{nexusoperations.CancelationMachineKey})
require.NoError(t, err)
err = hsm.MachineTransition(node, func(c nexusoperations.Cancelation) (hsm.TransitionOutput, error) {
return nexusoperations.TransitionCancelationAttemptFailed.Apply(c, nexusoperations.EventCancelationAttemptFailed{
Time: time.Now(),
Failure: &failurepb.Failure{Message: "test attempt failed"},
Node: node,
RetryPolicy: backoff.NewExponentialRetryPolicy(time.Second),
})
})
require.NoError(t, err)
env := fakeEnv{node}
require.NoError(t, nexusoperations.RegisterExecutor(reg, nexusoperations.TaskExecutorOptions{}))
err = reg.ExecuteTimerTask(
env,
node,
nexusoperations.CancelationBackoffTask{},
)
require.NoError(t, err)
c, err := hsm.MachineData[nexusoperations.Cancelation](node)
require.NoError(t, err)
require.Equal(t, enumspb.NEXUS_OPERATION_CANCELLATION_STATE_SCHEDULED, c.State())
require.Equal(t, 0, len(backend.Events))
}
// mockNexusOperationRoutingKey is a simple mock implementation of the routing key interface
type mockNexusOperationRoutingKey struct {
shardID int32
}
func (m mockNexusOperationRoutingKey) ShardID(numShards int32) int32 {
return m.shardID
}
type testOperationProcessorInput struct {
Value string
}
// testOperationProcessor is a simple operation processor for testing
type testOperationProcessor struct {
result *chasm.NexusOperationProcessorResult
err error
}
func (t *testOperationProcessor) ProcessInput(ctx chasm.NexusOperationProcessorContext, input *testOperationProcessorInput) (*chasm.NexusOperationProcessorResult, error) {
// Mutate the input to verify that the mutated input is passed to the history service.
input.Value = "processed:" + input.Value
return t.result, t.err
}
// createTestNexusEndpointProcessor creates a NexusEndpointProcessor with a mock operation for testing
func createTestNexusEndpointProcessor(result *chasm.NexusOperationProcessorResult, err error) *chasm.NexusEndpointProcessor {
processor := chasm.NewNexusEndpointProcessor()
serviceProcessor := chasm.NewNexusServiceProcessor("service")
testOp := &testOperationProcessor{
result: result,
err: err,
}
serviceProcessor.MustRegisterOperation("operation", chasm.NewRegisterableNexusOperationProcessor(testOp))
processor.MustRegisterServiceProcessor(serviceProcessor)
return processor
}
// TestProcessInvocationTask_SystemEndpoint tests the system endpoint execution path
func TestProcessInvocationTask_SystemEndpoint(t *testing.T) {
handlerLink := &commonpb.Link_WorkflowEvent{
Namespace: "handler-ns",
WorkflowId: "handler-wf-id",
RunId: "handler-run-id",
Reference: &commonpb.Link_WorkflowEvent_EventRef{
EventRef: &commonpb.Link_WorkflowEvent_EventReference{
EventType: enumspb.EVENT_TYPE_WORKFLOW_EXECUTION_STARTED,
},
},
}
cases := []struct {
name string
setupHistoryClient func(*testing.T, *gomock.Controller) resource.HistoryClient
setupChasmRegistry func(*testing.T) *chasm.Registry
expectedMetricOutcome string
checkOutcome func(t *testing.T, op nexusoperations.Operation, events []*historypb.HistoryEvent)
}{
{
name: "async start",
setupHistoryClient: func(t *testing.T, ctrl *gomock.Controller) resource.HistoryClient {
mockHistoryClient := historyservicemock.NewMockHistoryServiceClient(ctrl)
mockHistoryClient.EXPECT().StartNexusOperation(gomock.Any(), gomock.Any(), gomock.Any()).Return(
&historyservice.StartNexusOperationResponse{
Response: &nexuspb.StartOperationResponse{
Variant: &nexuspb.StartOperationResponse_AsyncSuccess{
AsyncSuccess: &nexuspb.StartOperationResponse_Async{
OperationToken: "system-op-token",
Links: commonnexus.ConvertLinksToProto([]nexus.Link{
commonnexus.ConvertLinkWorkflowEventToNexusLink(handlerLink),
}),
},
},
},
}, nil,
)
return mockHistoryClient
},
setupChasmRegistry: func(t *testing.T) *chasm.Registry {
endpointProcessor := createTestNexusEndpointProcessor(
&chasm.NexusOperationProcessorResult{
RoutingKey: mockNexusOperationRoutingKey{shardID: 1},
},
nil,
)
registry := &chasm.Registry{
NexusEndpointProcessor: endpointProcessor,
}
return registry
},
expectedMetricOutcome: "pending",
checkOutcome: func(t *testing.T, op nexusoperations.Operation, events []*historypb.HistoryEvent) {
require.Equal(t, enumsspb.NEXUS_OPERATION_STATE_STARTED, op.State())
require.Len(t, events, 1)
require.Equal(t, enumspb.EVENT_TYPE_NEXUS_OPERATION_STARTED, events[0].EventType)
protorequire.ProtoEqual(t, &historypb.NexusOperationStartedEventAttributes{
ScheduledEventId: 1,
OperationToken: "system-op-token",
OperationId: "system-op-token",
RequestId: op.RequestId,
}, events[0].GetNexusOperationStartedEventAttributes())
require.Len(t, events[0].Links, 1)
protorequire.ProtoEqual(t, handlerLink, events[0].Links[0].GetWorkflowEvent())
},
},
{
name: "sync start",
setupHistoryClient: func(t *testing.T, ctrl *gomock.Controller) resource.HistoryClient {
mockHistoryClient := historyservicemock.NewMockHistoryServiceClient(ctrl)
mockHistoryClient.EXPECT().StartNexusOperation(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(func(_ context.Context, request *historyservice.StartNexusOperationRequest, opts ...grpc.CallOption) (*historyservice.StartNexusOperationResponse, error) {
var input testOperationProcessorInput
if err := payloads.Decode(&commonpb.Payloads{Payloads: []*commonpb.Payload{request.Request.Payload}}, &input); err != nil {
return nil, err
}
if input.Value != "processed:test" {
return nil, fmt.Errorf("unexpected input: %v", input.Value)
}
return &historyservice.StartNexusOperationResponse{
Response: &nexuspb.StartOperationResponse{
Variant: &nexuspb.StartOperationResponse_SyncSuccess{
SyncSuccess: &nexuspb.StartOperationResponse_Sync{
Payload: mustToPayload(t, "result"),
},
},
},
}, nil
})
return mockHistoryClient
},
setupChasmRegistry: func(t *testing.T) *chasm.Registry {
endpointProcessor := createTestNexusEndpointProcessor(
&chasm.NexusOperationProcessorResult{
RoutingKey: mockNexusOperationRoutingKey{shardID: 1},
},
nil,
)
registry := &chasm.Registry{
NexusEndpointProcessor: endpointProcessor,
}
return registry
},
expectedMetricOutcome: "successful",
checkOutcome: func(t *testing.T, op nexusoperations.Operation, events []*historypb.HistoryEvent) {
require.Equal(t, enumsspb.NEXUS_OPERATION_STATE_SUCCEEDED, op.State())
require.Len(t, events, 1)
require.Equal(t, enumspb.EVENT_TYPE_NEXUS_OPERATION_COMPLETED, events[0].EventType)
protorequire.ProtoEqual(t, &historypb.NexusOperationCompletedEventAttributes{
ScheduledEventId: 1,
Result: mustToPayload(t, "result"),
RequestId: op.RequestId,
}, events[0].GetNexusOperationCompletedEventAttributes())
},
},
{
name: "operation error",
setupHistoryClient: func(t *testing.T, ctrl *gomock.Controller) resource.HistoryClient {
mockHistoryClient := historyservicemock.NewMockHistoryServiceClient(ctrl)
mockHistoryClient.EXPECT().StartNexusOperation(gomock.Any(), gomock.Any(), gomock.Any()).Return(
&historyservice.StartNexusOperationResponse{
Response: &nexuspb.StartOperationResponse{
Variant: &nexuspb.StartOperationResponse_Failure{
Failure: &failurepb.Failure{
Message: "operation failed",
FailureInfo: &failurepb.Failure_ApplicationFailureInfo{
ApplicationFailureInfo: &failurepb.ApplicationFailureInfo{},
},
},
},
},
}, nil,
)
return mockHistoryClient
},
setupChasmRegistry: func(t *testing.T) *chasm.Registry {
endpointProcessor := createTestNexusEndpointProcessor(
&chasm.NexusOperationProcessorResult{
RoutingKey: mockNexusOperationRoutingKey{shardID: 1},
},
nil,
)
registry := &chasm.Registry{
NexusEndpointProcessor: endpointProcessor,
}
return registry
},
expectedMetricOutcome: "operation-unsuccessful:failed",
checkOutcome: func(t *testing.T, op nexusoperations.Operation, events []*historypb.HistoryEvent) {
require.Equal(t, enumsspb.NEXUS_OPERATION_STATE_FAILED, op.State())
require.Len(t, events, 1)
require.Equal(t, enumspb.EVENT_TYPE_NEXUS_OPERATION_FAILED, events[0].EventType)
attrs := events[0].GetNexusOperationFailedEventAttributes()
require.Equal(t, int64(1), attrs.ScheduledEventId)
require.Equal(t, "nexus operation completed unsuccessfully", attrs.Failure.Message)
},
},
{
name: "history service error - retryable",
setupHistoryClient: func(t *testing.T, ctrl *gomock.Controller) resource.HistoryClient {
mockHistoryClient := historyservicemock.NewMockHistoryServiceClient(ctrl)
mockHistoryClient.EXPECT().StartNexusOperation(gomock.Any(), gomock.Any(), gomock.Any()).Return(
nil, serviceerror.NewUnavailable("service unavailable"),
)
return mockHistoryClient
},
setupChasmRegistry: func(t *testing.T) *chasm.Registry {
endpointProcessor := createTestNexusEndpointProcessor(
&chasm.NexusOperationProcessorResult{
RoutingKey: mockNexusOperationRoutingKey{shardID: 1},
},
nil,
)
registry := &chasm.Registry{
NexusEndpointProcessor: endpointProcessor,
}
return registry
},
expectedMetricOutcome: "service-error:Unavailable",
checkOutcome: func(t *testing.T, op nexusoperations.Operation, events []*historypb.HistoryEvent) {
require.Equal(t, enumsspb.NEXUS_OPERATION_STATE_BACKING_OFF, op.State())
require.NotNil(t, op.LastAttemptFailure.GetServerFailureInfo())
require.Contains(t, op.LastAttemptFailure.Message, "Unavailable")
require.Empty(t, events)
},
},
{
name: "chasm processor error",
setupHistoryClient: func(t *testing.T, ctrl *gomock.Controller) resource.HistoryClient {
// Should not be called if processor fails
return historyservicemock.NewMockHistoryServiceClient(ctrl)
},
setupChasmRegistry: func(t *testing.T) *chasm.Registry {
endpointProcessor := createTestNexusEndpointProcessor(
nil,
errors.New("processor failed"),
)
registry := &chasm.Registry{
NexusEndpointProcessor: endpointProcessor,
}
return registry
},
expectedMetricOutcome: "operation-processor-failed",
checkOutcome: func(t *testing.T, op nexusoperations.Operation, events []*historypb.HistoryEvent) {
require.Equal(t, enumsspb.NEXUS_OPERATION_STATE_BACKING_OFF, op.State())
require.Contains(t, op.LastAttemptFailure.Message, "processor failed")
require.Empty(t, events)
},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
reg := newRegistry(t)
event := mustNewScheduledEvent(time.Now(), &historypb.NexusOperationScheduledEventAttributes{
ScheduleToCloseTimeout: durationpb.New(time.Hour),
})
event.GetNexusOperationScheduledEventAttributes().Input = mustToPayload(t, testOperationProcessorInput{"test"})
// Set endpoint to SystemEndpoint
event.GetNexusOperationScheduledEventAttributes().Endpoint = commonnexus.SystemEndpoint
backend := &hsmtest.NodeBackend{Events: []*historypb.HistoryEvent{event}}
node := newOperationNode(t, backend, backend.Events[0])
env := fakeEnv{node}
namespaceRegistry := namespace.NewMockRegistry(ctrl)
namespaceRegistry.EXPECT().GetNamespaceByID(namespace.ID("ns-id")).Return(
namespace.NewNamespaceForTest(&persistencespb.NamespaceInfo{Name: "ns-name"}, nil, false, nil, 0), nil)
metricsHandler := metrics.NewMockHandler(ctrl)
if tc.expectedMetricOutcome != "" {
counter := metrics.NewMockCounterIface(ctrl)
timer := metrics.NewMockTimerIface(ctrl)
metricsHandler.EXPECT().Counter(chasmnexus.OutboundRequestCounter.Name()).Return(counter)
counter.EXPECT().Record(int64(1),
metrics.NamespaceTag("ns-name"),
metrics.DestinationTag(commonnexus.SystemEndpoint),
metrics.NexusMethodTag("StartOperation"),
metrics.OutcomeTag(tc.expectedMetricOutcome),
metrics.FailureSourceTag("_unknown_"))
metricsHandler.EXPECT().Timer(chasmnexus.OutboundRequestLatency.Name()).Return(timer)
timer.EXPECT().Record(gomock.Any(),
metrics.NamespaceTag("ns-name"),
metrics.DestinationTag(commonnexus.SystemEndpoint),
metrics.NexusMethodTag("StartOperation"),
metrics.OutcomeTag(tc.expectedMetricOutcome),
metrics.FailureSourceTag("_unknown_"))
}
historyClient := tc.setupHistoryClient(t, ctrl)
chasmRegistry := tc.setupChasmRegistry(t)
require.NoError(t, nexusoperations.RegisterExecutor(reg, nexusoperations.TaskExecutorOptions{
Config: &nexusoperations.Config{
RequestTimeout: dynamicconfig.GetDurationPropertyFnFilteredByDestination(time.Hour),
MinRequestTimeout: dynamicconfig.GetDurationPropertyFnFilteredByNamespace(time.Millisecond),
PayloadSizeLimit: dynamicconfig.GetIntPropertyFnFilteredByNamespace(2 * 1024 * 1024),
MaxOperationTokenLength: dynamicconfig.GetIntPropertyFnFilteredByNamespace(1000),
CallbackURLTemplate: dynamicconfig.GetStringPropertyFn("http://localhost/callback"),
UseSystemCallbackURL: dynamicconfig.GetBoolPropertyFn(true),
UseNewFailureWireFormat: dynamicconfig.GetBoolPropertyFnFilteredByNamespace(false),
NumHistoryShards: 4,
RetryPolicy: func() backoff.RetryPolicy {
return backoff.NewExponentialRetryPolicy(time.Second)
},
},
CallbackTokenGenerator: commonnexus.NewCallbackTokenGenerator(),
NamespaceRegistry: namespaceRegistry,
MetricsHandler: metricsHandler,
Logger: log.NewNoopLogger(),
HistoryClient: historyClient,
ChasmRegistry: chasmRegistry,
// No EndpointRegistry or ClientProvider needed for system endpoint
}))
err := reg.ExecuteImmediateTask(
context.Background(),
env,
hsm.Ref{
WorkflowKey: definition.NewWorkflowKey("ns-id", "wf-id", "run-id"),
StateMachineRef: &persistencespb.StateMachineRef{},
},
nexusoperations.InvocationTask{EndpointName: commonnexus.SystemEndpoint},
)
// For chasm processor error, expect a destination down error
var destinationDownErr *queueserrors.DestinationDownError
require.NotErrorAs(t, err, &destinationDownErr)
op, err := hsm.MachineData[nexusoperations.Operation](node)
require.NoError(t, err)
tc.checkOutcome(t, op, backend.Events[1:]) // Ignore the original scheduled event.
})
}
}