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
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_lock to accept an optional instance and use the scoped lock name.
  • Pass self into with_advisory_lock at 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}"
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.

2 participants