Skip to content

feat: scope advisory lock names by scope column values#490

Open
zakky21 wants to merge 1 commit intoClosureTree:masterfrom
zakky21:feat/scope-advisory-lock
Open

feat: scope advisory lock names by scope column values#490
zakky21 wants to merge 1 commit intoClosureTree:masterfrom
zakky21:feat/scope-advisory-lock

Conversation

@zakky21
Copy link
Copy Markdown

@zakky21 zakky21 commented Apr 8, 2026

Summary

  • When scope: option is configured (e.g., scope: :company_id), advisory lock names now include the scope values from the instance
  • This prevents unnecessary lock contention across different tenants in multi-tenant environments
  • Previously, all operations shared a single lock per model class (ct_{CRC32(class_name)}). Now, scoped models use per-scope locks (e.g., ct_{CRC32(class_name)}_{company_id})

Changes

  • Add advisory_lock_name_for(instance) method to SupportAttributes that appends scope values to the base lock name
  • Update with_advisory_lock in Support to accept an optional instance argument
  • Pass self at all 5 instance-method call sites (_ct_before_destroy, rebuild!, delete_hierarchy_references, add_sibling, find_or_create_by_path)
  • Class-method call sites (rebuild!, find_or_create_by_path) pass nil, falling back to model-wide lock

Backward Compatibility

  • instance argument defaults to nil — existing callers are unaffected
  • Unscoped models return the same lock name as before
  • Custom advisory_lock_name option is preserved as the base name

Test plan

  • Scoped model (scope: :user_id) generates lock name with scope value suffix
  • Different scope values produce different lock names
  • Same scope values produce identical lock names
  • Multi-scope model (scope: [:user_id, :group_id]) includes all scope values
  • Unscoped model returns base lock name unchanged
  • nil instance returns base lock name (class-method fallback)
  • Full test suite passes (332 tests, 1123 assertions, 0 failures)

Related: #240 (deadlocks during concurrent operations)

When `scope:` option is configured (e.g., `scope: :company_id`),
advisory lock names now include the scope values from the instance.
This prevents unnecessary lock contention across different tenants
in multi-tenant environments.

Previously, all operations shared a single lock per model class
(`ct_{CRC32(class_name)}`). Now, scoped models use per-scope locks
(e.g., `ct_{CRC32(class_name)}_{company_id}`), allowing concurrent
operations on different tenants.

- Add `advisory_lock_name_for(instance)` to SupportAttributes
- Update `with_advisory_lock` to accept an optional instance argument
- Pass `self` at all instance-method call sites (5 locations)
- Class-method call sites pass `nil` (fallback to model-wide lock)
- Fully backward compatible: no change for unscoped models or
  existing callers without the instance argument
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