Skip to content

[idea] simplify account locking / creation#230

Open
eadz wants to merge 3 commits intoenvato:mainfrom
eadz:feature/simplify-account-locking-and-creating
Open

[idea] simplify account locking / creation#230
eadz wants to merge 3 commits intoenvato:mainfrom
eadz:feature/simplify-account-locking-and-creating

Conversation

@eadz
Copy link
Copy Markdown
Contributor

@eadz eadz commented Mar 27, 2026

Changes to lib/double_entry/locking.rb

190 lines → 133 lines (30% reduction)

What was removed

  1. Thread-local {account => account_balance} cache — The old code stored locked AccountBalance records in Thread.current[:double_entry_locks] as a hash mapping accounts to their balance objects. This was used by balance_for_locked_account to avoid re-querying. The new code stores only the list of locked accounts (for nested-lock validation).
  2. Two-pass lock-then-retry pattern — The old perform_lock tried lock_and_call, and if any AccountBalance record was missing, it exited the transaction, created the missing records, then retried the entire lock. This required tracking @accounts_without_balances, a boolean return from grab_locks, and the LockDisaster failsafe.
  3. grab_locks method — A complex method that returned a boolean, set either the locks hash or the missing-accounts
    list, and mixed lock acquisition with existence checking.
  4. lock_and_call method — Wrapped the transaction/deadlock/yield/boolean-return dance.
  5. balance_for / lock? / locks / locks= / remove_locks — Five methods for the thread-local cache lifecycle.

What replaced it

  1. ensure_account_balances_exist — Creates missing AccountBalance records before the locking transaction (same create_ignoring_duplicates! for safety). This separates the "ensure records exist" concern from the "lock records" concern.
  2. lock_and_execute — A single-pass method: start a restartable transaction, lock all accounts with FOR UPDATE, yield, clean up. No boolean return, no retry.
  3. Simplified balance_for_locked_account — Instead of looking up a cached object from thread-local storage, it validates the lock is held then queries AccountBalance.find_by_account(account, lock: true) directly. Within the same transaction, re-acquiring a FOR UPDATE lock on an already-locked row is a no-op — the DB knows the lock is already held.

What stayed the same

  • All specs
  • Public API: lock_accounts, balance_for_locked_account, all error classes
  • Account sorting for deadlock prevention
  • Outermost transaction enforcement
  • Nested lock validation
  • Deadlock restart via restartable_transaction / with_restart_on_deadlock
  • LockDisaster class kept for backward compatibility (just not raised anymore)

Note: untested, AI generated, this class has just bugged me for a long time so thought I'd try something. All the tests pass, so really a question of how good are the tests.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant