Skip to content

Commit 0eb42a6

Browse files
feat: raise error when advisory lock cannot be acquired within configured timeout
1 parent 42186b8 commit 0eb42a6

4 files changed

Lines changed: 39 additions & 29 deletions

File tree

README.md

Lines changed: 2 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,4 @@
1-
# Closure Tree (OpsLevel Fork)
2-
3-
This is the OpsLevel fork of [closure_tree](https://github.com/ClosureTree/closure_tree), based on upstream v9.5.0.
4-
5-
## Fork-Specific Changes
6-
7-
This fork adds one enhancement to the advisory lock behavior:
8-
9-
### Advisory Lock Timeout with Error on Failure
10-
11-
- Adds `advisory_lock_timeout_seconds` option (default: 15 seconds)
12-
- Raises an error when the advisory lock cannot be acquired within the timeout period
13-
14-
The upstream version has no timeout and hangs forever if lock acquisition fails. This fork uses `with_advisory_lock!` (with bang) which raises `WithAdvisoryLock::FailedToAcquireLock` when the lock cannot be obtained, providing explicit failure behavior for lock contention issues.
15-
16-
```ruby
17-
class Tag < ApplicationRecord
18-
# Use default 15 second timeout
19-
has_closure_tree
20-
21-
# Or customize the timeout
22-
has_closure_tree advisory_lock_timeout_seconds: 30
23-
end
24-
```
25-
26-
---
1+
# Closure Tree
272

283
### Closure_tree lets your ActiveRecord models act as nodes in a [tree data structure](http://en.wikipedia.org/wiki/Tree_%28data_structure%29)
294

@@ -347,6 +322,7 @@ When you include ```has_closure_tree``` in your model, you can provide a hash to
347322
* ```:order``` used to set up [deterministic ordering](#deterministic-ordering)
348323
* ```:scope``` restricts root nodes and sibling ordering to specific columns. Can be a single symbol or an array of symbols. Example: ```scope: :user_id``` or ```scope: [:user_id, :group_id]```. This ensures that root nodes and siblings are scoped correctly when reordering. See [Ordering Roots](#ordering-roots) for more details.
349324
* ```:touch``` delegates to the `belongs_to` annotation for the parent, so `touch`ing cascades to all children (the performance of this for deep trees isn't currently optimal).
325+
* ```:advisory_lock_timeout_seconds``` Raises an error when the advisory lock cannot be acquired within the timeout period
350326
351327
## Accessing Data
352328

lib/closure_tree/has_closure_tree.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ def has_closure_tree(options = {})
1515
:touch,
1616
:with_advisory_lock,
1717
:advisory_lock_name,
18+
:advisory_lock_timeout_seconds,
1819
:scope
1920
)
2021

lib/closure_tree/support.rb

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ def initialize(model_class, options)
1919
dependent: :nullify, # or :destroy, :delete_all, or :adopt -- see the README
2020
name_column: 'name',
2121
with_advisory_lock: true, # This will be overridden by adapter support
22-
advisory_lock_timeout_seconds: 15,
22+
advisory_lock_timeout_seconds: nil,
2323
numeric_order: false
2424
}.merge(options)
2525
raise ArgumentError, "name_column can't be 'path'" if options[:name_column] == 'path'
@@ -31,6 +31,10 @@ def initialize(model_class, options)
3131
end
3232
end
3333

34+
if !options[:with_advisory_lock] && options[:advisory_lock_timeout_seconds].present?
35+
raise ArgumentError, "advisory_lock_timeout_seconds can't be specified when advisory_lock is disabled"
36+
end
37+
3438
return unless order_is_numeric?
3539

3640
extend NumericOrderSupport.adapter_for_connection(connection)
@@ -155,7 +159,9 @@ def build_scope_where_clause(scope_conditions)
155159

156160
def with_advisory_lock(&block)
157161
if options[:with_advisory_lock] && connection.supports_advisory_locks? && model_class.respond_to?(:with_advisory_lock!)
158-
model_class.with_advisory_lock!(advisory_lock_name, advisory_lock_options) do
162+
lock_method = options[:advisory_lock_timeout_seconds].present? ? :with_advisory_lock! : :with_advisory_lock
163+
164+
model_class.public_send(lock_method, advisory_lock_name, advisory_lock_options) do
159165
transaction(&block)
160166
end
161167
else

test/closure_tree/support_test.rb

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
require 'test_helper'
44

55
describe ClosureTree::Support do
6-
let(:sut) { Tag._ct }
6+
let(:model) { Tag }
7+
let(:options) { {} }
8+
let(:sut) { ClosureTree::Support.new(model, options) }
79

810
it 'passes through table names without prefix and suffix' do
911
expected = 'some_random_table_name'
@@ -15,4 +17,29 @@
1517
tn = ActiveRecord::Base.table_name_prefix + expected + ActiveRecord::Base.table_name_suffix
1618
assert_equal expected, sut.remove_prefix_and_suffix(tn)
1719
end
20+
21+
it 'calls :with_advisory_lock! when with_advisory_lock is true and timeout is 10' do
22+
sut = ClosureTree::Support.new(model, { with_advisory_lock: true, advisory_lock_timeout_seconds: 10 })
23+
called = false
24+
model.stub(:with_advisory_lock!, ->(_lock_name, _options, &block) { block.call }) do
25+
sut.with_advisory_lock { called = true }
26+
end
27+
assert called, 'block should have been called'
28+
end
29+
30+
it 'calls :with_advisory_lock when with_advisory_lock is true and timeout is nil' do
31+
sut = ClosureTree::Support.new(model, { with_advisory_lock: true, advisory_lock_timeout_seconds: nil })
32+
called = false
33+
model.stub(:with_advisory_lock, ->(_lock_name, _options, &block) { block.call }) do
34+
sut.with_advisory_lock { called = true }
35+
end
36+
assert called, 'block should have been called'
37+
end
38+
39+
it 'does not call advisory lock methods when with_advisory_lock is false' do
40+
sut = ClosureTree::Support.new(model, { with_advisory_lock: false, advisory_lock_timeout_seconds: nil })
41+
called = false
42+
sut.with_advisory_lock { called = true }
43+
assert called, 'block should have been called'
44+
end
1845
end

0 commit comments

Comments
 (0)