sidekiq_lockable_job 0.1.0 → 0.1.1

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: 2c4078ebd3f7c4c03e6167d73f0ec1c4510403045ad09f09bef28386321932ef
4
- data.tar.gz: 87b5bb9b9f8a2b8a61b5b69b63487fa2a44d992e43aa2090db017c69bfdc0229
3
+ metadata.gz: f55d67805fbdfb7c7dab6270ed0b74620e59ba5ac3022a1c5cb97e5ebc2dad20
4
+ data.tar.gz: c47ff60f8342d3d4432a8afddcb4b4bae330e08e43ef3573a3d6e175c9c9bad8
5
5
  SHA512:
6
- metadata.gz: 72966453281278f758578bb8e904e9a61c0fab7e05e7cceaa47184a61974dc4277d4620185d592410f042a2b98f5650d2595b310aeb14554e5cd90890c2ece6b
7
- data.tar.gz: 7b194fced1ac5ec4e12f148c67b70202680bc88bfe0a03dfd720dcddc6aa2ba2c725791c417f9341399b0211192d375b7aed7a0e54a65f53ddffaee9705772ed
6
+ metadata.gz: abeb924fe55362a5cfc2ee4f2e4b904d8c8f18f07f9bea6ad02b09b5d946bf4f34eb0355917ac2ab0fb593ba969323820840284186b3630b738da07ebb44db4d
7
+ data.tar.gz: 4a7855f124c9b2b5e4a6cb4a8ade26aac03d00527d300f8b764ee97487d9f82019de823f5f2f1a3874d9e4edb9604ac404a7493f453eec01a05db55c6c23426b
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- sidekiq_lockable_job (0.1.0)
4
+ sidekiq_lockable_job (0.1.1)
5
5
  redis (= 4.0.1)
6
6
  sidekiq (= 5.0.5)
7
7
 
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # SidekiqLockableJob
1
+ # Sidekiq::LockableJob
2
2
 
3
3
  Prevent a job to run until another one complete.
4
4
 
@@ -6,7 +6,21 @@ Prevent a job to run until another one complete.
6
6
 
7
7
  But sometime your jobs will be enqueued independently, then for you do not know the job id on which you depend on (you could parse Sidekiq queue, but...)
8
8
 
9
- `SidekiqLockableJob` allows you to set some locks ( based on job params ) when a job is enqueued or processed (store in redis), to prevent any other jobs to run if locked ( based on job params ) and will unlock any previously set locks ( based on job params ) when a job is **succesfully** completed.
9
+ `Sidekiq::LockableJob` allows you to set some locks ( based on job params ) when a job is enqueued or processed (store in redis), to prevent any other jobs to run if locked ( based on job params ) and will unlock any previously set locks ( based on job params ) when a job is **succesfully** completed.
10
+
11
+ ## Use cases
12
+
13
+ For a real exemple at @Babylist.
14
+
15
+ Let's say a third party service send you a webhook request when some products of an order are shipped. (-> job A)
16
+ Then send you an eventual webhook request when some products of this order can not be shipped (without explicitely telling you which ones). (-> job B), on which you want to cancelled any non shipped products for this order.
17
+
18
+ Your third party service obviously send you this two webhook in the right order.
19
+ But if something went wrong processing the first job (database issue, ...), you might ended cancelled the full order because the first job A haven't run prior to the second job B.
20
+
21
+ In this scenario, you will request a lock `denied_order_cancellation_ABC` to be set when you first job A is enqueued (order key `ABC` being extracted from the job params), and you will request to raise a lock error (which will retry the job later) when the second job B is about to be processed if any lock exist for `denied_order_cancellation_ABC (order key `ABC` being extracted from the job params)`. And finally you will request an unlock of `denied_order_cancellation_ABC` when the first job A is **successfully** completed later on, which will allow the second job to succeed on its later retry.
22
+
23
+ > If you expect multiple job of type A to be enqueued and needs to wait for all this locks to be lift (using the same key), see `MultiLockService`
10
24
 
11
25
  ## Installation
12
26
 
@@ -30,7 +44,7 @@ The gem is compose of four parts:
30
44
 
31
45
  - Setting locks when job is **enqueued**
32
46
  - Setting locks when job is **processed**
33
- - Raising `LockableJob::LockedJobError` when job **start** to processed but is locked
47
+ - Raising `Sidekiq::LockableJob::LockedJobError` when job **start** to processed but is locked
34
48
  - Unsetting locks when job is **succesfully** processed
35
49
 
36
50
  ### Setting locks when job is **enqueued**
