activejob-unique 0.5.0 → 0.5.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: 061e508b94c278bf8ccd34aa73f19461625922aec84783d6337968342ef7a8db
4
- data.tar.gz: 135d045c623b181d2bb86bd2234b72f25c84f7c4549829e4d40d0908fd4dd838
3
+ metadata.gz: 3d3d98846579f10faf49bb988ff0a81d4d314698d973d9456a39f034aed7a2c5
4
+ data.tar.gz: 6b8ab6c42aaf7b66fc5bbdd3eefbff5a08f7432be9d3d9e33b196c490a1c9c06
5
5
  SHA512:
6
- metadata.gz: 14b1956520105caeef38dcb7e5c4b5f51dbc197d575f3f839d6d9d5a7ca2502cda85ebeacf56b7dff206589566bc1680fa20707d7437a74c95509706e4ce253b
7
- data.tar.gz: a821c7491fd5f0768863d0ed475ea72f81ee318ce19ac5999765c6bde3f156bda1e78baeda5390e0fb0dc73d14f9453b20bc10365f9a5c0352f4452a2dcf9b1e
6
+ metadata.gz: 87c694b5db932ec8eb855a22e0318a4b71d522489310a6347340024e77ce0c27a56697054e7ff2d0a12c3f65041f43de36de4ffb3c9e337bb760ae6a8947e0c9
7
+ data.tar.gz: 3fcc445bc7487fa1a1e97efa56779d66ee37354f1f2c39758f6964d1f63afc8ccad5361caf92fd63ef643dbb66afab63bcd08ad3fdeda52c26ec5ba39b38f716
data/CHANGELOG.md CHANGED
@@ -3,13 +3,34 @@ All notable changes to this project will be documented in this file.
3
3
 
4
4
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
5
5
 
