sidekiq-unique-jobs 5.0.11 → 6.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of sidekiq-unique-jobs might be problematic. Click here for more details.

Files changed (94) hide show
  1. checksums.yaml +4 -4
  2. data/.codeclimate.yml +17 -9
  3. data/.gitignore +1 -3
  4. data/.reek +105 -0
  5. data/.rubocop.yml +36 -1
  6. data/.simplecov +7 -2
  7. data/.travis.yml +11 -10
  8. data/Appraisals +3 -7
  9. data/CHANGELOG.md +17 -0
  10. data/Gemfile +16 -13
  11. data/Guardfile +55 -0
  12. data/README.md +85 -73
  13. data/examples/another_unique_job.rb +13 -0
  14. data/examples/custom_queue_job.rb +12 -0
  15. data/examples/custom_queue_job_with_filter_method.rb +13 -0
  16. data/examples/custom_queue_job_with_filter_proc.rb +16 -0
  17. data/examples/expiring_job.rb +12 -0
  18. data/examples/inline_worker.rb +12 -0
  19. data/examples/just_a_worker.rb +13 -0
  20. data/examples/long_running_job.rb +12 -0
  21. data/examples/main_job.rb +13 -0
  22. data/examples/my_job.rb +12 -0
  23. data/examples/my_unique_job.rb +16 -0
  24. data/examples/my_unique_job_with_filter_method.rb +21 -0
  25. data/examples/my_unique_job_with_filter_proc.rb +19 -0
  26. data/examples/notify_worker.rb +14 -0
  27. data/examples/plain_class.rb +13 -0
  28. data/examples/simple_worker.rb +15 -0
  29. data/examples/spawn_simple_worker.rb +12 -0
  30. data/examples/test_class.rb +9 -0
  31. data/examples/unique_across_workers_job.rb +20 -0
  32. data/examples/unique_job_with_conditional_parameter.rb +18 -0
  33. data/examples/unique_job_with_filter_method.rb +18 -0
  34. data/examples/unique_job_with_nil_unique_args.rb +20 -0
  35. data/examples/unique_job_with_no_unique_args_method.rb +16 -0
  36. data/examples/unique_job_withthout_unique_args_parameter.rb +18 -0
  37. data/examples/unique_on_all_queues_job.rb +16 -0
  38. data/examples/until_and_while_executing_job.rb +13 -0
  39. data/examples/until_executed_2_job.rb +24 -0
  40. data/examples/until_executed_job.rb +25 -0
  41. data/examples/until_executing_job.rb +11 -0
  42. data/examples/until_expired_job.rb +12 -0
  43. data/examples/until_global_expired_job.rb +12 -0
  44. data/examples/while_executing_job.rb +15 -0
  45. data/examples/while_executing_reject_job.rb +14 -0
  46. data/examples/without_argument_job.rb +13 -0
  47. data/lib/sidekiq-unique-jobs.rb +1 -91
  48. data/lib/sidekiq/simulator.rb +15 -16
  49. data/lib/sidekiq_unique_jobs.rb +79 -0
  50. data/lib/sidekiq_unique_jobs/cli.rb +9 -18
  51. data/lib/sidekiq_unique_jobs/client/middleware.rb +16 -18
  52. data/lib/sidekiq_unique_jobs/connection.rb +18 -0
  53. data/lib/sidekiq_unique_jobs/constants.rb +13 -17
  54. data/lib/sidekiq_unique_jobs/core_ext.rb +28 -55
  55. data/lib/sidekiq_unique_jobs/exceptions.rb +30 -0
  56. data/lib/sidekiq_unique_jobs/lock/base_lock.rb +84 -0
  57. data/lib/sidekiq_unique_jobs/lock/until_and_while_executing.rb +11 -6
  58. data/lib/sidekiq_unique_jobs/lock/until_executed.rb +6 -58
  59. data/lib/sidekiq_unique_jobs/lock/until_executing.rb +6 -5
  60. data/lib/sidekiq_unique_jobs/lock/until_expired.rb +17 -0
  61. data/lib/sidekiq_unique_jobs/lock/while_executing.rb +20 -34
  62. data/lib/sidekiq_unique_jobs/lock/while_executing_reject.rb +78 -0
  63. data/lib/sidekiq_unique_jobs/lock/while_executing_requeue.rb +20 -0
  64. data/lib/sidekiq_unique_jobs/locksmith.rb +149 -0
  65. data/lib/sidekiq_unique_jobs/logging.rb +30 -0
  66. data/lib/sidekiq_unique_jobs/options_with_fallback.rb +27 -41
  67. data/lib/sidekiq_unique_jobs/scripts.rb +25 -24
  68. data/lib/sidekiq_unique_jobs/server/middleware.rb +12 -20
  69. data/lib/sidekiq_unique_jobs/sidekiq_unique_ext.rb +5 -5
  70. data/lib/sidekiq_unique_jobs/sidekiq_worker_methods.rb +42 -0
  71. data/lib/sidekiq_unique_jobs/testing.rb +40 -50
  72. data/lib/sidekiq_unique_jobs/timeout.rb +15 -0
  73. data/lib/sidekiq_unique_jobs/timeout/calculator.rb +49 -0
  74. data/lib/sidekiq_unique_jobs/unique_args.rb +33 -77
  75. data/lib/sidekiq_unique_jobs/unlockable.rb +5 -14
  76. data/lib/sidekiq_unique_jobs/util.rb +28 -90
  77. data/lib/sidekiq_unique_jobs/version.rb +1 -1
  78. data/redis/acquire_lock.lua +5 -3
  79. data/redis/create.lua +58 -0
  80. data/redis/delete.lua +11 -0
  81. data/redis/release_stale_locks.lua +90 -0
  82. data/redis/signal.lua +21 -0
  83. data/sidekiq-unique-jobs.gemspec +15 -8
  84. metadata +108 -32
  85. data/lib/sidekiq_unique_jobs/config.rb +0 -17
  86. data/lib/sidekiq_unique_jobs/lock.rb +0 -12
  87. data/lib/sidekiq_unique_jobs/lock/until_timeout.rb +0 -17
  88. data/lib/sidekiq_unique_jobs/run_lock_failed.rb +0 -3
  89. data/lib/sidekiq_unique_jobs/script_mock.rb +0 -66
  90. data/lib/sidekiq_unique_jobs/scripts/acquire_lock.rb +0 -47
  91. data/lib/sidekiq_unique_jobs/scripts/release_lock.rb +0 -49
  92. data/lib/sidekiq_unique_jobs/testing/sidekiq_overrides.rb +0 -50
  93. data/lib/sidekiq_unique_jobs/timeout_calculator.rb +0 -67
  94. data/redis/synchronize.lua +0 -16
