mirror of
https://github.com/hanzoai/tasks.git
synced 2026-10-04 02:04:03 +00:00
Wires the native engine end-to-end: workflow start → server-pushed task
→ worker dispatch → activity scheduled → activity result pushed →
workflow completion. Replaces the long-poll worker surface with
ZAP-native bidirectional delivery.
Server (pkg/tasks):
- dispatch.go: subscriptions, FIFO task queues per (ns,queue,kind),
activity ledger, server-push delivery encoders. HMAC-signed task
tokens.
- engine.StartWorkflow enqueues a workflow task. terminalTransition
handles CompleteWorkflow + FailWorkflow command kinds.
- sweepSchedules fires StartWorkflow on cron tick using robfig/cron/v3.
- embed.go wires disp.send to node.Send and adds 8 ZAP handlers:
SubscribeWorkflowTasks(0xA0), SubscribeActivityTasks(0xA1),
UnsubscribeTasks(0xA6), RespondWorkflowTaskCompleted(0xA2),
RespondActivityTaskCompleted(0xA3), RespondActivityTaskFailed(0xA4),
RecordActivityTaskHeartbeat(0xA5), ScheduleActivity(0x6B).
- StartChildWorkflow(0x6D) returns 501 (out of scope).
Worker SDK (pkg/sdk):
- Transport.Handle added so workers can register inbound delivery
callbacks alongside Call.
- WorkerTransport: Subscribe{Workflow,Activity}Tasks + Unsubscribe +
On{WorkflowTask,ActivityTask,ActivityResult} replace the old
Poll/Wait surface.
- pollers.go: zero polling loops. Subscribe once at Start, dispatch
pushed tasks on bounded goroutines.
- workerEnv.ExecuteActivity: future-driven via per-activityID channels
in a worker-owned pendingResults registry. OnActivityResult unblocks
the workflow goroutine.
- inproc.Handle no-op.
Tests:
- pkg/tasks/dispatch_test.go: round-trip + queues-until-subscribe.
- pkg/tasks/e2e_test.go: full TestE2E_WorkflowExecutesActivityAndCompletes
spins up tasksd, runs hello-world workflow that calls one activity,
asserts COMPLETED status + activity invoked exactly once.
- All existing tests green; race detector clean.
139 lines
3.8 KiB
Go
139 lines
3.8 KiB
Go
// Copyright © 2026 Hanzo AI. MIT License.
|
|
|
|
package worker
|
|
|
|
import (
|
|
"fmt"
|
|
"reflect"
|
|
"runtime"
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
// RegisterWorkflowOptions customises a workflow registration.
|
|
type RegisterWorkflowOptions struct {
|
|
// Name overrides the registry key. Empty uses the reflected
|
|
// Go function name (see funcName).
|
|
Name string
|
|
|
|
// DisableAlreadyRegisteredCheck lets a caller replace a prior
|
|
// registration without panicking — useful for hot-reload in
|
|
// development. Default false: double-registration is a bug.
|
|
DisableAlreadyRegisteredCheck bool
|
|
}
|
|
|
|
// RegisterActivityOptions customises an activity registration.
|
|
type RegisterActivityOptions struct {
|
|
// Name overrides the registry key.
|
|
Name string
|
|
}
|
|
|
|
// registry holds the worker's workflow and activity maps. All
|
|
// operations are guarded by a single RWMutex; registrations happen
|
|
// pre-Start and reads dominate at runtime, so contention is nil.
|
|
type registry struct {
|
|
mu sync.RWMutex
|
|
workflows map[string]any
|
|
activities map[string]any
|
|
}
|
|
|
|
// newRegistry returns an empty registry.
|
|
func newRegistry() *registry {
|
|
return ®istry{
|
|
workflows: make(map[string]any),
|
|
activities: make(map[string]any),
|
|
}
|
|
}
|
|
|
|
// registerWorkflow adds fn to the workflow map under opts.Name, or
|
|
// under its reflected function name if opts.Name is empty. Panics
|
|
// on double-registration unless opts.DisableAlreadyRegisteredCheck
|
|
// is set — matches the upstream Temporal contract.
|
|
func (r *registry) registerWorkflow(fn any, opts RegisterWorkflowOptions) {
|
|
if fn == nil {
|
|
panic("hanzo/tasks/worker: RegisterWorkflow called with nil fn")
|
|
}
|
|
name := opts.Name
|
|
if name == "" {
|
|
name = funcName(fn)
|
|
}
|
|
if name == "" {
|
|
panic("hanzo/tasks/worker: cannot register workflow with empty name")
|
|
}
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
if _, exists := r.workflows[name]; exists && !opts.DisableAlreadyRegisteredCheck {
|
|
panic(fmt.Sprintf("hanzo/tasks/worker: workflow %q already registered", name))
|
|
}
|
|
r.workflows[name] = fn
|
|
}
|
|
|
|
// registerActivity adds fn to the activity map.
|
|
func (r *registry) registerActivity(fn any, opts RegisterActivityOptions) {
|
|
if fn == nil {
|
|
panic("hanzo/tasks/worker: RegisterActivity called with nil fn")
|
|
}
|
|
name := opts.Name
|
|
if name == "" {
|
|
name = funcName(fn)
|
|
}
|
|
if name == "" {
|
|
panic("hanzo/tasks/worker: cannot register activity with empty name")
|
|
}
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
if _, exists := r.activities[name]; exists {
|
|
panic(fmt.Sprintf("hanzo/tasks/worker: activity %q already registered", name))
|
|
}
|
|
r.activities[name] = fn
|
|
}
|
|
|
|
// workflowFn returns the registered workflow function for name, or
|
|
// nil and false if none.
|
|
func (r *registry) workflowFn(name string) (any, bool) {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
fn, ok := r.workflows[name]
|
|
return fn, ok
|
|
}
|
|
|
|
// workflowNames returns all registered workflow names (debug helper).
|
|
func (r *registry) workflowNames() []string {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
out := make([]string, 0, len(r.workflows))
|
|
for k := range r.workflows {
|
|
out = append(out, k)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// activityFn returns the registered activity function for name.
|
|
func (r *registry) activityFn(name string) (any, bool) {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
fn, ok := r.activities[name]
|
|
return fn, ok
|
|
}
|
|
|
|
// funcName returns the Go function name of fn. Format matches the
|
|
// upstream Temporal SDK: the trailing "PackagePath.FunctionName" is
|
|
// trimmed to just "FunctionName". Identical to the helper in
|
|
// workflow.reflect but kept locally so this package can stay
|
|
// consumption-agnostic about workflow.
|
|
func funcName(fn any) string {
|
|
rv := reflect.ValueOf(fn)
|
|
if !rv.IsValid() || rv.Kind() != reflect.Func {
|
|
return ""
|
|
}
|
|
rf := runtime.FuncForPC(rv.Pointer())
|
|
if rf == nil {
|
|
return ""
|
|
}
|
|
full := rf.Name()
|
|
if i := strings.LastIndex(full, "."); i >= 0 && i < len(full)-1 {
|
|
return full[i+1:]
|
|
}
|
|
return full
|
|
}
|