Files
tasks/service/history/ndc/workflow.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

327 lines
9.2 KiB
Go

//go:generate mockgen -package $GOPACKAGE -source $GOFILE -destination workflow_mock.go
package ndc
import (
"context"
"fmt"
enumspb "go.temporal.io/api/enums/v1"
historypb "go.temporal.io/api/history/v1"
"go.temporal.io/api/serviceerror"
enumsspb "go.temporal.io/server/api/enums/v1"
"go.temporal.io/server/chasm"
"go.temporal.io/server/common"
"go.temporal.io/server/common/cluster"
"go.temporal.io/server/common/payloads"
"go.temporal.io/server/common/primitives"
"go.temporal.io/server/service/history/consts"
historyi "go.temporal.io/server/service/history/interfaces"
"go.temporal.io/server/service/history/workflow"
)
type (
Workflow interface {
GetContext() historyi.WorkflowContext
GetMutableState() historyi.MutableState
GetReleaseFn() historyi.ReleaseWorkflowContextFunc
GetVectorClock() (int64, int64, error)
HappensAfter(that Workflow) (bool, error)
Revive(ctx context.Context, taskRefresher workflow.TaskRefresher) error
SuppressBy(incomingWorkflow Workflow) (historyi.TransactionPolicy, error)
FlushBufferedEvents() error
}
WorkflowImpl struct {
clusterMetadata cluster.Metadata
context historyi.WorkflowContext
mutableState historyi.MutableState
releaseFn historyi.ReleaseWorkflowContextFunc
}
)
func NewWorkflow(
clusterMetadata cluster.Metadata,
wfContext historyi.WorkflowContext,
mutableState historyi.MutableState,
releaseFn historyi.ReleaseWorkflowContextFunc,
) *WorkflowImpl {
return &WorkflowImpl{
clusterMetadata: clusterMetadata,
context: wfContext,
mutableState: mutableState,
releaseFn: releaseFn,
}
}
func (r *WorkflowImpl) GetContext() historyi.WorkflowContext {
return r.context
}
func (r *WorkflowImpl) GetMutableState() historyi.MutableState {
return r.mutableState
}
func (r *WorkflowImpl) GetReleaseFn() historyi.ReleaseWorkflowContextFunc {
return r.releaseFn
}
func (r *WorkflowImpl) GetVectorClock() (int64, int64, error) {
var version int64
var err error
if r.mutableState.IsWorkflowExecutionRunning() {
version, err = r.mutableState.GetLastWriteVersion()
if err != nil {
return 0, 0, err
}
} else {
// TODO: If workflow is in zombie state, it should call GetLastWriteVersion() instead of
// GetCloseVersion().
// Zombie state is handled specially in GetCloseVersion() implementation today, but we should
// eventually remove that special handling.
version, err = r.mutableState.GetCloseVersion()
if err != nil {
return 0, 0, err
}
}
lastRunningClock := r.mutableState.GetExecutionInfo().LastRunningClock
return version, lastRunningClock, nil
}
func (r *WorkflowImpl) HappensAfter(
that Workflow,
) (bool, error) {
thisLastWriteVersion, thisLastRunningClock, err := r.GetVectorClock()
if err != nil {
return false, err
}
thatLastWriteVersion, thatLastRunningClock, err := that.GetVectorClock()
if err != nil {
return false, err
}
return WorkflowHappensAfter(
thisLastWriteVersion,
thisLastRunningClock,
thatLastWriteVersion,
thatLastRunningClock,
), nil
}
func (r *WorkflowImpl) Revive(ctx context.Context, taskRefresher workflow.TaskRefresher) error {
state, _ := r.mutableState.GetWorkflowStateStatus()
if state != enumsspb.WORKFLOW_EXECUTION_STATE_ZOMBIE {
return nil
}
// mutable state is in zombie state, need to set the state correctly accordingly
state = enumsspb.WORKFLOW_EXECUTION_STATE_RUNNING
if r.mutableState.IsWorkflow() && !r.mutableState.HadOrHasWorkflowTask() {
state = enumsspb.WORKFLOW_EXECUTION_STATE_CREATED
}
_, err := r.mutableState.UpdateWorkflowStateStatus(
state,
enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING,
)
if err != nil {
return err
}
return taskRefresher.Refresh(ctx, r.mutableState, false)
}
func (r *WorkflowImpl) SuppressBy(
incomingWorkflow Workflow,
) (historyi.TransactionPolicy, error) {
// NOTE: READ BEFORE MODIFICATION
//
// if the workflow to be suppressed has last write version being local active
// then use active logic to terminate this workflow
// if the workflow to be suppressed has last write version being remote active
// then turn this workflow into a zombie
lastWriteVersion, lastRunningClock, err := r.GetVectorClock()
if err != nil {
return historyi.TransactionPolicyActive, err
}
incomingLastWriteVersion, incomingLastRunningClock, err := incomingWorkflow.GetVectorClock()
if err != nil {
return historyi.TransactionPolicyActive, err
}
if WorkflowHappensAfter(
lastWriteVersion,
lastRunningClock,
incomingLastWriteVersion,
incomingLastRunningClock,
) {
return historyi.TransactionPolicyActive, serviceerror.NewInternal("Workflow cannot suppress workflow by older workflow")
}
// if workflow is in zombie or finished state, keep as is
if !r.mutableState.IsWorkflowExecutionRunning() {
return historyi.TransactionPolicyPassive, nil
}
lastWriteCluster := r.clusterMetadata.ClusterNameForFailoverVersion(true, lastWriteVersion)
currentCluster := r.clusterMetadata.GetCurrentClusterName()
if currentCluster == lastWriteCluster {
return historyi.TransactionPolicyActive, r.terminateMutableState(lastWriteVersion, incomingLastWriteVersion)
}
_, err = r.mutableState.UpdateWorkflowStateStatus(
enumsspb.WORKFLOW_EXECUTION_STATE_ZOMBIE,
enumspb.WORKFLOW_EXECUTION_STATUS_RUNNING,
)
return historyi.TransactionPolicyPassive, err
}
func (r *WorkflowImpl) FlushBufferedEvents() error {
if !r.mutableState.IsWorkflow() {
return nil
}
if !r.mutableState.IsWorkflowExecutionRunning() {
return nil
}
if !r.mutableState.HasBufferedEvents() {
return nil
}
// TODO: Same as the reasoning in mutableState.startTransactionHandleWorkflowTaskFailover()
// LastWriteVersion is only correct when replication task processing logic flush buffered
// events for state only changes as well.
// Transition history is not enabled today so LastWriteVersion == LastEventVersion
lastWriteVersion, err := r.mutableState.GetLastWriteVersion()
if err != nil {
return err
}
lastWriteCluster := r.clusterMetadata.ClusterNameForFailoverVersion(true, lastWriteVersion)
currentCluster := r.clusterMetadata.GetCurrentClusterName()
if lastWriteCluster != currentCluster {
return serviceerror.NewInternal("Workflow encountered workflow with buffered events but last write not from current cluster")
}
if err := r.mutableState.UpdateCurrentVersion(lastWriteVersion, true); err != nil {
return err
}
if _, err = r.failWorkflowTask(); err != nil {
return err
}
// Don't schedule a new workflow task if the workflow is paused.
if r.mutableState.IsWorkflowExecutionStatusPaused() {
return nil
}
if _, err := r.mutableState.AddWorkflowTaskScheduledEvent(
false,
enumsspb.WORKFLOW_TASK_TYPE_NORMAL,
); err != nil {
return err
}
return nil
}
func (r *WorkflowImpl) failWorkflowTask() (*historypb.HistoryEvent, error) {
// do not persist the change right now, Workflow requires transaction
workflowTask := r.mutableState.GetStartedWorkflowTask()
if workflowTask == nil {
return nil, nil
}
wtFailedEvent, err := r.mutableState.AddWorkflowTaskFailedEvent(
workflowTask,
enumspb.WORKFLOW_TASK_FAILED_CAUSE_FAILOVER_CLOSE_COMMAND,
nil,
consts.IdentityHistoryService,
nil,
"",
"",
"",
0,
)
if err != nil {
return nil, err
}
r.mutableState.FlushBufferedEvents()
return wtFailedEvent, nil
}
func (r *WorkflowImpl) terminateMutableState(
lastWriteVersion int64,
incomingLastWriteVersion int64,
) error {
if err := r.mutableState.UpdateCurrentVersion(lastWriteVersion, true); err != nil {
return err
}
if !r.mutableState.IsWorkflow() {
return r.mutableState.ChasmTree().Terminate(chasm.TerminateComponentRequest{
Identity: consts.IdentityHistoryService,
Reason: common.FailureReasonWorkflowTerminationDueToVersionConflict,
Details: payloads.EncodeString(fmt.Sprintf("terminated by version: %v", incomingLastWriteVersion)),
RequestID: primitives.NewUUID().String(),
})
}
eventBatchFirstEventID := r.GetMutableState().GetNextEventID()
wtFailedEvent, err := r.failWorkflowTask()
if err != nil {
return err
}
if wtFailedEvent != nil {
eventBatchFirstEventID = wtFailedEvent.GetEventId()
}
// do not persist the change right now, Workflow requires transaction
_, err = r.mutableState.AddWorkflowExecutionTerminatedEvent(
eventBatchFirstEventID,
common.FailureReasonWorkflowTerminationDueToVersionConflict,
payloads.EncodeString(fmt.Sprintf("terminated by version: %v", incomingLastWriteVersion)),
consts.IdentityHistoryService,
false,
nil, // No links necessary.
)
// Don't abort updates here for a few reasons:
// 1. There probably no update waiters for Wf which is about to be terminated,
// 2. MS is not persisted yet, and updates should be aborted after MS is persisted, which is not trivial in this case,
// 3. New replication version will force update registry reload and waiters will get errors.
// r.GetContext().UpdateRegistry(context.Background(), nil).Abort(update.AbortReasonWorkflowTerminated)
return err
}
func WorkflowHappensAfter(
thisLastWriteVersion int64,
thisLastRunningClock int64,
thatLastWriteVersion int64,
thatLastRunningClock int64,
) bool {
if thisLastWriteVersion != thatLastWriteVersion {
return thisLastWriteVersion > thatLastWriteVersion
}
// thisLastWriteVersion == thatLastWriteVersion
return thisLastRunningClock > thatLastRunningClock
}