closure_tree 9.6.2 → 9.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7326f71d858362cf125f0db24593f22441bcfb1c1fc81124036d8d4cdaa7110f
4
- data.tar.gz: 0b14cd7c237b4b48e7f04777c213dcaed3cfe526eb74d26672f1736ee5bf89ec
3
+ metadata.gz: 3b33b2d3bff238b8d3a42e41b94ece6f9b6c9efbcacd23628972d6237f2c3bda
4
+ data.tar.gz: 82a70b8fd95a7bdc6726e3408765027ff45caaa6c21080326966d39eeba89dd8
5
5
  SHA512:
6
- metadata.gz: 53ae09bebff724ee6540d5b274330ef489726dfd690a164774a140382bc631ee0e10a8f4ad522e04f8a997714567bb2151d514b203aa6b14bc24785507a3c660
7
- data.tar.gz: 14cc8606e54f4adf1de9331edc0833e7e4521a3b8a5fbb0bf9a99dad2fe619c69e6f22362f549ee44c36b21785ec14a0804a0ed3dd83976ad4350e839a6aba61
6
+ metadata.gz: 51f1f00fcc5c81e2fa380540178f63581aa1f66bfc941777480fb28ac87b2fbd00881e067940f62b5ae031e7a38d16a71f6b4a5e2b757e444df6e4343be09e73
7
+ data.tar.gz: 8116299896bd8f6eacdbfd7a4ffc8453d2328bc773760cb4d028c4444280b46513efcd7587c806bdd2194df46a0fba53847f232df99f7eadff99a3596606e5f3
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [9.7.0](https://github.com/ClosureTree/closure_tree/compare/closure_tree/v9.6.2...closure_tree/v9.7.0) (2026-05-06)
4
+
5
+
6
+ ### Features
7
+
8
+ * allow advisory_lock_name proc to receive instance for per-tenant lock granularity ([#491](https://github.com/ClosureTree/closure_tree/issues/491)) ([a7a554c](https://github.com/ClosureTree/closure_tree/commit/a7a554c004fb865ab4ee721f929a8a00bb5cdc46))
9
+
3
10
  ## [9.6.2](https://github.com/ClosureTree/closure_tree/compare/closure_tree/v9.6.1...closure_tree/v9.6.2) (2026-04-07)
4
11
 
5
12
 
data/README.md CHANGED
@@ -560,7 +560,7 @@ class Tag < ApplicationRecord
560
560
  has_closure_tree advisory_lock_name: 'custom_tag_lock'
561
561
  end
562
562
 
563
- # Dynamic via Proc
563
+ # Dynamic via Proc (1-arity: receives model class only)
564
564
  class Tag < ApplicationRecord
565
565
  has_closure_tree advisory_lock_name: ->(model_class) { "#{Rails.env}_#{model_class.name.underscore}" }
566
566
  end
@@ -568,16 +568,36 @@ end
568
568
  # Delegate to model method
569
569
  class Tag < ApplicationRecord
570
570
  has_closure_tree advisory_lock_name: :custom_lock_name
571
-
571
+
572
572
  def self.custom_lock_name
573
573
  "tag_lock_#{current_tenant_id}"
574
574
  end
575
575
  end
576
576
  ```
577
577
 
578
+ #### Per-instance lock names (multi-tenant / scoped models)
579
+
580
+ Pass a 2-arity proc to receive both the model class and the current record instance.
581
+ This is the recommended approach for scoped models where each tenant should have its own lock,
582
+ avoiding unnecessary serialization across tenants.
583
+
584
+ ```ruby
585
+ class Node < ApplicationRecord
586
+ has_closure_tree scope: :company_id,
587
+ advisory_lock_name: ->(klass, instance) {
588
+ company = instance&.company_id
589
+ company ? "ct_#{klass.name}_#{company}" : "ct_#{klass.name}"
590
+ }
591
+ end
592
+ ```
593
+
594
+ When `instance` is `nil` (class-level operations like `Node.rebuild!`), the proc should
595
+ fall back to a model-wide name. Instance-level operations (`save`, `destroy`, `add_sibling`,
596
+ `find_or_create_by_path`) pass the record itself so the lock is scoped to that tenant.
597
+
578
598
  This is particularly useful when:
579
599
  * You need environment-specific lock names
580
- * You're using multi-tenancy and need tenant-specific locks
600
+ * You're using multi-tenancy and need per-tenant locks (avoiding cross-tenant contention)
581
601
  * You want to avoid lock name collisions between similar model names
582
602
 
583
603
  ## Multi-Database Support
@@ -20,7 +20,7 @@ module ClosureTree
20
20
  return found if found
21
21
 
22
22
  attrs = subpath.shift
23
- _ct.with_advisory_lock do
23
+ _ct.with_advisory_lock(self) do
24
24
  # shenanigans because children.create is bound to the superclass
25
25
  # (in the case of polymorphism):
26
26
  child = children.where(attrs).first || begin
@@ -64,7 +64,7 @@ module ClosureTree
64
64
  end
65
65
 
66
66
  def _ct_before_destroy
67
- _ct.with_advisory_lock do
67
+ _ct.with_advisory_lock(self) do
68
68
  _ct_adopt_children_to_grandparent if _ct.options[:dependent] == :adopt
69
69
  delete_hierarchy_references
70
70
  self.class.find(id).children.find_each(&:rebuild!) if _ct.options[:dependent] == :nullify
@@ -86,7 +86,7 @@ module ClosureTree
86
86
  end
87
87
 
88
88
  def rebuild!(called_by_rebuild = false)
89
- _ct.with_advisory_lock do
89
+ _ct.with_advisory_lock(self) do
90
90
  delete_hierarchy_references unless (defined? @was_new_record) && @was_new_record
91
91
  hierarchy_class.create!(ancestor: self, descendant: self, generations: 0)
92
92
  unless root?
@@ -112,7 +112,7 @@ module ClosureTree
112
112
  end
113
113
 
114
114
  def delete_hierarchy_references
115
- _ct.with_advisory_lock do
115
+ _ct.with_advisory_lock(self) do
116
116
  # The crazy double-wrapped sub-subselect works around MySQL's limitation of subselects on the same table that is being mutated.
117
117
  # It shouldn't affect performance of postgresql.
118
118
  # See http://dev.mysql.com/doc/refman/5.0/en/subquery-errors.html
@@ -162,7 +162,7 @@ module ClosureTree
162
162
  # Make sure self isn't dirty, because we're going to call reload:
163
163
  save
164
164
 
165
- _ct.with_advisory_lock do
165
+ _ct.with_advisory_lock(self) do
166
166
  prior_sibling_parent = sibling.parent
167
167
 
168
168
  sibling.order_value = order_value
@@ -157,10 +157,10 @@ module ClosureTree
157
157
  " AND #{conditions.join(' AND ')}"
158
158
  end
159
159
 
160
- def with_advisory_lock(&block)
160
+ def with_advisory_lock(instance = nil, &block)
161
161
  lock_method = options[:advisory_lock_timeout_seconds].present? ? :with_advisory_lock! : :with_advisory_lock
162
162
  if options[:with_advisory_lock] && connection.supports_advisory_locks? && model_class.respond_to?(lock_method)
163
- model_class.public_send(lock_method, advisory_lock_name, advisory_lock_options) do
163
+ model_class.public_send(lock_method, advisory_lock_name(instance), advisory_lock_options) do
164
164
  transaction(&block)
165
165
  end
166
166
  else
@@ -8,28 +8,22 @@ module ClosureTree
8
8
  extend Forwardable
9
9
  def_delegators :model_class, :connection, :transaction, :table_name, :base_class, :inheritance_column, :column_names
10
10
 
11
- def advisory_lock_name
12
- # Allow customization via options or instance method
11
+ def advisory_lock_name(instance = nil)
13
12
  if options[:advisory_lock_name]
14
13
  case options[:advisory_lock_name]
15
14
  when Proc
16
- # Allow dynamic generation via proc
17
- options[:advisory_lock_name].call(base_class)
15
+ proc = options[:advisory_lock_name]
16
+ proc.arity == 1 ? proc.call(base_class) : proc.call(base_class, instance)
18
17
  when Symbol
19
- # Allow delegation to a model method
20
18
  if model_class.respond_to?(options[:advisory_lock_name])
21
19
  model_class.send(options[:advisory_lock_name])
22
20
  else
23
21
  raise ArgumentError, "Model #{model_class} does not respond to #{options[:advisory_lock_name]}"
24
22
  end
25
23
  else
26
- # Use static string value
27
24
  options[:advisory_lock_name].to_s
28
25
  end
29
26
  else
30
- # Default: Use CRC32 for a shorter, consistent hash
31
- # This gives us 8 hex characters which is plenty for uniqueness
32
- # and leaves room for prefixes
33
27
  "ct_#{Zlib.crc32(base_class.name.to_s).to_s(16)}"
34
28
  end
35
29
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ClosureTree
4
- VERSION = Gem::Version.new('9.6.2')
4
+ VERSION = Gem::Version.new('9.7.0')
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: closure_tree
3
3
  version: !ruby/object:Gem::Version
4
- version: 9.6.2
4
+ version: 9.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matthew McEachen
@@ -178,7 +178,7 @@ licenses:
178
178
  metadata:
179
179
  bug_tracker_uri: https://github.com/ClosureTree/closure_tree/issues
180
180
  changelog_uri: https://github.com/ClosureTree/closure_tree/blob/master/CHANGELOG.md
181
- documentation_uri: https://www.rubydoc.info/gems/closure_tree/9.6.2
181
+ documentation_uri: https://www.rubydoc.info/gems/closure_tree/9.7.0
182
182
  homepage_uri: https://closuretree.github.io/closure_tree/
183
183
  source_code_uri: https://github.com/ClosureTree/closure_tree
184
184
  rubygems_mfa_required: 'true'