PhoenixKitAgent.Turns.Locks (PhoenixKitAgent v0.1.0)

Copy Markdown View Source

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).

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

acquire(user_uuid, conversation_uuid, turn_ref, ttl_s)

@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(user_uuid, ref, ttl_s, hold_s)

@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()

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

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

fresh?(conversation_uuid, turn_ref, ttl_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.

held?(user_uuid, conversation_uuid, turn_ref)

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

Whether turn_ref holds both locks.

keep_user(user_uuid, ref, ttl_s, hold_s)

@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(user_uuid, conversation_uuid, turn_ref)

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

Releases both locks if turn_ref still holds them.

release_stale(user_uuid, conversation_uuid, turn_ref, ttl_s)

@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(user_uuid, ref)

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

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