@@ -39,6 +53,8 @@ The gem is compose of four parts:
39
53
  > including `Sidekiq::LockableJob` auto set the middleware chain
40
54
 
41
55
  ```ruby
56
+ require 'sidekiq_lockable_job'
57
+
42
58
  class Worker
43
59
  include Sidekiq::Worker
44
60
  include Sidekiq::LockableJob
@@ -62,6 +78,8 @@ When your job is **ENQUEUED** (`Worker.perform_async`), sidekiq LockableJob clie
62
78
  > including `Sidekiq::LockableJob` auto set the middleware chain
63
79
 
64
80
  ```ruby
81
+ require 'sidekiq_lockable_job'
82
+
65
83
  class Worker
66
84
  include Sidekiq::Worker
67
85
  include Sidekiq::LockableJob
@@ -79,12 +97,14 @@ end
79
97
 
80
98
  When your job is **PROCESSED**, sidekiq LockableJob server middleware, will call `lockable_job_client_lock_keys` (before running the job) with the jobs arguments and set a lock for any returned keys
81
99
 
82
- ### Raising `LockableJob::LockedJobError` when job start to processed but is locked
100
+ ### Raising `Sidekiq::LockableJob::LockedJobError` when job start to processed but is locked
83
101
 
84
102
  > happens in the sidekiq server middleware (on you rails worker)
85
103
  > including `Sidekiq::LockableJob` auto set the middleware chain
86
104
 
87
105
  ```ruby
106
+ require 'sidekiq_lockable_job'
107
+
88
108
  class Worker
89
109
  include Sidekiq::Worker
90
110
  include Sidekiq::LockableJob
@@ -100,7 +120,10 @@ class Worker
100
120
  end
101
121
  ```
102
122
 
