activejob-uniqueness 0.3.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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