mirror of
https://github.com/hanzoai/tasks.git
synced 2026-10-04 02:04:03 +00:00
## What changed? This PR adds `SignalWithStartWorkflowExecution` as a synchronous Nexus operation exposed via `__temporal_system endpoint`, allowing workflows to signal-with-start other workflows through the CHASM Nexus operation framework. Key changes: - `chasm/lib/workflow/nexus_service.go`: `workflowServiceNexusHandler` implements the `SignalWithStartWorkflowExecution` Nexus sync operation by resolving the namespace and delegating to the History service. A `SignalWithStartOperationProcessor` handles input enrichment (namespace, request ID, links) and routing via CHASM's `NexusOperationProcessorResult`. - `chasm/lib/workflow/library.go` — library now holds the `workflowServiceNexusHandler`, the workflow `Config`, the SA mapper provider, and the SA validator. `newLibrary` (used by fx) takes those dependencies, while the public `NewLibrary` keeps its old signature for external callers. Adds `NexusServices()` so the library registers its Nexus service via CHASM. - `chasm/lib/workflow/validator.go`: `RequestValidator` consolidates the `SignalWithStartWorkflowExecution` validation logic (previously inlined in `WorkflowHandler`) into a reusable, injectable struct. This same validator is used by both the frontend handler and the new CHASM processor. - common/dynamicconfig/constants.go — adds `EnableSignalWithStartFromWorkflow` (namespace-scoped, default false). - `service/frontend/workflow_handler.go`: Removed the `SignalWithStart` validation block - `service/history/fx.go`: Provides a `HistoryServiceServerProvider` so the CHASM workflow library can call the history handler directly. - `temporal/fx.go`: Removes the now-redundant `ChasmLibraryOptions` grouping; each service module registers its own CHASM libraries. - `components/nexusoperations/workflow/commands.go`: `NotFound` and `InvalidArgument` errors during Nexus command handling are now surfaced as workflow task failures instead of being treated as transient handler errors. - `common/payloads`: Adds `EncodeSingle`, `MustEncodeSingle`, and `MustEncode` helpers used in tests. - `cmd/tools/getproto`: Adds support for nexus-proto-annotations proto imports. - `tests/signal_with_start_from_workflow_test.go`: Functional test suite covering the happy path, duplicate detection, conflict policies, and validation rejection for the new Nexus operation. ## Why? This functionality is one of our most requested GitHub issues. ## How did you test it? - [X] built - [X] run locally and tested manually - [ ] covered by existing tests - [X] added new unit test(s) - [X] added new functional test(s) ## Potential risks The history service now directly exposes a `HistoryServiceServer` interface via `fx` for injection into the CHASM workflow library. This tight coupling between the CHASM workflow library and the history handler could complicate future layering — callers outside the history service should not adopt this pattern. The feature is gated by `history.enableSignalWithStartFromWorkflow` for rollout. --------- Co-authored-by: Roey Berman <[email protected]>
157 lines
5.6 KiB
Go
157 lines
5.6 KiB
Go
package workflow
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
commonpb "go.temporal.io/api/common/v1"
|
|
enumspb "go.temporal.io/api/enums/v1"
|
|
taskqueuepb "go.temporal.io/api/taskqueue/v1"
|
|
"go.temporal.io/api/workflowservice/v1"
|
|
"go.temporal.io/server/common/dynamicconfig"
|
|
"go.temporal.io/server/common/retrypolicy"
|
|
"go.temporal.io/server/common/searchattribute"
|
|
"google.golang.org/protobuf/types/known/durationpb"
|
|
)
|
|
|
|
const testMaxIDLen = 1000
|
|
|
|
func newTestValidator() *RequestValidator {
|
|
saValidator := searchattribute.NewValidator(
|
|
searchattribute.NewTestProvider(),
|
|
searchattribute.NewTestMapperProvider(nil),
|
|
dynamicconfig.GetIntPropertyFnFilteredByNamespace(100),
|
|
dynamicconfig.GetIntPropertyFnFilteredByNamespace(1024),
|
|
dynamicconfig.GetIntPropertyFnFilteredByNamespace(4096),
|
|
nil, // visibility manager not needed when SA is nil
|
|
dynamicconfig.GetBoolPropertyFnFilteredByNamespace(true),
|
|
dynamicconfig.GetBoolPropertyFnFilteredByNamespace(false),
|
|
)
|
|
return NewValidator(
|
|
Config{
|
|
maxIDLengthLimit: func() int { return testMaxIDLen },
|
|
defaultWorkflowRetrySettings: func(ns string) retrypolicy.DefaultRetrySettings {
|
|
return retrypolicy.DefaultDefaultRetrySettings
|
|
},
|
|
maxLinksPerRequest: func(ns string) int { return 10 },
|
|
linkMaxSize: func(ns string) int { return 1024 },
|
|
},
|
|
searchattribute.NewTestMapperProvider(nil),
|
|
saValidator,
|
|
)
|
|
}
|
|
|
|
func validSWSRequest() *workflowservice.SignalWithStartWorkflowExecutionRequest {
|
|
return &workflowservice.SignalWithStartWorkflowExecutionRequest{
|
|
WorkflowId: "test-workflow-id",
|
|
SignalName: "test-signal",
|
|
WorkflowType: &commonpb.WorkflowType{Name: "test-workflow-type"},
|
|
TaskQueue: &taskqueuepb.TaskQueue{Name: "test-task-queue"},
|
|
}
|
|
}
|
|
|
|
func TestValidateSignalWithStartRequest(t *testing.T) {
|
|
v := newTestValidator()
|
|
|
|
t.Run("HappyPath", func(t *testing.T) {
|
|
req := validSWSRequest()
|
|
err := v.ValidateSignalWithStartRequest(req)
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("NilRequest", func(t *testing.T) {
|
|
err := v.ValidateSignalWithStartRequest(nil)
|
|
require.ErrorContains(t, err, "request is empty")
|
|
})
|
|
|
|
t.Run("EmptyWorkflowID", func(t *testing.T) {
|
|
req := validSWSRequest()
|
|
req.WorkflowId = ""
|
|
err := v.ValidateSignalWithStartRequest(req)
|
|
require.ErrorIs(t, err, ErrWorkflowIDNotSet)
|
|
})
|
|
|
|
t.Run("WorkflowIDTooLong", func(t *testing.T) {
|
|
req := validSWSRequest()
|
|
req.WorkflowId = strings.Repeat("a", testMaxIDLen+1)
|
|
err := v.ValidateSignalWithStartRequest(req)
|
|
require.ErrorContains(t, err, "WorkflowId exceeds maximum allowed length")
|
|
})
|
|
|
|
t.Run("EmptySignalName", func(t *testing.T) {
|
|
req := validSWSRequest()
|
|
req.SignalName = ""
|
|
err := v.ValidateSignalWithStartRequest(req)
|
|
require.ErrorContains(t, err, "signal not set")
|
|
})
|
|
|
|
t.Run("SignalNameTooLong", func(t *testing.T) {
|
|
req := validSWSRequest()
|
|
req.SignalName = strings.Repeat("s", testMaxIDLen+1)
|
|
err := v.ValidateSignalWithStartRequest(req)
|
|
require.ErrorContains(t, err, "signal name exceeds maximum allowed length")
|
|
})
|
|
|
|
t.Run("EmptyWorkflowType", func(t *testing.T) {
|
|
req := validSWSRequest()
|
|
req.WorkflowType = &commonpb.WorkflowType{Name: ""}
|
|
err := v.ValidateSignalWithStartRequest(req)
|
|
require.ErrorContains(t, err, "workflow type not set")
|
|
})
|
|
|
|
t.Run("WorkflowTypeTooLong", func(t *testing.T) {
|
|
req := validSWSRequest()
|
|
req.WorkflowType = &commonpb.WorkflowType{Name: strings.Repeat("t", testMaxIDLen+1)}
|
|
err := v.ValidateSignalWithStartRequest(req)
|
|
require.ErrorContains(t, err, "workflow type name exceeds maximum allowed length")
|
|
})
|
|
|
|
t.Run("AutoGeneratesRequestID", func(t *testing.T) {
|
|
req := validSWSRequest()
|
|
req.RequestId = ""
|
|
err := v.ValidateSignalWithStartRequest(req)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, req.RequestId, "empty RequestId should be auto-populated")
|
|
})
|
|
|
|
t.Run("RequestIDTooLong", func(t *testing.T) {
|
|
req := validSWSRequest()
|
|
req.RequestId = strings.Repeat("r", testMaxIDLen+1)
|
|
err := v.ValidateSignalWithStartRequest(req)
|
|
require.ErrorContains(t, err, "Request ID exceeds maximum allowed length")
|
|
})
|
|
|
|
t.Run("ConflictPolicyFailNotSupported", func(t *testing.T) {
|
|
req := validSWSRequest()
|
|
req.WorkflowIdConflictPolicy = enumspb.WORKFLOW_ID_CONFLICT_POLICY_FAIL
|
|
err := v.ValidateSignalWithStartRequest(req)
|
|
require.ErrorContains(t, err, "WORKFLOW_ID_CONFLICT_POLICY_FAIL is not supported")
|
|
})
|
|
|
|
t.Run("IncompatibleTerminateIfRunningWithConflictPolicy", func(t *testing.T) {
|
|
req := validSWSRequest()
|
|
req.WorkflowIdReusePolicy = enumspb.WORKFLOW_ID_REUSE_POLICY_TERMINATE_IF_RUNNING //nolint:staticcheck // SA1019: testing backwards-compatible validation path
|
|
req.WorkflowIdConflictPolicy = enumspb.WORKFLOW_ID_CONFLICT_POLICY_USE_EXISTING
|
|
err := v.ValidateSignalWithStartRequest(req)
|
|
require.ErrorContains(t, err, "WORKFLOW_ID_REUSE_POLICY_TERMINATE_IF_RUNNING cannot be used together with a WorkflowIDConflictPolicy")
|
|
})
|
|
|
|
t.Run("IncompatibleRejectDuplicateWithTerminateExisting", func(t *testing.T) {
|
|
req := validSWSRequest()
|
|
req.WorkflowIdReusePolicy = enumspb.WORKFLOW_ID_REUSE_POLICY_REJECT_DUPLICATE
|
|
req.WorkflowIdConflictPolicy = enumspb.WORKFLOW_ID_CONFLICT_POLICY_TERMINATE_EXISTING
|
|
err := v.ValidateSignalWithStartRequest(req)
|
|
require.ErrorContains(t, err, "WORKFLOW_ID_REUSE_POLICY_REJECT_DUPLICATE cannot be used together with WorkflowIdConflictPolicy WORKFLOW_ID_CONFLICT_POLICY_TERMINATE_EXISTING")
|
|
})
|
|
|
|
t.Run("CronAndStartDelaySetTogether", func(t *testing.T) {
|
|
req := validSWSRequest()
|
|
req.CronSchedule = "0 * * * *"
|
|
req.WorkflowStartDelay = durationpb.New(5 * time.Second)
|
|
err := v.ValidateSignalWithStartRequest(req)
|
|
require.ErrorIs(t, err, ErrCronAndStartDelaySet)
|
|
})
|
|
}
|