103
- When your job is **about** to be **processed**, sidekiq LockableJob server middleware, will call `lockable_job_locked_by_keys` (before processing the job) with the jobs arguments and raise `Sidekiq::LockableJob::LockedJobError` if any of the returned keys is locked
123
+ When your job is **about** to be **processed**, sidekiq LockableJob server middleware, will call `lockable_job_locked_by_keys` (before processing the job) with the jobs arguments and raise `Sidekiq::LockableJob::LockedJobError` if any of the returned keys is locked.
124
+
125
+ > [Sidekiq Error Handling](https://github.com/mperham/sidekiq/wiki/Error-Handling)
126
+ > Sidekiq will retry failures with an exponential backoff using the formula (retry_count ** 4) + 15 + (rand(30) * (retry_count + 1)) (i.e. 15, 16, 31, 96, 271, ... seconds + a random amount of time). It will perform 25 retries over approximately 21 days. Assuming you deploy a bug fix within that time, the job will get retried and successfully processed. After 25 times, Sidekiq will move that job to the Dead Job queue, assuming that it will need manual intervention to work.
104
127
 
105
128
  ### Unsetting locks when job is **succesfully** processed
106
129
 
@@ -108,6 +131,8 @@ When your job is **about** to be **processed**, sidekiq LockableJob server middl
108
131
  > including `Sidekiq::LockableJob` auto set the middleware chain
109
132
 
110
133
  ```ruby
134
+ require 'sidekiq_lockable_job'
135
+
111
136
  class Worker
112
137
  include Sidekiq::Worker
113
138
  include Sidekiq::LockableJob
@@ -125,6 +150,72 @@ end
125
150
 
126
151
  When your job is **successfully** to be **performed**, sidekiq LockableJob server middleware, will call `lockable_job_unlock_keys` (after processing the job) with the jobs arguments and unset lock for any returned keys
127
152
 
153
+
154
+ ## LockService vs MultiLockService vs CustomLockService
155
+
156
+ By default `Sidekiq::LockableJob` use `Sidekiq::LockableJob::LockService` to lock, unlock and check lock.
157
+ This service set lock time in redis when lock, unset redis key when unlock and check for redis key existance at lock check.
158
+
159
+ This is enough for most common scenario, but you can use `Sidekiq::LockableJob::MultiLockService` if you need to count the number of lock.
160
+ Each lock will increase the lock count, each unlock will decrease it, job will raise only if lock exist and count if > 0.
161
+
162
+ > If you're using both default lock and multi lock, keys handle by a lock should all use the same lock service
163
+ > see `lib/sidekiq/lockable_job/multi_lock_service.rb`
164
+
165
+ ```ruby
166
+ require 'sidekiq_lockable_job'
167
+
168
+ class MultiLockWorker
169
+ include Sidekiq::Worker
170
+ include Sidekiq::LockableJob
171
+ lockable_job_lock_service Sidekiq::LockableJob::MultiLockService
172
+ end
173
+ ```
174
+
175
+ If you want to use your own locking mechanism (to store somewhere else than redis, or handle locked differently), you can set your own `LockService` class.
176
+
177
+ ```ruby
178
+ module CustomLockService
179
+ def self.lock(key)
180
+ // do you own lock
181
+ end
182
+
183
+ def self.unlock(key)
184
+ // do you own unlock
185
+ end
186
+
187
+ # non required helper method
188
+ def self.locked?(key)
189
+ // return true if locked
190
+ end
191
+
192
+ def self.handle_locked_by(key, worker_instance:, job:)
193
+ // do you own handle if locked
194
+ if locked?(key)
195
+ worker_instance.class.perform_in(3.hours, **job['args'])
196
+ return true
197
+ end
198
+ // return true if job should NOT run or false if it should
199
+ false
200
+ end
201
+ end
202
+
203
+ class Worker
204
+ include Sidekiq::Worker
205
+ include Sidekiq::LockableJob
206
+ lockable_job_lock_service CustomLockService
207
+ end
208
+ ```
209
+
210
+ ## Roadmap
211
+
212
+ - [x] `Sidekiq::LockableJob` lock (on enqueuing, processing), unlock (on successfully processed), raise if locked (before processing)
213
+ - [x] `Sidekiq::LockableJob` auto add itself to sidekiq middleware
214
+ - [x] Supporting lock/unlock count (if a job is queued 3 times, will increase the lock count to 3, and will require 3 unlock to be lifted)
215
+ - [x] Externalize locking/unlocking/locked? mechanism (`LockableJobService`), and give option to use different service (ie.: not storing in Redis)
216
+ - [x] Option to requeue job (with delay), or swallow job failure if locked
217
+ - [ ] Option to no auto include to middleware (and use locks manually or add in different order in middleware chain)
218
+
128
219
  ## Specs
129
220
 
130
221
  ```
@@ -136,7 +227,7 @@ Sidekiq::LockableJob
136
227
  .locked?
137
228
  true if locked
138
229
  false if NOT locked
139
- .raise_if_locked_by
230
+ .handle_locked_by
140
231
  raise if locked by any key
141
232
  raise if locked by single key
142
233
  DOT NOT raise if not locked by
@@ -155,7 +246,7 @@ Sidekiq::LockableJob::Middleware::Client::SetLocks
155
246
  behaves like set locks
156
247
  set locks
157
248
 
158
- Sidekiq::LockableJob::Middleware::Server::RaiseIfLocked
249
+ Sidekiq::LockableJob::Middleware::Server::HandleLockedBy
159
250
  with no lock
160
251
  behaves like perform the job
161
252
  example at ./spec/sidekiq/lockable_job/middleware/server/shared.rb:13
@@ -0,0 +1,38 @@
1
+ require 'redis'
2
+
3
+ module Sidekiq::LockableJob
4
+ module LockService
5
+ class Error < StandardError; end
6
+
7
+ REDIS_PREFIX_KEY = self.to_s.downcase
8
+
9
+ def self.lock(key)
10
+ redis.set(redis_key(key), Time.now.to_i)
11
+ end
12
+
13
+ def self.unlock(key)
14
+ redis.del(redis_key(key))
15
+ end
16
+
17
+ def self.locked?(key)
18
+ redis.exists(redis_key(key))
19
+ end
20
+
21
+ def self.handle_locked_by(keys, worker_instance:, job:)
22
+ keys = [keys] unless keys.nil? || keys.is_a?(Array)
23
+ keys&.each do |key|
24
+ raise LockedJobError.new("Locked by #{key}") if locked?(key)
25
+ end
26
+ # job is not locked and should be processed
27
+ false
28
+ end
29
+
30
+ def self.redis_key(key)
31
+ "#{REDIS_PREFIX_KEY}:#{key}"
32
+ end
33
+
34
+ def self.redis
35
+ $redis
36
+ end
37
+ end
38
+ end
@@ -20,7 +20,7 @@ module Sidekiq::LockableJob
20
20
  keys = worker_klass.send(:lockable_job_client_lock_keys, job['args'])
21
21
  keys = [keys] unless keys.nil? || keys.is_a?(Array)
22
22
  keys&.compact&.each do |key|
23
- Sidekiq::LockableJob.lock(key)
23
+ worker_klass.current_lockable_job_lock_service.lock(key)
24
24
  end
25
25
  end
26
26
  yield
@@ -1,5 +1,5 @@
1
1
 
2
2
  require_relative 'client/set_locks.rb'
3
- require_relative 'server/raise_if_locked.rb'
3
+ require_relative 'server/handle_locked_by.rb'
4
4
  require_relative 'server/unset_locks.rb'
5
5
  require_relative 'server/set_locks.rb'
@@ -4,7 +4,7 @@ require 'sidekiq'
4
4
  module Sidekiq::LockableJob
5
5
  module Middleware
6
6
  module Server
7
- class RaiseIfLocked
7
+ class HandleLockedBy
8
8
  # @param [Object] worker the worker instance
9
9
  # @param [Hash] job the full job payload
10
10
  # * @see https://github.com/mperham/sidekiq/wiki/Job-Format
@@ -16,7 +16,11 @@ module Sidekiq::LockableJob
16
16
  if worker_klass.respond_to?(:lockable_job_locked_by_keys)
17
17
  keys = worker_klass.send(:lockable_job_locked_by_keys, job['args'])
18
18
  keys = [keys] unless keys.nil? || keys.is_a?(Array)
19
- Sidekiq::LockableJob.raise_if_locked_by(keys)
19
+ locked = worker_klass.current_lockable_job_lock_service.handle_locked_by(keys, worker_instance: worker, job: job)
20
+ # LockableJobService && MultiLockService raise if job is lock
21
+ # but a CustomLockService could decide to re-enqueued the job
22
+ # if the service return true, it mean the job is lock and we skip it's processing (don't yield)
23
+ return if locked
20
24
  end
21
25
  yield
22
26
  end
@@ -17,7 +17,7 @@ module Sidekiq::LockableJob
17
17
  keys = worker_klass.send(:lockable_job_server_lock_keys, job['args'])
18
18
  keys = [keys] unless keys.nil? || keys.is_a?(Array)
19
19
  keys&.compact&.each do |key|
20
- Sidekiq::LockableJob.lock(key)
20
+ worker_klass.current_lockable_job_lock_service.lock(key)
21
21
  end
22
22
  end
23
23
  yield
@@ -18,7 +18,7 @@ module Sidekiq::LockableJob
18
18
  keys = worker_klass.send(:lockable_job_unlock_keys, job['args'])
19
19
  keys = [keys] unless keys.nil? || keys.is_a?(Array)
20
20
  keys&.compact&.each do |key|
21
- Sidekiq::LockableJob.unlock(key)
21
+ worker_klass.current_lockable_job_lock_service.unlock(key)
22
22
  end
23
23
  end
24
24
  end
@@ -0,0 +1,38 @@
1
+ require 'redis'
2
+
3
+ module Sidekiq::LockableJob
4
+ module MultiLockService
5
+ class Error < StandardError; end
6
+
7
+ REDIS_PREFIX_KEY = self.to_s.downcase
8
+
9
+ def self.lock(key)
10
+ redis.incr(redis_key(key))
11
+ end
12
+
13
+ def self.unlock(key)
14
+ redis.decr(redis_key(key))
15
+ end
16
+
17
+ def self.locked?(key)
18
+ (redis.get(redis_key(key))&.to_i || 0) > 0
19
+ end
20
+
21
+ def self.handle_locked_by(keys, worker_instance:, job:)
22
+ keys = [keys] unless keys.nil? || keys.is_a?(Array)
23
+ keys&.each do |key|
24
+ raise LockedJobError.new("Locked by #{key}") if locked?(key)
25
+ end
26
+ # job is not locked and should be processed
27
+ false
28
+ end
29
+
30
+ def self.redis_key(key)
31
+ "#{REDIS_PREFIX_KEY}:#{key}"
32
+ end
33
+
34
+ def self.redis
35
+ $redis
36
+ end
37
+ end
38
+ end
@@ -1,49 +1,22 @@
1
1
  require 'sidekiq'
2
2
  require 'redis'
3
- require_relative 'middleware/middleware.rb'
3
+ require_relative 'lockable_job/middleware/middleware.rb'
4
+ require_relative 'lockable_job/lock_service'
5
+ require_relative 'lockable_job/multi_lock_service'
4
6
 
5
7
  module Sidekiq
6
8
  module LockableJob
7
9
  class Error < StandardError; end
8
10
  class LockedJobError < StandardError; end
9
11
 
10
- LOCKABLE_JOB_REDIS_PREFIX_KEY = 'sidekiq_locks'
11
-
12
- def self.lock(key)
13
- redis.set(redis_key(key), Time.now)
14
- end
15
-
16
- def self.unlock(key)
17
- redis.del(redis_key(key))
18
- end
19
-
20
- def self.locked?(key)
21
- redis.exists(redis_key(key))
22
- end
23
-
24
- def self.raise_if_locked_by(keys)
25
- keys = [keys] unless keys.nil? || keys.is_a?(Array)
26
- keys&.each do |key|
27
- # perform in instead of retry?
28
- # perform_in(2.minutes, *args)
29
- raise LockedJobError.new("Locked by #{key}") if locked?(key)
30
- end
31
- end
32
-
33
- def self.redis_key(key)
34
- "#{LOCKABLE_JOB_REDIS_PREFIX_KEY}:#{key}"
35
- end
36
-
37
- def self.redis
38
- $redis = Redis.new
39
- end
12
+ DEFAULT_LOCKABLE_JOB_SERVICE = LockService
40
13
 
41
14
  def self.included(base)
42
15
  unless base.ancestors.include? Sidekiq::Worker
43
16
  raise ArgumentError, "Sidekiq::LockableJob can only be included in a Sidekiq::Worker"
44
17
  end
45
18
 
46
- # base.extend(ClassMethods)
19
+ base.extend(ClassMethods)
47
20
 
48
21
  # Automatically add sidekiq middleware when we're first included
49
22
  #
@@ -52,8 +25,11 @@ module Sidekiq
52
25
  # chain is invoked so we're all good.
53
26
  #
54
27
  Sidekiq.configure_server do |config|
55
- unless config.server_middleware.exists? Sidekiq::LockableJob::Middleware::Server::RaiseIfLocked
56
- config.server_middleware.add Sidekiq::LockableJob::Middleware::Server::RaiseIfLocked
28
+ unless config.client_middleware.exists? Sidekiq::LockableJob::Middleware::Server::SetLocks
29
+ config.client_middleware.add Sidekiq::LockableJob::Middleware::Server::SetLocks
30
+ end
31
+ unless config.server_middleware.exists? Sidekiq::LockableJob::Middleware::Server::HandleLockedBy
32
+ config.server_middleware.add Sidekiq::LockableJob::Middleware::Server::HandleLockedBy
57
33
  end
58
34
  unless config.server_middleware.exists? Sidekiq::LockableJob::Middleware::Server::UnsetLocks
59
35
  config.server_middleware.add Sidekiq::LockableJob::Middleware::Server::UnsetLocks
@@ -65,5 +41,15 @@ module Sidekiq
65
41
  end
66
42
  end
67
43
  end
44
+
45
+ module ClassMethods
46
+ def current_lockable_job_lock_service
47
+ @lockable_job_lock_service || DEFAULT_LOCKABLE_JOB_SERVICE
48
+ end
49
+
50
+ def lockable_job_lock_service(service)
51
+ @lockable_job_lock_service = service
52
+ end
53
+ end
68
54
  end
69
55
  end
@@ -1,3 +1,3 @@
1
1
  module SidekiqLockableJob
2
- VERSION = "0.1.0"
2
+ VERSION = "0.1.1"
3
3
  end
@@ -1,5 +1,5 @@
1
1
  require "sidekiq_lockable_job/version"
2
- require "sidekiq/lockable_job/lockable_job.rb"
2
+ require_relative "sidekiq/lockable_job"
3
3
 
4
4
  module SidekiqLockableJob
5
5
  class Error < StandardError; end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sidekiq_lockable_job
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Hugues Bernet-Rollande
@@ -118,12 +118,14 @@ files:
118
118
  - bin/console
119
119
  - bin/rspec
120
120
  - bin/setup
121
- - lib/sidekiq/lockable_job/lockable_job.rb
121
+ - lib/sidekiq/lockable_job.rb
122
+ - lib/sidekiq/lockable_job/lock_service.rb
122
123
  - lib/sidekiq/lockable_job/middleware/client/set_locks.rb
123
124
  - lib/sidekiq/lockable_job/middleware/middleware.rb
124
- - lib/sidekiq/lockable_job/middleware/server/raise_if_locked.rb
125
+ - lib/sidekiq/lockable_job/middleware/server/handle_locked_by.rb
125
126
  - lib/sidekiq/lockable_job/middleware/server/set_locks.rb
126
127
  - lib/sidekiq/lockable_job/middleware/server/unset_locks.rb
128
+ - lib/sidekiq/lockable_job/multi_lock_service.rb
127
129
  - lib/sidekiq_lockable_job.rb
128
130
  - lib/sidekiq_lockable_job/version.rb
129
131
  - sidekiq_lockable_job.gemspec