# `PhoenixKitAgent.Turns.Locks`
[🔗](https://github.com/BeamLabEU/phoenix_kit_agent/blob/v0.1.0/lib/phoenix_kit_agent/turns/locks.ex#L1)

The two locks of a turn (spec §6.1 step 3), both rows in the database:

  * the chat's — `busy_since` / `busy_turn_ref` of
    `phoenix_kit_agent_conversations`: one turn per chat;
  * the user's — the same columns of `phoenix_kit_agent_user_settings` (the
    row is created empty when the user has none): one turn in flight per
    user, so parallel chats cannot run past a limit.

Each is taken by one atomic statement that succeeds only when the lock is
free or older than its TTL (`busy_since < now - ttl`), so a lock left by a
node that went down frees itself. No advisory lock: it would hold a pool
connection for the whole turn. A lock is released only by the turn that
holds it (`busy_turn_ref`), so a late release cannot free a newer turn, and
a running turn checks before each round that it still holds both
(`held?/3`): a turn whose lock was taken over stops there. Times are the
database's (`now()`), so the TTL does not depend on the clocks of the nodes.
Neither statement touches `updated_at` of a settings row: that stays the
time the settings themselves last changed.

A click on a confirmation card (`PhoenixKitAgent.Confirmations`) takes the
user's lock alone (`acquire_user/4`) under a ref of its own (`click_ref/0`):
it is refused while a turn of the user runs, and no turn starts while it
runs — the click keeps its lock fresh (`keep_user/4`) for as long as it
runs. Whoever is refused the user's lock learns what holds it:
`:turn_in_flight` (a turn) or `:click_in_flight` (a click).

# `acquire`

```elixir
@spec acquire(String.t(), String.t(), String.t(), pos_integer()) ::
  :ok | {:error, :conversation_busy | :turn_in_flight | :click_in_flight}
```

Takes both locks for `turn_ref`, or neither. The chat is tried first, so a
second turn in the same chat is `:conversation_busy`; a turn of the user in
another chat is `:turn_in_flight`, a click of the user `:click_in_flight`.

# `acquire_user`

```elixir
@spec acquire_user(String.t(), String.t(), pos_integer(), pos_integer()) ::
  :ok | {:error, :turn_in_flight | :click_in_flight}
```

Takes the user's lock alone for `ref` (a click on a confirmation card), or
`{:error, :turn_in_flight | :click_in_flight}`. `ttl_s` is how old another
holder's lock must be to be taken over — the TTL of a turn's locks, so a
turn still running is never taken over; `hold_s` (at most `ttl_s`) is how
long this lock blocks a turn: it is dated back by `ttl_s - hold_s`, so a
turn takes it over `hold_s` after it was taken (or last kept,
`keep_user/4`), should it never be released (its node went down).
Released with `release_user/2`.

# `click_ref`

```elixir
@spec click_ref() :: String.t()
```

A new ref for a click on a confirmation card, told apart from a turn's.

# `fresh?`

```elixir
@spec fresh?(String.t(), String.t(), pos_integer()) :: boolean()
```

Whether the chat's lock is `turn_ref`'s and younger than `ttl_s` (the
database's clock): the turn may still run, on this node or another.

# `held?`

```elixir
@spec held?(String.t(), String.t(), String.t()) :: boolean()
```

Whether `turn_ref` holds both locks.

# `keep_user`

```elixir
@spec keep_user(String.t(), String.t(), pos_integer(), pos_integer()) :: boolean()
```

Dates the user's lock of `ref` as if taken now (with `acquire_user/4`'s
`ttl_s` and `hold_s`), while `ref` still holds it: a click that runs longer
than its hold keeps blocking a turn. `true` when `ref` held it.

# `release`

```elixir
@spec release(String.t(), String.t(), String.t()) :: :ok
```

Releases both locks if `turn_ref` still holds them.

# `release_stale`

```elixir
@spec release_stale(String.t(), String.t(), String.t(), pos_integer()) :: boolean()
```

Releases the locks of `turn_ref` only while the chat's lock is still its and
older than `ttl_s` (the database's clock): what is left of a turn lost with
its node. `true` when this call released it, so of two callers at most one
does; the user's lock goes too, if `turn_ref` still holds it.

# `release_user`

```elixir
@spec release_user(String.t(), String.t()) :: :ok
```

Releases the user's lock if `ref` still holds it.

---

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