Skip to content

Enforce key locking for nested transactions (#431) - #439

Open
makoto-developer wants to merge 1 commit into
whitfin:mainfrom
makoto-developer:fix/nested-transaction-locking
Open

Enforce key locking for nested transactions (#431)#439
makoto-developer wants to merge 1 commit into
whitfin:mainfrom
makoto-developer:fix/nested-transaction-locking

Conversation

@makoto-developer

Copy link
Copy Markdown

What

Fixes #431.

Cachex.Services.Locksmith.transaction/3 previously short-circuited on a boolean flag when already inside a transactional context and ran the function immediately, without acquiring any locks. As noted in the issue, an inner transaction that locks a different key than the enclosing one then proceeds with no locking actually in place — a silent correctness bug.

How

Following the approach suggested in the issue, :cachex_transaction now stores the set of keys locked by the current transaction instead of a boolean:

  • start_transaction/0 stores an empty key list; transaction?/0 still returns a boolean (!= false), so existing callers (e.g. write/3) are unaffected.
  • Locksmith.Queue's :transaction handler records the keys it locks in the process dictionary for the duration of the function, so nested transactions can see what is currently held.
  • transaction/3, when already inside a transactional context, verifies that every requested key is already locked by the enclosing transaction. If so it runs inline (a redundant lock, as before); if any requested key is not locked it raises Cachex.Error, since executing against unlocked keys inside a transaction is almost certainly a logic error. Keyless transactions (clear/purge/reset) and lock-free serialized contexts remain unrestricted.

Tests

Added a LocksmithTest case covering both the allowed (subset of locked keys → runs inline) and rejected (unlocked key → surfaces as an error) nested paths, and updated the existing internal-representation test for the new :cachex_transaction value. mix test, mix format --check-formatted and mix credo --all all pass locally.

Question re: write/3

You mentioned this "likely applies to the similar block" in write/3 too. I kept this PR focused on transaction/3, because enforcing the same rule on write/3 is a wider behavioural change (writes to non-locked keys inside a transaction would start raising) and needs a carve-out to keep the lock-free execute path working, since it shares the empty-lock-set representation. I have that version ready — happy to fold it in here or send it as a follow-up. Which would you prefer?

Nested transactions previously short-circuited on a boolean flag and ran
the function without acquiring locks, so an inner transaction locking a
different key than the enclosing one would proceed with no locking in place.

Store the set of locked keys in :cachex_transaction instead of a boolean so
a nested transaction can verify that every key it requests is already locked
by the enclosing transaction, raising a Cachex.Error otherwise.

Refs whitfin#431
@makoto-developer
makoto-developer force-pushed the fix/nested-transaction-locking branch from d213d5b to d60cdbd Compare July 28, 2026 16:08
@whitfin
whitfin self-requested a review July 28, 2026 18:39
@whitfin whitfin self-assigned this Jul 28, 2026
@makoto-developer
makoto-developer marked this pull request as ready for review August 5, 2026 06:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

2 participants