distributed_job 1.0.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 +7 -0
- data/.github/workflows/test.yml +30 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.rubocop.yml +19 -0
- data/.travis.yml +6 -0
- data/CHANGELOG.md +5 -0
- data/Gemfile +9 -0
- data/Gemfile.lock +57 -0
- data/LICENSE.txt +21 -0
- data/README.md +100 -0
- data/Rakefile +8 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/distributed_job.gemspec +33 -0
- data/docker-compose.yml +6 -0
- data/lib/distributed_job/version.rb +5 -0
- data/lib/distributed_job.rb +275 -0
- metadata +105 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: e8aa1822c85ccb44521e470a312b430b9409dd537add85122045f7cd71d075ed
|
4
|
+
data.tar.gz: a781158c7699303544cb78185a200059da25013830f677a44a704b6a0c115af8
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 6885d739ae86c615588dfd85ca1b4d0b0b3e4bcdf8cca1ddf1aebcb806a703c1980ca2d1e6f758d22b38a24a3859d5673fa47e7b2438db4635a4e24351bff4eb
|
7
|
+
data.tar.gz: ffbea863763a62d8f22a2e8c2f2b8555a910fc908801e9a93157703a3a2e7e6fbd9bee3b90e6c35971d430b8b720781b02c1d116159e1ea2c3cb00b9fa57b7d4
|
@@ -0,0 +1,30 @@
|
|
1
|
+
on: push
|
2
|
+
name: test
|
3
|
+
jobs:
|
4
|
+
test:
|
5
|
+
runs-on: ubuntu-latest
|
6
|
+
strategy:
|
7
|
+
fail-fast: false
|
8
|
+
matrix:
|
9
|
+
redis:
|
10
|
+
- redis:5.0
|
11
|
+
- redis:6.0
|
12
|
+
ruby:
|
13
|
+
- 2.6
|
14
|
+
- 2.7
|
15
|
+
- 3.0
|
16
|
+
services:
|
17
|
+
elasticsearch:
|
18
|
+
image: ${{ matrix.redis }}
|
19
|
+
ports:
|
20
|
+
- 6379:6379
|
21
|
+
steps:
|
22
|
+
- uses: actions/checkout@v1
|
23
|
+
- uses: actions/setup-ruby@v1
|
24
|
+
with:
|
25
|
+
ruby-version: ${{ matrix.ruby }}
|
26
|
+
- run: gem install bundler
|
27
|
+
- run: bundle
|
28
|
+
- run: sleep 3
|
29
|
+
- run: bundle exec rspec
|
30
|
+
- run: bundle exec rubocop
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
AllCops:
|
2
|
+
NewCops: enable
|
3
|
+
TargetRubyVersion: 2.5
|
4
|
+
SuggestExtensions: false
|
5
|
+
|
6
|
+
Metrics/MethodLength:
|
7
|
+
Enabled: false
|
8
|
+
|
9
|
+
Metrics/BlockLength:
|
10
|
+
Enabled: false
|
11
|
+
|
12
|
+
Layout/LineLength:
|
13
|
+
Enabled: false
|
14
|
+
|
15
|
+
Style/Documentation:
|
16
|
+
Enabled: false
|
17
|
+
|
18
|
+
Style/NumericPredicate:
|
19
|
+
Enabled: false
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,57 @@
|
|
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
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2021 Benjamin Vetter
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
# DistributedJob
|
2
|
+
|
3
|
+
Easily keep track of distributed jobs consisting of an arbitrary number of
|
4
|
+
parts spanning multiple workers using redis. Can be used with any kind of
|
5
|
+
backround job processing queue.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Add this line to your application's Gemfile:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
gem 'distributed_job'
|
13
|
+
```
|
14
|
+
|
15
|
+
And then execute:
|
16
|
+
|
17
|
+
$ bundle install
|
18
|
+
|
19
|
+
Or install it yourself as:
|
20
|
+
|
21
|
+
$ gem install distributed_job
|
22
|
+
|
23
|
+
## Usage
|
24
|
+
|
25
|
+
Getting started is very easy. A `DistributedJob` allows to keep track of a
|
26
|
+
distributed job, i.e. a job which is split into multiple units running in
|
27
|
+
parallel and in multiple workers.
|
28
|
+
|
29
|
+
To create a distributed job and add parts, i.e. units of work, to it, simply:
|
30
|
+
|
31
|
+
```ruby
|
32
|
+
distributed_job = DistributedJob.new(redis: Redis.new, token: SecureRandom.hex)
|
33
|
+
|
34
|
+
distributed_job.push_each(Date.parse('2021-01-01')..Date.today) do |date, part|
|
35
|
+
SomeBackgroundJob.perform_async(date, distributed_job.token, part)
|
36
|
+
end
|
37
|
+
|
38
|
+
distributed_job.token # can be used to query the status of the distributed job
|
39
|
+
```
|
40
|
+
|
41
|
+
The `token` can be used to keep query the status of the distributed job, e.g.
|
42
|
+
on a job summary page or similar. You can show some progress bar in the browser
|
43
|
+
or in the terminal, etc:
|
44
|
+
|
45
|
+
```ruby
|
46
|
+
# token is given via URL or via some other means
|
47
|
+
distributed_job = Distributed.new(redis: Redis.new, token: params[:token])
|
48
|
+
|
49
|
+
distributed_job.total # total number of parts
|
50
|
+
distributed_job.count # number of unfinished parts
|
51
|
+
distributed_job.finished?
|
52
|
+
```
|
53
|
+
|
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
|
56
|
+
that you can use whatever background job processing tool you like most.
|
57
|
+
|
58
|
+
```ruby
|
59
|
+
class SomeBackgroundJob
|
60
|
+
def perform(whatever, token, part)
|
61
|
+
distributed_job = DistributedJob.new(redis: Redis.new, token: token)
|
62
|
+
|
63
|
+
return if distributed_job.stopped?
|
64
|
+
|
65
|
+
# ...
|
66
|
+
|
67
|
+
distributed_job.done(part)
|
68
|
+
|
69
|
+
if distributed_job.finished?
|
70
|
+
# perform e.g. cleanup or the some other job
|
71
|
+
end
|
72
|
+
rescue
|
73
|
+
distributed_job.stop
|
74
|
+
|
75
|
+
raise
|
76
|
+
end
|
77
|
+
end
|
78
|
+
```
|
79
|
+
|
80
|
+
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.
|
85
|
+
|
86
|
+
## Development
|
87
|
+
|
88
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run
|
89
|
+
`bundle exec rspec` to run the tests. You can also run `bin/console` for an
|
90
|
+
interactive prompt that will allow you to experiment.
|
91
|
+
|
92
|
+
## Contributing
|
93
|
+
|
94
|
+
Bug reports and pull requests are welcome on GitHub at
|
95
|
+
https://github.com/mrkamel/distributed_job.
|
96
|
+
|
97
|
+
## License
|
98
|
+
|
99
|
+
The gem is available as open source under the terms of the [MIT
|
100
|
+
License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'bundler/setup'
|
5
|
+
require 'distributed_job'
|
6
|
+
|
7
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
8
|
+
# with your gem easier. You can also use a different console, if you like.
|
9
|
+
|
10
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
11
|
+
# require "pry"
|
12
|
+
# Pry.start
|
13
|
+
|
14
|
+
require 'irb'
|
15
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'lib/distributed_job/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = 'distributed_job'
|
7
|
+
spec.version = DistributedJob::VERSION
|
8
|
+
spec.authors = ['Benjamin Vetter']
|
9
|
+
spec.email = ['benjamin.vetter@wlw.de']
|
10
|
+
|
11
|
+
spec.summary = 'Keep track of distributed jobs using redis'
|
12
|
+
spec.description = 'Keep track of distributed jobs spanning multiple workers using redis'
|
13
|
+
spec.homepage = 'https://github.com/mrkamel/distributed_job'
|
14
|
+
spec.license = 'MIT'
|
15
|
+
spec.required_ruby_version = Gem::Requirement.new('>= 2.5.0')
|
16
|
+
|
17
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
18
|
+
spec.metadata['source_code_uri'] = 'https://github.com/mrkamel/distributed_job'
|
19
|
+
spec.metadata['changelog_uri'] = 'https://github.com/mrkamel/distributed_job/blob/master/CHANGELOG.md'
|
20
|
+
|
21
|
+
# Specify which files should be added to the gem when it is released.
|
22
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
23
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
24
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
25
|
+
end
|
26
|
+
spec.bindir = 'exe'
|
27
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
28
|
+
spec.require_paths = ['lib']
|
29
|
+
|
30
|
+
spec.add_development_dependency 'rspec'
|
31
|
+
spec.add_development_dependency 'rubocop'
|
32
|
+
spec.add_dependency 'redis'
|
33
|
+
end
|
data/docker-compose.yml
ADDED
@@ -0,0 +1,275 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'distributed_job/version'
|
4
|
+
require 'redis'
|
5
|
+
|
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
|
metadata
ADDED
@@ -0,0 +1,105 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: distributed_job
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Benjamin Vetter
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2021-10-20 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rspec
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rubocop
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: redis
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
description: Keep track of distributed jobs spanning multiple workers using redis
|
56
|
+
email:
|
57
|
+
- benjamin.vetter@wlw.de
|
58
|
+
executables: []
|
59
|
+
extensions: []
|
60
|
+
extra_rdoc_files: []
|
61
|
+
files:
|
62
|
+
- ".github/workflows/test.yml"
|
63
|
+
- ".gitignore"
|
64
|
+
- ".rspec"
|
65
|
+
- ".rubocop.yml"
|
66
|
+
- ".travis.yml"
|
67
|
+
- CHANGELOG.md
|
68
|
+
- Gemfile
|
69
|
+
- Gemfile.lock
|
70
|
+
- LICENSE.txt
|
71
|
+
- README.md
|
72
|
+
- Rakefile
|
73
|
+
- bin/console
|
74
|
+
- bin/setup
|
75
|
+
- distributed_job.gemspec
|
76
|
+
- docker-compose.yml
|
77
|
+
- lib/distributed_job.rb
|
78
|
+
- lib/distributed_job/version.rb
|
79
|
+
homepage: https://github.com/mrkamel/distributed_job
|
80
|
+
licenses:
|
81
|
+
- MIT
|
82
|
+
metadata:
|
83
|
+
homepage_uri: https://github.com/mrkamel/distributed_job
|
84
|
+
source_code_uri: https://github.com/mrkamel/distributed_job
|
85
|
+
changelog_uri: https://github.com/mrkamel/distributed_job/blob/master/CHANGELOG.md
|
86
|
+
post_install_message:
|
87
|
+
rdoc_options: []
|
88
|
+
require_paths:
|
89
|
+
- lib
|
90
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
91
|
+
requirements:
|
92
|
+
- - ">="
|
93
|
+
- !ruby/object:Gem::Version
|
94
|
+
version: 2.5.0
|
95
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
96
|
+
requirements:
|
97
|
+
- - ">="
|
98
|
+
- !ruby/object:Gem::Version
|
99
|
+
version: '0'
|
100
|
+
requirements: []
|
101
|
+
rubygems_version: 3.0.3
|
102
|
+
signing_key:
|
103
|
+
specification_version: 4
|
104
|
+
summary: Keep track of distributed jobs using redis
|
105
|
+
test_files: []
|