Hourglass.Workflow.Evaluator (hourglass v0.1.0)

Copy Markdown View Source

Pure recursive evaluator for Hourglass workflows. Per activation:

  1. Replay-walks the activation's jobs to extend a Workflow.State: initialize_workflowstate.input; each resolve_activity{seq}state.resolved_results[seq] = decoded_value; remove_from_cache → return :evict shape.

  2. Re-executes the workflow module's run/1 inline (no Task spawn). The Workflow API primitives consult the evaluator state via the process dict (CommandAccumulator) and either return a resolved value or throw(:hourglass_temporal_suspend) once the body hits a command that has no result yet.

  3. Catches the suspend sentinel at the evaluate/3 boundary, sorts the accumulated commands by command_id (lexicographic), assigns monotonic global seqs starting at state.next_global_seq, and encodes a WorkflowActivationCompletion proto.

No GenServer. No :persistent_term. No send + receive suspension. State is threaded through arguments; the only mutable handle is the process-dict command accumulator + the evaluator-state pointer, both cleared on the way out.

Summary

Functions

Non-throwing variant of suspend_or_resolve/2 for racing primitives (e.g. await_signal/2 with a timeout). First call appends the command; returns :pending until resolved, then {:resolved, value}. Unlike suspend_or_resolve/2 it never throws the suspend sentinel — the caller decides whether to suspend after peeking multiple racers.

Called by Workflow.execute_activity (and friends) when the evaluator is active on this process. Either returns the resolved value (replay path) or appends a fresh command and throws the suspend sentinel.

Synchronous primitives (uuid, random) consume a deterministic command_id but never produce a Temporal command and never suspend. They short-circuit through this hook.

Functions

evaluate(workflow_module, activation, state)

@spec evaluate(
  workflow_module :: module(),
  activation :: map(),
  state :: Hourglass.Workflow.State.t()
) ::
  {:ok, Coresdk.WorkflowCompletion.WorkflowActivationCompletion.t(),
   Hourglass.Workflow.State.t()}

peek_command(command_id, command_term)

@spec peek_command(
  Hourglass.Workflow.State.command_id(),
  Hourglass.Workflow.State.command_term()
) ::
  :pending | {:resolved, term()}

Non-throwing variant of suspend_or_resolve/2 for racing primitives (e.g. await_signal/2 with a timeout). First call appends the command; returns :pending until resolved, then {:resolved, value}. Unlike suspend_or_resolve/2 it never throws the suspend sentinel — the caller decides whether to suspend after peeking multiple racers.

suspend_or_resolve(command_id, command_term)

Called by Workflow.execute_activity (and friends) when the evaluator is active on this process. Either returns the resolved value (replay path) or appends a fresh command and throws the suspend sentinel.

synchronous(command_id, arg)

Synchronous primitives (uuid, random) consume a deterministic command_id but never produce a Temporal command and never suspend. They short-circuit through this hook.