@@ -1,15 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SidekiqUniqueJobs
4
- module Lock
5
- class UntilAndWhileExecuting < UntilExecuting
4
+ class Lock
5
+ class UntilAndWhileExecuting < BaseLock
6
6
  def execute(callback)
7
- lock = WhileExecuting.new(item, redis_pool)
8
- lock.synchronize do
9
- callback.call if unlock(:server)
10
- yield
7
+ return unless locked?
8
+ delete!
9
+
10
+ runtime_lock.execute(callback) do
11
+ yield if block_given?
11
12
  end
12
13
  end
14
+
15
+ def runtime_lock
16
+ @runtime_lock ||= SidekiqUniqueJobs::Lock::WhileExecuting.new(item, redis_pool)
17
+ end
13
18
  end
14
19
  end
15
20
  end
@@ -1,68 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SidekiqUniqueJobs
4
- module Lock
5
- class UntilExecuted
4
+ class Lock
5
+ class UntilExecuted < BaseLock
6
6
  OK ||= 'OK'
7
7
 
8
- include SidekiqUniqueJobs::Unlockable
9
-
10
- extend Forwardable
11
- def_delegators :Sidekiq, :logger
12
-
13
- def initialize(item, redis_pool = nil)
14
- @item = item
15
- @redis_pool = redis_pool
16
- end
17
-
18
- def execute(callback, &blk)
19
- operative = true
20
- send(:after_yield_yield, &blk)
21
- rescue Sidekiq::Shutdown
22
- operative = false
23
- raise
24
- ensure
25
- if operative && unlock(:server)
26
- callback.call
27
- else
28
- logger.fatal { "the unique_key: #{unique_key} needs to be unlocked manually" }
29
- end
30
- end
31
-
32
- def unlock(scope)
33
- unless [:server, :api, :test].include?(scope)
34
- raise ArgumentError, "#{scope} middleware can't #{__method__} #{unique_key}"
8
+ def execute(callback)
9
+ return unless locked?
10
+ using_protection(callback) do
11
+ yield if block_given?
35
12
  end
