Module: IdempotencyLock
- Defined in:
- lib/idempotency_lock.rb,
lib/idempotency_lock/lock.rb,
lib/idempotency_lock/result.rb,
lib/idempotency_lock/railtie.rb,
lib/idempotency_lock/version.rb,
lib/generators/idempotency_lock/install_generator.rb
Overview
Database-backed idempotency locks for Ruby on Rails.
Ensures operations run exactly once using database-backed locks with support for TTL expiration, multiple error handling strategies, and clean return value handling.
Defined Under Namespace
Modules: Generators Classes: Error, Lock, Railtie, Result
Constant Summary collapse
- ON_ERROR_UNLOCK =
Error handling strategies
:unlock- ON_ERROR_KEEP_LOCKED =
Remove lock so operation can be retried
:keep- ON_ERROR_RAISE =
Keep lock in place (default)
:raise- WAIT_RETRY_INTERVAL =
Wait retry interval for temporarily with wait option
0.1- VERSION =
"0.2.0"
Class Attribute Summary collapse
Class Method Summary collapse
-
.cleanup_expired ⇒ Integer
Clean up all expired locks.
-
.locked?(name) ⇒ Boolean
Check if an operation is currently locked.
-
.once(name, ttl: nil, on_error: ON_ERROR_KEEP_LOCKED) { ... } ⇒ Result
(also: wrap)
Execute a block exactly once for the given operation name.
-
.release(name) ⇒ Boolean
Release a lock manually (useful for testing or manual intervention).
-
.temporarily(name, ttl: nil, wait: nil) { ... } ⇒ Result
Execute a block with a temporary lock that is automatically released when the block completes (success or error).
Class Attribute Details
.logger ⇒ Object
120 121 122 |
# File 'lib/idempotency_lock.rb', line 120 def logger @logger ||= defined?(Rails) ? Rails.logger : nil end |
Class Method Details
.cleanup_expired ⇒ Integer
Clean up all expired locks
113 114 115 |
# File 'lib/idempotency_lock.rb', line 113 def cleanup_expired Lock.cleanup_expired end |
.locked?(name) ⇒ Boolean
Check if an operation is currently locked
106 107 108 |
# File 'lib/idempotency_lock.rb', line 106 def locked?(name) Lock.locked?(name) end |
.once(name, ttl: nil, on_error: ON_ERROR_KEEP_LOCKED) { ... } ⇒ Result Also known as: wrap
Execute a block exactly once for the given operation name.
41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
# File 'lib/idempotency_lock.rb', line 41 def once(name, ttl: nil, on_error: ON_ERROR_KEEP_LOCKED, &) raise ArgumentError, "Block required" unless block_given? now = Time.current expires_at = calculate_expires_at(ttl, now) acquired = Lock.acquire(name, expires_at: expires_at, now: now) unless acquired log_debug("Lock already exists for '#{name}', skipping execution") return Result.new(executed: false, skipped: true) end log_debug("Lock acquired for '#{name}', executing block") execute_with_lock(name, on_error, &) end |
.release(name) ⇒ Boolean
Release a lock manually (useful for testing or manual intervention)
98 99 100 |
# File 'lib/idempotency_lock.rb', line 98 def release(name) Lock.release(name) end |
.temporarily(name, ttl: nil, wait: nil) { ... } ⇒ Result
Execute a block with a temporary lock that is automatically released when the block completes (success or error).
Unlike once, this method always releases the lock after execution,
making it suitable for mutex-style synchronization rather than idempotency.
71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 |
# File 'lib/idempotency_lock.rb', line 71 def temporarily(name, ttl: nil, wait: nil) raise ArgumentError, "Block required" unless block_given? acquired = acquire_with_wait(name, ttl: ttl, wait: wait) unless acquired log_debug("Lock already held for '#{name}', skipping execution") return Result.new(executed: false, skipped: true) end log_debug("Temporary lock acquired for '#{name}', executing block") begin value = yield Result.new(executed: true, value: value) rescue StandardError => e log_error("Exception in temporary lock block '#{name}': #{e.class} - #{e.}") Result.new(executed: true, error: e) ensure Lock.release(name) log_debug("Temporary lock released for '#{name}'") end end |