atomic-sidekiq 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +1 -0
- data/.rspec +1 -0
- data/Dockerfile +22 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +67 -0
- data/LICENSE +21 -0
- data/README.md +68 -0
- data/atomic-sidekiq.gemspec +26 -0
- data/bin/release +9 -0
- data/bin/sidekiqfail +66 -0
- data/bin/sidekiqload +227 -0
- data/bin/test +6 -0
- data/docker-compose.yml +31 -0
- data/docker.env +1 -0
- data/lib/atomic-sidekiq.rb +1 -0
- data/lib/atomic_sidekiq/atomic_fetch.rb +55 -0
- data/lib/atomic_sidekiq/atomic_operation/acknowledge.rb +11 -0
- data/lib/atomic_sidekiq/atomic_operation/base.rb +22 -0
- data/lib/atomic_sidekiq/atomic_operation/expire.rb +19 -0
- data/lib/atomic_sidekiq/atomic_operation/lua_scripts/expire.lua +15 -0
- data/lib/atomic_sidekiq/atomic_operation/lua_scripts/retrieve.lua +11 -0
- data/lib/atomic_sidekiq/atomic_operation/requeue.rb +20 -0
- data/lib/atomic_sidekiq/atomic_operation/retrieve.rb +23 -0
- data/lib/atomic_sidekiq/dead_job_collector.rb +43 -0
- data/lib/atomic_sidekiq/sidekiq.rb +6 -0
- data/lib/atomic_sidekiq/unit_of_work.rb +28 -0
- data/lib/atomic_sidekiq.rb +13 -0
- metadata +169 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 1af076c37beeb362724591b736eca75821223033
|
4
|
+
data.tar.gz: a8ffb7fcfafa8ee3f70e24e13b728703ad55e4c8
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 6ae93a470419fac19638dd02a0f92f7f6972a9129ac11fba26265062a3ab186dfb1962ed4718fdce84cf2a2c1ef0ecae07d0fb6df79d6316d78224cd33d6b755
|
7
|
+
data.tar.gz: 4ca5974dc84bd4d8fa1b8efbbf4bb5a862022039cc8336b149ba0287665324d5565846da977b21eaf1fd9574d666f03f455f15643d7713fa5168018946a53004
|
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
.byebug_history
|
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--require spec_helper -I ./lib
|
data/Dockerfile
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# Choose the official Ruby 2.3.4 image as our starting point
|
2
|
+
FROM ruby:2.5.1
|
3
|
+
|
4
|
+
ENV LC_ALL en_US.UTF-8
|
5
|
+
|
6
|
+
# install locked bundler version (1.16.1)
|
7
|
+
RUN gem install bundler -v 1.16.1
|
8
|
+
ENV BUNDLE_PATH=/bundle BUNDLE_JOBS=4
|
9
|
+
|
10
|
+
# Set up working directory
|
11
|
+
ENV APP_HOME /sidekiq-atomic
|
12
|
+
|
13
|
+
RUN mkdir $APP_HOME
|
14
|
+
|
15
|
+
WORKDIR $APP_HOME
|
16
|
+
|
17
|
+
ADD Gemfile .
|
18
|
+
ADD Gemfile.lock .
|
19
|
+
|
20
|
+
RUN bundle install
|
21
|
+
|
22
|
+
ADD . $APP_HOME
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
atomic-sidekiq (0.0.0)
|
5
|
+
sidekiq (~> 5.0)
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: https://rubygems.org/
|
9
|
+
specs:
|
10
|
+
ast (2.4.0)
|
11
|
+
byebug (10.0.2)
|
12
|
+
concurrent-ruby (1.0.5)
|
13
|
+
connection_pool (2.2.1)
|
14
|
+
diff-lcs (1.3)
|
15
|
+
parallel (1.12.1)
|
16
|
+
parser (2.5.0.5)
|
17
|
+
ast (~> 2.4.0)
|
18
|
+
powerpack (0.1.1)
|
19
|
+
rack (2.0.4)
|
20
|
+
rack-protection (2.0.1)
|
21
|
+
rack
|
22
|
+
rainbow (3.0.0)
|
23
|
+
rake (11.3.0)
|
24
|
+
redis (4.0.1)
|
25
|
+
rspec (3.7.0)
|
26
|
+
rspec-core (~> 3.7.0)
|
27
|
+
rspec-expectations (~> 3.7.0)
|
28
|
+
rspec-mocks (~> 3.7.0)
|
29
|
+
rspec-core (3.7.1)
|
30
|
+
rspec-support (~> 3.7.0)
|
31
|
+
rspec-expectations (3.7.0)
|
32
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
33
|
+
rspec-support (~> 3.7.0)
|
34
|
+
rspec-mocks (3.7.0)
|
35
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
36
|
+
rspec-support (~> 3.7.0)
|
37
|
+
rspec-support (3.7.1)
|
38
|
+
rubocop (0.54.0)
|
39
|
+
parallel (~> 1.10)
|
40
|
+
parser (>= 2.5)
|
41
|
+
powerpack (~> 0.1)
|
42
|
+
rainbow (>= 2.2.2, < 4.0)
|
43
|
+
ruby-progressbar (~> 1.7)
|
44
|
+
unicode-display_width (~> 1.0, >= 1.0.1)
|
45
|
+
ruby-progressbar (1.9.0)
|
46
|
+
sidekiq (5.1.2)
|
47
|
+
concurrent-ruby (~> 1.0)
|
48
|
+
connection_pool (~> 2.2, >= 2.2.0)
|
49
|
+
rack-protection (>= 1.5.0)
|
50
|
+
redis (>= 3.3.5, < 5)
|
51
|
+
timecop (0.9.1)
|
52
|
+
unicode-display_width (1.3.0)
|
53
|
+
|
54
|
+
PLATFORMS
|
55
|
+
ruby
|
56
|
+
|
57
|
+
DEPENDENCIES
|
58
|
+
atomic-sidekiq!
|
59
|
+
bundler (~> 1.12)
|
60
|
+
byebug (~> 10.0)
|
61
|
+
rake (~> 11.3)
|
62
|
+
rspec (~> 3.6)
|
63
|
+
rubocop (~> 0.54)
|
64
|
+
timecop (~> 0.9)
|
65
|
+
|
66
|
+
BUNDLED WITH
|
67
|
+
1.16.1
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2010-2018 Google, Inc. http://angularjs.org
|
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,68 @@
|
|
1
|
+
# AtomicSidekiq
|
2
|
+
AtomicSidekiq implements a reliable way of processing jobs using Sidekiq. By default, Sidekiq will retrieve jobs from the queue by removing it from Redis. If the job fails to complete (e.g. the process terminates unexpectdly mid-job), the job will be lost forever. This can be acceptable in many applications, but some application require higher levels of reliability, hence AtomicSidekiq will not erase any job from Redis until it's acknowledged that they have finished - otherwise, they are re-scheduled.
|
3
|
+
|
4
|
+
The algorithm used by AtomicSidekiq supports both queue prioritization mechanisms: strict priority and weighted random.
|
5
|
+
|
6
|
+
## Requirements
|
7
|
+
AtomicSidekiq supports only Sidekiq 5+.
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
```
|
11
|
+
gem install atomic-sidekiq
|
12
|
+
```
|
13
|
+
|
14
|
+
Add to your server configuration (or create a new one if you don't have):
|
15
|
+
```ruby
|
16
|
+
Sidekiq.configure_server do |config|
|
17
|
+
config.atomic_fetch!
|
18
|
+
end
|
19
|
+
```
|
20
|
+
|
21
|
+
## Configuration
|
22
|
+
By default, jobs will expire and be re-queued after 1 hour if not acknowledged, and the "Collector" will check if for expired jobs every 60 seconds. This can be reconfigured as desired: _(Note that collection adds some overhead)_
|
23
|
+
```ruby
|
24
|
+
Sidekiq.configure_server do |config|
|
25
|
+
config.atomic_fetch!({
|
26
|
+
collection_interval: 5, # Unit: seconds
|
27
|
+
expiration_time: 1800 # Unit: seconds (30 minutes)
|
28
|
+
})
|
29
|
+
end
|
30
|
+
```
|
31
|
+
|
32
|
+
## Benchmark
|
33
|
+
### Reliability
|
34
|
+
This benchmark tests Sidekiq's ability to recover from unexpected failures. The test script forces a failure randomly 1% of the time it's running a job and measures how many jobs are able to be completed:
|
35
|
+
|
36
|
+
| Version | Queued | Processed | Lost |
|
37
|
+
|---------------|---------|-----------|-------|
|
38
|
+
| Sidekiq | 10,000 | 1,838 | 8,162 |
|
39
|
+
| AtomicSidekiq | 10,000 | 10,000 | 0 |
|
40
|
+
|
41
|
+
Since jobs run in parallel, when the process crashes it loses all jobs that had been retrieved and were running at the moment. AtomicSidekiq manage to retrieve all jobs and finish the work. The reliability script can be found at `./bin/sidekiqfail`. It terminates and restores Sidekiq several times until all jobs are processed or a maximum number of tries is reached.
|
42
|
+
|
43
|
+
The test script can be run with the flag `-a` to use the **AtomicSidekiq::AtomicFetch** and without any flags to run with the default Sidekiq fetcher.
|
44
|
+
|
45
|
+
_(Note: Sidekiq PRO comes with its own reliable fetcher, no benchmarks were run against that version. Only the free version has been tested)_
|
46
|
+
|
47
|
+
### Performance
|
48
|
+
The performance test uses the default settings for both fetchers (default and AtomicFetch) and times how long Sidekiq takes to process two loads, one of 10,000 and another one of 100,000 jobs.
|
49
|
+
|
50
|
+
| Version | Time Ellapsed (10k) | Throughput (10k) | Time Ellapsed (100k) | Throughput (100k) |
|
51
|
+
|---------------|---------------------|------------------|----------------------|-------------------|
|
52
|
+
| Sidekiq | 6s | 166 jobs/sec | 30s | 3,333 jobs/sec |
|
53
|
+
| AtomicSidekiq | 8s | 125 jobs/sec | 1m10s | 1,429 jobs/sec |
|
54
|
+
|
55
|
+
The reliability improvements of AtomicSidekiq come at the cost of less throughput. AtomicSidekiq's algorithm is linear instead of constant like Sidekiq's default, meaning that the cost of performance increases linearly as more jobs are added to the queue.
|
56
|
+
|
57
|
+
## Tests
|
58
|
+
```sh
|
59
|
+
bundle exec rspec
|
60
|
+
```
|
61
|
+
|
62
|
+
You may also run the tests with Docker using `docker-compose` (it will automatially start a Redis server for the integration tests):
|
63
|
+
```sh
|
64
|
+
docker-compose run test
|
65
|
+
```
|
66
|
+
|
67
|
+
## Caveat
|
68
|
+
This ensures that your job will be run completely **at least once**. It may run more than once if your job fails to acknowledge (e.g. the process terminates after performing a job but right before the ack is sent). _Note: This is better than the default Sidekiq though, which cannot give any guarantees on the number of times a job will be run._
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = "atomic-sidekiq"
|
5
|
+
s.version = "1.0.0"
|
6
|
+
s.date = "2018-04-01"
|
7
|
+
s.summary = "Reliable fetcher for Sidekiq"
|
8
|
+
s.description = "Reliable fetcher for Sidekiq"
|
9
|
+
s.homepage = "https://github.com/Colex/atomic-sidekiq"
|
10
|
+
s.authors = ["Alex Correia Santos"]
|
11
|
+
s.email = ["alex.santios@visiblealpha.com"]
|
12
|
+
s.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
13
|
+
s.require_paths = ["lib"]
|
14
|
+
s.license = "MIT"
|
15
|
+
|
16
|
+
s.required_ruby_version = ">= 2.3"
|
17
|
+
|
18
|
+
s.add_development_dependency "bundler", "~> 1.12"
|
19
|
+
s.add_development_dependency "rake", "~> 11.3"
|
20
|
+
s.add_development_dependency "rspec", "~> 3.6"
|
21
|
+
s.add_development_dependency "rubocop", "~> 0.54"
|
22
|
+
s.add_development_dependency "byebug", "~> 10.0"
|
23
|
+
s.add_development_dependency "timecop", "~> 0.9"
|
24
|
+
|
25
|
+
s.add_runtime_dependency "sidekiq", "~> 5.0"
|
26
|
+
end
|
data/bin/release
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
cp atomic-sidekiq.gemspec atomic-sidekiq.gemspec.backup
|
3
|
+
sed -i.bak s/0.0.0/$1/g atomic-sidekiq.gemspec
|
4
|
+
sed -i.bak s/2001-01-01/`date +%Y-%m-%d`/g atomic-sidekiq.gemspec
|
5
|
+
gem build atomic-sidekiq.gemspec
|
6
|
+
mv atomic-sidekiq.gemspec.backup atomic-sidekiq.gemspec
|
7
|
+
gem push atomic-sidekiq-$1.gem
|
8
|
+
git tag -a v$1 -m "Release v$1"
|
9
|
+
git push origin --tags
|
data/bin/sidekiqfail
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'optparse'
|
4
|
+
require 'sidekiq'
|
5
|
+
|
6
|
+
max_tries = 10
|
7
|
+
cutoff_tries = 100
|
8
|
+
|
9
|
+
options = {
|
10
|
+
atomic_fetch: false
|
11
|
+
}
|
12
|
+
OptionParser.new do |opts|
|
13
|
+
opts.banner = "Usage: sidekiqfail [options]"
|
14
|
+
|
15
|
+
opts.on("-a", "--atomic-fetch", "Run the Sidekiq worker with the atomic-fetch fetcher") do |v|
|
16
|
+
options[:atomic_fetch] = v
|
17
|
+
end
|
18
|
+
end.parse!
|
19
|
+
|
20
|
+
Sidekiq.configure_client do |config|
|
21
|
+
config.redis = { db: 13 }
|
22
|
+
end
|
23
|
+
|
24
|
+
def processed
|
25
|
+
Sidekiq.redis { |conn| conn.get('done') }.to_i
|
26
|
+
end
|
27
|
+
|
28
|
+
def total
|
29
|
+
Sidekiq.redis { |conn| conn.get('total_enqueued') }.to_i
|
30
|
+
end
|
31
|
+
|
32
|
+
def inflight
|
33
|
+
counter = it = 0
|
34
|
+
loop do
|
35
|
+
it, keys = Sidekiq.redis { |c| c.scan(it, match: 'flight:*') }
|
36
|
+
counter += keys.count
|
37
|
+
it = it.to_i
|
38
|
+
break if it == 0
|
39
|
+
end
|
40
|
+
counter
|
41
|
+
end
|
42
|
+
|
43
|
+
def pending
|
44
|
+
total - processed
|
45
|
+
end
|
46
|
+
|
47
|
+
def print_report
|
48
|
+
Sidekiq.logger.error "Queued: #{total}"
|
49
|
+
Sidekiq.logger.error "Processed: #{processed}"
|
50
|
+
Sidekiq.logger.error "Lost: #{pending}"
|
51
|
+
end
|
52
|
+
|
53
|
+
args = ['-f', 'true', '-b', '10', '-j', '1000', '-t', '0.01']
|
54
|
+
args.push('-a') if options[:atomic_fetch]
|
55
|
+
pid = Process.fork { Process.exec('./bin/sidekiqload', *args) }
|
56
|
+
loop do
|
57
|
+
Process.wait(pid)
|
58
|
+
puts "Processed before failure #{processed} out of #{total}"
|
59
|
+
break if pending == 0 || max_tries == 0 || cutoff_tries == 0
|
60
|
+
max_tries -= 1 if inflight == 0
|
61
|
+
cutoff_tries -= 1
|
62
|
+
args = ['-f', 'false', '-b', '0', '-j', '0', '-t', '0.01']
|
63
|
+
args.push('-a') if options[:atomic_fetch]
|
64
|
+
pid = Process.fork { Process.exec('./bin/sidekiqload', *args) }
|
65
|
+
end
|
66
|
+
print_report
|
data/bin/sidekiqload
ADDED
@@ -0,0 +1,227 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# Quiet some warnings we see when running in warning mode:
|
4
|
+
# RUBYOPT=-w bundle exec sidekiq
|
5
|
+
$TESTING = false
|
6
|
+
|
7
|
+
#require 'ruby-prof'
|
8
|
+
Bundler.require(:default)
|
9
|
+
|
10
|
+
require 'sidekiq/cli'
|
11
|
+
require 'sidekiq/launcher'
|
12
|
+
require 'optparse'
|
13
|
+
require_relative '../lib/atomic-sidekiq'
|
14
|
+
|
15
|
+
$options = {
|
16
|
+
batches: 10,
|
17
|
+
jobs: 1_000,
|
18
|
+
terminate: 0,
|
19
|
+
flush: true,
|
20
|
+
expiration: 2,
|
21
|
+
atomic_fetch: false
|
22
|
+
}
|
23
|
+
|
24
|
+
OptionParser.new do |opts|
|
25
|
+
opts.banner = "Usage: sidekiqload [options]"
|
26
|
+
|
27
|
+
opts.on("-b n", "--batches=n", "Number of job batches to be created [default 10]") do |v|
|
28
|
+
$options[:batches] = v.to_i
|
29
|
+
end
|
30
|
+
opts.on("-j n", "--jobs=n", "Number of jobs in each batch [default 1000]") do |v|
|
31
|
+
$options[:jobs] = v.to_i
|
32
|
+
end
|
33
|
+
opts.on("-t n", "--terminate=n", "Probability of terminating the thread [default 0]") do |v|
|
34
|
+
$options[:terminate] = v.to_f
|
35
|
+
end
|
36
|
+
opts.on("-f n", "--flush=n", "Flush all jobs that have been created before [default true]") do |v|
|
37
|
+
$options[:flush] = (v == 'true')
|
38
|
+
end
|
39
|
+
opts.on("-e n", "--expiration=n", "Expiration time in seconds for a job [default 2]") do |v|
|
40
|
+
$options[:expiration] = v.to_i
|
41
|
+
end
|
42
|
+
opts.on("-a", "--atomic-fetch", "Run the Sidekiq worker with the atomic-fetch fetcher") do |v|
|
43
|
+
$options[:atomic_fetch] = v
|
44
|
+
end
|
45
|
+
end.parse!
|
46
|
+
|
47
|
+
include Sidekiq::Util
|
48
|
+
|
49
|
+
Sidekiq.configure_server do |config|
|
50
|
+
#config.options[:concurrency] = 1
|
51
|
+
config.redis = { db: 13 }
|
52
|
+
config.options[:queues] << 'default'
|
53
|
+
config.logger.level = Logger::ERROR
|
54
|
+
config.average_scheduled_poll_interval = 2
|
55
|
+
config.atomic_fetch!({
|
56
|
+
collection_interval: 10,
|
57
|
+
expiration_time: $options[:expiration],
|
58
|
+
}) if $options[:atomic_fetch]
|
59
|
+
end
|
60
|
+
|
61
|
+
class LoadWorker
|
62
|
+
include Sidekiq::Worker
|
63
|
+
sidekiq_options retry: 1
|
64
|
+
sidekiq_retry_in do |x|
|
65
|
+
1
|
66
|
+
end
|
67
|
+
|
68
|
+
def perform(batch, idx)
|
69
|
+
if rand < $options[:terminate]
|
70
|
+
Sidekiq.logger.error("Terminating on job #{idx}")
|
71
|
+
Process.kill("KILL", Process.pid)
|
72
|
+
end
|
73
|
+
begin
|
74
|
+
Sidekiq.redis do |conn|
|
75
|
+
conn.eval("""
|
76
|
+
local lock = redis.call('get', 'jobs:#{batch}:#{idx}')
|
77
|
+
if lock then return nil end
|
78
|
+
redis.call('set', 'jobs:#{batch}:#{idx}', '1')
|
79
|
+
redis.call('incr', 'done')
|
80
|
+
return nil
|
81
|
+
""")
|
82
|
+
end
|
83
|
+
rescue e
|
84
|
+
Sidekiq.logger.error(e)
|
85
|
+
end
|
86
|
+
#raise idx.to_s if idx % 100 == 1
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# brew tap shopify/shopify
|
91
|
+
# brew install toxiproxy
|
92
|
+
# gem install toxiproxy
|
93
|
+
#require 'toxiproxy'
|
94
|
+
# simulate a non-localhost network for realer-world conditions.
|
95
|
+
# adding 1ms of network latency has an ENORMOUS impact on benchmarks
|
96
|
+
#Toxiproxy.populate([{
|
97
|
+
#"name": "redis",
|
98
|
+
#"listen": "127.0.0.1:6380",
|
99
|
+
#"upstream": "127.0.0.1:6379"
|
100
|
+
#}])
|
101
|
+
|
102
|
+
self_read, self_write = IO.pipe
|
103
|
+
%w(INT TERM TSTP TTIN).each do |sig|
|
104
|
+
begin
|
105
|
+
trap sig do
|
106
|
+
puts("Killed with #{sig}")
|
107
|
+
self_write.puts(sig)
|
108
|
+
end
|
109
|
+
rescue ArgumentError
|
110
|
+
puts "Signal #{sig} not supported"
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
if ($options[:flush])
|
115
|
+
puts "Flushing database..."
|
116
|
+
Sidekiq.redis {|c| c.flushdb }
|
117
|
+
end
|
118
|
+
|
119
|
+
def handle_signal(launcher, sig)
|
120
|
+
Sidekiq.logger.debug "Got #{sig} signal"
|
121
|
+
case sig
|
122
|
+
when 'INT'
|
123
|
+
# Handle Ctrl-C in JRuby like MRI
|
124
|
+
# http://jira.codehaus.org/browse/JRUBY-4637
|
125
|
+
raise Interrupt
|
126
|
+
when 'TERM'
|
127
|
+
# Heroku sends TERM and then waits 10 seconds for process to exit.
|
128
|
+
raise Interrupt
|
129
|
+
when 'TSTP'
|
130
|
+
Sidekiq.logger.info "Received TSTP, no longer accepting new work"
|
131
|
+
launcher.quiet
|
132
|
+
when 'TTIN'
|
133
|
+
Thread.list.each do |thread|
|
134
|
+
Sidekiq.logger.warn "Thread TID-#{(thread.object_id ^ ::Process.pid).to_s(36)} #{thread['label']}"
|
135
|
+
if thread.backtrace
|
136
|
+
Sidekiq.logger.warn thread.backtrace.join("\n")
|
137
|
+
else
|
138
|
+
Sidekiq.logger.warn "<no backtrace available>"
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def Process.rss
|
145
|
+
`ps -o rss= -p #{Process.pid}`.chomp.to_i
|
146
|
+
end
|
147
|
+
|
148
|
+
iter = $options[:batches]
|
149
|
+
count = $options[:jobs]
|
150
|
+
|
151
|
+
iter.times do |batch|
|
152
|
+
arr = Array.new(count) do
|
153
|
+
[]
|
154
|
+
end
|
155
|
+
count.times do |idx|
|
156
|
+
arr[idx][0] = idx
|
157
|
+
arr[idx][1] = batch
|
158
|
+
end
|
159
|
+
Sidekiq::Client.push_bulk('class' => LoadWorker, 'args' => arr)
|
160
|
+
end
|
161
|
+
total_enqueued = Sidekiq.redis { |conn| conn.get('total_enqueued') }.to_i + count * iter
|
162
|
+
Sidekiq.redis { |conn| conn.set('total_enqueued', total_enqueued) }
|
163
|
+
Sidekiq.logger.error "Created #{count*iter} jobs (total: #{total_enqueued})"
|
164
|
+
|
165
|
+
Monitoring = Thread.new do
|
166
|
+
def total
|
167
|
+
qsize, retries = Sidekiq.redis do |conn|
|
168
|
+
conn.pipelined do
|
169
|
+
conn.llen "queue:default"
|
170
|
+
conn.zcard "retry"
|
171
|
+
end
|
172
|
+
end.map(&:to_i)
|
173
|
+
qsize + retries
|
174
|
+
end
|
175
|
+
|
176
|
+
def inflight
|
177
|
+
counter = it = 0
|
178
|
+
loop do
|
179
|
+
it, keys = Sidekiq.redis { |c| c.scan(it, match: 'flight:*') }
|
180
|
+
counter += keys.count
|
181
|
+
it = it.to_i
|
182
|
+
break if it == 0
|
183
|
+
end
|
184
|
+
counter
|
185
|
+
end
|
186
|
+
|
187
|
+
watchdog("monitor thread") do
|
188
|
+
while true
|
189
|
+
sleep 1
|
190
|
+
#GC.start
|
191
|
+
_total = total
|
192
|
+
_inflight = inflight
|
193
|
+
Sidekiq.logger.error("RSS: #{Process.rss} Pending: #{_total} Inflight: #{_inflight}")
|
194
|
+
if _total == 0 && _inflight == 0
|
195
|
+
Sidekiq.logger.error("Done")
|
196
|
+
exit(0)
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
begin
|
203
|
+
# RubyProf::exclude_threads = [ Monitoring ]
|
204
|
+
#RubyProf.start
|
205
|
+
fire_event(:startup)
|
206
|
+
#Sidekiq.logger.error "Simulating 1ms of latency between Sidekiq and redis"
|
207
|
+
#Toxiproxy[:redis].downstream(:latency, latency: 1).apply do
|
208
|
+
launcher = Sidekiq::Launcher.new(Sidekiq.options)
|
209
|
+
launcher.run
|
210
|
+
|
211
|
+
while readable_io = IO.select([self_read])
|
212
|
+
signal = readable_io.first[0].gets.strip
|
213
|
+
handle_signal(launcher, signal)
|
214
|
+
end
|
215
|
+
#end
|
216
|
+
rescue SystemExit => e
|
217
|
+
# Sidekiq.logger.error("Profiling...")
|
218
|
+
#result = RubyProf.stop
|
219
|
+
#printer = RubyProf::GraphHtmlPrinter.new(result)
|
220
|
+
#printer.print(File.new("output.html", "w"), :min_percent => 1)
|
221
|
+
# normal
|
222
|
+
rescue => e
|
223
|
+
raise e if $DEBUG
|
224
|
+
STDERR.puts e.message
|
225
|
+
STDERR.puts e.backtrace.join("\n")
|
226
|
+
exit 1
|
227
|
+
end
|
data/bin/test
ADDED
data/docker-compose.yml
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
version: '2'
|
2
|
+
|
3
|
+
services:
|
4
|
+
test:
|
5
|
+
build: .
|
6
|
+
command: bin/test
|
7
|
+
env_file:
|
8
|
+
- docker.env
|
9
|
+
depends_on:
|
10
|
+
- redis
|
11
|
+
volumes:
|
12
|
+
- .:/sidekiq-atomic
|
13
|
+
image: sidekiq-atomic
|
14
|
+
volumes_from:
|
15
|
+
- bundle
|
16
|
+
|
17
|
+
redis:
|
18
|
+
image: redis:3.2-alpine
|
19
|
+
ports:
|
20
|
+
- 6379:6379
|
21
|
+
volumes:
|
22
|
+
- redis:/var/lib/redis/data
|
23
|
+
|
24
|
+
bundle:
|
25
|
+
image: tianon/true
|
26
|
+
volumes:
|
27
|
+
- bundle:/bundle
|
28
|
+
|
29
|
+
volumes:
|
30
|
+
redis:
|
31
|
+
bundle:
|
data/docker.env
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
REDIS_URL=redis://redis:6379
|
@@ -0,0 +1 @@
|
|
1
|
+
require_relative './atomic_sidekiq'
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module AtomicSidekiq
|
2
|
+
class AtomicFetch
|
3
|
+
IN_FLIGHT_KEY_PREFIX = "flight:"
|
4
|
+
DEFAULT_POLL_INTERVAL = 10 # seconds
|
5
|
+
DEFAULT_EXPIRATION_TIME = 3600 # seconds
|
6
|
+
DEFAULT_COLLECTION_INTERVAL = 60 # seconds
|
7
|
+
|
8
|
+
def initialize(options)
|
9
|
+
@retrieve_op = AtomicOperation::Retrieve.new(in_flight_prefix: IN_FLIGHT_KEY_PREFIX)
|
10
|
+
@strictly_ordered_queues = !!options[:strict]
|
11
|
+
|
12
|
+
atomic_fetch_opts = options.fetch(:atomic_fetch, {})
|
13
|
+
@expiration_time = atomic_fetch_opts.fetch(:expiration_time, DEFAULT_EXPIRATION_TIME)
|
14
|
+
@collection_interval = atomic_fetch_opts.fetch(:collection_wait_time, DEFAULT_COLLECTION_INTERVAL)
|
15
|
+
@poll_interval = atomic_fetch_opts.fetch(:poll_interval, DEFAULT_POLL_INTERVAL)
|
16
|
+
@@next_collection ||= Time.now
|
17
|
+
set_queues(options)
|
18
|
+
end
|
19
|
+
|
20
|
+
def retrieve_work
|
21
|
+
collect_dead_jobs!
|
22
|
+
work = retrieve_op.perform(ordered_queues, expire_at)
|
23
|
+
return UnitOfWork.new(*work) if work
|
24
|
+
sleep(poll_interval)
|
25
|
+
nil
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
attr_reader :retrieve_op, :queues, :strictly_ordered_queues,
|
31
|
+
:collection_interval, :poll_interval, :expiration_time
|
32
|
+
|
33
|
+
def set_queues(options)
|
34
|
+
@queues ||= options[:queues].map { |q| "queue:#{q}" }
|
35
|
+
end
|
36
|
+
|
37
|
+
def ordered_queues
|
38
|
+
if strictly_ordered_queues
|
39
|
+
queues
|
40
|
+
else
|
41
|
+
queues.shuffle.uniq
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def collect_dead_jobs!
|
46
|
+
return if @@next_collection > Time.now
|
47
|
+
@@next_collection = Time.now + collection_interval
|
48
|
+
DeadJobCollector.collect!(ordered_queues)
|
49
|
+
end
|
50
|
+
|
51
|
+
def expire_at
|
52
|
+
Time.now.utc.to_i + expiration_time
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module AtomicSidekiq
|
2
|
+
module AtomicOperation
|
3
|
+
class Base
|
4
|
+
def initialize(in_flight_prefix:)
|
5
|
+
@in_flight_prefix = in_flight_prefix
|
6
|
+
end
|
7
|
+
|
8
|
+
protected
|
9
|
+
|
10
|
+
attr_reader :in_flight_prefix
|
11
|
+
|
12
|
+
def redis(&block)
|
13
|
+
Sidekiq.redis { |conn| block.call(conn) }
|
14
|
+
end
|
15
|
+
|
16
|
+
def in_flight_job_key(queue, job)
|
17
|
+
jid = JSON.parse(job)['jid']
|
18
|
+
"#{in_flight_prefix}#{queue}:#{jid}"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module AtomicSidekiq
|
2
|
+
module AtomicOperation
|
3
|
+
class Expire < Base
|
4
|
+
def initialize
|
5
|
+
super(in_flight_prefix: nil)
|
6
|
+
end
|
7
|
+
|
8
|
+
def perform(queue, in_flight_key)
|
9
|
+
redis do |conn|
|
10
|
+
conn.eval(EXPIRE_SCRIPT, [queue, in_flight_key], [Time.now.utc.to_i])
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
EXPIRE_SCRIPT = File.read(File.join(File.dirname(__FILE__), './lua_scripts/expire.lua'))
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
local queue = KEYS[1]
|
2
|
+
local in_flight_key = KEYS[2]
|
3
|
+
local now = tonumber(ARGV[1])
|
4
|
+
|
5
|
+
local job = redis.call('get', in_flight_key)
|
6
|
+
if (not job) then return nil end
|
7
|
+
|
8
|
+
local expiration = tonumber(string.match(job, '"expire_at":([0-9]*)'))
|
9
|
+
if expiration > now then return nil end
|
10
|
+
job = string.gsub(job, ',?"expire_at":[0-9]*', '')
|
11
|
+
|
12
|
+
redis.call('lpush', queue, job)
|
13
|
+
redis.call('del', in_flight_key)
|
14
|
+
|
15
|
+
return { queue, job }
|
@@ -0,0 +1,11 @@
|
|
1
|
+
local queue = KEYS[1]
|
2
|
+
local flight = KEYS[2]
|
3
|
+
local expire_at = tonumber(ARGV[1])
|
4
|
+
|
5
|
+
local job = redis.call('lpop', queue)
|
6
|
+
if (not job) then return nil end
|
7
|
+
job = job:sub(1,-2)..',"expire_at":'..expire_at.."}"
|
8
|
+
|
9
|
+
local flight_key = flight..queue..':'..string.match(job, '"jid":"([^"]*)"')
|
10
|
+
redis.call('set', flight_key, job)
|
11
|
+
return { queue, job }
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module AtomicSidekiq
|
2
|
+
module AtomicOperation
|
3
|
+
class Requeue < Base
|
4
|
+
def perform(queue:, job:)
|
5
|
+
redis do |conn|
|
6
|
+
requeue(conn, queue: queue, job: job)
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def requeue(conn, queue:, job:)
|
13
|
+
conn.multi do
|
14
|
+
conn.rpush(queue, job)
|
15
|
+
conn.del(in_flight_job_key(queue, job))
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module AtomicSidekiq
|
2
|
+
module AtomicOperation
|
3
|
+
class Retrieve < Base
|
4
|
+
def perform(queues, expire_at)
|
5
|
+
queues.each do |queue|
|
6
|
+
res = retrieve_from_queue(queue, expire_at.to_i)
|
7
|
+
return res if res
|
8
|
+
end
|
9
|
+
nil
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
RETRIEVE_SCRIPT = File.read(File.join(File.dirname(__FILE__), './lua_scripts/retrieve.lua'))
|
15
|
+
|
16
|
+
def retrieve_from_queue(queue, expire_at)
|
17
|
+
redis do |conn|
|
18
|
+
conn.eval(RETRIEVE_SCRIPT, [queue, in_flight_prefix], [expire_at])
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module AtomicSidekiq
|
2
|
+
class DeadJobCollector
|
3
|
+
class << self
|
4
|
+
def collect!(queues)
|
5
|
+
queues.each { |q| new(q).collect! }
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
def initialize(queue, in_flight_prefix: AtomicFetch::IN_FLIGHT_KEY_PREFIX)
|
10
|
+
@queue = queue
|
11
|
+
@in_flight_prefix = in_flight_prefix
|
12
|
+
@expire_op = AtomicOperation::Expire.new
|
13
|
+
end
|
14
|
+
|
15
|
+
def collect!
|
16
|
+
each_keys { |job_key| expire!(job_key) }
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
attr_reader :queue, :in_flight_prefix, :expire_op
|
22
|
+
|
23
|
+
def expire!(job_key)
|
24
|
+
expire_op.perform(queue, job_key)
|
25
|
+
end
|
26
|
+
|
27
|
+
def each_keys(&block)
|
28
|
+
it = 0
|
29
|
+
Sidekiq.redis do |conn|
|
30
|
+
loop do
|
31
|
+
it, job_keys = conn.scan(it, match: keys_prefix)
|
32
|
+
it = it.to_i
|
33
|
+
job_keys.each { |job_key| block.call(job_key) }
|
34
|
+
break if it == 0
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def keys_prefix
|
40
|
+
"#{in_flight_prefix}#{queue}:*"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module AtomicSidekiq
|
2
|
+
class UnitOfWork
|
3
|
+
attr_reader :queue, :job
|
4
|
+
|
5
|
+
def initialize(queue = nil, job = nil, in_flight_prefix: AtomicFetch::IN_FLIGHT_KEY_PREFIX)
|
6
|
+
@queue = queue
|
7
|
+
@job = job
|
8
|
+
@acknowledge_op = AtomicOperation::Acknowledge.new(in_flight_prefix: in_flight_prefix)
|
9
|
+
@requeue_op = AtomicOperation::Requeue.new(in_flight_prefix: in_flight_prefix)
|
10
|
+
end
|
11
|
+
|
12
|
+
def acknowledge
|
13
|
+
acknowledge_op.perform(queue: queue, job: job)
|
14
|
+
end
|
15
|
+
|
16
|
+
def queue_name
|
17
|
+
"queue:#{queue.sub(/.*queue:/, '')}"
|
18
|
+
end
|
19
|
+
|
20
|
+
def requeue
|
21
|
+
requeue_op.perform(queue: queue, job: job)
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
attr_reader :acknowledge_op, :requeue_op
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'sidekiq'
|
2
|
+
require_relative 'atomic_sidekiq/sidekiq'
|
3
|
+
require_relative 'atomic_sidekiq/unit_of_work'
|
4
|
+
require_relative 'atomic_sidekiq/atomic_fetch'
|
5
|
+
require_relative 'atomic_sidekiq/dead_job_collector'
|
6
|
+
require_relative 'atomic_sidekiq/atomic_operation/base'
|
7
|
+
require_relative 'atomic_sidekiq/atomic_operation/acknowledge'
|
8
|
+
require_relative 'atomic_sidekiq/atomic_operation/requeue'
|
9
|
+
require_relative 'atomic_sidekiq/atomic_operation/retrieve'
|
10
|
+
require_relative 'atomic_sidekiq/atomic_operation/expire'
|
11
|
+
|
12
|
+
module AtomicSidekiq
|
13
|
+
end
|
metadata
ADDED
@@ -0,0 +1,169 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: atomic-sidekiq
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Alex Correia Santos
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-04-01 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.12'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.12'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '11.3'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '11.3'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '3.6'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '3.6'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rubocop
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0.54'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0.54'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: byebug
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '10.0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '10.0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: timecop
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0.9'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0.9'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: sidekiq
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '5.0'
|
104
|
+
type: :runtime
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '5.0'
|
111
|
+
description: Reliable fetcher for Sidekiq
|
112
|
+
email:
|
113
|
+
- alex.santios@visiblealpha.com
|
114
|
+
executables: []
|
115
|
+
extensions: []
|
116
|
+
extra_rdoc_files: []
|
117
|
+
files:
|
118
|
+
- ".gitignore"
|
119
|
+
- ".rspec"
|
120
|
+
- Dockerfile
|
121
|
+
- Gemfile
|
122
|
+
- Gemfile.lock
|
123
|
+
- LICENSE
|
124
|
+
- README.md
|
125
|
+
- atomic-sidekiq.gemspec
|
126
|
+
- bin/release
|
127
|
+
- bin/sidekiqfail
|
128
|
+
- bin/sidekiqload
|
129
|
+
- bin/test
|
130
|
+
- docker-compose.yml
|
131
|
+
- docker.env
|
132
|
+
- lib/atomic-sidekiq.rb
|
133
|
+
- lib/atomic_sidekiq.rb
|
134
|
+
- lib/atomic_sidekiq/atomic_fetch.rb
|
135
|
+
- lib/atomic_sidekiq/atomic_operation/acknowledge.rb
|
136
|
+
- lib/atomic_sidekiq/atomic_operation/base.rb
|
137
|
+
- lib/atomic_sidekiq/atomic_operation/expire.rb
|
138
|
+
- lib/atomic_sidekiq/atomic_operation/lua_scripts/expire.lua
|
139
|
+
- lib/atomic_sidekiq/atomic_operation/lua_scripts/retrieve.lua
|
140
|
+
- lib/atomic_sidekiq/atomic_operation/requeue.rb
|
141
|
+
- lib/atomic_sidekiq/atomic_operation/retrieve.rb
|
142
|
+
- lib/atomic_sidekiq/dead_job_collector.rb
|
143
|
+
- lib/atomic_sidekiq/sidekiq.rb
|
144
|
+
- lib/atomic_sidekiq/unit_of_work.rb
|
145
|
+
homepage: https://github.com/Colex/atomic-sidekiq
|
146
|
+
licenses:
|
147
|
+
- MIT
|
148
|
+
metadata: {}
|
149
|
+
post_install_message:
|
150
|
+
rdoc_options: []
|
151
|
+
require_paths:
|
152
|
+
- lib
|
153
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
154
|
+
requirements:
|
155
|
+
- - ">="
|
156
|
+
- !ruby/object:Gem::Version
|
157
|
+
version: '2.3'
|
158
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
159
|
+
requirements:
|
160
|
+
- - ">="
|
161
|
+
- !ruby/object:Gem::Version
|
162
|
+
version: '0'
|
163
|
+
requirements: []
|
164
|
+
rubyforge_project:
|
165
|
+
rubygems_version: 2.5.2
|
166
|
+
signing_key:
|
167
|
+
specification_version: 4
|
168
|
+
summary: Reliable fetcher for Sidekiq
|
169
|
+
test_files: []
|