README.md

# Stagehand

An in-memory, GenStage-based background job processing library for Elixir.

Stagehand runs entirely in-memory with no database dependency. It is
built on GenStage and uses `PgRegistry` for cluster-wide producer
discovery and `Highlander` for singleton scheduling.

## Guarantees

- **Graceful shutdown** — executing jobs complete before the node stops.
  The producer drains in-flight work within a configurable grace period.
- **No new work during shutdown** — the producer leaves the cluster
  registry so no new jobs are routed to it. Any messages already in the
  mailbox are drained before redistribution.
- **Job redistribution** — on shutdown, scheduled, queued, and in-flight
  jobs are redistributed to surviving producers. If no surviving
  producers exist, these jobs are lost.
- **At-most-once delivery** — each job runs at most once. Jobs are
  in-memory with no persistence; a VM crash loses queued, scheduled,
  and executing jobs.
- **Unique jobs (best effort)** — deduplication is backed by an ETS
  table per node. Rendezvous hashing routes the same job fingerprint to
  the same node, and the dedup check runs on that node's Unique server.
  On graceful shutdown, dedup entries are transferred to surviving
  nodes. On node join, unique checks are blocked until all existing
  producers have synced their entries. On crashes, entries on the lost
  node are gone and duplicates are possible until the uniqueness period
  expires.

## Installation

```elixir
def deps do
  [
    {:stagehand, "~> 0.1.0"}
  ]
end
```

## Configuration

```elixir
# config/config.exs
config :my_app, Stagehand,
  queues: [default: 10, mailers: 20],
  plugins: [
    {Stagehand.Plugins.Cron, crontab: [
      {"* * * * *", MyApp.MinuteWorker},
      {"@daily", MyApp.DailyWorker}
    ]}
  ]

# config/test.exs
config :my_app, Stagehand, testing: :manual
```

Add Stagehand to the application supervision tree:

```elixir
children = [
  {Stagehand, otp_app: :my_app}
]
```

## Workers

```elixir
defmodule MyApp.EmailWorker do
  use Stagehand.Worker, queue: :mailers, max_attempts: 5

  @impl true
  def perform(%Stagehand.Job{args: %{"to" => to, "body" => body}}) do
    MyApp.Mailer.send(to, body)
    :ok
  end
end
```

Inserting jobs:

```elixir
%{"to" => "user@example.com", "body" => "hello"}
|> MyApp.EmailWorker.new()
|> Stagehand.insert()
```

### Return values

- `:ok` or `{:ok, value}` — job succeeded
- `{:error, reason}` — job failed, retries if attempts remain
- `{:snooze, seconds}` — re-enqueue after delay
- `{:cancel, reason}` — stop, no more retries

### Options

- `:queue` — queue name (default `:default`)
- `:max_attempts` — retry limit (default `20`)
- `:priority` — 0-9, lower is higher priority (default `0`)
- `:unique` — uniqueness configuration or `false`
- `:schedule_in` — delay in seconds or `{amount, :seconds | :minutes | :hours | :days}`
- `:scheduled_at` — specific `DateTime`

## Testing

```elixir
# config/test.exs
config :my_app, Stagehand, testing: :manual
```

```elixir
Stagehand.Testing.assert_enqueued(Stagehand, worker: MyApp.EmailWorker)
Stagehand.Testing.refute_enqueued(Stagehand, worker: MyApp.OtherWorker)
Stagehand.Testing.perform_job(MyApp.EmailWorker, %{"to" => "test@example.com"})
```

## License

MIT