feat: scope advisory lock names by scope column values#490
Open
zakky21 wants to merge 1 commit intoClosureTree:masterfrom
Open
feat: scope advisory lock names by scope column values#490zakky21 wants to merge 1 commit intoClosureTree:masterfrom
zakky21 wants to merge 1 commit intoClosureTree:masterfrom
Conversation
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
There was a problem hiding this comment.
Pull request overview
This PR updates ClosureTree’s advisory-lock strategy so that, when scope: is configured, lock names are derived from both the model class and the instance’s scope column values—reducing lock contention in multi-tenant / scoped data sets.
Changes:
- Add
advisory_lock_name_for(instance)to build scoped advisory-lock names. - Update
with_advisory_lockto accept an optionalinstanceand use the scoped lock name. - Pass
selfintowith_advisory_lockat key instance-method call sites; add tests for scoped lock naming.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| test/closure_tree/scoped_advisory_lock_test.rb | Adds coverage asserting scoped vs unscoped advisory lock name behavior. |
| lib/closure_tree/support_attributes.rb | Introduces advisory_lock_name_for to append scope-derived suffix to the base lock name. |
| lib/closure_tree/support.rb | Extends with_advisory_lock to accept an optional instance and use the new lock-name helper. |
| lib/closure_tree/numeric_deterministic_ordering.rb | Uses instance-scoped advisory locks during sibling reordering. |
| lib/closure_tree/hierarchy_maintenance.rb | Uses instance-scoped advisory locks during rebuild/delete/destroy maintenance operations. |
| lib/closure_tree/finders.rb | Uses instance-scoped advisory locks for instance find_or_create_by_path. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+44
to
+46
| suffix = scope_values.values.map(&:to_s).join('_') | ||
| "#{base}_#{suffix}" | ||
| end |
Comment on lines
+33
to
+36
| def test_advisory_lock_name_for_nil_instance_returns_base | ||
| item = ScopedItem.new(user_id: 1) | ||
| assert_equal item._ct.advisory_lock_name, item._ct.advisory_lock_name_for(nil) | ||
| end |
Comment on lines
+41
to
+45
| scope_values = scope_values_from_instance(instance) | ||
| return base if scope_values.empty? | ||
|
|
||
| suffix = scope_values.values.map(&:to_s).join('_') | ||
| "#{base}_#{suffix}" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
scope:option is configured (e.g.,scope: :company_id), advisory lock names now include the scope values from the instancect_{CRC32(class_name)}). Now, scoped models use per-scope locks (e.g.,ct_{CRC32(class_name)}_{company_id})Changes
advisory_lock_name_for(instance)method toSupportAttributesthat appends scope values to the base lock namewith_advisory_lockinSupportto accept an optionalinstanceargumentselfat all 5 instance-method call sites (_ct_before_destroy,rebuild!,delete_hierarchy_references,add_sibling,find_or_create_by_path)rebuild!,find_or_create_by_path) passnil, falling back to model-wide lockBackward Compatibility
instanceargument defaults tonil— existing callers are unaffectedadvisory_lock_nameoption is preserved as the base nameTest plan
scope: :user_id) generates lock name with scope value suffixscope: [:user_id, :group_id]) includes all scope valuesnilinstance returns base lock name (class-method fallback)Related: #240 (deadlocks during concurrent operations)