# `Honker.Lock`

Advisory locks on top of `honker_lock_*` SQL functions. Elixir has no
RAII, so the caller manages the lock lifetime explicitly:

    case Honker.Lock.try_acquire(db, "nightly-report", "worker-1", 60) do
      {:ok, nil} -> :already_held
      {:ok, %Honker.Lock{} = lock} ->
        try do
          do_work()
        after
          Honker.Lock.release(lock, db)
        end
    end

`heartbeat/3` refreshes the TTL. If it returns `{:ok, false}`, another
owner has stolen the lock — stop doing the work the lock guards.

# `heartbeat`

Refresh (extend) the lock's TTL.

`honker_lock_acquire` uses `INSERT OR IGNORE` internally, so it
can't extend the TTL on an already-held row — we have to UPDATE
`expires_at` directly (matching what the Python scheduler does).

Returns `{:ok, true}` if we still own it, `{:ok, false}` if the TTL
already expired and someone else took it (row now has a different
owner) or the row is gone entirely. Check the return value —
holding the `%Honker.Lock{}` struct is not a guarantee.

# `release`

Release the lock. Returns `{:ok, true}` if we still held it and it
was released, `{:ok, false}` if the TTL had already expired and the
lock wasn't ours anymore.

# `try_acquire`

Attempt to acquire the lock. Returns `{:ok, %Honker.Lock{}}` if
acquired, `{:ok, nil}` if another owner already holds it.

---

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