idempotency_lock 0.1.0 → 0.1.1

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: 2433a11192a9d10ad05520f94d7b60854531c2c23ee0e2c674318c34aa77739c
4
- data.tar.gz: 6b7e54105e42bee7122d20670a9bfc18192b4d0846e58b131939cf46274fa98f
3
+ metadata.gz: 3472cab3f3ed5118fb6b5843141d81c215efd5d0c7c7f1a12e88e3989a10315e
4
+ data.tar.gz: e070c49739921f73928eecc9ad94b97ce316c6cef1f4357ea109e211360c5ca5
5
5
  SHA512:
6
- metadata.gz: 102949944e74ef10b4f9ba6d3d62221abb307aaf79da156b6e25277eea4fd11032a435a1ea726044e228635c30e00511d7df46a10fe5f66621bf24dac901721b
7
- data.tar.gz: 110b05bb0e03d952e5a44051dc4dfbb51c728e06f63e58aa46d2379482ab34ee261a39410fed5d90db4c0abe800948cfb509b4f4a17ec03d0a2ffb8b5e2513e0
6
+ metadata.gz: adf210196f47721787c1bc87e22de3c83ec787cf0b2f9c5b2a630e52aa610ee86c3e1124c19e2cea0401fbfc92a6ebcc795d94a735539eec55d57967e6ab70a1
7
+ data.tar.gz: b85bc4c3e9d4713d3e75bbe0032964d746e5eccc13b185937bfcf66617e8542c390414920e8478429812198f5680ef03ce28f07482c017b15e9c9a8a8053120e
data/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.1.1] - 2025-12-06
4
+
5
+ ### Fixed
6
+
7
+ - Use optimistic locking when claiming expired locks to prevent race conditions
8
+ where two processes with clock skew could both claim the same lock
9
+
10
+ ### Changed
11
+
12
+ - `now` parameter is now injectable in `Lock.acquire`, `expired?`, and `valid_lock?` for testing
13
+
3
14
  ## [0.1.0] - 2025-12-06
4
15
 
5
16
  ### Added
@@ -24,14 +24,14 @@ module IdempotencyLock
24
24
 
25
25
  # Check if this lock has expired
26
26
  # @return [Boolean]
27
- def expired?
28
- expires_at.present? && expires_at < Time.current
27
+ def expired?(now: Time.current)
28
+ expires_at.present? && expires_at < now
29
29
  end
30
30
 
31
31
  # Check if this lock is still valid (not expired)
32
32
  # @return [Boolean]
33
- def valid_lock?
34
- expires_at.nil? || expires_at >= Time.current
33
+ def valid_lock?(now: Time.current)
34
+ expires_at.nil? || expires_at >= now
35
35
  end
36
36
 
37
37
  class << self
@@ -40,7 +40,7 @@ module IdempotencyLock
40
40
  #
41
41
  # @param name [String] unique identifier for the operation
42
42
  # @param expires_at [Time, nil] when the lock should expire (nil = never)
43
- # @param now [Time] current time reference
43
+ # @param now [Time] current time reference (injectable for testing)
44
44
  # @return [Boolean] true if lock was acquired
45
45
  def acquire(name, expires_at: nil, now: Time.current)
46
46
  # Try to insert first (most common case for new keys)
@@ -51,10 +51,17 @@ module IdempotencyLock
51
51
  # Lock exists, check if it's expired
52
52
  end
53
53
 
54
- # Atomic update: claim the lock only if it's expired
55
- # Returns number of rows updated (0 or 1)
56
- updated = where(name: name)
57
- .where("expires_at IS NOT NULL AND expires_at < ?", now)
54
+ # Check if an existing lock is expired and can be claimed
55
+ existing = find_by(name: name)
56
+ return false if existing.nil?
57
+ return false unless existing.expired?(now: now)
58
+
59
+ # Optimistic locking: use the observed expires_at in the WHERE clause.
60
+ # This prevents a race where two processes both see an expired lock and
61
+ # both try to claim it. Only one UPDATE will match; the other returns 0.
62
+ # This is safer than `WHERE expires_at < now` which could allow a second
63
+ # process to steal a lock if the first used a short TTL.
64
+ updated = where(name: name, expires_at: existing.expires_at)
58
65
  .update_all(expires_at: expires_at, executed_at: now, updated_at: now)
59
66
 
60
67
  updated.positive?
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module IdempotencyLock
4
- VERSION = "0.1.0"
4
+ VERSION = "0.1.1"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: idempotency_lock
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - John Nagro