activejob-uniqueness 0.3.1 → 0.4.0
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 +20 -1
- data/README.md +26 -0
- data/lib/active_job/uniqueness/active_job_patch.rb +4 -1
- data/lib/active_job/uniqueness/configuration.rb +13 -0
- data/lib/active_job/uniqueness/lock_manager.rb +7 -1
- data/lib/active_job/uniqueness/log_subscriber.rb +68 -66
- data/lib/active_job/uniqueness/sidekiq_patch.rb +2 -2
- data/lib/active_job/uniqueness/strategies/base.rb +16 -1
- data/lib/active_job/uniqueness/version.rb +1 -1
- data/lib/generators/active_job/uniqueness/templates/config/initializers/active_job_uniqueness.rb +7 -0
- metadata +4 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cd76f286ed7e6482860247e9b98b754dc1b73a381fcd2579d73e469da95d0200
|
4
|
+
data.tar.gz: 866f0949c68745c168217cbc70b10c65799f70aa372902dbd145781726217acd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5cb9299a82cc40a9fca3df73c771cf553ead9393b11eca505afea5e55fe3023722db67d509528d0886db66e64569ba1caea04622ebde3ca365df1d768f04d2f4
|
7
|
+
data.tar.gz: 205d78f3c41721bce16c16d991204423fe5429b23c1d6fad48eb1a4ac7f775057f7e15192e8d1c32c97fc0c2c4af60a1584d6a666c253df71e1acce9ba02e5fa
|
data/CHANGELOG.md
CHANGED
@@ -3,7 +3,26 @@ 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/veeqo/activejob-uniqueness/compare/v0.
|
6
|
+
## [Unreleased](https://github.com/veeqo/activejob-uniqueness/compare/v0.4.0...HEAD)
|
7
|
+
|
8
|
+
|
9
|
+
## [0.4.0](https://github.com/veeqo/activejob-uniqueness/compare/v0.3.2...v0.4.0) - 2024-12-07
|
10
|
+
|
11
|
+
### Added
|
12
|
+
|
13
|
+
- [#86](https://github.com/veeqo/activejob-uniqueness/pull/86) Add Rails 8.0 rc1 support by[@sharshenov](https://github.com/sharshenov)
|
14
|
+
- [#78](https://github.com/veeqo/activejob-uniqueness/pull/78) Add on_redis_connection_error config to adjust to new redlock behaviour by[@nduitz](https://github.com/nduitz)
|
15
|
+
|
16
|
+
### Changed
|
17
|
+
- [#82](https://github.com/veeqo/activejob-uniqueness/pull/82) Optimize bulk unlocking [@sharshenov](https://github.com/sharshenov)
|
18
|
+
|
19
|
+
## [0.3.2](https://github.com/veeqo/activejob-uniqueness/compare/v0.3.1...v0.3.2) - 2024-08-16
|
20
|
+
|
21
|
+
### Added
|
22
|
+
- [#80](https://github.com/veeqo/activejob-uniqueness/pull/80) Add rails 7.2 support by [@viralpraxis](https://github.com/viralpraxis)
|
23
|
+
|
24
|
+
### Changed
|
25
|
+
- [#74](https://github.com/veeqo/activejob-uniqueness/pull/74) Fix log subscriber by [@shahidkhaliq](https://github.com/shahidkhaliq)
|
7
26
|
|
8
27
|
## [0.3.1](https://github.com/veeqo/activejob-uniqueness/compare/v0.3.0...v0.3.1) - 2023-10-30
|
9
28
|
|
data/README.md
CHANGED
@@ -43,6 +43,19 @@ To override the defaults, create an initializer `config/initializers/active_job_
|
|
43
43
|
rails generate active_job:uniqueness:install
|
44
44
|
```
|
45
45
|
|
46
|
+
This gem relies on `redlock` for it's Redis connection, that means **it will not inherit global configuration of `Sidekiq`**. To configure the connection, you can use `config.redlock_servers`, for example to disable SSL verification for Redis/Key-Value cloud providers:
|
47
|
+
|
48
|
+
```ruby
|
49
|
+
ActiveJob::Uniqueness.configure do |config|
|
50
|
+
config.redlock_servers = [
|
51
|
+
RedisClient.new(
|
52
|
+
url: ENV['REDIS_URL'],
|
53
|
+
ssl_params: { verify_mode: OpenSSL::SSL::VERIFY_NONE }
|
54
|
+
)
|
55
|
+
]
|
56
|
+
end
|
57
|
+
```
|
58
|
+
|
46
59
|
## Usage
|
47
60
|
|
48
61
|
|
@@ -87,6 +100,19 @@ class MyJob < ActiveJob::Base
|
|
87
100
|
end
|
88
101
|
```
|
89
102
|
|
103
|
+
### Control redis connection errors
|
104
|
+
|
105
|
+
```ruby
|
106
|
+
class MyJob < ActiveJob::Base
|
107
|
+
# Proc gets the job instance including its arguments, and as keyword arguments the resource(lock key) `resource` and the original error `error`
|
108
|
+
unique :until_executing, on_redis_connection_error: ->(job, resource: _, error: _) { job.logger.info "Oops: #{job.arguments}" }
|
109
|
+
|
110
|
+
def perform(args)
|
111
|
+
# work
|
112
|
+
end
|
113
|
+
end
|
114
|
+
```
|
115
|
+
|
90
116
|
### Control lock key arguments
|
91
117
|
|
92
118
|
```ruby
|
@@ -27,6 +27,7 @@ module ActiveJob
|
|
27
27
|
def unique(strategy, options = {})
|
28
28
|
validate_on_conflict_action!(options[:on_conflict])
|
29
29
|
validate_on_conflict_action!(options[:on_runtime_conflict])
|
30
|
+
validate_on_redis_connection_error!(options[:on_redis_connection_error])
|
30
31
|
|
31
32
|
self.lock_strategy_class = ActiveJob::Uniqueness::Strategies.lookup(strategy)
|
32
33
|
self.lock_options = options
|
@@ -40,7 +41,9 @@ module ActiveJob
|
|
40
41
|
|
41
42
|
private
|
42
43
|
|
43
|
-
delegate :validate_on_conflict_action!,
|
44
|
+
delegate :validate_on_conflict_action!,
|
45
|
+
:validate_on_redis_connection_error!,
|
46
|
+
to: :'ActiveJob::Uniqueness.config'
|
44
47
|
end
|
45
48
|
|
46
49
|
included do
|
@@ -14,6 +14,7 @@ module ActiveJob
|
|
14
14
|
config_accessor(:lock_ttl) { 86_400 } # 1.day
|
15
15
|
config_accessor(:lock_prefix) { 'activejob_uniqueness' }
|
16
16
|
config_accessor(:on_conflict) { :raise }
|
17
|
+
config_accessor(:on_redis_connection_error) { :raise }
|
17
18
|
config_accessor(:redlock_servers) { [ENV.fetch('REDIS_URL', 'redis://localhost:6379')] }
|
18
19
|
config_accessor(:redlock_options) { { retry_count: 0 } }
|
19
20
|
config_accessor(:lock_strategies) { {} }
|
@@ -34,6 +35,18 @@ module ActiveJob
|
|
34
35
|
|
35
36
|
raise ActiveJob::Uniqueness::InvalidOnConflictAction, "Unexpected '#{action}' action on conflict"
|
36
37
|
end
|
38
|
+
|
39
|
+
def on_redis_connection_error=(action)
|
40
|
+
validate_on_redis_connection_error!(action)
|
41
|
+
|
42
|
+
config.on_redis_connection_error = action
|
43
|
+
end
|
44
|
+
|
45
|
+
def validate_on_redis_connection_error!(action)
|
46
|
+
return if action.nil? || action == :raise || action.respond_to?(:call)
|
47
|
+
|
48
|
+
raise ActiveJob::Uniqueness::InvalidOnConflictAction, "Unexpected '#{action}' action on_redis_connection_error"
|
49
|
+
end
|
37
50
|
end
|
38
51
|
end
|
39
52
|
end
|
@@ -17,11 +17,17 @@ module ActiveJob
|
|
17
17
|
true
|
18
18
|
end
|
19
19
|
|
20
|
+
DELETE_LOCKS_SCAN_COUNT = 1000
|
21
|
+
|
20
22
|
# Unlocks multiple resources by key wildcard.
|
21
23
|
def delete_locks(wildcard)
|
22
24
|
@servers.each do |server|
|
23
25
|
synced_redis_connection(server) do |conn|
|
24
|
-
|
26
|
+
cursor = 0
|
27
|
+
while cursor != '0'
|
28
|
+
cursor, keys = conn.call('SCAN', cursor, 'MATCH', wildcard, 'COUNT', DELETE_LOCKS_SCAN_COUNT)
|
29
|
+
conn.call('UNLINK', *keys) unless keys.empty?
|
30
|
+
end
|
25
31
|
end
|
26
32
|
end
|
27
33
|
|
@@ -3,98 +3,100 @@
|
|
3
3
|
require 'active_support/log_subscriber'
|
4
4
|
|
5
5
|
module ActiveJob
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
6
|
+
module Uniqueness
|
7
|
+
class LogSubscriber < ActiveSupport::LogSubscriber # :nodoc:
|
8
|
+
def lock(event)
|
9
|
+
job = event.payload[:job]
|
10
|
+
resource = event.payload[:resource]
|
11
|
+
|
12
|
+
debug do
|
13
|
+
"Locked #{lock_info(job, resource)}" + args_info(job)
|
14
|
+
end
|
13
15
|
end
|
14
|
-
end
|
15
16
|
|
16
|
-
|
17
|
-
|
18
|
-
|
17
|
+
def runtime_lock(event)
|
18
|
+
job = event.payload[:job]
|
19
|
+
resource = event.payload[:resource]
|
19
20
|
|
20
|
-
|
21
|
-
|
21
|
+
debug do
|
22
|
+
"Locked runtime #{lock_info(job, resource)}" + args_info(job)
|
23
|
+
end
|
22
24
|
end
|
23
|
-
end
|
24
25
|
|
25
|
-
|
26
|
-
|
27
|
-
|
26
|
+
def unlock(event)
|
27
|
+
job = event.payload[:job]
|
28
|
+
resource = event.payload[:resource]
|
28
29
|
|
29
|
-
|
30
|
-
|
30
|
+
debug do
|
31
|
+
"Unlocked #{lock_info(job, resource)}"
|
32
|
+
end
|
31
33
|
end
|
32
|
-
end
|
33
34
|
|
34
|
-
|
35
|
-
|
36
|
-
|
35
|
+
def runtime_unlock(event)
|
36
|
+
job = event.payload[:job]
|
37
|
+
resource = event.payload[:resource]
|
37
38
|
|
38
|
-
|
39
|
-
|
39
|
+
debug do
|
40
|
+
"Unlocked runtime #{lock_info(job, resource)}"
|
41
|
+
end
|
40
42
|
end
|
41
|
-
end
|
42
43
|
|
43
|
-
|
44
|
-
|
45
|
-
|
44
|
+
def conflict(event)
|
45
|
+
job = event.payload[:job]
|
46
|
+
resource = event.payload[:resource]
|
46
47
|
|
47
|
-
|
48
|
-
|
48
|
+
info do
|
49
|
+
"Not unique #{lock_info(job, resource)}" + args_info(job)
|
50
|
+
end
|
49
51
|
end
|
50
|
-
end
|
51
52
|
|
52
|
-
|
53
|
-
|
54
|
-
|
53
|
+
def runtime_conflict(event)
|
54
|
+
job = event.payload[:job]
|
55
|
+
resource = event.payload[:resource]
|
55
56
|
|
56
|
-
|
57
|
-
|
57
|
+
info do
|
58
|
+
"Not unique runtime #{lock_info(job, resource)}" + args_info(job)
|
59
|
+
end
|
58
60
|
end
|
59
|
-
end
|
60
61
|
|
61
|
-
|
62
|
+
private
|
62
63
|
|
63
|
-
|
64
|
-
|
65
|
-
|
64
|
+
def lock_info(job, resource)
|
65
|
+
"#{job.class.name} (Job ID: #{job.job_id}) (Lock key: #{resource})"
|
66
|
+
end
|
66
67
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
68
|
+
def args_info(job)
|
69
|
+
if job.arguments.any? && log_arguments?(job)
|
70
|
+
" with arguments: #{job.arguments.map { |arg| format(arg).inspect }.join(', ')}"
|
71
|
+
else
|
72
|
+
''
|
73
|
+
end
|
72
74
|
end
|
73
|
-
end
|
74
75
|
|
75
|
-
|
76
|
-
|
76
|
+
def log_arguments?(job)
|
77
|
+
return true unless job.class.respond_to?(:log_arguments?)
|
77
78
|
|
78
|
-
|
79
|
-
|
79
|
+
job.class.log_arguments?
|
80
|
+
end
|
80
81
|
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
82
|
+
def format(arg)
|
83
|
+
case arg
|
84
|
+
when Hash
|
85
|
+
arg.transform_values { |value| format(value) }
|
86
|
+
when Array
|
87
|
+
arg.map { |value| format(value) }
|
88
|
+
when GlobalID::Identification
|
89
|
+
arg.to_global_id rescue arg
|
90
|
+
else
|
91
|
+
arg
|
92
|
+
end
|
91
93
|
end
|
92
|
-
end
|
93
94
|
|
94
|
-
|
95
|
-
|
95
|
+
def logger
|
96
|
+
ActiveJob::Base.logger
|
97
|
+
end
|
96
98
|
end
|
97
99
|
end
|
98
100
|
end
|
99
101
|
|
100
|
-
ActiveJob::LogSubscriber.attach_to :active_job_uniqueness
|
102
|
+
ActiveJob::Uniqueness::LogSubscriber.attach_to :active_job_uniqueness
|
@@ -41,7 +41,7 @@ module ActiveJob
|
|
41
41
|
module ScheduledSet
|
42
42
|
def delete(score, job_id)
|
43
43
|
entry = find_job(job_id)
|
44
|
-
ActiveJob::Uniqueness.unlock_sidekiq_job!(entry.item) if super
|
44
|
+
ActiveJob::Uniqueness.unlock_sidekiq_job!(entry.item) if super
|
45
45
|
entry
|
46
46
|
end
|
47
47
|
end
|
@@ -67,7 +67,7 @@ module ActiveJob
|
|
67
67
|
end
|
68
68
|
|
69
69
|
def delete_by_value(name, value)
|
70
|
-
ActiveJob::Uniqueness.unlock_sidekiq_job!(Sidekiq.load_json(value)) if super
|
70
|
+
ActiveJob::Uniqueness.unlock_sidekiq_job!(Sidekiq.load_json(value)) if super
|
71
71
|
end
|
72
72
|
end
|
73
73
|
end
|
@@ -11,12 +11,13 @@ module ActiveJob
|
|
11
11
|
|
12
12
|
delegate :lock_manager, :config, to: :'ActiveJob::Uniqueness'
|
13
13
|
|
14
|
-
attr_reader :lock_key, :lock_ttl, :on_conflict, :job
|
14
|
+
attr_reader :lock_key, :lock_ttl, :on_conflict, :on_redis_connection_error, :job
|
15
15
|
|
16
16
|
def initialize(job:)
|
17
17
|
@lock_key = job.lock_key
|
18
18
|
@lock_ttl = (job.lock_options[:lock_ttl] || config.lock_ttl).to_i * 1000 # ms
|
19
19
|
@on_conflict = job.lock_options[:on_conflict] || config.on_conflict
|
20
|
+
@on_redis_connection_error = job.lock_options[:on_redis_connection_error] || config.on_redis_connection_error
|
20
21
|
@job = job
|
21
22
|
end
|
22
23
|
|
@@ -60,6 +61,12 @@ module ActiveJob
|
|
60
61
|
|
61
62
|
handle_conflict(resource: lock_key, on_conflict: on_conflict)
|
62
63
|
abort_job
|
64
|
+
rescue RedisClient::ConnectionError => e
|
65
|
+
handle_redis_connection_error(
|
66
|
+
resource: lock_key, on_redis_connection_error:
|
67
|
+
on_redis_connection_error, error: e
|
68
|
+
)
|
69
|
+
abort_job
|
63
70
|
end
|
64
71
|
|
65
72
|
def around_enqueue(block)
|
@@ -86,6 +93,14 @@ module ActiveJob
|
|
86
93
|
end
|
87
94
|
end
|
88
95
|
|
96
|
+
def handle_redis_connection_error(resource:, on_redis_connection_error:, error:)
|
97
|
+
case on_redis_connection_error
|
98
|
+
when :raise, nil then raise error
|
99
|
+
else
|
100
|
+
on_redis_connection_error.call(job, resource: resource, error: error)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
89
104
|
def abort_job
|
90
105
|
@job_aborted = true # ActiveJob 4.2 workaround
|
91
106
|
|
data/lib/generators/active_job/uniqueness/templates/config/initializers/active_job_uniqueness.rb
CHANGED
@@ -19,6 +19,13 @@ ActiveJob::Uniqueness.configure do |config|
|
|
19
19
|
#
|
20
20
|
# config.on_conflict = :raise
|
21
21
|
|
22
|
+
# Default action on redis connection error. Can be set per job.
|
23
|
+
# Allowed values are
|
24
|
+
# :raise - raises ActiveJob::Uniqueness::JobNotUnique
|
25
|
+
# proc - custom Proc. For example, ->(job, resource: _, error: _) { job.logger.info("Job already in queue: #{job.class.name} #{job.arguments.inspect} (#{job.job_id})") }
|
26
|
+
#
|
27
|
+
# config.on_redis_connection_error = :raise
|
28
|
+
|
22
29
|
# Digest method for lock keys generating. Expected to have `hexdigest` class method.
|
23
30
|
#
|
24
31
|
# config.digest_method = OpenSSL::Digest::MD5
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: activejob-uniqueness
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Rustam Sharshenov
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2024-12-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activejob
|
@@ -19,7 +19,7 @@ dependencies:
|
|
19
19
|
version: '4.2'
|
20
20
|
- - "<"
|
21
21
|
- !ruby/object:Gem::Version
|
22
|
-
version: '
|
22
|
+
version: '8.1'
|
23
23
|
type: :runtime
|
24
24
|
prerelease: false
|
25
25
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -29,7 +29,7 @@ dependencies:
|
|
29
29
|
version: '4.2'
|
30
30
|
- - "<"
|
31
31
|
- !ruby/object:Gem::Version
|
32
|
-
version: '
|
32
|
+
version: '8.1'
|
33
33
|
- !ruby/object:Gem::Dependency
|
34
34
|
name: redlock
|
35
35
|
requirement: !ruby/object:Gem::Requirement
|