distributed_job 1.0.0 → 3.0.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: e8aa1822c85ccb44521e470a312b430b9409dd537add85122045f7cd71d075ed
4
- data.tar.gz: a781158c7699303544cb78185a200059da25013830f677a44a704b6a0c115af8
3
+ metadata.gz: 8bda4273dc59888e0269d7d14d69180e9a67bdb793e9c7f9f24bcc9f88a1b8fd
4
+ data.tar.gz: 8a3108b573a9e46e78d57979dfab70906459947651c1b2cda1391f66aa8e0f93
5
5
  SHA512:
6
- metadata.gz: 6885d739ae86c615588dfd85ca1b4d0b0b3e4bcdf8cca1ddf1aebcb806a703c1980ca2d1e6f758d22b38a24a3859d5673fa47e7b2438db4635a4e24351bff4eb
7
- data.tar.gz: ffbea863763a62d8f22a2e8c2f2b8555a910fc908801e9a93157703a3a2e7e6fbd9bee3b90e6c35971d430b8b720781b02c1d116159e1ea2c3cb00b9fa57b7d4
6
+ metadata.gz: e553072546911dffba6bd7f40e9f4eb6c7fe4f0a0338437ffc21cdc8e442c91cb0b51fa2afbcf070e257d2caaade5954867710e14e7919fde335aaebb8ec3867
7
+ data.tar.gz: 86ae97dd953642cc225f6b4280776c84dfc080cc4af90d8519352f6d4faf0a26ab736df372cac734a1884382a0173e9a44784371858b5e5f137bf68b2b6193d0
data/.gitignore CHANGED
@@ -6,6 +6,7 @@
6
6
  /pkg/
7
7
  /spec/reports/
8
8
  /tmp/
9
+ Gemfile.lock
9
10
 
10
11
  # rspec failure tracking
11
12
  .rspec_status
data/.rubocop.yml CHANGED
@@ -3,6 +3,9 @@ AllCops:
3
3
  TargetRubyVersion: 2.5
4
4
  SuggestExtensions: false
5
5
 
6
+ Gemspec/RequireMFA:
7
+ Enabled: false
8
+
6
9
  Metrics/MethodLength:
7
10
  Enabled: false
8
11
 
@@ -17,3 +20,6 @@ Style/Documentation:
17
20
 
18
21
  Style/NumericPredicate:
19
22
  Enabled: false
23
+
24
+ Metrics/ModuleLength:
25
+ Enabled: false
data/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## v3.0.1
4
+
5
+ * Fix pipelining with regards to redis-rb 4.6.0
6
+
7
+ ## v3.0.0
8
+
9
+ * Split `DistributedJob` in `DistributedJob::Client` and `DistributedJob::Job`
10
+ * Add native namespace support and drop support for `Redis::Namespace`
11
+
12
+ ## v2.0.0
13
+
14
+ * `#push_each` no longer returns an enum when no block is given
15
+ * Renamed `#parts` to `#open_parts`
16
+
3
17
  ## v1.0.0
4
18
 
5
19
  * Initial release
data/README.md CHANGED
@@ -1,5 +1,8 @@
1
1
  # DistributedJob
2
2
 
