sidekiq-grouping 0.0.6
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/.gitignore +17 -0
- data/.travis.yml +8 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +100 -0
- data/Rakefile +4 -0
- data/lib/sidekiq/grouping.rb +40 -0
- data/lib/sidekiq/grouping/actor.rb +47 -0
- data/lib/sidekiq/grouping/batch.rb +128 -0
- data/lib/sidekiq/grouping/config.rb +19 -0
- data/lib/sidekiq/grouping/logging.rb +13 -0
- data/lib/sidekiq/grouping/middleware.rb +34 -0
- data/lib/sidekiq/grouping/redis.rb +86 -0
- data/lib/sidekiq/grouping/supervisor.rb +14 -0
- data/lib/sidekiq/grouping/version.rb +5 -0
- data/lib/sidekiq/grouping/views/index.erb +41 -0
- data/lib/sidekiq/grouping/web.rb +28 -0
- data/sidekiq-grouping.gemspec +30 -0
- data/spec/modules/batch_spec.rb +136 -0
- data/spec/modules/redis_spec.rb +47 -0
- data/spec/spec_helper.rb +43 -0
- data/spec/support/test_workers.rb +42 -0
- data/web.png +0 -0
- metadata +197 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: ae9e71d0d79bd9af61ed8c49fca7122219849a37
|
4
|
+
data.tar.gz: c5469b43d3780b36fbe343eca132c86d3d200c34
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: db9e641eb35867c42e2b17eb073b921c42ad992b4846452aff33afb927ea76df2811f7262f89a578c45dbd5d48a0f0e8a0a30be5ea531a8169f1cd9ea7a2ecb8
|
7
|
+
data.tar.gz: 1fde67a0b91497362100c2608582aaa52e36478a960ec33d9adc99f6552dbe4d2966b713ffa8ef87a3c77391f968894c34c9694801a717d6e68b28e03f148ba7
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Victor Sokolov
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
# Sidekiq::Grouping
|
2
|
+
|
3
|
+
Lets you batch similar tasks to run them all as a single task.
|
4
|
+
|
5
|
+
Allows identical sidekiq jobs to be processed with a single background call.
|
6
|
+
|
7
|
+
Useful for:
|
8
|
+
* Grouping asynchronous API index calls into bulks for bulk updating/indexing.
|
9
|
+
* Periodical batch updating of recently changing database counters.
|
10
|
+
|
11
|
+
Sponsored by [Evil Martians](http://evilmartians.com)
|
12
|
+
|
13
|
+
## Usage
|
14
|
+
|
15
|
+
Create a worker:
|
16
|
+
|
17
|
+
```ruby
|
18
|
+
class ElasticBulkIndexWorker
|
19
|
+
include Sidekiq::Worker
|
20
|
+
|
21
|
+
sidekiq_options(
|
22
|
+
queue: :batched_by_size,
|
23
|
+
batch_size: 30, # Jobs will be combined to groups of 30 items
|
24
|
+
batch_flush_interval: 60, # Combined jobs will be executed at least every 60 seconds
|
25
|
+
retry: 5
|
26
|
+
)
|
27
|
+
|
28
|
+
def perform(group)
|
29
|
+
client = Elasticsearch::Client.new
|
30
|
+
client.bulk(body: group.flatten)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
```
|
34
|
+
|
35
|
+
Perform a jobs:
|
36
|
+
|
37
|
+
```ruby
|
38
|
+
ElasticBulkIndexWorker.perform_async({ delete: { _index: 'test', _id: 5, _type: 'user' } })
|
39
|
+
ElasticBulkIndexWorker.perform_async({ delete: { _index: 'test', _id: 6, _type: 'user' } })
|
40
|
+
ElasticBulkIndexWorker.perform_async({ delete: { _index: 'test', _id: 7, _type: 'user' } })
|
41
|
+
...
|
42
|
+
```
|
43
|
+
|
44
|
+
This jobs will be grouped into a single job which will be performed with the single argument containing:
|
45
|
+
|
46
|
+
```ruby
|
47
|
+
[
|
48
|
+
[{ delete: { _index: 'test', _id: 5, _type: 'user' } }],
|
49
|
+
[{ delete: { _index: 'test', _id: 6, _type: 'user' } }],
|
50
|
+
[{ delete: { _index: 'test', _id: 7, _type: 'user' } }]
|
51
|
+
...
|
52
|
+
]
|
53
|
+
```
|
54
|
+
|
55
|
+
This will happen for every 30 jobs in a row or every 60 seconds.
|
56
|
+
|
57
|
+
## Web UI
|
58
|
+
|
59
|
+

|
60
|
+
|
61
|
+
Add this line to your `config/routes.rb` to activate web UI:
|
62
|
+
|
63
|
+
```ruby
|
64
|
+
require "sidekiq/grouping/web"
|
65
|
+
```
|
66
|
+
|
67
|
+
## Configuration
|
68
|
+
|
69
|
+
```ruby
|
70
|
+
Sidekiq::Grouping::Config.poll_interval = 5 # Amount of time between polling batches
|
71
|
+
Sidekiq::Grouping::Config.max_batch_size = 5000 # Maximum batch size allowed
|
72
|
+
Sidekiq::Grouping::Config.lock_ttl = 1 # Timeout of lock set when batched job enqueues
|
73
|
+
```
|
74
|
+
|
75
|
+
## TODO
|
76
|
+
|
77
|
+
1. Add support redis_pool option.
|
78
|
+
2. Make able to work together with sidekiq-unique-jobs.
|
79
|
+
|
80
|
+
## Installation
|
81
|
+
|
82
|
+
Add this line to your application's Gemfile:
|
83
|
+
|
84
|
+
gem 'sidekiq-grouping'
|
85
|
+
|
86
|
+
And then execute:
|
87
|
+
|
88
|
+
$ bundle
|
89
|
+
|
90
|
+
Or install it yourself as:
|
91
|
+
|
92
|
+
$ gem install sidekiq-grouping
|
93
|
+
|
94
|
+
## Contributing
|
95
|
+
|
96
|
+
1. Fork it ( http://github.com/gzigzigzeo/sidekiq-grouping/fork )
|
97
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
98
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
99
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
100
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'active_support/core_ext/string'
|
2
|
+
require 'active_support/configurable'
|
3
|
+
require 'active_support/core_ext/numeric/time'
|
4
|
+
|
5
|
+
require 'sidekiq/grouping/config'
|
6
|
+
require 'sidekiq/grouping/redis'
|
7
|
+
require 'sidekiq/grouping/batch'
|
8
|
+
require 'sidekiq/grouping/middleware'
|
9
|
+
require 'sidekiq/grouping/logging'
|
10
|
+
require 'sidekiq/grouping/actor'
|
11
|
+
require 'sidekiq/grouping/supervisor'
|
12
|
+
require 'sidekiq/grouping/version'
|
13
|
+
|
14
|
+
module Sidekiq
|
15
|
+
module Grouping
|
16
|
+
class << self
|
17
|
+
attr_writer :logger
|
18
|
+
|
19
|
+
def logger
|
20
|
+
@logger ||= Sidekiq.logger
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
Sidekiq.configure_client do |config|
|
27
|
+
config.client_middleware do |chain|
|
28
|
+
chain.add Sidekiq::Grouping::Middleware
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
Sidekiq.configure_server do |config|
|
33
|
+
config.client_middleware do |chain|
|
34
|
+
chain.add Sidekiq::Grouping::Middleware
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
if Sidekiq.server?
|
39
|
+
Sidekiq::Grouping::Supervisor.run!
|
40
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Sidekiq
|
2
|
+
module Grouping
|
3
|
+
class Actor
|
4
|
+
include Sidekiq::Grouping::Logging
|
5
|
+
include ::Celluloid
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
link_to_sidekiq_manager
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
def start_polling
|
13
|
+
interval = Sidekiq::Grouping::Config.poll_interval
|
14
|
+
info "Start polling of queue batches every #{interval} seconds"
|
15
|
+
every(interval) { flush_batches }
|
16
|
+
end
|
17
|
+
|
18
|
+
def flush_batches
|
19
|
+
batches = []
|
20
|
+
|
21
|
+
Sidekiq::Grouping::Batch.all.map do |batch|
|
22
|
+
if batch.could_flush?
|
23
|
+
batches << batch
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
flush(batches)
|
28
|
+
end
|
29
|
+
|
30
|
+
def link_to_sidekiq_manager
|
31
|
+
Sidekiq::CLI.instance.launcher.manager.link(current_actor)
|
32
|
+
start_polling
|
33
|
+
rescue NoMethodError
|
34
|
+
debug "Can't link #{self.class.name}. Sidekiq::Manager not running. Retrying in 5 seconds ..."
|
35
|
+
after(5) { link_to_sidekiq_manager }
|
36
|
+
end
|
37
|
+
|
38
|
+
def flush(batches)
|
39
|
+
if batches.any?
|
40
|
+
names = batches.map { |batch| "#{batch.worker_class} in #{batch.queue}" }
|
41
|
+
info "Trying to flush batched queues: #{names.join(',')}"
|
42
|
+
batches.each { |batch| batch.flush }
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,128 @@
|
|
1
|
+
module Sidekiq
|
2
|
+
module Grouping
|
3
|
+
class Batch
|
4
|
+
|
5
|
+
def initialize(worker_class, queue, redis_pool = nil)
|
6
|
+
@worker_class = worker_class
|
7
|
+
@queue = queue
|
8
|
+
@name = "#{worker_class.underscore}:#{queue}"
|
9
|
+
@redis = Sidekiq::Grouping::Redis.new
|
10
|
+
end
|
11
|
+
|
12
|
+
attr_reader :name, :worker_class, :queue
|
13
|
+
|
14
|
+
def add(msg)
|
15
|
+
msg = msg.to_json
|
16
|
+
@redis.push_msg(@name, msg, enqueue_similar_once?) if should_add? msg
|
17
|
+
end
|
18
|
+
|
19
|
+
def should_add? msg
|
20
|
+
return true unless enqueue_similar_once?
|
21
|
+
!@redis.enqueued?(@name, msg)
|
22
|
+
end
|
23
|
+
|
24
|
+
def size
|
25
|
+
@redis.batch_size(@name)
|
26
|
+
end
|
27
|
+
|
28
|
+
def chunk_size
|
29
|
+
worker_class_options['batch_size'] ||
|
30
|
+
Sidekiq::Grouping::Config.max_batch_size
|
31
|
+
end
|
32
|
+
|
33
|
+
def pluck
|
34
|
+
if @redis.lock(@name)
|
35
|
+
@redis.pluck(@name, chunk_size).map { |value| JSON.parse(value) }
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def flush
|
40
|
+
chunk = pluck
|
41
|
+
if chunk
|
42
|
+
set_current_time_as_last
|
43
|
+
Sidekiq::Client.push(
|
44
|
+
'class' => @worker_class,
|
45
|
+
'queue' => @queue,
|
46
|
+
'args' => [true, chunk]
|
47
|
+
)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def worker_class_constant
|
52
|
+
@worker_class.constantize
|
53
|
+
end
|
54
|
+
|
55
|
+
def worker_class_options
|
56
|
+
worker_class_constant.get_sidekiq_options
|
57
|
+
rescue NameError
|
58
|
+
{}
|
59
|
+
end
|
60
|
+
|
61
|
+
def could_flush?
|
62
|
+
could_flush_on_overflow? || could_flush_on_time?
|
63
|
+
end
|
64
|
+
|
65
|
+
def last_execution_time
|
66
|
+
last_time = @redis.get_last_execution_time(@name)
|
67
|
+
Time.parse(last_time) if last_time
|
68
|
+
end
|
69
|
+
|
70
|
+
def next_execution_time
|
71
|
+
if interval = worker_class_options['batch_flush_interval']
|
72
|
+
last_time = last_execution_time
|
73
|
+
last_time + interval.seconds if last_time
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def delete
|
78
|
+
@redis.delete(@name)
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
def could_flush_on_overflow?
|
83
|
+
worker_class_options['batch_size'] &&
|
84
|
+
size >= worker_class_options['batch_size']
|
85
|
+
end
|
86
|
+
|
87
|
+
def could_flush_on_time?
|
88
|
+
return false if size.zero?
|
89
|
+
|
90
|
+
last_time = last_execution_time
|
91
|
+
next_time = next_execution_time
|
92
|
+
|
93
|
+
if last_time.blank?
|
94
|
+
set_current_time_as_last
|
95
|
+
false
|
96
|
+
else
|
97
|
+
if next_time
|
98
|
+
next_time < Time.now
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def enqueue_similar_once?
|
104
|
+
worker_class_options['batch_unique'] == true
|
105
|
+
end
|
106
|
+
|
107
|
+
def set_current_time_as_last
|
108
|
+
@redis.set_last_execution_time(@name, Time.now)
|
109
|
+
end
|
110
|
+
|
111
|
+
class << self
|
112
|
+
def all
|
113
|
+
redis = Sidekiq::Grouping::Redis.new
|
114
|
+
|
115
|
+
redis.batches.map do |name|
|
116
|
+
new(*extract_worker_klass_and_queue(name))
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def extract_worker_klass_and_queue(name)
|
121
|
+
klass, queue = name.split(':')
|
122
|
+
[klass.classify, queue]
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Sidekiq
|
2
|
+
module Grouping
|
3
|
+
module Config
|
4
|
+
include ActiveSupport::Configurable
|
5
|
+
|
6
|
+
# Interval batch queue polling
|
7
|
+
config_accessor :poll_interval
|
8
|
+
self.config.poll_interval = 3
|
9
|
+
|
10
|
+
# Maximum batch size
|
11
|
+
config_accessor :max_batch_size
|
12
|
+
self.config.max_batch_size = 500
|
13
|
+
|
14
|
+
# Batch queue lock timeout (set during flush)
|
15
|
+
config_accessor :lock_ttl
|
16
|
+
self.config.lock_ttl = 1
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Sidekiq
|
2
|
+
module Grouping
|
3
|
+
module Logging
|
4
|
+
%w(fatal error warn info debug).each do |level|
|
5
|
+
level = level.to_sym
|
6
|
+
|
7
|
+
define_method(level) do |msg|
|
8
|
+
Sidekiq::Grouping.logger.public_send(level, "[Sidekiq::Grouping] #{msg}")
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Sidekiq
|
2
|
+
module Grouping
|
3
|
+
class Middleware
|
4
|
+
def call(worker_class, msg, queue, redis_pool = nil)
|
5
|
+
worker_class = worker_class.classify.constantize if worker_class.is_a?(String)
|
6
|
+
options = worker_class.get_sidekiq_options
|
7
|
+
|
8
|
+
batch =
|
9
|
+
options.keys.include?('batch_size') ||
|
10
|
+
options.keys.include?('batch_flush_interval')
|
11
|
+
|
12
|
+
passthrough =
|
13
|
+
msg['args'] &&
|
14
|
+
msg['args'].is_a?(Array) &&
|
15
|
+
msg['args'].try(:first) == true
|
16
|
+
|
17
|
+
if batch && not(passthrough)
|
18
|
+
add_to_batch(worker_class, queue, msg, redis_pool)
|
19
|
+
else
|
20
|
+
if batch && passthrough
|
21
|
+
msg['args'].shift
|
22
|
+
end
|
23
|
+
yield
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
def add_to_batch(worker_class, queue, msg, redis_pool = nil)
|
29
|
+
Sidekiq::Grouping::Batch.new(worker_class.name, queue, redis_pool).add(msg['args'])
|
30
|
+
nil
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
module Sidekiq
|
2
|
+
module Grouping
|
3
|
+
class Redis
|
4
|
+
|
5
|
+
PLUCK_SCRIPT = <<-SCRIPT
|
6
|
+
local pluck_values = redis.call('lrange', KEYS[1], 0, ARGV[1] - 1)
|
7
|
+
redis.call('ltrim', KEYS[1], ARGV[1], -1)
|
8
|
+
for k, v in pairs(pluck_values) do
|
9
|
+
redis.call('srem', KEYS[2], v)
|
10
|
+
end
|
11
|
+
return pluck_values
|
12
|
+
SCRIPT
|
13
|
+
|
14
|
+
def push_msg(name, msg, remember_unique = false)
|
15
|
+
redis do |conn|
|
16
|
+
conn.multi do
|
17
|
+
conn.sadd(ns('batches'), name)
|
18
|
+
conn.rpush(ns(name), msg)
|
19
|
+
conn.sadd(unique_messages_key(name), msg) if remember_unique
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def enqueued?(name, msg)
|
25
|
+
redis do |conn|
|
26
|
+
conn.sismember(unique_messages_key(name), msg)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def batch_size(name)
|
31
|
+
redis { |conn| conn.llen(ns(name)) }
|
32
|
+
end
|
33
|
+
|
34
|
+
def batches
|
35
|
+
redis { |conn| conn.smembers(ns('batches')) }
|
36
|
+
end
|
37
|
+
|
38
|
+
def pluck(name, limit)
|
39
|
+
keys = [ns(name), unique_messages_key(name)]
|
40
|
+
args = [limit]
|
41
|
+
redis { |conn| conn.eval PLUCK_SCRIPT, keys, args }
|
42
|
+
end
|
43
|
+
|
44
|
+
def get_last_execution_time(name)
|
45
|
+
redis { |conn| conn.get(ns("last_execution_time:#{name}")) }
|
46
|
+
end
|
47
|
+
|
48
|
+
def set_last_execution_time(name, time)
|
49
|
+
redis { |conn| conn.set(ns("last_execution_time:#{name}"), time.to_json) }
|
50
|
+
end
|
51
|
+
|
52
|
+
def lock(name)
|
53
|
+
redis do |conn|
|
54
|
+
id = ns("lock:#{name}")
|
55
|
+
conn.setnx(id, true).tap do |obtained|
|
56
|
+
if obtained
|
57
|
+
conn.expire(id, Sidekiq::Grouping::Config.lock_ttl)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def delete(name)
|
64
|
+
redis do |conn|
|
65
|
+
conn.del(ns("last_execution_time:#{name}"))
|
66
|
+
conn.del(ns(name))
|
67
|
+
conn.srem(ns('batches'), name)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
def unique_messages_key name
|
74
|
+
ns("#{name}:unique_messages")
|
75
|
+
end
|
76
|
+
|
77
|
+
def ns(key = nil)
|
78
|
+
"batching:#{key}"
|
79
|
+
end
|
80
|
+
|
81
|
+
def redis(&block)
|
82
|
+
Sidekiq.redis(&block)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Sidekiq
|
2
|
+
module Grouping
|
3
|
+
module Supervisor
|
4
|
+
class << self
|
5
|
+
include Sidekiq::Grouping::Logging
|
6
|
+
|
7
|
+
def run!
|
8
|
+
info 'Sidekiq::Grouping starts supervision'
|
9
|
+
Sidekiq::Grouping::Actor.supervise_as(:sidekiq_grouping)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
<header class="row">
|
2
|
+
<div class="col-sm-5">
|
3
|
+
<h3>Grouped jobs</h3>
|
4
|
+
</div>
|
5
|
+
</header>
|
6
|
+
|
7
|
+
<div class="container">
|
8
|
+
<div class="row">
|
9
|
+
<div class="col-sm-12">
|
10
|
+
<% if true %>
|
11
|
+
<table class="table table-striped table-bordered table-white" style="width: 100%; margin: 0; table-layout:fixed;">
|
12
|
+
<thead>
|
13
|
+
<th style="width: 50%">Worker</th>
|
14
|
+
<th style="width: 30%">Queue</th>
|
15
|
+
<th style="width: 10%">Count</th>
|
16
|
+
<th style="width: 30%">Last execution time</th>
|
17
|
+
<th style="width: 30%">Next enqueue</th>
|
18
|
+
<th style="width: 10%">Actions</th>
|
19
|
+
</thead>
|
20
|
+
<% @batches.each do |batch| %>
|
21
|
+
<tr>
|
22
|
+
<td><%= batch.worker_class %></td>
|
23
|
+
<td><%= batch.queue %></td>
|
24
|
+
<td><%= batch.size %></td>
|
25
|
+
<td><%= batch.last_execution_time || "–"%></td>
|
26
|
+
<td><%= batch.next_execution_time || "–"%></td>
|
27
|
+
<td>
|
28
|
+
<form action="<%= "#{root_path}grouping/#{batch.name}/delete" %>" method="post">
|
29
|
+
<input class="btn btn-danger btn-xs" type="submit" name="delete" value="Delete" data-confirm="Are you sure you want to delete this batch?" />
|
30
|
+
</form>
|
31
|
+
</td>
|
32
|
+
</tr>
|
33
|
+
<% end %>
|
34
|
+
</table>
|
35
|
+
<% else %>
|
36
|
+
<div class="alert alert-success">No recurring jobs found.</div>
|
37
|
+
<% end %>
|
38
|
+
</div>
|
39
|
+
</div>
|
40
|
+
</div>
|
41
|
+
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'sidekiq/web'
|
2
|
+
|
3
|
+
module Sidetiq
|
4
|
+
module Grouping
|
5
|
+
module Web
|
6
|
+
VIEWS = File.expand_path('views', File.dirname(__FILE__))
|
7
|
+
|
8
|
+
def self.registered(app)
|
9
|
+
app.get "/grouping" do
|
10
|
+
@batches = Sidekiq::Grouping::Batch.all
|
11
|
+
erb File.read(File.join(VIEWS, 'index.erb')), locals: {view_path: VIEWS}
|
12
|
+
end
|
13
|
+
|
14
|
+
app.post "/grouping/:name/delete" do
|
15
|
+
worker_class, queue = Sidekiq::Grouping::Batch.extract_worker_klass_and_queue(params['name'])
|
16
|
+
batch = Sidekiq::Grouping::Batch.new(worker_class, queue)
|
17
|
+
batch.delete
|
18
|
+
redirect "#{root_path}/grouping"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
Sidekiq::Web.register(Sidetiq::Grouping::Web)
|
27
|
+
Sidekiq::Web.tabs["Grouping"] = "grouping"
|
28
|
+
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'sidekiq/grouping/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "sidekiq-grouping"
|
8
|
+
spec.version = Sidekiq::Grouping::VERSION
|
9
|
+
spec.authors = ["Victor Sokolov"]
|
10
|
+
spec.email = ["gzigzigzeo@gmail.com"]
|
11
|
+
spec.summary = %q{Allows identical sidekiq jobs to be processed with a single background call}
|
12
|
+
spec.homepage = "http://github.com/gzigzigzeo/sidekiq-grouping"
|
13
|
+
spec.license = "MIT"
|
14
|
+
|
15
|
+
spec.files = `git ls-files -z`.split("\x0")
|
16
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
17
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
18
|
+
spec.require_paths = ["lib"]
|
19
|
+
|
20
|
+
spec.add_development_dependency "bundler", "~> 1.5"
|
21
|
+
spec.add_development_dependency "rake"
|
22
|
+
spec.add_development_dependency "rspec"
|
23
|
+
spec.add_development_dependency "simplecov"
|
24
|
+
spec.add_development_dependency "rspec-sidekiq"
|
25
|
+
spec.add_development_dependency "activesupport"
|
26
|
+
spec.add_development_dependency "timecop"
|
27
|
+
|
28
|
+
spec.add_dependency "sidekiq"
|
29
|
+
spec.add_dependency "celluloid"
|
30
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Sidekiq::Grouping::Batch do
|
4
|
+
subject { Sidekiq::Grouping::Batch }
|
5
|
+
|
6
|
+
context 'adding' do
|
7
|
+
it 'must enqueue unbatched worker' do
|
8
|
+
RegularWorker.perform_async('bar')
|
9
|
+
expect(RegularWorker).to have_enqueued_job('bar')
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'must not enqueue batched worker' do
|
13
|
+
BatchedSizeWorker.perform_async('bar')
|
14
|
+
expect_batch(BatchedSizeWorker, 'batched_size')
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'must not enqueue batched worker' do
|
18
|
+
BatchedIntervalWorker.perform_async('bar')
|
19
|
+
expect_batch(BatchedIntervalWorker, 'batched_interval')
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'must not enqueue batched worker' do
|
23
|
+
BatchedBothWorker.perform_async('bar')
|
24
|
+
expect_batch(BatchedBothWorker, 'batched_both')
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
context 'checking if should flush' do
|
29
|
+
it 'must flush if limit exceeds for limit worker' do
|
30
|
+
batch = subject.new(BatchedSizeWorker.name, 'batched_size')
|
31
|
+
|
32
|
+
expect(batch.could_flush?).to be_falsy
|
33
|
+
BatchedSizeWorker.perform_async('bar')
|
34
|
+
expect(batch.could_flush?).to be_falsy
|
35
|
+
4.times { BatchedSizeWorker.perform_async('bar') }
|
36
|
+
expect(batch.could_flush?).to be_truthy
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'must flush if limit exceeds for both worker' do
|
40
|
+
batch = subject.new(BatchedBothWorker.name, 'batched_both')
|
41
|
+
|
42
|
+
expect(batch.could_flush?).to be_falsy
|
43
|
+
BatchedBothWorker.perform_async('bar')
|
44
|
+
expect(batch.could_flush?).to be_falsy
|
45
|
+
4.times { BatchedBothWorker.perform_async('bar') }
|
46
|
+
expect(batch.could_flush?).to be_truthy
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'must flush if limit okay but time came' do
|
50
|
+
batch = subject.new(BatchedIntervalWorker.name, 'batched_interval')
|
51
|
+
|
52
|
+
expect(batch.could_flush?).to be_falsy
|
53
|
+
BatchedIntervalWorker.perform_async('bar')
|
54
|
+
expect(batch.could_flush?).to be_falsy
|
55
|
+
expect(batch.size).to eq(1)
|
56
|
+
|
57
|
+
Timecop.travel(2.hours.since)
|
58
|
+
|
59
|
+
expect(batch.could_flush?).to be_truthy
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
context 'flushing' do
|
64
|
+
it 'must put wokrer to queue on flush' do
|
65
|
+
batch = subject.new(BatchedSizeWorker.name, 'batched_size')
|
66
|
+
|
67
|
+
expect(batch.could_flush?).to be_falsy
|
68
|
+
10.times { BatchedSizeWorker.perform_async('bar') }
|
69
|
+
batch.flush
|
70
|
+
expect(BatchedSizeWorker).to have_enqueued_job([["bar"], ["bar"], ["bar"]])
|
71
|
+
expect(batch.size).to eq(7)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
context 'with similar args' do
|
76
|
+
context 'option batch_unique = true' do
|
77
|
+
it 'enqueues once' do
|
78
|
+
batch = subject.new(BatchedUniqueArgsWorker.name, 'batched_unique_args')
|
79
|
+
3.times { BatchedUniqueArgsWorker.perform_async('bar', 1) }
|
80
|
+
expect(batch.size).to eq(1)
|
81
|
+
end
|
82
|
+
|
83
|
+
it 'enqueues once each unique set of args' do
|
84
|
+
batch = subject.new(BatchedUniqueArgsWorker.name, 'batched_unique_args')
|
85
|
+
3.times { BatchedUniqueArgsWorker.perform_async('bar', 1) }
|
86
|
+
6.times { BatchedUniqueArgsWorker.perform_async('baz', 1) }
|
87
|
+
3.times { BatchedUniqueArgsWorker.perform_async('bar', 1) }
|
88
|
+
2.times { BatchedUniqueArgsWorker.perform_async('baz', 3) }
|
89
|
+
7.times { BatchedUniqueArgsWorker.perform_async('bar', 1) }
|
90
|
+
expect(batch.size).to eq(3)
|
91
|
+
end
|
92
|
+
|
93
|
+
context 'flushing' do
|
94
|
+
|
95
|
+
it 'works' do
|
96
|
+
batch = subject.new(BatchedUniqueArgsWorker.name, 'batched_unique_args')
|
97
|
+
2.times { BatchedUniqueArgsWorker.perform_async('bar', 1) }
|
98
|
+
2.times { BatchedUniqueArgsWorker.perform_async('baz', 1) }
|
99
|
+
batch.flush
|
100
|
+
expect(batch.size).to eq(0)
|
101
|
+
end
|
102
|
+
|
103
|
+
it 'allows to enqueue again after flush' do
|
104
|
+
batch = subject.new(BatchedUniqueArgsWorker.name, 'batched_unique_args')
|
105
|
+
2.times { BatchedUniqueArgsWorker.perform_async('bar', 1) }
|
106
|
+
2.times { BatchedUniqueArgsWorker.perform_async('baz', 1) }
|
107
|
+
batch.flush
|
108
|
+
BatchedUniqueArgsWorker.perform_async('bar', 1)
|
109
|
+
BatchedUniqueArgsWorker.perform_async('baz', 1)
|
110
|
+
expect(batch.size).to eq(2)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
end
|
115
|
+
|
116
|
+
context 'batch_unique is not specified' do
|
117
|
+
it 'enqueues all' do
|
118
|
+
batch = subject.new(BatchedSizeWorker.name, 'batched_size')
|
119
|
+
3.times { BatchedSizeWorker.perform_async('bar', 1) }
|
120
|
+
expect(batch.size).to eq(3)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
private
|
126
|
+
def expect_batch(klass, queue)
|
127
|
+
expect(klass).to_not have_enqueued_job('bar')
|
128
|
+
batch = subject.new(klass.name, queue)
|
129
|
+
stats = subject.all
|
130
|
+
expect(batch.size).to eq(1)
|
131
|
+
expect(stats.size).to eq(1)
|
132
|
+
expect(stats.first.worker_class).to eq(klass.name)
|
133
|
+
expect(stats.first.queue).to eq(queue)
|
134
|
+
expect(batch.pluck).to eq [['bar']]
|
135
|
+
end
|
136
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Sidekiq::Grouping::Redis do
|
4
|
+
subject { Sidekiq::Grouping::Redis.new }
|
5
|
+
|
6
|
+
let(:queue_name) { "my_queue" }
|
7
|
+
let(:key) { "batching:#{queue_name}" }
|
8
|
+
let(:unique_key) { "batching:#{queue_name}:unique_messages" }
|
9
|
+
|
10
|
+
describe "#push_msg" do
|
11
|
+
it "adds message to queue" do
|
12
|
+
subject.push_msg(queue_name, 'My message')
|
13
|
+
expect(redis { |c| c.llen key }).to eq 1
|
14
|
+
expect(redis { |c| c.lrange key, 0, 1 }).to eq ['My message']
|
15
|
+
expect(redis { |c| c.smembers unique_key}).to eq []
|
16
|
+
end
|
17
|
+
|
18
|
+
it "remembers unique message if specified" do
|
19
|
+
subject.push_msg(queue_name, 'My message', true)
|
20
|
+
expect(redis { |c| c.smembers unique_key}).to eq ['My message']
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
describe "#pluck" do
|
25
|
+
it "removes messages from queue" do
|
26
|
+
subject.push_msg(queue_name, "Message 1")
|
27
|
+
subject.push_msg(queue_name, "Message 2")
|
28
|
+
subject.pluck(queue_name, 2)
|
29
|
+
expect(redis { |c| c.llen key }).to eq 0
|
30
|
+
end
|
31
|
+
|
32
|
+
it "forgets unique messages" do
|
33
|
+
subject.push_msg(queue_name, "Message 1", true)
|
34
|
+
subject.push_msg(queue_name, "Message 2", true)
|
35
|
+
expect(redis { |c| c.scard unique_key }).to eq 2
|
36
|
+
subject.pluck(queue_name, 2)
|
37
|
+
expect(redis { |c| c.smembers unique_key }).to eq []
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def redis(&block)
|
44
|
+
Sidekiq.redis(&block)
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
$LOAD_PATH << "." unless $LOAD_PATH.include?(".")
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'bundler/setup'
|
5
|
+
require 'timecop'
|
6
|
+
require 'simplecov'
|
7
|
+
require 'celluloid/autostart'
|
8
|
+
require 'sidekiq'
|
9
|
+
require 'rspec-sidekiq'
|
10
|
+
require 'support/test_workers'
|
11
|
+
|
12
|
+
SimpleCov.start do
|
13
|
+
add_filter 'spec'
|
14
|
+
end
|
15
|
+
|
16
|
+
require 'sidekiq/grouping'
|
17
|
+
|
18
|
+
Sidekiq::Grouping.logger = nil
|
19
|
+
Sidekiq.redis = { namespace: ENV['namespace'] }
|
20
|
+
Sidekiq.logger = nil
|
21
|
+
|
22
|
+
RSpec::Sidekiq.configure do |config|
|
23
|
+
config.clear_all_enqueued_jobs = true
|
24
|
+
end
|
25
|
+
|
26
|
+
RSpec.configure do |config|
|
27
|
+
config.order = :random
|
28
|
+
config.run_all_when_everything_filtered = true
|
29
|
+
config.filter_run :focus
|
30
|
+
|
31
|
+
config.before :each do
|
32
|
+
Sidekiq.redis do |conn|
|
33
|
+
keys = conn.keys '*batching*'
|
34
|
+
keys.each { |key| conn.del key }
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
config.after :each do
|
39
|
+
Timecop.return
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
$: << File.join(File.dirname(__FILE__), '..', 'lib')
|
@@ -0,0 +1,42 @@
|
|
1
|
+
class RegularWorker
|
2
|
+
include Sidekiq::Worker
|
3
|
+
|
4
|
+
def perform(foo)
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
class BatchedSizeWorker
|
9
|
+
include Sidekiq::Worker
|
10
|
+
|
11
|
+
sidekiq_options queue: :batched_size, batch_size: 3
|
12
|
+
|
13
|
+
def perform(foo)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class BatchedIntervalWorker
|
18
|
+
include Sidekiq::Worker
|
19
|
+
|
20
|
+
sidekiq_options queue: :batched_interval, batch_flush_interval: 3600
|
21
|
+
|
22
|
+
def perform(foo)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class BatchedBothWorker
|
27
|
+
include Sidekiq::Worker
|
28
|
+
|
29
|
+
sidekiq_options queue: :batched_both, batch_flush_interval: 3600, batch_size: 3
|
30
|
+
|
31
|
+
def perform(foo)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class BatchedUniqueArgsWorker
|
36
|
+
include Sidekiq::Worker
|
37
|
+
|
38
|
+
sidekiq_options queue: :batched_unique_args, batch_size: 3, batch_unique: true
|
39
|
+
|
40
|
+
def perform(foo)
|
41
|
+
end
|
42
|
+
end
|
data/web.png
ADDED
Binary file
|
metadata
ADDED
@@ -0,0 +1,197 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: sidekiq-grouping
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.6
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Victor Sokolov
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-04-24 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.5'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.5'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
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: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: simplecov
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rspec-sidekiq
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: activesupport
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: timecop
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: sidekiq
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :runtime
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: celluloid
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
type: :runtime
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
description:
|
140
|
+
email:
|
141
|
+
- gzigzigzeo@gmail.com
|
142
|
+
executables: []
|
143
|
+
extensions: []
|
144
|
+
extra_rdoc_files: []
|
145
|
+
files:
|
146
|
+
- ".gitignore"
|
147
|
+
- ".travis.yml"
|
148
|
+
- Gemfile
|
149
|
+
- LICENSE.txt
|
150
|
+
- README.md
|
151
|
+
- Rakefile
|
152
|
+
- lib/sidekiq/grouping.rb
|
153
|
+
- lib/sidekiq/grouping/actor.rb
|
154
|
+
- lib/sidekiq/grouping/batch.rb
|
155
|
+
- lib/sidekiq/grouping/config.rb
|
156
|
+
- lib/sidekiq/grouping/logging.rb
|
157
|
+
- lib/sidekiq/grouping/middleware.rb
|
158
|
+
- lib/sidekiq/grouping/redis.rb
|
159
|
+
- lib/sidekiq/grouping/supervisor.rb
|
160
|
+
- lib/sidekiq/grouping/version.rb
|
161
|
+
- lib/sidekiq/grouping/views/index.erb
|
162
|
+
- lib/sidekiq/grouping/web.rb
|
163
|
+
- sidekiq-grouping.gemspec
|
164
|
+
- spec/modules/batch_spec.rb
|
165
|
+
- spec/modules/redis_spec.rb
|
166
|
+
- spec/spec_helper.rb
|
167
|
+
- spec/support/test_workers.rb
|
168
|
+
- web.png
|
169
|
+
homepage: http://github.com/gzigzigzeo/sidekiq-grouping
|
170
|
+
licenses:
|
171
|
+
- MIT
|
172
|
+
metadata: {}
|
173
|
+
post_install_message:
|
174
|
+
rdoc_options: []
|
175
|
+
require_paths:
|
176
|
+
- lib
|
177
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
178
|
+
requirements:
|
179
|
+
- - ">="
|
180
|
+
- !ruby/object:Gem::Version
|
181
|
+
version: '0'
|
182
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
183
|
+
requirements:
|
184
|
+
- - ">="
|
185
|
+
- !ruby/object:Gem::Version
|
186
|
+
version: '0'
|
187
|
+
requirements: []
|
188
|
+
rubyforge_project:
|
189
|
+
rubygems_version: 2.4.3
|
190
|
+
signing_key:
|
191
|
+
specification_version: 4
|
192
|
+
summary: Allows identical sidekiq jobs to be processed with a single background call
|
193
|
+
test_files:
|
194
|
+
- spec/modules/batch_spec.rb
|
195
|
+
- spec/modules/redis_spec.rb
|
196
|
+
- spec/spec_helper.rb
|
197
|
+
- spec/support/test_workers.rb
|