# `Outboxer.Storage`
[🔗](https://github.com/epinault/outboxer/blob/v0.1.0/lib/outboxer/storage.ex#L1)

The behaviour between Outboxer and the database that holds the outbox.

Two adapters ship with the library, both built on Ecto:

  * `Outboxer.Storage.Postgres` - multi-node safe. Each delivery batch runs in
    a short transaction that holds a transaction-scoped advisory lock on the
    partition.
  * `Outboxer.Storage.SQLite` - single node. The one worker per partition is
    the only owner; delivery happens outside any transaction because SQLite
    has a single writer.

The adapter is chosen from the repo's Ecto adapter, or set explicitly with
the `:storage` option of `Outboxer`.

## Writing another adapter

The contract an adapter has to honour:

  * `c:insert/2` must take part in the caller's database transaction. That is
    the whole point of the pattern: the message commits or rolls back with
    the business write. A store that cannot do this (Redis, for example) can
    still implement the behaviour, but what you get is a best-effort queue,
    not a transactional outbox.
  * `c:with_partition/4` must guarantee that at most one process in the whole
    cluster runs the function for a given outbox partition at a time, and
    must release that ownership if the process dies.
  * `c:fetch/5` must return ready messages in id order and must leave out
    every group that has a failed message waiting for its retry.

# `conf`

```elixir
@type conf() :: %{
  :repo =&gt; module(),
  :outboxes =&gt; %{
    optional(atom()) =&gt; %{:table =&gt; String.t(), optional(atom()) =&gt; term()}
  },
  :prefix =&gt; String.t() | nil,
  :name =&gt; atom(),
  optional(atom()) =&gt; term()
}
```

The instance configuration. An adapter can rely on these fields:

  * `:repo` - the `Ecto.Repo` module
  * `:prefix` - the schema of the outbox tables, or `nil`
  * `:name` - the Outboxer instance name
  * `:outboxes` - a map of outbox name to its configuration; `.table` is the
    table that outbox lives in

Message ids are unique within a table only, which is why every callback that
takes ids also takes the outbox they belong to.

# `outbox`

```elixir
@type outbox() :: atom()
```

# `row`

```elixir
@type row() :: %{
  outbox: outbox(),
  partition: non_neg_integer(),
  group_key: String.t() | nil,
  type: String.t(),
  payload: binary(),
  headers: map()
}
```

# `ack`

```elixir
@callback ack(
  conf(),
  outbox(),
  ids :: [pos_integer()],
  retention :: :delete | {:keep, pos_integer()},
  now :: DateTime.t()
) :: :ok
```

Records successful delivery: deletes the rows or marks them delivered.

# `fail`

```elixir
@callback fail(
  conf(),
  outbox(),
  ids :: [pos_integer()],
  retry_at :: DateTime.t(),
  error :: String.t()
) ::
  :ok
```

Records a failed attempt and schedules the retry.

# `fetch`

```elixir
@callback fetch(
  conf(),
  outbox(),
  partition :: non_neg_integer(),
  limit :: pos_integer(),
  now :: DateTime.t()
) :: [Outboxer.Envelope.t()]
```

Returns up to `limit` deliverable messages of a partition, oldest first.

# `insert`

```elixir
@callback insert(conf(), [row()]) :: [Outboxer.Envelope.t()]
```

Inserts rows in the caller's transaction and returns their envelopes.

# `kill`

```elixir
@callback kill(
  conf(),
  outbox(),
  ids :: [pos_integer()],
  error :: String.t(),
  now :: DateTime.t()
) :: :ok
```

Moves messages to the dead state after their last allowed attempt.

# `lock_group`

```elixir
@callback lock_group(conf(), outbox(), group_key :: String.t()) :: :ok
```

Serialises concurrent publishers of the same group until their transaction ends.
A no-op where the database already has a single writer.

# `notify`
*optional* 

```elixir
@callback notify(conf(), outbox(), partition :: non_neg_integer()) :: :ok
```

Tells other nodes that a partition has new messages. Optional.

# `prune`

```elixir
@callback prune(conf(), outbox(), cutoff :: DateTime.t(), limit :: pos_integer()) ::
  non_neg_integer()
```

Deletes up to `limit` delivered messages older than `cutoff`. Returns how many.

# `with_partition`

```elixir
@callback with_partition(conf(), outbox(), partition :: non_neg_integer(), (-&gt; result)) ::
  {:ok, result} | :busy
when result: term()
```

Runs `fun` as the exclusive owner of a partition, or returns `:busy`.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
