mirror of
https://github.com/hanzoai/tasks.git
synced 2026-10-04 02:04:03 +00:00
Ports the workflow-code-facing API that base/plugins/tasks and
commerce/infra rely on today, with zero go.temporal.io or grpc
imports. Wire shape mirrors Temporal exactly so call-site migration
is a pure import-path swap.
Package layout (pkg/sdk/workflow):
env.go — CoroutineEnv interface, the one seam the worker
(pkg/sdk/worker, separately) must satisfy; Info,
SelectCase, CancelFunc. NewContextFromEnv is the
single entry point user ctx flows through.
context.go — Context interface (Deadline/Done/Err/Value), ctx
chain with WithCancel / WithValue / WithActivityOptions;
GetLogger, Now, Sleep, GetInfo.
future.go — Future + settleable impl. NewFuture / Settle split
so both the worker and the in-memory stub drive
completion through the same concrete. EncodePayload
is the Phase-1 JSON codec; Phase 2 swaps for ZAP.
channel.go — ReceiveChannel / Channel; unbuffered + buffered
chanImpl backing both user channels and signal
queues. Send/Receive/ReceiveAsync/Close.
selector.go — Selector with AddFuture / AddReceive / AddDefault;
Select delegates to env.Select and dispatches to
the matched callback.
timer.go — NewTimer schedules a durable timer Future.
signal.go — GetSignalChannel returns shared-by-name signal queues.
activity.go — ActivityOptions mirror of Temporal shape;
WithActivityOptions attaches per-call opts;
ExecuteActivity reads opts from ctx and dispatches.
reflect.go — Sole reflect use site (activity name resolution).
stub.go — StubEnv: synchronous, in-memory CoroutineEnv for
unit-testing workflow code without a worker.
OnActivity(...).Return(...), SignalAsync, AdvanceClock,
Cancel. Used throughout workflow_test.go.
workflow_test.go — 27 tests exercising Context, cancel scopes,
Now/Sleep, timers, activities (success/error/nil),
signal channels, user channels, Selector (future/
receive/default), and an end-to-end AgentTaskWorkflow-
shaped test that mirrors base/plugins/tasks.
Phase 1 determinism caveats (documented in env.go):
* No event-sourced replay yet — crashes restart from the top with
the original input. Same idempotency contract as Temporal.
* Payloads encoded as JSON, not the canonical ZAP codec.
* time.Now / rand / goroutines inside workflow code remain banned;
CI linter comes in a follow-up.
go vet clean. 27 tests pass including -race.
go list -deps reports zero go.temporal.io or google.golang.org/grpc.
103 lines
3.7 KiB
Go
103 lines
3.7 KiB
Go
package workflow
|
|
|
|
import (
|
|
"errors"
|
|
"time"
|
|
|
|
"github.com/hanzoai/tasks/pkg/sdk/temporal"
|
|
)
|
|
|
|
// ActivityOptions configures a single activity invocation. Attached
|
|
// to a Context via WithActivityOptions; every ExecuteActivity call
|
|
// below reads the attached options at dispatch time.
|
|
//
|
|
// Shape matches go.temporal.io/sdk/workflow.ActivityOptions so
|
|
// existing base/commerce call sites compile unchanged after the
|
|
// import swap.
|
|
type ActivityOptions struct {
|
|
// TaskQueue routes the activity to a specific worker pool.
|
|
// Empty means "the workflow's own task queue" (the common case).
|
|
TaskQueue string
|
|
|
|
// ScheduleToStartTimeout caps the wait between enqueue and
|
|
// worker pick-up. 0 = unlimited.
|
|
ScheduleToStartTimeout time.Duration
|
|
|
|
// StartToCloseTimeout caps a single activity run. Exceeding it
|
|
// forces a retry per RetryPolicy. 0 = unlimited (NOT
|
|
// recommended; configure something reasonable).
|
|
StartToCloseTimeout time.Duration
|
|
|
|
// ScheduleToCloseTimeout is the end-to-end budget including
|
|
// retries. 0 means "derive from StartToCloseTimeout *
|
|
// MaximumAttempts".
|
|
ScheduleToCloseTimeout time.Duration
|
|
|
|
// HeartbeatTimeout is the max interval between heartbeats for
|
|
// a long-running activity. Exceeding it aborts the attempt
|
|
// and triggers retry. 0 = no heartbeat required.
|
|
HeartbeatTimeout time.Duration
|
|
|
|
// RetryPolicy controls attempt count and backoff. nil = the
|
|
// SDK default (unlimited attempts, exponential backoff starting
|
|
// at 1s, capped at 100s). Pass &temporal.RetryPolicy{MaximumAttempts: 1}
|
|
// to disable retries entirely.
|
|
RetryPolicy *temporal.RetryPolicy
|
|
|
|
// WaitForCancellation, when true, makes cancellation wait for
|
|
// the activity to acknowledge before returning. Default false:
|
|
// cancellation unblocks the caller immediately and the
|
|
// activity is expected to observe ctx.Done and exit.
|
|
WaitForCancellation bool
|
|
}
|
|
|
|
// WithActivityOptions returns a Context carrying opts. The ctx
|
|
// returned is what ExecuteActivity(ctx, …) consumes when it
|
|
// dispatches the activity.
|
|
func WithActivityOptions(parent Context, opts ActivityOptions) Context {
|
|
if parent == nil {
|
|
panic("workflow.WithActivityOptions: nil context")
|
|
}
|
|
cp := opts // copy so later caller mutation doesn't leak
|
|
return &ctxImpl{parent: parent, opts: &cp}
|
|
}
|
|
|
|
// ExecuteActivity dispatches an activity for execution and returns
|
|
// the Future that settles with its (result, error). activity may
|
|
// be a function value or a registered name (string). args are the
|
|
// activity's input parameters; they will be JSON-encoded for the
|
|
// wire.
|
|
//
|
|
// The activity's attached ActivityOptions are read from ctx. If no
|
|
// options were attached, the SDK default is used (unlimited retry,
|
|
// no timeouts — configure something).
|
|
//
|
|
// Callers MUST NOT spawn goroutines that call ExecuteActivity; in
|
|
// Phase 1 the workflow is a single coroutine and parallel dispatch
|
|
// is done by calling ExecuteActivity N times in a loop and then
|
|
// Get-ing the Futures (or selecting on them).
|
|
func ExecuteActivity(ctx Context, activity any, args ...any) Future {
|
|
if ctx == nil {
|
|
f := NewFuture()
|
|
f.Settle(nil, errNilContext)
|
|
return f
|
|
}
|
|
if activity == nil {
|
|
f := NewFuture()
|
|
f.Settle(nil, temporal.NewError("nil activity", temporal.CodeApplication, true))
|
|
return f
|
|
}
|
|
opts := ctx.activityOptions()
|
|
return ctx.env().ExecuteActivity(opts, activity, args)
|
|
}
|
|
|
|
// errNilContext is returned when user code passes a nil Context
|
|
// into a helper. We surface it as a non-retryable Error so retry
|
|
// policies don't try to recover from a programming bug.
|
|
var errNilContext = &temporal.Error{
|
|
Message: "workflow.Context is nil",
|
|
Code: temporal.CodeApplication,
|
|
NonRetryable: true,
|
|
Cause: errors.New("nil context"),
|
|
}
|