Files
tasks/components/nexusoperations/workflow/commands.go
zeekay 5a601d081b rebrand: go.temporal.io/server -> github.com/hanzoai/tasks
Full module path rebrand from Temporal to Hanzo Tasks:
- go.mod module: github.com/hanzoai/tasks
- All internal Go imports updated (1906 files)
- cmd/server renamed to cmd/tasksd
- Binary name: tasksd (was temporal-server)
- Docker images: hanzoai/tasks (was temporalio/server)
- README rewritten with Hanzo Tasks branding
- LLM.md created with integration notes
- Makefile, goreleaser, CI workflows updated
- go.temporal.io/sdk and go.temporal.io/api imports preserved (separate repos)

Fork of Temporal.io (MIT license).
2026-03-19 10:36:40 -07:00

347 lines
14 KiB
Go

package workflow
import (
"context"
"errors"
"fmt"
"slices"
"strconv"
"strings"
"github.com/google/uuid"
"github.com/nexus-rpc/sdk-go/nexus"
commandpb "go.temporal.io/api/command/v1"
enumspb "go.temporal.io/api/enums/v1"
historypb "go.temporal.io/api/history/v1"
"go.temporal.io/api/serviceerror"
"github.com/hanzoai/tasks/chasm"
commonnexus "github.com/hanzoai/tasks/common/nexus"
"github.com/hanzoai/tasks/common/primitives/timestamp"
"github.com/hanzoai/tasks/components/nexusoperations"
"github.com/hanzoai/tasks/service/history/hsm"
historyi "github.com/hanzoai/tasks/service/history/interfaces"
"github.com/hanzoai/tasks/service/history/workflow"
"google.golang.org/protobuf/types/known/durationpb"
)
type commandHandler struct {
config *nexusoperations.Config
endpointRegistry commonnexus.EndpointRegistry
nexusProcessor *chasm.NexusEndpointProcessor
}
func (ch *commandHandler) HandleScheduleCommand(
ctx context.Context,
ms historyi.MutableState,
validator workflow.CommandValidator,
workflowTaskCompletedEventID int64,
command *commandpb.Command,
) error {
ns := ms.GetNamespaceEntry()
nsName := ms.GetNamespaceEntry().Name().String()
attrs := command.GetScheduleNexusOperationCommandAttributes()
if attrs == nil {
return workflow.FailWorkflowTaskError{
Cause: enumspb.WORKFLOW_TASK_FAILED_CAUSE_BAD_SCHEDULE_NEXUS_OPERATION_ATTRIBUTES,
Message: "empty ScheduleNexusOperationCommandAttributes",
}
}
requestID := uuid.NewString()
var endpointID string
// Skip endpoint registry lookup for __temporal_system endpoint
if attrs.Endpoint == commonnexus.SystemEndpoint {
if len(attrs.NexusHeader) > 0 {
return workflow.FailWorkflowTaskError{
Cause: enumspb.WORKFLOW_TASK_FAILED_CAUSE_BAD_SCHEDULE_NEXUS_OPERATION_ATTRIBUTES,
Message: fmt.Sprintf("ScheduleNexusOperationCommandAttributes.NexusHeader must be empty when using %s endpoint", commonnexus.SystemEndpoint),
}
}
// Run ProcessInput for validation.
_, err := ch.nexusProcessor.ProcessInput(chasm.NexusOperationProcessorContext{
Namespace: ns,
RequestID: requestID,
// Links are not needed for validation.
}, attrs.Service, attrs.Operation, attrs.Input)
if err != nil {
var handlerErr *nexus.HandlerError
if errors.As(err, &handlerErr) {
// nolint:exhaustive
switch handlerErr.Type {
case nexus.HandlerErrorTypeNotFound, nexus.HandlerErrorTypeBadRequest:
return workflow.FailWorkflowTaskError{
Cause: enumspb.WORKFLOW_TASK_FAILED_CAUSE_BAD_SCHEDULE_NEXUS_OPERATION_ATTRIBUTES,
Message: handlerErr.Message,
}
}
}
return err
}
} else {
endpoint, err := ch.endpointRegistry.GetByName(ctx, ns.ID(), attrs.Endpoint)
if err != nil {
if errors.As(err, new(*serviceerror.NotFound)) {
return workflow.FailWorkflowTaskError{
Cause: enumspb.WORKFLOW_TASK_FAILED_CAUSE_BAD_SCHEDULE_NEXUS_OPERATION_ATTRIBUTES,
Message: fmt.Sprintf("endpoint %q not found", attrs.Endpoint),
}
} else if errors.As(err, new(*serviceerror.PermissionDenied)) {
return workflow.FailWorkflowTaskError{
Cause: enumspb.WORKFLOW_TASK_FAILED_CAUSE_BAD_SCHEDULE_NEXUS_OPERATION_ATTRIBUTES,
Message: fmt.Sprintf("caller namespace %q unauthorized for %q", ns.Name(), attrs.Endpoint),
}
}
return err
}
endpointID = endpoint.Id
}
if len(attrs.Service) > ch.config.MaxServiceNameLength(nsName) {
return workflow.FailWorkflowTaskError{
Cause: enumspb.WORKFLOW_TASK_FAILED_CAUSE_BAD_SCHEDULE_NEXUS_OPERATION_ATTRIBUTES,
Message: fmt.Sprintf(
"ScheduleNexusOperationCommandAttributes.Service exceeds length limit of %d",
ch.config.MaxServiceNameLength(nsName),
),
}
}
if len(attrs.Operation) > ch.config.MaxOperationNameLength(nsName) {
return workflow.FailWorkflowTaskError{
Cause: enumspb.WORKFLOW_TASK_FAILED_CAUSE_BAD_SCHEDULE_NEXUS_OPERATION_ATTRIBUTES,
Message: fmt.Sprintf(
"ScheduleNexusOperationCommandAttributes.Operation exceeds length limit of %d",
ch.config.MaxOperationNameLength(nsName),
),
}
}
if err := timestamp.ValidateAndCapProtoDuration(attrs.ScheduleToCloseTimeout); err != nil {
return workflow.FailWorkflowTaskError{
Cause: enumspb.WORKFLOW_TASK_FAILED_CAUSE_BAD_SCHEDULE_NEXUS_OPERATION_ATTRIBUTES,
Message: fmt.Sprintf(
"ScheduleNexusOperationCommandAttributes.ScheduleToCloseTimeout is invalid: %v", err),
}
}
if err := timestamp.ValidateAndCapProtoDuration(attrs.ScheduleToStartTimeout); err != nil {
return workflow.FailWorkflowTaskError{
Cause: enumspb.WORKFLOW_TASK_FAILED_CAUSE_BAD_SCHEDULE_NEXUS_OPERATION_ATTRIBUTES,
Message: fmt.Sprintf(
"ScheduleNexusOperationCommandAttributes.ScheduleToStartTimeout is invalid: %v", err),
}
}
if err := timestamp.ValidateAndCapProtoDuration(attrs.StartToCloseTimeout); err != nil {
return workflow.FailWorkflowTaskError{
Cause: enumspb.WORKFLOW_TASK_FAILED_CAUSE_BAD_SCHEDULE_NEXUS_OPERATION_ATTRIBUTES,
Message: fmt.Sprintf(
"ScheduleNexusOperationCommandAttributes.StartToCloseTimeout is invalid: %v", err),
}
}
if attrs.Endpoint != commonnexus.SystemEndpoint && !validator.IsValidPayloadSize(attrs.Input.Size()) {
return workflow.FailWorkflowTaskError{
Cause: enumspb.WORKFLOW_TASK_FAILED_CAUSE_BAD_SCHEDULE_NEXUS_OPERATION_ATTRIBUTES,
Message: "ScheduleNexusOperationCommandAttributes.Input exceeds size limit",
TerminateWorkflow: true,
}
}
headerLength := 0
lowerCaseHeader := make(map[string]string, len(attrs.NexusHeader))
for k, v := range attrs.NexusHeader {
lowerK := strings.ToLower(k)
lowerCaseHeader[lowerK] = v
headerLength += len(lowerK) + len(v)
if slices.Contains(ch.config.DisallowedOperationHeaders(), lowerK) {
return workflow.FailWorkflowTaskError{
Cause: enumspb.WORKFLOW_TASK_FAILED_CAUSE_BAD_SCHEDULE_NEXUS_OPERATION_ATTRIBUTES,
Message: fmt.Sprintf("ScheduleNexusOperationCommandAttributes.NexusHeader contains a disallowed header key: %q", k),
}
}
}
if headerLength > ch.config.MaxOperationHeaderSize(nsName) {
return workflow.FailWorkflowTaskError{
Cause: enumspb.WORKFLOW_TASK_FAILED_CAUSE_BAD_SCHEDULE_NEXUS_OPERATION_ATTRIBUTES,
Message: "ScheduleNexusOperationCommandAttributes.NexusHeader exceeds size limit",
}
}
root := ms.HSM()
coll := nexusoperations.MachineCollection(root)
maxPendingOperations := ch.config.MaxConcurrentOperations(nsName)
if coll.Size() >= ch.config.MaxConcurrentOperations(nsName) {
return workflow.FailWorkflowTaskError{
Cause: enumspb.WORKFLOW_TASK_FAILED_CAUSE_PENDING_NEXUS_OPERATIONS_LIMIT_EXCEEDED,
Message: fmt.Sprintf("workflow has reached the pending nexus operation limit of %d for this namespace", maxPendingOperations),
}
}
// Trim timeout to workflow run timeout.
runTimeout := ms.GetExecutionInfo().WorkflowRunTimeout.AsDuration()
opTimeout := attrs.ScheduleToCloseTimeout.AsDuration()
if runTimeout > 0 && (opTimeout == 0 || opTimeout > runTimeout) {
attrs.ScheduleToCloseTimeout = ms.GetExecutionInfo().WorkflowRunTimeout
opTimeout = attrs.ScheduleToCloseTimeout.AsDuration()
}
// Trim timeout to max allowed timeout.
if maxTimeout := ch.config.MaxOperationScheduleToCloseTimeout(nsName); maxTimeout > 0 && opTimeout > maxTimeout {
attrs.ScheduleToCloseTimeout = durationpb.New(maxTimeout)
}
// Trim secondary timeouts to the primary timeout.
scheduleToCloseTimeout := attrs.ScheduleToCloseTimeout.AsDuration()
scheduleToStartTimeout := attrs.ScheduleToStartTimeout.AsDuration()
startToCloseTimeout := attrs.StartToCloseTimeout.AsDuration()
if scheduleToCloseTimeout > 0 {
if scheduleToStartTimeout > scheduleToCloseTimeout {
attrs.ScheduleToStartTimeout = attrs.ScheduleToCloseTimeout
}
if startToCloseTimeout > scheduleToCloseTimeout {
attrs.StartToCloseTimeout = attrs.ScheduleToCloseTimeout
}
}
event := ms.AddHistoryEvent(enumspb.EVENT_TYPE_NEXUS_OPERATION_SCHEDULED, func(he *historypb.HistoryEvent) {
he.Attributes = &historypb.HistoryEvent_NexusOperationScheduledEventAttributes{
NexusOperationScheduledEventAttributes: &historypb.NexusOperationScheduledEventAttributes{
Endpoint: attrs.Endpoint,
EndpointId: endpointID,
Service: attrs.Service,
Operation: attrs.Operation,
Input: attrs.Input,
ScheduleToCloseTimeout: attrs.ScheduleToCloseTimeout,
ScheduleToStartTimeout: attrs.ScheduleToStartTimeout,
StartToCloseTimeout: attrs.StartToCloseTimeout,
NexusHeader: lowerCaseHeader,
RequestId: requestID,
WorkflowTaskCompletedEventId: workflowTaskCompletedEventID,
},
}
he.UserMetadata = command.UserMetadata
})
return nexusoperations.ScheduledEventDefinition{}.Apply(root, event)
}
func (ch *commandHandler) HandleCancelCommand(
ctx context.Context,
ms historyi.MutableState,
validator workflow.CommandValidator,
workflowTaskCompletedEventID int64,
command *commandpb.Command,
) error {
attrs := command.GetRequestCancelNexusOperationCommandAttributes()
if attrs == nil {
return workflow.FailWorkflowTaskError{
Cause: enumspb.WORKFLOW_TASK_FAILED_CAUSE_BAD_REQUEST_CANCEL_NEXUS_OPERATION_ATTRIBUTES,
Message: "empty CancelNexusOperationCommandAttributes",
}
}
coll := nexusoperations.MachineCollection(ms.HSM())
nodeID := strconv.FormatInt(attrs.ScheduledEventId, 10)
node, err := coll.Node(nodeID)
hasBufferedEvent := ms.HasAnyBufferedEvent(makeNexusOperationTerminalEventFilter(attrs.ScheduledEventId))
if err != nil {
if errors.Is(err, hsm.ErrStateMachineNotFound) {
if !hasBufferedEvent {
return workflow.FailWorkflowTaskError{
Cause: enumspb.WORKFLOW_TASK_FAILED_CAUSE_BAD_REQUEST_CANCEL_NEXUS_OPERATION_ATTRIBUTES,
Message: fmt.Sprintf("requested cancelation for a non-existing or already completed operation with scheduled event ID of %d", attrs.ScheduledEventId),
}
}
// Fallthrough and apply the event, there's special logic that will handle state machine not found below.
} else {
return err
}
}
if node != nil {
// TODO(bergundy): Remove this when operation auto-deletes itself on terminal state.
// Operation may already be in a terminal state because it doesn't yet delete itself. We don't want to accept
// cancelation in this case.
op, err := hsm.MachineData[nexusoperations.Operation](node)
if err != nil {
return err
}
// The operation is already in a terminal state and the terminal NexusOperation event has not just been buffered.
// We allow the workflow to request canceling an operation that has just completed while a workflow task is in
// flight since it cannot know about the state of the operation.
if !nexusoperations.TransitionCanceled.Possible(op) && !hasBufferedEvent {
return workflow.FailWorkflowTaskError{
Cause: enumspb.WORKFLOW_TASK_FAILED_CAUSE_BAD_REQUEST_CANCEL_NEXUS_OPERATION_ATTRIBUTES,
Message: fmt.Sprintf("requested cancelation for an already complete operation with scheduled event ID of %d", attrs.ScheduledEventId),
}
}
// END TODO
}
// Always create the event even if there's a buffered completion to avoid breaking replay in the SDK.
// The event will be applied before the completion since buffered events are reordered and put at the end of the
// batch, after command events from the workflow task.
event := ms.AddHistoryEvent(enumspb.EVENT_TYPE_NEXUS_OPERATION_CANCEL_REQUESTED, func(he *historypb.HistoryEvent) {
he.Attributes = &historypb.HistoryEvent_NexusOperationCancelRequestedEventAttributes{
NexusOperationCancelRequestedEventAttributes: &historypb.NexusOperationCancelRequestedEventAttributes{
ScheduledEventId: attrs.ScheduledEventId,
WorkflowTaskCompletedEventId: workflowTaskCompletedEventID,
},
}
he.UserMetadata = command.UserMetadata
})
err = nexusoperations.CancelRequestedEventDefinition{}.Apply(ms.HSM(), event)
// Cancel spawns a child Cancelation machine, if that machine already exists we got a duplicate cancelation request.
if errors.Is(err, hsm.ErrStateMachineAlreadyExists) {
return workflow.FailWorkflowTaskError{
Cause: enumspb.WORKFLOW_TASK_FAILED_CAUSE_BAD_REQUEST_CANCEL_NEXUS_OPERATION_ATTRIBUTES,
Message: fmt.Sprintf("cancelation was already requested for an operation with scheduled event ID %d", attrs.ScheduledEventId),
}
} else if errors.Is(err, hsm.ErrStateMachineNotFound) {
// This may happen if there's a buffered completion. Ignore.
return nil
}
return err
}
func RegisterCommandHandlers(
reg *workflow.CommandHandlerRegistry,
chasmRegistry *chasm.Registry,
endpointRegistry commonnexus.EndpointRegistry,
config *nexusoperations.Config,
) error {
h := commandHandler{
config: config,
endpointRegistry: endpointRegistry,
nexusProcessor: chasmRegistry.NexusEndpointProcessor,
}
if err := reg.Register(enumspb.COMMAND_TYPE_SCHEDULE_NEXUS_OPERATION, h.HandleScheduleCommand); err != nil {
return err
}
return reg.Register(enumspb.COMMAND_TYPE_REQUEST_CANCEL_NEXUS_OPERATION, h.HandleCancelCommand)
}
func makeNexusOperationTerminalEventFilter(scheduledEventID int64) func(event *historypb.HistoryEvent) bool {
return func(event *historypb.HistoryEvent) bool {
switch event.EventType {
case enumspb.EVENT_TYPE_NEXUS_OPERATION_COMPLETED:
return event.GetNexusOperationCompletedEventAttributes().GetScheduledEventId() == scheduledEventID
case enumspb.EVENT_TYPE_NEXUS_OPERATION_FAILED:
return event.GetNexusOperationFailedEventAttributes().GetScheduledEventId() == scheduledEventID
case enumspb.EVENT_TYPE_NEXUS_OPERATION_CANCELED:
return event.GetNexusOperationCanceledEventAttributes().GetScheduledEventId() == scheduledEventID
case enumspb.EVENT_TYPE_NEXUS_OPERATION_TIMED_OUT:
return event.GetNexusOperationTimedOutEventAttributes().GetScheduledEventId() == scheduledEventID
default:
return false
}
}
}