6
- ## [Unreleased](https://github.com/nordinvestments/activejob-uniqueness/compare/v0.5.0...HEAD)
6
+ ## [Unreleased](https://github.com/nordinvestments/activejob-uniqueness/compare/v0.5.1...HEAD)
7
+
8
+ ## [0.5.1](https://github.com/nordinvestments/activejob-uniqueness/compare/v0.5.0...v0.5.1) - 2026-08-25
9
+
10
+ ### Fixed
11
+ - [#11](https://github.com/nordinvestments/activejob-uniqueness/pull/11) `TestLockManager` now stubs the full public API of `LockManager` so `test_mode!` no longer raises `NoMethodError` for methods like `locked?`, `unlock`, `lock!`, `valid_lock?` or the TTL lookups ([#7](https://github.com/nordinvestments/activejob-uniqueness/issues/7))
12
+ - Release the correct lock when Sidekiq deletes a job with custom lock key arguments
13
+ - Allow Sidekiq to delete jobs whose ActiveJob class no longer exists
14
+ - Preserve uniqueness locks when Sidekiq moves jobs between sets and queues
15
+ - [#10](https://github.com/nordinvestments/activejob-uniqueness/pull/10) Release the lock for jobs that override `#lock_key` when deleted via the Sidekiq API, instead of a class + arguments wildcard that never matches a custom key
16
+ - [#10](https://github.com/nordinvestments/activejob-uniqueness/pull/10) Only release enqueue locks during Sidekiq queue/set cleanup: the runtime lock of an `:until_and_while_executing` job and the `:while_executing` guard (which is not an enqueue lock) are left untouched, so removing a queued duplicate can no longer free the guard of an instance that is currently executing
17
+
18
+ ### Changed
19
+ - Added `gemfiles/activejob_8.0.x.gemfile` to the CI matrix, so ActiveJob 8.0 (allowed by the gemspec) is now exercised
7
20
 
8
21
  ## [0.5.0](https://github.com/nordinvestments/activejob-uniqueness/compare/v0.4.0...v0.5.0) - 2026-01-12
9
22
 
23
+ ### Added
24
+ - [#4](https://github.com/nordinvestments/activejob-uniqueness/pull/4) Support for Rails 8.1 (ActiveJob `>= 7.1, < 8.2`) and Sidekiq 8
25
+ - CI coverage for ActiveJob 8.1 and Sidekiq 8 (`gemfiles/activejob_8.1.x.gemfile`, `gemfiles/sidekiq_8.x.gemfile`)
26
+
10
27
  ### Changed
11
28
  - Fork published as `activejob-unique` gem by [@nordinvestments](https://github.com/nordinvestments)
12
29
  - Updated gem metadata to point to fork repository
30
+ - Replaced the deprecated `ActiveSupport::Configurable` in `ActiveJob::Uniqueness::Configuration`
31
+ with `class_attribute`, required for ActiveSupport 8.1 compatibility
32
+ - Minimum supported Ruby raised to 3.1
33
+ - ActiveJob 8.1 and Sidekiq 8 require Ruby >= 3.2, encoded as `exclude:` entries in the CI matrix
13
34
 
14
35
  ---
15
36
 
data/README.md CHANGED
@@ -11,9 +11,12 @@
11
11
  | Dependency | Version |
12
12
  |------------|---------|
13
13
  | Ruby | >= 3.1 |
14
- | Rails (ActiveJob) | >= 7.1, < 7.3 |
14
+ | Rails (ActiveJob) | >= 7.1, < 8.2 |
15
+ | Sidekiq (optional) | 6.x - 8.x (CI-tested) |
15
16
  | Redis | >= 2.8 |
16
17
 
18
+ > **Note:** ActiveJob 8.1 and Sidekiq 8 require Ruby >= 3.2.
19
+
17
20
  > **Note:** This fork targets modern Ruby and Rails versions. For older Ruby (2.5-3.0) or Rails (4.2-7.0) support, use the [original gem](https://github.com/veeqo/activejob-uniqueness).
18
21
 
19
22
  ## Overview
@@ -13,18 +13,54 @@ module ActiveJob
13
13
  def self.unlock_sidekiq_job!(job_data)
14
14
  return unless SIDEKIQ_JOB_WRAPPERS.include?(job_data['class'])
15
15
 
16
- job = ActiveJob::Base.deserialize(job_data.fetch('args').first)
16
+ job = deserialize_sidekiq_job(job_data)
17
17
 
18
- return unless job.class.lock_strategy_class
18
+ return unless job&.class&.lock_strategy_class
19
+ return unless locks_on_enqueue?(job)
19
20
 
20
- begin
21
- job.send(:deserialize_arguments_if_needed)
22
- rescue ActiveJob::DeserializationError
23
- # Most probably, GlobalID fails to locate AR record (record is deleted)
24
- else
25
- ActiveJob::Uniqueness.unlock!(job_class_name: job.class.name, arguments: job.arguments)
26
- end
21
+ lock_key = enqueue_lock_key(job)
22
+ return unless lock_key
23
+
24
+ ActiveJob::Uniqueness.lock_manager.delete_lock(lock_key)
25
+ end
26
+
27
+ # Only strategies that lock on enqueue store an enqueue lock under #lock_key.
28
+ # :while_executing does not lock on enqueue: its #lock_key holds the *runtime*
29
+ # guard of a currently-executing instance, so cleaning up a queued duplicate
30
+ # must not touch it. Strategies advertise this via #locks_on_enqueue?, which
31
+ # custom strategies can override.
32
+ def self.locks_on_enqueue?(job)
33
+ job.class.lock_strategy_class.locks_on_enqueue?
34
+ end
35
+ private_class_method :locks_on_enqueue?
36
+
37
+ def self.deserialize_sidekiq_job(job_data)
38
+ serialized_job = job_data.fetch('args').first
39
+
40
+ # ActiveJob only wraps a missing job class in UnknownJobClassError as of version 8.1
41
+ return unless serialized_job.fetch('job_class').safe_constantize
42
+
43
+ ActiveJob::Base.deserialize(serialized_job)
44
+ end
45
+ private_class_method :deserialize_sidekiq_job
46
+
47
+ # Returns the enqueue lock key to release, or nil when it cannot be rebuilt.
48
+ #
49
+ # The key is rebuilt from the job's own #lock_key, so custom overrides are
50
+ # honored (a class + arguments wildcard never matched them). ActiveJob does
51
+ # not deserialize arguments until asked, so we trigger it explicitly: if a
52
+ # referenced record no longer exists (e.g. a deleted GlobalID) it raises
53
+ # DeserializationError and we skip the unlock, letting the lock expire by its
54
+ # TTL. Rebuilding a key from an incomplete argument list and deleting it could
55
+ # free another job's live lock, so skipping is the safe choice. Any other
56
+ # error is a genuine bug (e.g. in a custom #lock_key) and is left to surface.
57
+ def self.enqueue_lock_key(job)
58
+ job.send(:deserialize_arguments_if_needed)
59
+ job.lock_key
60
+ rescue ActiveJob::DeserializationError
61
+ nil
27
62
  end
63
+ private_class_method :enqueue_lock_key
28
64
 
29
65
  module SidekiqPatch
30
66
  module SortedEntry
@@ -32,15 +68,6 @@ module ActiveJob
32
68
  ActiveJob::Uniqueness.unlock_sidekiq_job!(item) if super
33
69
  item
34
70
  end
35
-
36
- private
37
-
38
- def remove_job
39
- super do |message|
40
- ActiveJob::Uniqueness.unlock_sidekiq_job!(Sidekiq.load_json(message))
41
- yield message
42
- end
43
- end
44
71
  end
45
72
 
46
73
  module ScheduledSet
@@ -13,6 +13,18 @@ module ActiveJob
13
13
 
14
14
  attr_reader :lock_key, :lock_ttl, :on_conflict, :on_redis_connection_error, :job
15
15
 
16
+ # Whether the strategy acquires its #lock_key at enqueue time.
17
+ #
18
+ # Sidekiq queue/set cleanup only releases enqueue locks, so a strategy
19
+ # whose #lock_key is instead a runtime guard (e.g. :while_executing) must
20
+ # report false to avoid freeing the guard of a currently executing
21
+ # instance. Built-in enqueue-locking strategies signal this by including
22
+ # LockingOnEnqueue; a custom strategy that locks on enqueue by other means
23
+ # can override this method to return true.
24
+ def self.locks_on_enqueue?
25
+ include?(LockingOnEnqueue)
26
+ end
27
+
16
28
  def initialize(job:)
17
29
  @lock_key = job.lock_key
18
30
  @lock_ttl = (job.lock_options[:lock_ttl] || config.lock_ttl).to_i * 1000 # ms
@@ -2,15 +2,52 @@
2
2
 
3
3
  module ActiveJob
4
4
  module Uniqueness
5
- # Mocks ActiveJob::Uniqueness::LockManager methods.
5
+ # Mocks the full public API of ActiveJob::Uniqueness::LockManager so that
6
+ # enabling ActiveJob::Uniqueness.test_mode! never raises NoMethodError for a
7
+ # method the real manager (a Redlock::Client subclass) responds to.
8
+ #
9
+ # Return values are consistent with "no lock is ever actually held": locking
10
+ # always succeeds, nothing is ever locked, and TTL lookups are nil.
11
+ #
6
12
  # See ActiveJob::Uniqueness.test_mode!
7
13
  class TestLockManager
8
14
  def lock(*_args)
15
+ block_given? ? yield(true) : true
16
+ end
17
+
18
+ def lock!(*args, &block)
19
+ return lock(*args, &block) if block
20
+
21
+ true
22
+ end
23
+
24
+ def unlock(*_args)
25
+ nil
26
+ end
27
+
28
+ def locked?(*_args)
29
+ false
30
+ end
31
+
32
+ def valid_lock?(*_args)
33
+ false
34
+ end
35
+
36
+ def get_remaining_ttl_for_lock(*_args)
37
+ nil
38
+ end
39
+
40
+ def get_remaining_ttl_for_resource(*_args)
41
+ nil
42
+ end
43
+
44
+ def delete_lock(*_args)
9
45
  true
10
46
  end
11
47
 
12
- alias delete_lock lock
13
- alias delete_locks lock
48
+ def delete_locks(*_args)
49
+ true
50
+ end
14
51
  end
15
52
  end
16
53
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module ActiveJob
4
4
  module Uniqueness
5
- VERSION = '0.5.0'
5
+ VERSION = '0.5.1'
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activejob-unique
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.5.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rustam Sharshenov