36
-
37
- unlock_by_key(unique_key, item[JID_KEY], redis_pool)
38
13
  end
39
-
40
- def lock(scope)
41
- raise ArgumentError, "#{scope} middleware can't #{__method__} #{unique_key}" if scope.to_sym != :client
42
-
43
- Scripts::AcquireLock.execute(
44
- redis_pool,
45
- unique_key,
46
- item[JID_KEY],
47
- max_lock_time,
48
- )
49
- end
50
-
51
- def unique_key
52
- @unique_key ||= UniqueArgs.digest(item)
53
- end
54
-
55
- def max_lock_time
56
- @max_lock_time ||= QueueLockTimeoutCalculator.for_item(item).seconds
57
- end
58
-
59
- def after_yield_yield
60
- yield
61
- end
62
-
63
- private
64
-
65
- attr_reader :item, :redis_pool
66
14
  end
67
15
  end
68
16
  end
@@ -1,11 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SidekiqUniqueJobs
4
- module Lock
5
- class UntilExecuting < UntilExecuted
6
- def execute(callback, &_block)
7
- callback.call if unlock(:server)
8
- yield
4
+ class Lock
5
+ class UntilExecuting < BaseLock
6
+ def execute(callback)
7
+ delete
8
+ callback.call
9
+ yield if block_given?
9
10
  end
10
11
  end
11
12
  end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SidekiqUniqueJobs
4
+ class Lock
5
+ class UntilExpired < BaseLock
6
+ def unlock
7
+ true
8
+ end
9
+
10
+ def execute(callback)
11
+ return unless locked?
12
+ yield if block_given?
13
+ callback.call
14
+ end
15
+ end
16
+ end
17
+ end
@@ -1,50 +1,36 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SidekiqUniqueJobs
4
- module Lock
5
- class WhileExecuting
6
- def self.synchronize(item, redis_pool = nil)
7
- new(item, redis_pool).synchronize { yield }
8
- end
4
+ class Lock
5
+ class WhileExecuting < BaseLock
6
+ RUN_SUFFIX ||= ':RUN'
9
7
 
10
8
  def initialize(item, redis_pool = nil)
11
- @item = item
12
- @redis_pool = redis_pool
13
- @unique_digest = "#{create_digest}:run"
14
- @mutex = Mutex.new
15
- end
16
-
17
- def synchronize
18
- @mutex.synchronize do
19
- sleep 0.1 until locked?
20
- yield
21
- end
22
- rescue Sidekiq::Shutdown
23
- logger.fatal { "the unique_key: #{@unique_digest} needs to be unlocked manually" }
24
- raise
25
- ensure
26
- SidekiqUniqueJobs.connection(@redis_pool) { |conn| conn.del @unique_digest }
9
+ super(item, redis_pool)
10
+ append_unique_key_suffix
27
11
  end
28
12
 
29
- def locked?
30
- Scripts.call(:synchronize, @redis_pool,
31
- keys: [@unique_digest],
32
- argv: [Time.now.to_i, max_lock_time]) == 1
13
+ # Returning true makes sure the client
14
+ # can push the job on the queue
15
+ def lock
16
+ true
33
17
  end
34
18
 
35
- def max_lock_time
36
- @max_lock_time ||= RunLockTimeoutCalculator.for_item(@item).seconds
37
- end
19
+ # Locks the job with the RUN_SUFFIX appended
20
+ def execute(callback)
21
+ return unless locksmith.lock(item[LOCK_TIMEOUT_KEY])
38
22
 
39
- def execute(_callback)
40
- synchronize do
41
- yield
23
+ using_protection(callback) do
24
+ yield if block_given?
42
25
  end