3
+ [![Build](https://github.com/mrkamel/distributed_job/workflows/test/badge.svg)](https://github.com/mrkamel/distributed_job/actions?query=workflow%3Atest+branch%3Amaster)
4
+ [![Gem Version](https://badge.fury.io/rb/distributed_job.svg)](http://badge.fury.io/rb/distributed_job)
5
+
3
6
  Easily keep track of distributed jobs consisting of an arbitrary number of
4
7
  parts spanning multiple workers using redis. Can be used with any kind of
5
8
  backround job processing queue.
@@ -26,10 +29,22 @@ Getting started is very easy. A `DistributedJob` allows to keep track of a
26
29
  distributed job, i.e. a job which is split into multiple units running in
27
30
  parallel and in multiple workers.
28
31
 
29
- To create a distributed job and add parts, i.e. units of work, to it, simply:
32
+ First, create a `DistributedJob::Client`:
33
+
34
+ ```ruby
35
+ DistributedJobClient = DistributedJob::Client.new(redis: Redis.new)
36
+ ```
37
+
38
+ You can specify a `namespace` to be additionally used for redis keys and set a
39
+ `default_ttl` for keys (Default is `86_400`, i.e. one day), which will be used
40
+ every time when keys in redis are updated to guarantee that the distributed
41
+ job metadata is cleaned up properly from redis at some point in time.
42
+
43
+ Afterwards, to create a distributed job and add parts, i.e. units of work, to
44
+ it, simply do:
30
45
 
31
46
  ```ruby
32
- distributed_job = DistributedJob.new(redis: Redis.new, token: SecureRandom.hex)
47
+ distributed_job = DistributedJobClient.build(token: SecureRandom.hex)
33
48
 
34
49
  distributed_job.push_each(Date.parse('2021-01-01')..Date.today) do |date, part|
35
50
  SomeBackgroundJob.perform_async(date, distributed_job.token, part)
@@ -38,35 +53,40 @@ To create a distributed job and add parts, i.e. units of work, to it, simply:
38
53
  distributed_job.token # can be used to query the status of the distributed job
39
54
  ```
40
55
 
41
- The `token` can be used to keep query the status of the distributed job, e.g.
56
+ The `part` which is passed to the block is some id for one particular part of
57
+ the distributed job. It must be used in a respective background job to mark
58
+ this part finished after it has been successfully processed. Therefore, when
59
+ all those background jobs have successfully finished, all parts will be marked
60
+ as finished, such that the distributed job will finally be finished as well.
61
+
62
+ The `token` can also be used to query the status of the distributed job, e.g.
42
63
  on a job summary page or similar. You can show some progress bar in the browser
43
64
  or in the terminal, etc:
44
65
 
45
66
  ```ruby
46
67
  # token is given via URL or via some other means
47
- distributed_job = Distributed.new(redis: Redis.new, token: params[:token])
68
+ distributed_job = DistributedJobClient.build(token: params[:token])
48
69
 
49
70
  distributed_job.total # total number of parts
50
71
  distributed_job.count # number of unfinished parts
51
- distributed_job.finished?
72
+ distributed_job.finished? # whether or not all parts are finished
73
+ distributed_job.open_parts # returns all not yet finished part id's
52
74
  ```
53
75
 
54
- Within the background job, you use the passed token and part to query and
55
- update the status of the distributed job and part accordingly. Please note
76
+ Within the background job, you must use the passed `token` and `part` to query
77
+ and update the status of the distributed job and part accordingly. Please note
56
78
  that you can use whatever background job processing tool you like most.
57
79
 
58
80
  ```ruby
59
81
  class SomeBackgroundJob
60
82
  def perform(whatever, token, part)
61
- distributed_job = DistributedJob.new(redis: Redis.new, token: token)
83
+ distributed_job = DistributedJobClient.build(redis: Redis.new, token: token)
62
84
 
63
85
  return if distributed_job.stopped?
64
86
 
65
87
  # ...
66
88
 
67
- distributed_job.done(part)
68
-
69
- if distributed_job.finished?
89
+ if distributed_job.done(part)
70
90
  # perform e.g. cleanup or the some other job
71
91
  end
72
92
  rescue
@@ -78,10 +98,17 @@ end
78
98
  ```
79
99
 
80
100
  The `#stop` and `#stopped?` methods can be used to globally stop a distributed
81
- job in case of errors. Contrary, the `#done` method tells the `DistributedJob` that the
82
- specified part has successfully finished. Finally, the `#finished?` method
83
- returns true when all parts of the distributed job are finished, which is useful
84
- to start cleanup jobs or to even start another subsequent distributed job.
101
+ job in case of errors. Contrary, the `#done` method tells the distributed job
102
+ that the specified part has successfully finished. The `#done` method returns
103
+ true when all parts of the distributed job have finished, which is useful to
104
+ start cleanup jobs or to even start another subsequent distributed job.
105
+
106
+ That's it.
107
+
108
+ ## Reference docs
109
+
110
+ Please find the reference docs at
111
+ [http://www.rubydoc.info/github/mrkamel/distributed_job](http://www.rubydoc.info/github/mrkamel/distributed_job)
85
112
 
86
113
  ## Development
87
114
 
@@ -29,5 +29,5 @@ Gem::Specification.new do |spec|
29
29
 
30
30
  spec.add_development_dependency 'rspec'
31
31
  spec.add_development_dependency 'rubocop'
32
- spec.add_dependency 'redis'
32
+ spec.add_dependency 'redis', '>= 4.1.0'
33
33
  end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DistributedJob
4
+ # A `DistributedJob::Client` allows to easily manage distributed jobs. The
5
+ # main purpose of the client object is to configure settings to all
6
+ # distributed jobs or a group of distributed jobs like e.g. the redis
7
+ # connection and an optional namespace to be used to prefix all redis keys.
8
+ #
9
+ # @example
10
+ # DistributedJobClient = DistributedJob::Client.new(redis: Redis.new)
11
+ #
12
+ # distributed_job = DistributedJobClient.build(token: SecureRandom.hex)
13
+ #
14
+ # # Add job parts and queue background jobs
15
+ # distributed_job.push_each(Date.parse('2021-01-01')..Date.today) do |date, part|
16
+ # SomeBackgroundJob.perform_async(date, distributed_job.token, part)
17
+ # end
18
+ #
19
+ # distributed_job.token # can be used to query the status of the distributed job
20
+
21
+ class Client
22
+ attr_reader :redis, :namespace, :default_ttl
23
+
24
+ # Creates a new `DistributedJob::Client`.
25
+ #
26
+ # @param redis [Redis] The redis connection instance
27
+ # @param namespace [String] An optional namespace used to prefix redis keys
28
+ # @param default_ttl [Integer] The default number of seconds the jobs will
29
+ # stay available in redis. This value is used to automatically expire and
30
+ # clean up the jobs in redis. Default is 86400, i.e. one day. The ttl is
31
+ # used everytime the job is modified in redis.
32
+ #
33
+ # @example
34
+ # DistributedJobClient = DistributedJob::Client.new(redis: Redis.new)
35
+
36
+ def initialize(redis:, namespace: nil, default_ttl: 86_400)
37
+ @redis = redis
38
+ @namespace = namespace
39
+ @default_ttl = default_ttl
40
+ end
41
+
42
+ # Builds a new `DistributedJob::Job` instance.
43
+ #
44
+ # @param token [String] Some token to be used to identify the job. You can
45
+ # e.g. use SecureRandom.hex to generate one.
46
+ # @param ttl [Integer] The number of seconds the job will stay available
47
+ # in redis. This value is used to automatically expire and clean up the
48
+ # job in redis. Default is `default_ttl`, i.e. one day. The ttl is used
49
+ # everytime the job is modified in redis.
50
+
51
+ def build(token:, ttl: default_ttl)
52
+ Job.new(client: self, token: token, ttl: ttl)
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,279 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DistributedJob
4
+ # A `DistributedJob::Job` instance allows to keep track of a distributed job, i.e.
5
+ # a job which is split into multiple units running in parallel and in multiple
6
+ # workers using redis.
7
+ #
8
+ # @example Creating a distributed job
9
+ # distributed_job = DistributedJobClient.build(token: SecureRandom.hex)
10
+ #
11
+ # # Add job parts and queue background jobs
12
+ # distributed_job.push_each(Date.parse('2021-01-01')..Date.today) do |date, part|
13
+ # SomeBackgroundJob.perform_async(date, distributed_job.token, part)
14
+ # end
15
+ #
16
+ # distributed_job.token # can be used to query the status of the distributed job
17
+ #
18
+ # @example Processing a distributed job part
19
+ # class SomeBackgroundJob
20
+ # def perform(whatever, token, part)
21
+ # distributed_job = DistributedJobClient.build(token: token)
22
+ #
23
+ # return if distributed_job.stopped?
24
+ #
25
+ # # ...
26
+ #
27
+ # if distributed_job.done(part)
28
+ # # perform e.g. cleanup or the some other job
29
+ # end
30
+ # rescue
31
+ # distributed_job.stop
32
+ #
33
+ # raise
34
+ # end
35
+ # end
36
+
37
+ class Job
38
+ attr_reader :client, :token, :ttl
39
+
40
+ # Initializes a new distributed job.
41
+ #
42
+ # @param client [DistributedJob::Client] The client instance
43
+ # @param token [String] Some token to be used to identify the job. You can
44
+ # e.g. use SecureRandom.hex to generate one.
45
+ # @param ttl [Integer] The number of seconds this job will stay available
46
+ # in redis. This value is used to automatically expire and clean up the
47
+ # job in redis. Default is 86400, i.e. one day. The ttl is used everytime
48
+ # the job is modified in redis.
49
+ #
50
+ # @example
51
+ # DistributedJobClient = DistributedJob::Client.new(redis: Redis.new)
52
+ #
53
+ # distributed_job = DistributedJob::Job.new(client: DistributedJobClient, token: SecureRandom.hex)
54
+ #
55
+ # # However, the preferred way to build a distributed job is:
56
+ #
57
+ # distributed_job = DistributedJobClient.build(token: SecureRandom.hex)
58
+
59
+ def initialize(client:, token:, ttl: 86_400)
60
+ @client = client
61
+ @token = token
62
+ @ttl = ttl
63
+ end
64
+
65
+ # Pass an enum to be used to iterate all the units of work of the distributed
66
+ # job. The distributed job needs to know all of them to keep track of the
67
+ # overall number and status of the parts. Passing an enum is much better
68
+ # compared to pushing the parts manually, because the distributed job needs
69
+ # to be closed before the last part of the distributed job is enqueued into
70
+ # some job queue. Otherwise it could potentially happen that the last part is
71
+ # already processed in the job queue before it is pushed to redis, such that
72
+ # the last job doesn't know that the distributed job is finished.
73
+ #
74
+ # @param enum [#each_with_index] The enum which can be iterated to get all
75
+ # job parts
76
+ #
77
+ # @example
78
+ # distributed_job.push_each(Date.parse('2021-01-01')..Date.today) do |date, part|
79
+ # # e.g. SomeBackgroundJob.perform_async(date, distributed_job.token, part)
80
+ # end
81
+ #
82
+ # @example ActiveRecord
83
+ # distributed_job.push_each(User.select(:id).find_in_batches) do |batch, part|
84
+ # # e.g. SomeBackgroundJob.perform_async(batch.first.id, batch.last.id, distributed_job.token, part)
85
+ # end
86
+
87
+ def push_each(enum)
88
+ previous_object = nil
89
+ previous_index = nil
90
+
91
+ enum.each_with_index do |current_object, current_index|
92
+ push(current_index)
93
+
94
+ yield(previous_object, previous_index.to_s) if previous_index
95
+
96
+ previous_object = current_object
97
+ previous_index = current_index
98
+ end
99
+
100
+ close
101
+
102
+ yield(previous_object, previous_index.to_s) if previous_index
103
+ end
104
+
105
+ # Returns all parts of the distributed job which are not yet finished.
106
+ #
107
+ # @return [Enumerator] The enum which allows to iterate all parts
108
+
109
+ def open_parts
110
+ redis.sscan_each("#{redis_key}:parts")
111
+ end
112
+
113
+ # Removes the specified part from the distributed job, i.e. from the set of
114
+ # unfinished parts. Use this method when the respective job part has been
115
+ # successfully processed, i.e. finished.
116
+ #
117
+ # @param part [String] The job part
118
+ # @returns [Boolean] Returns true when there are no more unfinished parts
119
+ # left or false otherwise
120
+ #
121
+ # @example
122
+ # class SomeBackgroundJob
123
+ # def perform(whatever, token, part)
124
+ # distributed_job = DistributedJobClient.build(token: token)
125
+ #
126
+ # # ...
127
+ #
128
+ # distributed_job.done(part)
129
+ # end
130
+ # end
131
+
132
+ def done(part)
133
+ @done_script ||= <<~SCRIPT
134
+ local key, part, ttl = ARGV[1], ARGV[2], tonumber(ARGV[3])
135
+
136
+ if redis.call('srem', key .. ':parts', part) == 0 then return end
137
+
138
+ redis.call('expire', key .. ':parts', ttl)
139
+ redis.call('expire', key .. ':state', ttl)
140
+
141
+ return redis.call('scard', key .. ':parts')
142
+ SCRIPT
143
+
144
+ redis.eval(@done_script, argv: [redis_key, part.to_s, ttl]) == 0 && closed?
145
+ end
146
+
147
+ # Returns the total number of pushed parts, no matter if finished or not.
148
+ #
149
+ # @example
150
+ # distributed_job.total # => e.g. 13
151
+
152
+ def total
153
+ redis.hget("#{redis_key}:state", 'total').to_i
154
+ end
155
+
156
+ # Returns the number of pushed parts which are not finished.
157
+ #
158
+ # @example
159
+ # distributed_job.count # => e.g. 8
160
+
161
+ def count
162
+ redis.scard("#{redis_key}:parts")
163
+ end
164
+
165
+ # Returns true if there are no more unfinished parts.
166
+ #
167
+ # @example
168
+ # distributed_job.finished? #=> true/false
169
+
170
+ def finished?
171
+ closed? && count.zero?
172
+ end
173
+
174
+ # Allows to stop a distributed job. This is useful if some error occurred in
175
+ # some part, i.e. background job, of the distributed job and you then want to
176
+ # stop all other not yet finished parts. Please note that only jobs can be
177
+ # stopped which ask the distributed job actively whether or not it was
178
+ # stopped.
179
+ #
180
+ # @returns [Boolean] Always returns true
181
+ #
182
+ # @example
183
+ # class SomeBackgroundJob
184
+ # def perform(whatever, token, part)
185
+ # distributed_job = DistributedJobClient.build(token: token)
186
+ #
187
+ # return if distributed_job.stopped?
188
+ #
189
+ # # ...
190
+ #
191
+ # distributed_job.done(part)
192
+ # rescue
193
+ # distributed_job.stop
194
+ #
195
+ # raise
196
+ # end
197
+ # end
198
+
199
+ def stop
200
+ redis.multi do |transaction|
201
+ transaction.hset("#{redis_key}:state", 'stopped', 1)
202
+
203
+ transaction.expire("#{redis_key}:state", ttl)
204
+ transaction.expire("#{redis_key}:parts", ttl)
205
+ end
206
+
207
+ true
208
+ end
209
+
210
+ # Returns true when the distributed job was stopped or false otherwise.
211
+ #
212
+ # @returns [Boolean] Returns true or false
213
+ #
214
+ # @example
215
+ # class SomeBackgroundJob
216
+ # def perform(whatever, token, part)
217
+ # distributed_job = DistributedJobClient.build(token: token)
218
+ #
219
+ # return if distributed_job.stopped?
220
+ #
221
+ # # ...
222
+ #
223
+ # distributed_job.done(part)
224
+ # rescue
225
+ # distributed_job.stop
226
+ #
227
+ # raise
228
+ # end
229
+ # end
230
+
231
+ def stopped?
232
+ redis.hget("#{redis_key}:state", 'stopped') == '1'
233
+ end
234
+
235
+ private
236
+
237
+ def redis
238
+ client.redis
239
+ end
240
+
241
+ def namespace
242
+ client.namespace
243
+ end
244
+
245
+ def close
246
+ redis.multi do |transaction|
247
+ transaction.hset("#{redis_key}:state", 'closed', 1)
248
+
249
+ transaction.expire("#{redis_key}:state", ttl)
250
+ transaction.expire("#{redis_key}:parts", ttl)
251
+ end
252
+
253
+ true
254
+ end
255
+
256
+ def closed?
257
+ redis.hget("#{redis_key}:state", 'closed') == '1'
258
+ end
259
+
260
+ def push(part)
261
+ @push_script ||= <<~SCRIPT
262
+ local key, part, ttl = ARGV[1], ARGV[2], tonumber(ARGV[3])
263
+
264
+ if redis.call('sadd', key .. ':parts', part) == 1 then
265
+ redis.call('hincrby', key .. ':state', 'total', 1)
266
+ end
267
+
268
+ redis.call('expire', key .. ':parts', ttl)
269
+ redis.call('expire', key .. ':state', ttl)
270
+ SCRIPT
271
+
272
+ redis.eval(@push_script, argv: [redis_key, part.to_s, ttl])
273
+ end
274
+
275
+ def redis_key
276
+ @redis_key ||= [namespace, 'distributed_jobs', token].compact.join(':')
277
+ end
278
+ end
279
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class DistributedJob
4
- VERSION = '1.0.0'
3
+ module DistributedJob
4
+ VERSION = '3.0.1'
5
5
  end
@@ -1,275 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'distributed_job/version'
4
+ require 'distributed_job/client'
5
+ require 'distributed_job/job'
4
6
  require 'redis'
5
7
 
6
- # A distributed job instance allows to keep track of distributed jobs, i.e.
7
- # jobs which are split into multiple units running in parallel and in multiple
8
- # workers using redis.
9
- #
10
- # @example Creating a distributed job
11
- # distributed_job = DistributedJob.new(redis: Redis.new, token: SecureRandom.hex)
12
- #
13
- # distributed_job.push_each(Date.parse('2021-01-01')..Date.today) do |date, part|
14
- # SomeBackgroundJob.perform_async(date, distributed_job.token, part)
15
- # end
16
- #
17
- # distributed_job.token # can be used to query the status of the distributed job
18
- #
19
- # @example Processing a distributed job part
20
- # class SomeBackgroundJob
21
- # def perform(whatever, token, part)
22
- # distributed_job = DistributedJob.new(redis: Redis.new, token: token)
23
- #
24
- # return if distributed_job.stopped?
25
- #
26
- # # ...
27
- #
28
- # distributed_job.done(part)
29
- #
30
- # if distributed_job.finished?
31
- # # perform e.g. cleanup or the some other job
32
- # end
33
- # rescue
34
- # distributed_job.stop
35
- #
36
- # raise
37
- # end
38
- # end
39
-
40
- class DistributedJob
41
- attr_reader :redis, :token, :ttl
42
-
43
- # Initializes a new distributed job.
44
- #
45
- # @param redis [Redis] The redis connection instance
46
- # @param token [String] Some token to be used to identify the job. You can
47
- # e.g. use SecureRandom.hex to generate one.
48
- # @param ttl [Integer] The number of seconds this job will stay available
49
- # in redis. This value is used to automatically expire and clean up the
50
- # job in redis. Default is 86400, i.e. one day. The ttl is used everytime
51
- # the job is modified in redis.
52
- #
53
- # @example
54
- # distributed_job = DistributedJob.new(redis: Redis.new, token: SecureRandom.hex)
55
-
56
- def initialize(redis:, token:, ttl: 86_400)
57
- @redis = redis
58
- @token = token
59
- @ttl = ttl
60
- end
61
-
62
- # Pass an enum to be used to iterate all the units of work of the distributed
63
- # job. The distributed job needs to know all of them to keep track of the
64
- # overall number and status of the parts. Passing an enum is much better
65
- # compared to pushing the parts manually, because the distributed job needs
66
- # to be closed before the last part of the distributed job is enqueued into
67
- # some job queue. Otherwise it could potentially happen that the last part is
68
- # already processed in the job queue before it is pushed to redis, such that
69
- # the last job doesn't know that the distributed job is finished.
70
- #
71
- # @param enum [#each_with_index] The enum which can be iterated to get all
72
- # job parts
73
- #
74
- # @example
75
- # distributed_job.push_each(Date.parse('2021-01-01')..Date.today) do |date, part|
76
- # # e.g. SomeBackgroundJob.perform_async(date, distributed_job.token, part)
77
- # end
78
- #
79
- # @example ActiveRecord
80
- # distributed_job.push_each(User.select(:id).find_in_batches) do |batch, part|
81
- # # e.g. SomeBackgroundJob.perform_async(batch.first.id, batch.last.id, distributed_job.token, part)
82
- # end
83
-
84
- def push_each(enum)
85
- return enum_for(:push_each, enum) unless block_given?
86
-
87
- previous_object = nil
88
- previous_index = nil
89
-
90
- enum.each_with_index do |current_object, current_index|
91
- push(current_index)
92
-
93
- yield(previous_object, previous_index.to_s) if previous_index
94
-
95
- previous_object = current_object
96
- previous_index = current_index
97
- end
98
-
99
- close
100
-
101
- yield(previous_object, previous_index.to_s) if previous_index
102
- end
103
-
104
- # Returns all pushed parts of the distributed job
105
- #
106
- # @return [Enumerator] The enum which allows to iterate all parts
107
-
108
- def parts
109
- redis.sscan_each("#{redis_key}:parts")
110
- end
111
-
112
- # Removes the specified part from the distributed job, i.e. from the set of
113
- # unfinished parts. Use this method when the respective job part has been
114
- # successfully processed, i.e. finished.
115
- #
116
- # @param part [String] The job part
117
- # @returns [Boolean] Returns true when there are no more unfinished parts
118
- # left or false otherwise
119
- #
120
- # @example
121
- # class SomeBackgroundJob
122
- # def perform(whatever, token, part)
123
- # distributed_job = DistributedJob.new(redis: Redis.new, token: token)
124
- #
125
- # # ...
126
- #
127
- # distributed_job.done(part)
128
- # end
129
- # end
130
-
131
- def done(part)
132
- @done_script ||= <<~SCRIPT
133
- local key, part, ttl = ARGV[1], ARGV[2], tonumber(ARGV[3])
134
-
135
- if redis.call('srem', key .. ':parts', part) == 0 then return end
136
-
137
- redis.call('expire', key .. ':parts', ttl)
138
- redis.call('expire', key .. ':state', ttl)
139
-
140
- return redis.call('scard', key .. ':parts')
141
- SCRIPT
142
-
143
- redis.eval(@done_script, argv: [redis_script_key, part.to_s, ttl]) == 0 && closed?
144
- end
145
-
146
- # Returns the total number of pushed parts, no matter if finished or not.
147
- #
148
- # @example
149
- # distributed_job.total # => e.g. 13
150
-
151
- def total
152
- redis.hget("#{redis_key}:state", 'total').to_i
153
- end
154
-
155
- # Returns the number of pushed parts which are not finished.
156
- #
157
- # @example
158
- # distributed_job.count # => e.g. 8
159
-
160
- def count
161
- redis.scard("#{redis_key}:parts")
162
- end
163
-
164
- # Returns true if there are no more unfinished parts.
165
- #
166
- # @example
167
- # distributed_job.finished? #=> true/false
168
-
169
- def finished?
170
- closed? && count.zero?
171
- end
172
-
173
- # Allows to stop a distributed job. This is useful if some error occurred in
174
- # some part, i.e. background job, of the distributed job and you then want to
175
- # stop all other not yet finished parts. Please note that only jobs can be
176
- # stopped which ask the distributed job actively whether or not it was
177
- # stopped.
178
- #
179
- # @returns [Boolean] Always returns true
180
- #
181
- # @example
182
- # class SomeBackgroundJob
183
- # def perform(whatever, token, part)
184
- # distributed_job = DistributedJob.new(redis: Redis.new, token: token)
185
- #
186
- # return if distributed_job.stopped?
187
- #
188
- # # ...
189
- #
190
- # distributed_job.done(part)
191
- # rescue
192
- # distributed_job.stop
193
- #
194
- # raise
195
- # end
196
- # end
197
-
198
- def stop
199
- redis.multi do
200
- redis.hset("#{redis_key}:state", 'stopped', 1)
201
-
202
- redis.expire("#{redis_key}:state", ttl)
203
- redis.expire("#{redis_key}:parts", ttl)
204
- end
205
-
206
- true
207
- end
208
-
209
- # Returns true when the distributed job was stopped or false otherwise.
210
- #
211
- # @returns [Boolean] Returns true or false
212
- #
213
- # @example
214
- # class SomeBackgroundJob
215
- # def perform(whatever, token, part)
216
- # distributed_job = DistributedJob.new(redis: Redis.new, token: token)
217
- #
218
- # return if distributed_job.stopped?
219
- #
220
- # # ...
221
- #
222
- # distributed_job.done(part)
223
- # rescue
224
- # distributed_job.stop
225
- #
226
- # raise
227
- # end
228
- # end
229
-
230
- def stopped?
231
- redis.hget("#{redis_key}:state", 'stopped') == '1'
232
- end
233
-
234
- private
235
-
236
- def close
237
- redis.multi do
238
- redis.hset("#{redis_key}:state", 'closed', 1)
239
-
240
- redis.expire("#{redis_key}:state", ttl)
241
- redis.expire("#{redis_key}:parts", ttl)
242
- end
243
-
244
- true
245
- end
246
-
247
- def closed?
248
- redis.hget("#{redis_key}:state", 'closed') == '1'
249
- end
250
-
251
- def push(part)
252
- @push_script ||= <<~SCRIPT
253
- local key, part, ttl = ARGV[1], ARGV[2], tonumber(ARGV[3])
254
-
255
- if redis.call('sadd', key .. ':parts', part) == 1 then
256
- redis.call('hincrby', key .. ':state', 'total', 1)
257
- end
258
-
259
- redis.call('expire', key .. ':parts', ttl)
260
- redis.call('expire', key .. ':state', ttl)
261
- SCRIPT
262
-
263
- redis.eval(@push_script, argv: [redis_script_key, part.to_s, ttl])
264
- end
265
-
266
- def redis_key
267
- @redis_key ||= "distributed_jobs:#{token}"
268
- end
269
-
270
- def redis_script_key
271
- return "#{redis.namespace}:#{redis_key}" if redis.respond_to?(:namespace)
272
-
273
- redis_key
274
- end
275
- end
8
+ module DistributedJob; end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: distributed_job
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 3.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Benjamin Vetter
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-10-20 00:00:00.000000000 Z
11
+ date: 2022-02-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rspec
@@ -44,14 +44,14 @@ dependencies:
44
44
  requirements:
45
45
  - - ">="
46
46
  - !ruby/object:Gem::Version
47
- version: '0'
47
+ version: 4.1.0
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
- version: '0'
54
+ version: 4.1.0
55
55
  description: Keep track of distributed jobs spanning multiple workers using redis
56
56
  email:
57
57
  - benjamin.vetter@wlw.de
@@ -63,10 +63,8 @@ files:
63
63
  - ".gitignore"
64
64
  - ".rspec"
65
65
  - ".rubocop.yml"
66
- - ".travis.yml"
67
66
  - CHANGELOG.md
68
67
  - Gemfile
69
- - Gemfile.lock
70
68
  - LICENSE.txt
71
69
  - README.md
72
70
  - Rakefile
@@ -75,6 +73,8 @@ files:
75
73
  - distributed_job.gemspec
76
74
  - docker-compose.yml
77
75
  - lib/distributed_job.rb
76
+ - lib/distributed_job/client.rb
77
+ - lib/distributed_job/job.rb
78
78
  - lib/distributed_job/version.rb
79
79
  homepage: https://github.com/mrkamel/distributed_job
80
80
  licenses:
@@ -83,7 +83,7 @@ metadata:
83
83
  homepage_uri: https://github.com/mrkamel/distributed_job
84
84
  source_code_uri: https://github.com/mrkamel/distributed_job
85
85
  changelog_uri: https://github.com/mrkamel/distributed_job/blob/master/CHANGELOG.md
86
- post_install_message:
86
+ post_install_message:
87
87
  rdoc_options: []
88
88
  require_paths:
89
89
  - lib
@@ -98,8 +98,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
98
98
  - !ruby/object:Gem::Version
99
99
  version: '0'
100
100
  requirements: []
101
- rubygems_version: 3.0.3
102
- signing_key:
101
+ rubygems_version: 3.3.3
102
+ signing_key:
103
103
  specification_version: 4
104
104
  summary: Keep track of distributed jobs using redis
105
105
  test_files: []
data/.travis.yml DELETED
@@ -1,6 +0,0 @@
1
- ---
2
- language: ruby
3
- cache: bundler
4
- rvm:
5
- - 2.6.6
6
- before_install: gem install bundler -v 2.1.4
data/Gemfile.lock DELETED
@@ -1,57 +0,0 @@
1
- PATH
2
- remote: .
3
- specs:
4
- distributed_job (1.0.0)
5
- redis
6
-
7
- GEM
8
- remote: https://rubygems.org/
9
- specs:
10
- ast (2.4.2)
11
- diff-lcs (1.4.4)
12
- parallel (1.21.0)
13
- parser (3.0.2.0)
14
- ast (~> 2.4.1)
15
- rainbow (3.0.0)
16
- rake (12.3.3)
17
- redis (4.5.1)
18
- regexp_parser (2.1.1)
19
- rexml (3.2.5)
20
- rspec (3.10.0)
21
- rspec-core (~> 3.10.0)
22
- rspec-expectations (~> 3.10.0)
23
- rspec-mocks (~> 3.10.0)
24
- rspec-core (3.10.1)
25
- rspec-support (~> 3.10.0)
26
- rspec-expectations (3.10.1)
27
- diff-lcs (>= 1.2.0, < 2.0)
28
- rspec-support (~> 3.10.0)
29
- rspec-mocks (3.10.2)
30
- diff-lcs (>= 1.2.0, < 2.0)
31
- rspec-support (~> 3.10.0)
32
- rspec-support (3.10.2)
33
- rubocop (1.22.1)
34
- parallel (~> 1.10)
35
- parser (>= 3.0.0.0)
36
- rainbow (>= 2.2.2, < 4.0)
37
- regexp_parser (>= 1.8, < 3.0)
38
- rexml
39
- rubocop-ast (>= 1.12.0, < 2.0)
40
- ruby-progressbar (~> 1.7)
41
- unicode-display_width (>= 1.4.0, < 3.0)
42
- rubocop-ast (1.12.0)
43
- parser (>= 3.0.1.1)
44
- ruby-progressbar (1.11.0)
45
- unicode-display_width (2.1.0)
46
-
47
- PLATFORMS
48
- ruby
49
-
50
- DEPENDENCIES
51
- distributed_job!
52
- rake (~> 12.0)
53
- rspec (~> 3.0)
54
- rubocop
55
-
56
- BUNDLED WITH
57
- 2.1.4