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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 120483e1019cb9e65e1f270ae21d040d17a37d54e7fa6616800cdc7ff8180251
4
- data.tar.gz: c9b299e5e444e19cb1f2a3230447eb44ebd719bb2a93e3c0b2abe22f77782e47
3
+ metadata.gz: cd76f286ed7e6482860247e9b98b754dc1b73a381fcd2579d73e469da95d0200
4
+ data.tar.gz: 866f0949c68745c168217cbc70b10c65799f70aa372902dbd145781726217acd
5
5
  SHA512:
6
- metadata.gz: 5742577b735a3749612b194b6d8912a72938ffaab6b2cb470448fce0342c39a265e99a0fa043f405eaf90ef03f1844fa913331095d13a9a9f968ece3e75265fa
7
- data.tar.gz: 4d22df0933705d5aa22fec6d47add7a6d61e6adab20afb9d8a0557cc6c35907d75491ac07a1bd0e71ea2e4fb59a6d6468ece7e8387c2ee954cc6e1078aa55326
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.3.1...HEAD)
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!, to: :'ActiveJob::Uniqueness.config'
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
- conn.scan('MATCH', wildcard).each { |key| conn.call('DEL', key) }
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
- class LogSubscriber < ActiveSupport::LogSubscriber # :nodoc:
7
- def lock(event)
8
- job = event.payload[:job]
9
- resource = event.payload[:resource]
10
-
11
- debug do
12
- "Locked #{lock_info(job, resource)}" + args_info(job)
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
- def runtime_lock(event)
17
- job = event.payload[:job]
18
- resource = event.payload[:resource]
17
+ def runtime_lock(event)
18
+ job = event.payload[:job]
19
+ resource = event.payload[:resource]
19
20
 
20
- debug do
21
- "Locked runtime #{lock_info(job, resource)}" + args_info(job)
21
+ debug do
22
+ "Locked runtime #{lock_info(job, resource)}" + args_info(job)
23
+ end
22
24
  end
23
- end
24
25
 
25
- def unlock(event)
26
- job = event.payload[:job]
27
- resource = event.payload[:resource]
26
+ def unlock(event)
27
+ job = event.payload[:job]
28
+ resource = event.payload[:resource]
28
29
 
29
- debug do
30
- "Unlocked #{lock_info(job, resource)}"
30
+ debug do
31
+ "Unlocked #{lock_info(job, resource)}"
32
+ end
31
33
  end
32
- end
33
34
 
34
- def runtime_unlock(event)
35
- job = event.payload[:job]
36
- resource = event.payload[:resource]
35
+ def runtime_unlock(event)
36
+ job = event.payload[:job]
37
+ resource = event.payload[:resource]
37
38
 
38
- debug do
39
- "Unlocked runtime #{lock_info(job, resource)}"
39
+ debug do
40
+ "Unlocked runtime #{lock_info(job, resource)}"
41
+ end
40
42
  end
41
- end
42
43
 
43
- def conflict(event)
44
- job = event.payload[:job]
45
- resource = event.payload[:resource]
44
+ def conflict(event)
45
+ job = event.payload[:job]
46
+ resource = event.payload[:resource]
46
47
 
47
- info do
48
- "Not unique #{lock_info(job, resource)}" + args_info(job)
48
+ info do
49
+ "Not unique #{lock_info(job, resource)}" + args_info(job)
50
+ end
49
51
  end
50
- end
51
52
 
52
- def runtime_conflict(event)
53
- job = event.payload[:job]
54
- resource = event.payload[:resource]
53
+ def runtime_conflict(event)
54
+ job = event.payload[:job]
55
+ resource = event.payload[:resource]
55
56
 
56
- info do
57
- "Not unique runtime #{lock_info(job, resource)}" + args_info(job)
57
+ info do
58
+ "Not unique runtime #{lock_info(job, resource)}" + args_info(job)
59
+ end
58
60
  end
59
- end
60
61
 
61
- private
62
+ private
62
63
 
63
- def lock_info(job, resource)
64
- "#{job.class.name} (Job ID: #{job.job_id}) (Lock key: #{resource})"
65
- end
64
+ def lock_info(job, resource)
65
+ "#{job.class.name} (Job ID: #{job.job_id}) (Lock key: #{resource})"
66
+ end
66
67
 
67
- def args_info(job)
68
- if job.arguments.any? && log_arguments?(job)
69
- " with arguments: #{job.arguments.map { |arg| format(arg).inspect }.join(', ')}"
70
- else
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
- def log_arguments?(job)
76
- return true unless job.class.respond_to?(:log_arguments?)
76
+ def log_arguments?(job)
77
+ return true unless job.class.respond_to?(:log_arguments?)
77
78
 
78
- job.class.log_arguments?
79
- end
79
+ job.class.log_arguments?
80
+ end
80
81
 
81
- def format(arg)
82
- case arg
83
- when Hash
84
- arg.transform_values { |value| format(value) }
85
- when Array
86
- arg.map { |value| format(value) }
87
- when GlobalID::Identification
88
- arg.to_global_id rescue arg
89
- else
90
- arg
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
- def logger
95
- ActiveJob::Base.logger
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(score, job_id)
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(name, value)
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
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module ActiveJob
4
4
  module Uniqueness
5
- VERSION = '0.3.1'
5
+ VERSION = '0.4.0'
6
6
  end
7
7
  end
@@ -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.3.1
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: 2023-10-30 00:00:00.000000000 Z
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: '7.2'
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: '7.2'
32
+ version: '8.1'
33
33
  - !ruby/object:Gem::Dependency
34
34
  name: redlock
35
35
  requirement: !ruby/object:Gem::Requirement