Per-task-queue Worker — a GenServer that owns the bridge-handle
registration for one task queue and runs a small inner Supervisor
hosting the two poll loops driving Temporal Core.
Why a GenServer + inner Supervisor (not just use Supervisor)
This module needs init/1 and terminate/2 callbacks so it can
bracket the BridgeHolder registration with the lifetime of the
Worker. use Supervisor doesn't expose terminate; wrapping a
Supervisor inside a trap-exits GenServer gives us the hook.
Sequence on graceful start:
init/1callsBridgeHolder.register_worker(task_queue, opts). The Application-level holder allocates a fresh bridge worker handle and stores it keyed by task_queue.init/1starts the inner Supervisor with the two poll loops. The poll loops callBridgeHolder.poll_*against the now-live handle.
Sequence on graceful stop:
- Caller invokes
Worker.Supervisor.stop_worker(task_queue)— OR — Application shutdown reverse-walks children and stops this GenServer. terminate/2runs (we trap exits) and callsBridgeHolder.unregister_worker(task_queue). The bridge handle is destroyed; any in-flight long-poll on it returns{:error, %Bridge.Error{kind: :shutdown}}.- The poll loops see
:shutdownand exit:normal. The inner Supervisor reaps them and exits:normal. terminate/2returns. The GenServer exits.
Children of the inner Supervisor (:one_for_one)
Hourglass.Worker.WorkflowPollLoop— drivesBridgeHolder.poll_workflow_activation/1; dispatches each activation to the sharedHourglass.WorkflowEvaluator.DynamicSupervisor.Hourglass.Worker.ActivityPollLoop— drivesBridgeHolder.poll_activity_task/1; dispatches each task to the sharedHourglass.ActivityExecutor.DynamicSupervisor.
Loops are independent — neither holds state the other depends on —
so :one_for_one is the natural fit. Both are :transient: a
graceful :normal exit (the bridge returned :shutdown) does not
trigger a restart; only abnormal exits do.
Why bridge ownership lives in this Worker (not Worker.Supervisor)
Putting register_worker in init/1 (rather than in
Worker.Supervisor.start_worker/1) makes the OTP shape symmetric:
the same process that allocated the handle also frees it on exit.
Direct callers of Worker.start_link/1 (typical in tests via
start_supervised/1) get correct lifecycle without having to
remember the bracket.
Cascade-restart safety
In-flight evaluator and executor Tasks live under shared
Application-level DynSups
(Hourglass.WorkflowEvaluator.DynamicSupervisor,
Hourglass.ActivityExecutor.DynamicSupervisor), not under
this Worker's tree. A Worker child crash never destroys those
Tasks. They survive, finish their work, and ship completions via
BridgeHolder (which they reach by name, not by holding a
reference). If the Worker exits between dispatch and completion,
the in-flight Tasks see {:error, :worker_not_registered} and
Core redelivers via heartbeat / start-to-close timeout.
Naming
start_link/1 registers the GenServer under
Hourglass.WorkerRegistry.via(task_queue) for via-name
reachability. The poll loops do not need per-task-queue names —
they receive task_queue as an arg and call BridgeHolder with it.
Summary
Functions
Returns the inner Supervisor pid for task_queue, primarily for
test introspection (e.g. Supervisor.which_children/1).
Types
@type opts() :: [ task_queue: String.t(), namespace: String.t(), max_cached_workflows: pos_integer(), target_url: String.t() ]
Functions
Returns the inner Supervisor pid for task_queue, primarily for
test introspection (e.g. Supervisor.which_children/1).
@spec start_link(opts()) :: GenServer.on_start()