43
26
  end
44
27
 
45
- def create_digest
46
- @create_digest ||= @item[UNIQUE_DIGEST_KEY]
47
- @create_digest ||= SidekiqUniqueJobs::UniqueArgs.digest(@item)
28
+ private
29
+
30
+ # This is safe as the base_lock always creates a new digest
31
+ # The append there for needs to be done every time
32
+ def append_unique_key_suffix
33
+ item[UNIQUE_DIGEST_KEY] = item[UNIQUE_DIGEST_KEY] + RUN_SUFFIX
48
34
  end
49
35
  end
50
36
  end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SidekiqUniqueJobs
4
+ class Lock
5
+ class WhileExecutingReject < WhileExecuting
6
+ def execute(callback)
7
+ return reject unless locksmith.lock(item[LOCK_TIMEOUT_KEY])
8
+
9
+ using_protection(callback) do
10
+ yield if block_given?
11
+ end
12
+ end
13
+
14
+ # Private below here, keeping public due to testing reasons
15
+
16
+ def reject
17
+ log_debug { "Rejecting job with jid: #{item[JID_KEY]} already running" }
18
+ send_to_deadset
19
+ end
20
+
21
+ def send_to_deadset
22
+ log_info { "Adding dead #{item[CLASS_KEY]} job #{item[JID_KEY]}" }
23
+
24
+ if deadset_kill?
25
+ deadset_kill
26
+ else
27
+ push_to_deadset
28
+ end
29
+ end
30
+
31
+ def deadset_kill?
32
+ deadset.respond_to?(:kill)
33
+ end
34
+
35
+ def deadset_kill
36
+ if kill_with_options?
37
+ kill_job_with_options
38
+ else
39
+ kill_job_without_options
40
+ end
41
+ end
42
+
43
+ def kill_with_options?
44
+ Sidekiq::DeadSet.instance_method(:kill).arity > 1
45
+ end
46
+
47
+ def kill_job_without_options
48
+ deadset.kill(payload)
49
+ end
50
+
51
+ def kill_job_with_options
52
+ deadset.kill(payload, notify_failure: false)
53
+ end
54
+
55
+ def deadset
56
+ @deadset ||= Sidekiq::DeadSet.new
57
+ end
58
+
59
+ def push_to_deadset
60
+ Sidekiq.redis do |conn|
61
+ conn.multi do
62
+ conn.zadd('dead', current_time, payload)
63
+ conn.zremrangebyscore('dead', '-inf', current_time - Sidekiq::DeadSet.timeout)
64
+ conn.zremrangebyrank('dead', 0, -Sidekiq::DeadSet.max_jobs)
65
+ end
66
+ end
67
+ end
68
+
69
+ def current_time
70
+ @current_time ||= Time.now.to_f
71
+ end
72
+
73
+ def payload
74
+ @payload ||= Sidekiq.dump_json(item)
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SidekiqUniqueJobs
4
+ class Lock
5
+ class WhileExecutingRequeue < WhileExecuting
6
+ def lock
7
+ true
8
+ end
9
+
10
+ def execute(callback)
11
+ locksmith.lock(item[LOCK_TIMEOUT_KEY], raise: true) do
12
+ yield
13
+ callback.call
14
+ end
15
+
16
+ Sidekiq::Client.push(item) unless locksmith.locked?
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SidekiqUniqueJobs
4
+ class Locksmith # rubocop:disable ClassLength
5
+ API_VERSION = '1'
6
+ EXPIRES_IN = 10
7
+
8
+ include SidekiqUniqueJobs::Connection
9
+
10
+ def initialize(item, redis_pool = nil)
11
+ @concurrency = 1 # removed in a0cff5bc42edbe7190d6ede7e7f845074d2d7af6
12
+ @expiration = item[LOCK_EXPIRATION_KEY]
13
+ @jid = item[JID_KEY]
14
+ @unique_digest = item[UNIQUE_DIGEST_KEY]
15
+ @redis_pool = redis_pool
16
+ end
17
+
18
+ def create
19
+ Scripts.call(
20
+ :create,
21
+ redis_pool,
22
+ keys: [exists_key, grabbed_key, available_key, version_key, unique_digest],
23
+ argv: [jid, expiration, API_VERSION, concurrency],
24
+ )
25
+ end
26
+
27
+ def exists?
28
+ redis(redis_pool) { |conn| conn.exists(exists_key) }
29
+ end
30
+
31
+ def available_count
32
+ return concurrency unless exists?
33
+
34
+ redis(redis_pool) { |conn| conn.llen(available_key) }
35
+ end
36
+
37
+ def delete
38
+ return unless expiration.nil?
39
+ delete!
40
+ end
41
+
42
+ def delete!
43
+ Scripts.call(
44
+ :delete,
45
+ redis_pool,
46
+ keys: [exists_key, grabbed_key, available_key, version_key, unique_digest],
47
+ )
48
+ end
49
+
50
+ def lock(timeout = nil, &block)
51
+ create
52
+
53
+ grab_token(timeout) do |token|
54
+ touch_grabbed_token(token)
55
+ return_token_or_block_value(token, &block)
56
+ end
57
+ end
58
+ alias wait lock
59
+
60
+ def unlock
61
+ return false unless locked?
62
+ signal(jid)
63
+ end
64
+
65
+ def locked?(token = nil)
66
+ token ||= jid
67
+ redis(redis_pool) { |conn| conn.hexists(grabbed_key, token) }
68
+ end
69
+
70
+ def signal(token = nil)
71
+ token ||= jid
72
+
73
+ Scripts.call(
74
+ :signal,
75
+ redis_pool,
76
+ keys: [exists_key, grabbed_key, available_key, version_key, unique_digest],
77
+ argv: [token, expiration],
78
+ )
79
+ end
80
+
81
+ private
82
+
83
+ attr_reader :concurrency, :unique_digest, :expiration, :jid, :redis_pool
84
+
85
+ def grab_token(timeout = nil)
86
+ redis(redis_pool) do |conn|
87
+ if timeout.nil? || timeout.positive?
88
+ # passing timeout 0 to blpop causes it to block
89
+ _key, token = conn.blpop(available_key, timeout || 0)
90
+ else
91
+ token = conn.lpop(available_key)
92
+ end
93
+ yield token if token == jid
94
+ end
95
+ end
96
+
97
+ def touch_grabbed_token(token)
98
+ redis(redis_pool) { |conn| conn.hset(grabbed_key, token, current_time.to_f) }
99
+ end
100
+
101
+ def return_token_or_block_value(token)
102
+ return token unless block_given?
103
+
104
+ begin
105
+ yield token
106
+ ensure
107
+ signal(token)
108
+ end
109
+ end
110
+
111
+ def available_key
112
+ @available_key ||= namespaced_key('AVAILABLE')
113
+ end
114
+
115
+ def exists_key
116
+ @exists_key ||= namespaced_key('EXISTS')
117
+ end
118
+
119
+ def grabbed_key
120
+ @grabbed_key ||= namespaced_key('GRABBED')
121
+ end
122
+
123
+ def version_key
124
+ @version_key ||= namespaced_key('VERSION')
125
+ end
126
+
127
+ def namespaced_key(variable)
128
+ "#{unique_digest}:#{variable}"
129
+ end
130
+
131
+ def current_time
132
+ seconds, microseconds_with_frac = redis_time
133
+ Time.at(seconds, microseconds_with_frac)
134
+ end
135
+
136
+ def redis_time
137
+ redis(&:time)
138
+ end
139
+ end
140
+ end
141
+
142
+ require 'sidekiq_unique_jobs/lock/base_lock'
143
+ require 'sidekiq_unique_jobs/lock/until_executed'
144
+ require 'sidekiq_unique_jobs/lock/until_executing'
145
+ require 'sidekiq_unique_jobs/lock/until_expired'
146
+ require 'sidekiq_unique_jobs/lock/while_executing'
147
+ require 'sidekiq_unique_jobs/lock/while_executing_reject'
148
+ require 'sidekiq_unique_jobs/lock/while_executing_requeue'
149
+ require 'sidekiq_unique_jobs/lock/until_and_while_executing'