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 +4 -4
- data/CHANGELOG.md +22 -1
- data/README.md +4 -1
- data/lib/active_job/uniqueness/sidekiq_patch.rb +45 -18
- data/lib/active_job/uniqueness/strategies/base.rb +12 -0
- data/lib/active_job/uniqueness/test_lock_manager.rb +40 -3
- data/lib/active_job/uniqueness/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3d3d98846579f10faf49bb988ff0a81d4d314698d973d9456a39f034aed7a2c5
|
|
4
|
+
data.tar.gz: 6b8ab6c42aaf7b66fc5bbdd3eefbff5a08f7432be9d3d9e33b196c490a1c9c06
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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, <
|
|
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 =
|
|
16
|
+
job = deserialize_sidekiq_job(job_data)
|
|
17
17
|
|
|
18
|
-
return unless job
|
|
18
|
+
return unless job&.class&.lock_strategy_class
|
|
19
|
+
return unless locks_on_enqueue?(job)
|
|
19
20
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
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
|
-
|
|
13
|
-
|
|
48
|
+
def delete_locks(*_args)
|
|
49
|
+
true
|
|
50
|
+
end
|
|
14
51
|
end
|
|
15
52
|
end
|
|
16
53
|
end
|