Hourglass.Bridge (hourglass v0.1.0)

Copy Markdown View Source

Rustler NIF wrapper for the hourglass crate.

NIF-reload rescue

When Phoenix.CodeReloader (or any other reload mechanism) re-loads this module mid-BEAM-session, the Rustler @on_load :load_nif callback re-runs and installs a fresh resource-type registry. Every pre-reload resource ref (CoreRuntime, Worker, Client, Replayer) becomes opaque, and the next NIF call that decodes one raises ArgumentError from the Rustler decoder.

with_nif_reload_rescue/2 is the canonical wrapper for any Bridge call that operates on a resource ref. It catches ArgumentError, exits Hourglass.Runtime with :nif_reloaded (which cascades the Hourglass.Subsystem :rest_for_one subtree, re-acquiring every ref against the freshly loaded NIF), and returns {:error, :nif_reloaded} so the caller's with chain unwinds cleanly. The rescue does NOT loop: a second ArgumentError from a caller-side retry is treated identically to the first — the cascade is idempotent for a single Runtime pid and Process.exit/2 on a dead pid is a no-op.

See signal_nif_reload!/1 for the cascade-signal internals.

Summary

Functions

Signal the Hourglass.Subsystem :rest_for_one cascade by exiting Hourglass.Runtime with :nif_reloaded. Public so BridgeHolder (which has its own retry/recycle semantics around worker handles and cannot simply return {:error, :nif_reloaded} from every call shape) can share the same signal path as the generic with_nif_reload_rescue/2 wrapper.

Wrap a Bridge NIF call so a post-hot-reload ArgumentError (from a stale resource ref decoded against a freshly loaded NIF resource-type registry) cascades a Hourglass.Subsystem restart instead of bubbling up as an opaque crash.

Functions

client_await_workflow(client, bin, timeout_ms)

@spec client_await_workflow(reference(), binary(), pos_integer()) ::
  {:ok, binary()} | {:error, Hourglass.Bridge.Error.t()}

client_cancel_workflow(client, request_bin)

@spec client_cancel_workflow(reference(), binary()) ::
  :ok | {:error, Hourglass.Bridge.Error.t()}

client_describe_namespace(client, namespace)

@spec client_describe_namespace(reference(), String.t()) ::
  {:ok, binary()} | {:error, Hourglass.Bridge.Error.t()}

client_describe_workflow_execution(client, workflow_id, run_id)

@spec client_describe_workflow_execution(reference(), String.t(), String.t()) ::
  {:ok, binary()} | {:error, Hourglass.Bridge.Error.t()}

client_fetch_history(client, workflow_id)

@spec client_fetch_history(reference(), String.t()) ::
  {:ok, binary()} | {:error, Hourglass.Bridge.Error.t()}

client_new(runtime, config_bin)

@spec client_new(reference(), binary()) ::
  {:ok, reference()} | {:error, Hourglass.Bridge.Error.t()}

client_register_namespace(client, namespace)

@spec client_register_namespace(reference(), String.t()) ::
  :ok | {:error, Hourglass.Bridge.Error.t()}

client_signal_workflow(client, request_bin)

@spec client_signal_workflow(reference(), binary()) ::
  :ok | {:error, Hourglass.Bridge.Error.t()}

client_start_workflow(client, bin)

@spec client_start_workflow(reference(), binary()) ::
  {:ok, binary()} | {:error, Hourglass.Bridge.Error.t()}

fail(message)

@spec fail(String.t()) :: {:error, Hourglass.Bridge.Error.t()}

ping()

@spec ping() :: String.t()

replayer_close_feeder(replayer)

@spec replayer_close_feeder(reference()) :: :ok

replayer_complete_workflow_activation(replayer, bin)

@spec replayer_complete_workflow_activation(reference(), binary()) ::
  :ok | {:error, Hourglass.Bridge.Error.t()}

replayer_new(runtime, config_bin)

@spec replayer_new(reference(), binary()) ::
  {:ok, reference()} | {:error, Hourglass.Bridge.Error.t()}

replayer_poll_workflow_activation(replayer)

@spec replayer_poll_workflow_activation(reference()) ::
  {:ok, binary()} | {:error, Hourglass.Bridge.Error.t()}

replayer_push_history(replayer, workflow_id, history_bin)

@spec replayer_push_history(reference(), String.t(), binary()) ::
  :ok | {:error, Hourglass.Bridge.Error.t()}

runtime_new()

@spec runtime_new() :: {:ok, reference()} | {:error, Hourglass.Bridge.Error.t()}

signal_nif_reload!(call_site)

@spec signal_nif_reload!(String.t()) :: :ok

Signal the Hourglass.Subsystem :rest_for_one cascade by exiting Hourglass.Runtime with :nif_reloaded. Public so BridgeHolder (which has its own retry/recycle semantics around worker handles and cannot simply return {:error, :nif_reloaded} from every call shape) can share the same signal path as the generic with_nif_reload_rescue/2 wrapper.

call_site is logged at :error level for post-mortem attribution.

with_nif_reload_rescue(call_site, fun)

@spec with_nif_reload_rescue(String.t(), (-> result)) ::
  result | {:error, :nif_reloaded}
when result: term()

Wrap a Bridge NIF call so a post-hot-reload ArgumentError (from a stale resource ref decoded against a freshly loaded NIF resource-type registry) cascades a Hourglass.Subsystem restart instead of bubbling up as an opaque crash.

fun is a zero-arity function that performs the NIF call. On success, its return value is propagated unchanged. On ArgumentError, the helper:

  1. logs the call site at :error level,
  2. signals Hourglass.Runtime to exit with :nif_reloaded (which cascades the rest of the :rest_for_one subtree — BridgeHolder, WorkerRegistry, Worker.Supervisor, WorkerLauncher — so every pre-reload resource ref is dropped and re-acquired against the freshly loaded NIF), and
  3. returns {:error, :nif_reloaded} so callers' with chains unwind cleanly.

The rescue is intentionally narrow: only ArgumentError, not rescue _. Widening would silently catch real argument bugs at the Bridge boundary.

No automatic retry inside the helper: the cascade is async and the caller's process may itself be killed by the cascade (e.g. if the caller is BridgeHolder). A second NIF call from the same caller would re-raise ArgumentError, which the helper handles identically; Process.exit/2 on a now-dead Runtime pid is a no-op, so there is no loop hazard, but there is also no point in adding a built-in retry. Callers that want one (e.g. Replayer.replay_history/2, which is a stateless one-shot) wrap a fresh Runtime.handle() + call themselves.

Use this helper for any new Bridge call site that allocates or operates on a resource ref from process-lifecycle code paths.

worker_complete_activity_task(worker, bin)

@spec worker_complete_activity_task(reference(), binary()) ::
  :ok | {:error, Hourglass.Bridge.Error.t()}

worker_complete_workflow_activation(worker, bin)

@spec worker_complete_workflow_activation(reference(), binary()) ::
  :ok | {:error, Hourglass.Bridge.Error.t()}

worker_new(runtime, config_bin)

@spec worker_new(reference(), binary()) ::
  {:ok, reference()} | {:error, Hourglass.Bridge.Error.t()}

worker_poll_activity_task(worker)

@spec worker_poll_activity_task(reference()) ::
  {:ok, binary()} | {:error, Hourglass.Bridge.Error.t()}

worker_poll_workflow_activation(worker)

@spec worker_poll_workflow_activation(reference()) ::
  {:ok, binary()} | {:error, Hourglass.Bridge.Error.t()}

worker_shutdown(worker)

@spec worker_shutdown(reference()) :: :ok | {:error, Hourglass.Bridge.Error.t()}