The two locks of a turn (spec §6.1 step 3), both rows in the database:
- the chat's —
busy_since/busy_turn_refofphoenix_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).
Summary
Functions
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.
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.
A new ref for a click on a confirmation card, told apart from a turn's.
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.
Whether turn_ref holds both locks.
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.
Releases both locks if turn_ref still holds them.
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.
Releases the user's lock if ref still holds it.
Functions
@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.
@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.
@spec click_ref() :: String.t()
A new ref for a click on a confirmation card, told apart from a turn's.
@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.
Whether turn_ref holds both locks.
@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.
Releases both locks if turn_ref still holds them.
@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.
Releases the user's lock if ref still holds it.