Hourglass.Activity (hourglass v0.1.0)

Copy Markdown View Source

Activity-side use macro. Activities are plain Elixir modules — no determinism constraint. Each activity module implements execute(input) and is dispatched by the runner based on its module name (the activity_type). The runner calls execute/1 with the deserialized input.

Activity context

Inside execute/1, an activity body can call info/0 to read the per-dispatch context — workflow_id, run_id, activity_id, attempt — populated by Hourglass.ActivityRunner from the inbound Coresdk.ActivityTask.Start. attempt/0 is a thin delegate that returns just the attempt count. Both raise when called outside an active dispatch.

try_info/0 is the non-raising sibling: returns the same struct inside an activity dispatch, nil outside.

See info/0 for field details and the (run_id, activity_id) retry-stability guarantee.

Options

use Hourglass.Activity, input: X, output: Y, retry: [...] declares input and output schemas and the module-default Temporal RetryPolicy applied by Hourglass.ActivityRunner when an activity returns {:error, _} or raises and the Hourglass.Activity.RetryClassifier classifies the failure as :retryable.

  • :input — a Hourglass.Schema module or scalar atom (e.g. :map, :string). Default: :map.
  • :output — a Hourglass.Schema module or scalar atom. Default: :map.
  • :retry — keyword list of retry policy options (optional).

Allowed retry keys (mirror Temporal's RetryPolicy proto fields):

  • :max_attempts — non-negative integer. 0 means "unlimited" per Temporal's spec; positive values cap retries. Default: 1 (no retry).
  • :initial_interval — milliseconds before the first retry.
  • :backoff_coefficient — float >= 1.0 controlling exponential backoff growth.
  • :max_interval — millisecond cap on the backoff interval.

Disallowed keys: :retryable_error_types and :non_retryable_error_types. The classifier owns eligibility (which error shapes retry); the policy only controls quantity (how many times + how fast). Mixing both controls would let an activity silently widen retry eligibility around the classifier's audited taxonomy.

Compile-time validation raises CompileError for unknown keys, for :max_attempts < 0, and for :backoff_coefficient < 1.0.

Default policy

Activities that omit :retry get default_retry_policy/0[max_attempts: 1] (no retry). Retry is opt-in: the policy plus a :retryable classification are both required for the runner to retry.

Example

defmodule MyApp.Activities.Download do
  use Hourglass.Activity,
    input: MyApp.Download.Args,
    output: MyApp.Download.Result,
    retry: [
      max_attempts: 5,
      initial_interval: 1_000,
      backoff_coefficient: 2.0,
      max_interval: 30_000
    ]

  @impl true
  def execute(%MyApp.Download.Args{url: url}), do: do_fetch(url)
end

Summary

Functions

Returns the current attempt count for the running activity (1-based).

Default retry policy for activities that don't specify one.

Returns the per-dispatch context as a Hourglass.Activity.Info struct. Fields: workflow_id, run_id, activity_id, attempt.

Non-raising variant of info/0: returns the per-dispatch Hourglass.Activity.Info struct inside an activity dispatch, nil outside.

Functions

attempt()

@spec attempt() :: pos_integer()

Returns the current attempt count for the running activity (1-based).

attempt is 1 on the first dispatch, 2 on the first retry, and so on — Temporal Server increments the count per retry. Delegates to info/0; raises if called outside an active activity dispatch.

default_retry_policy()

@spec default_retry_policy() :: keyword()

Default retry policy for activities that don't specify one.

The default is "no retry" (max_attempts: 1). Activities that genuinely need retries opt in via use Hourglass.Activity, retry: [...].

The classifier still owns eligibility — even if max_attempts is set, the runner only retries when the classifier returns :retryable.

info()

@spec info() :: Hourglass.Activity.Info.t()

Returns the per-dispatch context as a Hourglass.Activity.Info struct. Fields: workflow_id, run_id, activity_id, attempt.

Read from a process-dictionary key ({Hourglass.Activity, :info}) that Hourglass.ActivityRunner sets immediately before each dispatch and clears (via try ... after) immediately after. Calling outside an active activity dispatch raises — there is no meaningful context outside that scope.

The (run_id, activity_id) pair is stable across retries of the same activity invocation.

try_info()

@spec try_info() :: Hourglass.Activity.Info.t() | nil

Non-raising variant of info/0: returns the per-dispatch Hourglass.Activity.Info struct inside an activity dispatch, nil outside.