sidekiq-priority_queue 1.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.github/workflows/tests.yml +29 -0
- data/.gitignore +2 -0
- data/Gemfile +11 -0
- data/Gemfile.lock +50 -0
- data/LICENSE +5 -0
- data/README.md +64 -0
- data/Rakefile +8 -0
- data/bin/sidekiqload +159 -0
- data/docker-compose.yml +7 -0
- data/lib/sidekiq/priority_queue.rb +15 -0
- data/lib/sidekiq/priority_queue/api.rb +75 -0
- data/lib/sidekiq/priority_queue/client.rb +68 -0
- data/lib/sidekiq/priority_queue/combined_fetch.rb +44 -0
- data/lib/sidekiq/priority_queue/fetch.rb +87 -0
- data/lib/sidekiq/priority_queue/reliable_fetch.rb +127 -0
- data/lib/sidekiq/priority_queue/scripts.rb +33 -0
- data/lib/sidekiq/priority_queue/testing.rb +26 -0
- data/lib/sidekiq/priority_queue/web.rb +42 -0
- data/lib/sidekiq/priority_queue/web/views/_paging.erb +24 -0
- data/lib/sidekiq/priority_queue/web/views/priority_queue.erb +74 -0
- data/lib/sidekiq/priority_queue/web/views/priority_queues.erb +27 -0
- data/sidekiq-priority_queue.gemspec +16 -0
- metadata +100 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 23819a77a9986da48fe0e15a590bd948abf62cafa7d48f7ad8aa2d6129dbb476
|
4
|
+
data.tar.gz: cc8e7a8a99c095237a6b44c0a266753d98707c724e5af70b494a71b3a25476ac
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: '0884b842b53184df96c7e753d0d8d6b04621b2b5810a33e5798e95d76b13e09776ac4db62c520c0e822be0987b798deaa473e76898ecdd92b4095230b6b65501'
|
7
|
+
data.tar.gz: f1bb0dfb7a3048ce7ca80924916ac6817ed16262a2d98596f1da50db35292b2a2cb3287f0fcdb61a468f21f39d0dac5de3f8989b7bdc87515f728179fbb7576a
|
@@ -0,0 +1,29 @@
|
|
1
|
+
name: Tests
|
2
|
+
on:
|
3
|
+
pull_request:
|
4
|
+
push:
|
5
|
+
branches:
|
6
|
+
- main
|
7
|
+
jobs:
|
8
|
+
tests:
|
9
|
+
name: Run tests
|
10
|
+
outputs:
|
11
|
+
job-status: ${{ job.status }}
|
12
|
+
runs-on: ubuntu-18.04
|
13
|
+
timeout-minutes: 10
|
14
|
+
services:
|
15
|
+
redis:
|
16
|
+
image: redis:alpine
|
17
|
+
ports:
|
18
|
+
- 6379:6379
|
19
|
+
steps:
|
20
|
+
- name: Checkout
|
21
|
+
uses: actions/checkout@v2.3.1
|
22
|
+
- name: Set up correct version of Ruby
|
23
|
+
uses: actions/setup-ruby@v1
|
24
|
+
with:
|
25
|
+
ruby-version: 2.7
|
26
|
+
- name: Install dependencies via Bundler
|
27
|
+
run: bundle install --jobs 4 --retry 3
|
28
|
+
- name: Run tests
|
29
|
+
run: bundle exec rake
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
sidekiq-priority_queue (1.0.2)
|
5
|
+
sidekiq (>= 6)
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: https://rubygems.org/
|
9
|
+
specs:
|
10
|
+
byebug (11.1.3)
|
11
|
+
coderay (1.1.3)
|
12
|
+
connection_pool (2.2.3)
|
13
|
+
docile (1.3.4)
|
14
|
+
method_source (1.0.0)
|
15
|
+
minitest (5.14.3)
|
16
|
+
pry (0.13.1)
|
17
|
+
coderay (~> 1.1)
|
18
|
+
method_source (~> 1.0)
|
19
|
+
pry-byebug (3.9.0)
|
20
|
+
byebug (~> 11.0)
|
21
|
+
pry (~> 0.13.0)
|
22
|
+
rack (2.2.3)
|
23
|
+
rack-test (1.1.0)
|
24
|
+
rack (>= 1.0, < 3)
|
25
|
+
rake (13.0.3)
|
26
|
+
redis (4.2.5)
|
27
|
+
sidekiq (6.1.3)
|
28
|
+
connection_pool (>= 2.2.2)
|
29
|
+
rack (~> 2.0)
|
30
|
+
redis (>= 4.2.0)
|
31
|
+
simplecov (0.21.2)
|
32
|
+
docile (~> 1.1)
|
33
|
+
simplecov-html (~> 0.11)
|
34
|
+
simplecov_json_formatter (~> 0.1)
|
35
|
+
simplecov-html (0.12.3)
|
36
|
+
simplecov_json_formatter (0.1.2)
|
37
|
+
|
38
|
+
PLATFORMS
|
39
|
+
ruby
|
40
|
+
|
41
|
+
DEPENDENCIES
|
42
|
+
minitest
|
43
|
+
pry-byebug
|
44
|
+
rack-test
|
45
|
+
rake
|
46
|
+
sidekiq-priority_queue!
|
47
|
+
simplecov
|
48
|
+
|
49
|
+
BUNDLED WITH
|
50
|
+
2.1.4
|
data/LICENSE
ADDED
data/README.md
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
Sidekiq Priority Queue
|
2
|
+
==============
|
3
|
+
Extends Sidekiq with support for queuing jobs with a fine grained priority and emulating multiple queues using a single Redis sorted set, ideal for multi-tenant applications.
|
4
|
+
|
5
|
+
The standard Sidekiq setup performs really well using Redis lists, but lists can only be strict FIFO queues, which can be hugely problematic when they process slowly and one user may need to wait hours behind a backlog of jobs.
|
6
|
+
|
7
|
+
Sidekiq Priority Queue offers a plug-in solution retaining the simplicity and performance of Sidekiq. The priority queue is a building block for emulating sub-queues (per tenant or user) by de-prioritising jobs according to how many jobs are already in this sub-queue.
|
8
|
+
|
9
|
+
Sources of inspiration are naturally Sidekiq itself, the fantastic Redis documentation, and https://github.com/gocraft/work
|
10
|
+
|
11
|
+
Installation
|
12
|
+
-----------------
|
13
|
+
|
14
|
+
gem install sidekiq-priority_queue
|
15
|
+
|
16
|
+
Configuration
|
17
|
+
-----------------
|
18
|
+
```
|
19
|
+
Sidekiq.configure_server do |config|
|
20
|
+
config.options[:fetch] = Sidekiq::PriorityQueue::Fetch
|
21
|
+
end
|
22
|
+
|
23
|
+
Sidekiq.configure_client do |config|
|
24
|
+
config.client_middleware do |chain|
|
25
|
+
chain.add Sidekiq::PriorityQueue::Client
|
26
|
+
end
|
27
|
+
end
|
28
|
+
```
|
29
|
+
Usage
|
30
|
+
-----------------
|
31
|
+
```
|
32
|
+
class Worker
|
33
|
+
include Sidekiq::Worker
|
34
|
+
sidekiq_options priority: 1000
|
35
|
+
end
|
36
|
+
```
|
37
|
+
Alternatively, you can split jobs into subqueues (via a proc) which are deprioritised based on the subqueue size:
|
38
|
+
```
|
39
|
+
class Worker
|
40
|
+
include Sidekiq::Worker
|
41
|
+
|
42
|
+
# args[0] will take the `user_id` argument below, and assign priority dynamically.
|
43
|
+
sidekiq_options subqueue: ->(args){ args[0] }
|
44
|
+
|
45
|
+
def perform(user_id, other_args)
|
46
|
+
# do jobs
|
47
|
+
end
|
48
|
+
end
|
49
|
+
```
|
50
|
+
|
51
|
+
Testing
|
52
|
+
-----------------
|
53
|
+
For example in your `spec/rails_helper.rb` you can include this line:
|
54
|
+
```ruby
|
55
|
+
require 'sidekiq/priority_queue/testing'
|
56
|
+
```
|
57
|
+
next to the call to `Sidekiq::Testing.inline!`. It disables the feature for testing and falls back to `inline`/`fake` modes.
|
58
|
+
If you accidentally require this in production code, it will likewise fall back to normal Sidekiq scheduling.
|
59
|
+
|
60
|
+
Development
|
61
|
+
-----------------
|
62
|
+
- Run `docker-compose up -d` to start up a temporary redis instance.
|
63
|
+
- Run `bundle install` to install dependencies.
|
64
|
+
- Run the tests with `bundle exec rake`
|
data/Rakefile
ADDED
data/bin/sidekiqload
ADDED
@@ -0,0 +1,159 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# Copyright (c) Contributed Systems LLC, ChartMogul Ltd
|
4
|
+
|
5
|
+
# Sidekiq-priority_queue is an Open Source project licensed under the terms of
|
6
|
+
# the LGPLv3 license. Please see <http://www.gnu.org/licenses/lgpl-3.0.html>
|
7
|
+
# for license text.
|
8
|
+
|
9
|
+
# Quiet some warnings we see when running in warning mode:
|
10
|
+
# RUBYOPT=-w bundle exec sidekiq
|
11
|
+
$TESTING = false
|
12
|
+
|
13
|
+
#require 'ruby-prof'
|
14
|
+
Bundler.require(:default)
|
15
|
+
|
16
|
+
require 'sidekiq/cli'
|
17
|
+
require 'sidekiq/launcher'
|
18
|
+
require 'sidekiq/priority_queue'
|
19
|
+
|
20
|
+
include Sidekiq::Util
|
21
|
+
|
22
|
+
Sidekiq.configure_server do |config|
|
23
|
+
#config.options[:concurrency] = 1
|
24
|
+
config.redis = { db: 13 }
|
25
|
+
config.options[:queues] << 'default'
|
26
|
+
config.logger.level = Logger::ERROR
|
27
|
+
config.average_scheduled_poll_interval = 2
|
28
|
+
config.options[:fetch] = Sidekiq::PriorityQueue::Fetch
|
29
|
+
end
|
30
|
+
|
31
|
+
Sidekiq.configure_client do |config|
|
32
|
+
config.client_middleware do |chain|
|
33
|
+
chain.add Sidekiq::PriorityQueue::Client
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
class LoadWorker
|
38
|
+
include Sidekiq::Worker
|
39
|
+
sidekiq_options retry: 1, subqueue: ->(args){ args[0] }
|
40
|
+
sidekiq_retry_in do |x|
|
41
|
+
1
|
42
|
+
end
|
43
|
+
|
44
|
+
def perform(idx)
|
45
|
+
#raise idx.to_s if idx % 100 == 1
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# brew tap shopify/shopify
|
50
|
+
# brew install toxiproxy
|
51
|
+
# gem install toxiproxy
|
52
|
+
#require 'toxiproxy'
|
53
|
+
# simulate a non-localhost network for realer-world conditions.
|
54
|
+
# adding 1ms of network latency has an ENORMOUS impact on benchmarks
|
55
|
+
#Toxiproxy.populate([{
|
56
|
+
#"name": "redis",
|
57
|
+
#"listen": "127.0.0.1:6380",
|
58
|
+
#"upstream": "127.0.0.1:6379"
|
59
|
+
#}])
|
60
|
+
|
61
|
+
self_read, self_write = IO.pipe
|
62
|
+
%w(INT TERM TSTP TTIN).each do |sig|
|
63
|
+
begin
|
64
|
+
trap sig do
|
65
|
+
self_write.puts(sig)
|
66
|
+
end
|
67
|
+
rescue ArgumentError
|
68
|
+
puts "Signal #{sig} not supported"
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
Sidekiq.redis {|c| c.flushdb}
|
73
|
+
def handle_signal(launcher, sig)
|
74
|
+
Sidekiq.logger.debug "Got #{sig} signal"
|
75
|
+
case sig
|
76
|
+
when 'INT'
|
77
|
+
# Handle Ctrl-C in JRuby like MRI
|
78
|
+
# http://jira.codehaus.org/browse/JRUBY-4637
|
79
|
+
raise Interrupt
|
80
|
+
when 'TERM'
|
81
|
+
# Heroku sends TERM and then waits 10 seconds for process to exit.
|
82
|
+
raise Interrupt
|
83
|
+
when 'TSTP'
|
84
|
+
Sidekiq.logger.info "Received TSTP, no longer accepting new work"
|
85
|
+
launcher.quiet
|
86
|
+
when 'TTIN'
|
87
|
+
Thread.list.each do |thread|
|
88
|
+
Sidekiq.logger.warn "Thread TID-#{(thread.object_id ^ ::Process.pid).to_s(36)} #{thread['label']}"
|
89
|
+
if thread.backtrace
|
90
|
+
Sidekiq.logger.warn thread.backtrace.join("\n")
|
91
|
+
else
|
92
|
+
Sidekiq.logger.warn "<no backtrace available>"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def Process.rss
|
99
|
+
`ps -o rss= -p #{Process.pid}`.chomp.to_i
|
100
|
+
end
|
101
|
+
|
102
|
+
|
103
|
+
Sidekiq.redis do |con|
|
104
|
+
count = 100_000
|
105
|
+
|
106
|
+
count.times do |idx|
|
107
|
+
#TODO why does Sidekiq::Client.push not work as expected?
|
108
|
+
con.zadd('priority-queue:default', idx, { 'class' => LoadWorker, 'args' => [idx] }.to_json)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
Sidekiq.logger.error "Created #{ Sidekiq::PriorityQueue::Queue.new().size } jobs"
|
112
|
+
|
113
|
+
Monitoring = Thread.new do
|
114
|
+
watchdog("monitor thread") do
|
115
|
+
while true
|
116
|
+
sleep 1
|
117
|
+
qsize, retries = Sidekiq.redis do |conn|
|
118
|
+
conn.pipelined do
|
119
|
+
conn.zcard "priority-queue:default"
|
120
|
+
conn.zcard "retry"
|
121
|
+
end
|
122
|
+
end.map(&:to_i)
|
123
|
+
total = qsize + retries
|
124
|
+
#GC.start
|
125
|
+
Sidekiq.logger.error("RSS: #{Process.rss} Pending: #{total}")
|
126
|
+
if total == 0
|
127
|
+
Sidekiq.logger.error("Done")
|
128
|
+
exit(0)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
begin
|
135
|
+
#RubyProf::exclude_threads = [ Monitoring ]
|
136
|
+
#RubyProf.start
|
137
|
+
fire_event(:startup)
|
138
|
+
#Sidekiq.logger.error "Simulating 1ms of latency between Sidekiq and redis"
|
139
|
+
#Toxiproxy[:redis].downstream(:latency, latency: 1).apply do
|
140
|
+
launcher = Sidekiq::Launcher.new(Sidekiq.options)
|
141
|
+
launcher.run
|
142
|
+
|
143
|
+
while readable_io = IO.select([self_read])
|
144
|
+
signal = readable_io.first[0].gets.strip
|
145
|
+
handle_signal(launcher, signal)
|
146
|
+
end
|
147
|
+
#end
|
148
|
+
rescue SystemExit => e
|
149
|
+
#Sidekiq.logger.error("Profiling...")
|
150
|
+
#result = RubyProf.stop
|
151
|
+
#printer = RubyProf::GraphHtmlPrinter.new(result)
|
152
|
+
#printer.print(File.new("output.html", "w"), :min_percent => 1)
|
153
|
+
# normal
|
154
|
+
rescue => e
|
155
|
+
raise e if $DEBUG
|
156
|
+
STDERR.puts e.message
|
157
|
+
STDERR.puts e.backtrace.join("\n")
|
158
|
+
exit 1
|
159
|
+
end
|
data/docker-compose.yml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'sidekiq'
|
4
|
+
require 'sidekiq/priority_queue/api'
|
5
|
+
require 'sidekiq/priority_queue/client'
|
6
|
+
require 'sidekiq/priority_queue/combined_fetch'
|
7
|
+
require 'sidekiq/priority_queue/fetch'
|
8
|
+
require 'sidekiq/priority_queue/reliable_fetch'
|
9
|
+
require 'sidekiq/priority_queue/scripts'
|
10
|
+
require 'sidekiq/priority_queue/web'
|
11
|
+
|
12
|
+
module Sidekiq
|
13
|
+
module PriorityQueue
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'sidekiq/api'
|
3
|
+
|
4
|
+
|
5
|
+
module Sidekiq
|
6
|
+
module PriorityQueue
|
7
|
+
class Queue
|
8
|
+
include Enumerable
|
9
|
+
|
10
|
+
attr_reader :name
|
11
|
+
|
12
|
+
def initialize(name='default')
|
13
|
+
@name = name
|
14
|
+
@rname = "priority-queue:#{name}"
|
15
|
+
end
|
16
|
+
|
17
|
+
def size
|
18
|
+
Sidekiq.redis { |con| con.zcard(@rname) }
|
19
|
+
end
|
20
|
+
|
21
|
+
def each
|
22
|
+
initial_size = size
|
23
|
+
deleted_size = 0
|
24
|
+
page = 0
|
25
|
+
page_size = 50
|
26
|
+
|
27
|
+
while true do
|
28
|
+
range_start = page * page_size - deleted_size
|
29
|
+
range_end = range_start + page_size - 1
|
30
|
+
entries = Sidekiq.redis do |conn|
|
31
|
+
conn.zrange @rname, range_start, range_end, withscores: true
|
32
|
+
end
|
33
|
+
break if entries.empty?
|
34
|
+
page += 1
|
35
|
+
entries.each do |entry, priority|
|
36
|
+
yield Job.new(entry, @name, priority)
|
37
|
+
end
|
38
|
+
deleted_size = initial_size - size
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.all
|
43
|
+
Sidekiq.redis { |con| con.smembers('priority-queues') }
|
44
|
+
.map{ |key| key.gsub('priority-queue:', '') }
|
45
|
+
.sort
|
46
|
+
.map { |q| Queue.new(q) }
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
|
51
|
+
SubqueueCount = Struct.new(:name, :size)
|
52
|
+
|
53
|
+
class Job < Sidekiq::Job
|
54
|
+
|
55
|
+
attr_reader :priority
|
56
|
+
attr_reader :subqueue
|
57
|
+
|
58
|
+
def initialize(item, queue_name = nil, priority = nil)
|
59
|
+
@args = nil
|
60
|
+
@value = item
|
61
|
+
@item = item.is_a?(Hash) ? item : parse(item)
|
62
|
+
@queue = queue_name || @item['queue']
|
63
|
+
@subqueue = @item['subqueue']
|
64
|
+
@priority = priority
|
65
|
+
end
|
66
|
+
|
67
|
+
def delete
|
68
|
+
count = Sidekiq.redis do |conn|
|
69
|
+
conn.zrem("priority-queue:#{@queue}", @value)
|
70
|
+
end
|
71
|
+
count != 0
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module Sidekiq
|
2
|
+
module PriorityQueue
|
3
|
+
class Client
|
4
|
+
|
5
|
+
# inserted into Sidekiq's Client as middleware
|
6
|
+
def call(worker_class, item, queue, redis_pool)
|
7
|
+
if item['priority']
|
8
|
+
sadd('priority-queues', queue)
|
9
|
+
zadd(queue, item['priority'], item)
|
10
|
+
return item['jid']
|
11
|
+
elsif item['subqueue']
|
12
|
+
# replace the proc with what it returns
|
13
|
+
sadd('priority-queues', queue)
|
14
|
+
item['subqueue'] = resolve_subqueue(item['subqueue'], item['args'])
|
15
|
+
priority = fetch_and_add(queue, item['subqueue'], item)
|
16
|
+
zadd(queue, priority, item)
|
17
|
+
return item['jid']
|
18
|
+
else
|
19
|
+
# continue pushing the normal Sidekiq way
|
20
|
+
yield
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def resolve_subqueue(subqueue, job_args)
|
27
|
+
return subqueue unless subqueue.respond_to?(:call)
|
28
|
+
|
29
|
+
subqueue.call(job_args)
|
30
|
+
end
|
31
|
+
|
32
|
+
def zadd(queue, score, item)
|
33
|
+
Sidekiq.redis do |conn|
|
34
|
+
queue = "priority-queue:#{queue}"
|
35
|
+
conn.zadd(queue, score, item.to_json)
|
36
|
+
return item
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def sadd(set, member)
|
41
|
+
Sidekiq.redis do |conn|
|
42
|
+
conn.sadd(set,member)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def fetch_and_add(queue, subqueue, item)
|
47
|
+
Sidekiq.redis do |conn|
|
48
|
+
priority = conn.zincrby("priority-queue-counts:#{queue}", 1, subqueue)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
Sidekiq::Client.class_eval do
|
56
|
+
def push(item)
|
57
|
+
normed = normalize_item(item)
|
58
|
+
payload = process_single(item['class'], normed)
|
59
|
+
|
60
|
+
# if payload is a JID because the middleware already pushed then just return the JID
|
61
|
+
return payload if payload.is_a?(String)
|
62
|
+
|
63
|
+
if payload
|
64
|
+
raw_push([payload])
|
65
|
+
payload['jid']
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sidekiq
|
4
|
+
module PriorityQueue
|
5
|
+
class CombinedFetch
|
6
|
+
attr_reader :fetches
|
7
|
+
|
8
|
+
def initialize(fetches = [])
|
9
|
+
@fetches = fetches
|
10
|
+
end
|
11
|
+
|
12
|
+
def retrieve_work
|
13
|
+
fetches.each do |fetch|
|
14
|
+
work = fetch.retrieve_work
|
15
|
+
return work if work
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.configure(&block)
|
20
|
+
combined_fetch = self.new
|
21
|
+
yield combined_fetch
|
22
|
+
|
23
|
+
combined_fetch
|
24
|
+
end
|
25
|
+
|
26
|
+
def add(fetch)
|
27
|
+
fetches << fetch
|
28
|
+
end
|
29
|
+
|
30
|
+
def bulk_requeue(inprogress, options)
|
31
|
+
# ReliableFetch#bulk_equeue ignores inprogress, so it's safe to call both
|
32
|
+
fetches.each do |f|
|
33
|
+
if [Fetch, ReliableFetch].any? { |klass| f.is_a?(klass) }
|
34
|
+
jobs_to_requeue = inprogress.select{|uow| uow.queue.start_with?('priority-queue:') }
|
35
|
+
f.bulk_requeue(jobs_to_requeue, options)
|
36
|
+
else
|
37
|
+
jobs_to_requeue = inprogress.reject{|uow| uow.queue.start_with?('priority-queue:') }
|
38
|
+
f.bulk_requeue(jobs_to_requeue, options)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'sidekiq'
|
3
|
+
|
4
|
+
module Sidekiq
|
5
|
+
module PriorityQueue
|
6
|
+
class Fetch
|
7
|
+
|
8
|
+
UnitOfWork = Struct.new(:queue, :job) do
|
9
|
+
def acknowledge
|
10
|
+
Sidekiq.redis do |conn|
|
11
|
+
unless subqueue.nil?
|
12
|
+
count = conn.zincrby(subqueue_counts, -1, subqueue)
|
13
|
+
conn.zrem(subqueue_counts, subqueue) if count < 1
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def queue_name
|
19
|
+
queue.sub(/.*queue:/, '')
|
20
|
+
end
|
21
|
+
|
22
|
+
def subqueue
|
23
|
+
@parsed_job ||= JSON.parse(job)
|
24
|
+
@parsed_job['subqueue']
|
25
|
+
end
|
26
|
+
|
27
|
+
def subqueue_counts
|
28
|
+
"priority-queue-counts:#{queue_name}"
|
29
|
+
end
|
30
|
+
|
31
|
+
def requeue
|
32
|
+
Sidekiq.redis do |conn|
|
33
|
+
conn.zadd(queue, 0, job)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def initialize(options)
|
39
|
+
@strictly_ordered_queues = !!options[:strict]
|
40
|
+
@queues = options[:queues].map { |q| "priority-queue:#{q}" }
|
41
|
+
@queues = @queues.uniq if @strictly_ordered_queues
|
42
|
+
end
|
43
|
+
|
44
|
+
def retrieve_work
|
45
|
+
work = @queues.detect{ |q| job = zpopmin(q); break [q,job] if job }
|
46
|
+
UnitOfWork.new(*work) if work
|
47
|
+
end
|
48
|
+
|
49
|
+
def zpopmin(queue)
|
50
|
+
Sidekiq.redis do |con|
|
51
|
+
@script_sha ||= con.script(:load, Sidekiq::PriorityQueue::Scripts::ZPOPMIN)
|
52
|
+
con.evalsha(@script_sha, [queue])
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def queues_cmd
|
57
|
+
if @strictly_ordered_queues
|
58
|
+
@queues
|
59
|
+
else
|
60
|
+
@queues.shuffle.uniq
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def bulk_requeue(inprogress, options)
|
65
|
+
return if inprogress.empty?
|
66
|
+
|
67
|
+
Sidekiq.logger.debug { "Re-queueing terminated jobs" }
|
68
|
+
jobs_to_requeue = {}
|
69
|
+
inprogress.each do |unit_of_work|
|
70
|
+
jobs_to_requeue[unit_of_work.queue] ||= []
|
71
|
+
jobs_to_requeue[unit_of_work.queue] << unit_of_work.job
|
72
|
+
end
|
73
|
+
|
74
|
+
Sidekiq.redis do |conn|
|
75
|
+
conn.pipelined do
|
76
|
+
jobs_to_requeue.each do |queue, jobs|
|
77
|
+
conn.zadd(queue, jobs.map{|j| [0,j] })
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
Sidekiq.logger.info("Pushed #{inprogress.size} jobs back to Redis")
|
82
|
+
rescue => ex
|
83
|
+
Sidekiq.logger.warn("Failed to requeue #{inprogress.size} jobs: #{ex.message}")
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'sidekiq'
|
3
|
+
|
4
|
+
module Sidekiq
|
5
|
+
module PriorityQueue
|
6
|
+
class ReliableFetch
|
7
|
+
|
8
|
+
UnitOfWork = Struct.new(:queue, :job, :wip_queue) do
|
9
|
+
def acknowledge
|
10
|
+
Sidekiq.redis do |conn|
|
11
|
+
conn.srem(wip_queue, job)
|
12
|
+
unless subqueue.nil?
|
13
|
+
count = conn.zincrby(subqueue_counts, -1, subqueue)
|
14
|
+
conn.zrem(subqueue_counts, subqueue) if count < 1
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def queue_name
|
20
|
+
queue.sub(/.*queue:/, '')
|
21
|
+
end
|
22
|
+
|
23
|
+
def subqueue
|
24
|
+
@parsed_job ||= JSON.parse(job)
|
25
|
+
@parsed_job['subqueue']
|
26
|
+
end
|
27
|
+
|
28
|
+
def subqueue_counts
|
29
|
+
"priority-queue-counts:#{queue_name}"
|
30
|
+
end
|
31
|
+
|
32
|
+
def requeue
|
33
|
+
# Nothing needed. Jobs are in WIP queue.
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def initialize(options)
|
38
|
+
@strictly_ordered_queues = !!options[:strict]
|
39
|
+
@queues = options[:queues].map { |q| "priority-queue:#{q}" }
|
40
|
+
@queues = @queues.uniq if @strictly_ordered_queues
|
41
|
+
@process_index = options[:index] || ENV['PROCESS_INDEX']
|
42
|
+
end
|
43
|
+
|
44
|
+
def retrieve_work
|
45
|
+
work = @queues.detect do |q|
|
46
|
+
job = zpopmin_sadd(q, wip_queue(q));
|
47
|
+
break [q,job] if job
|
48
|
+
end
|
49
|
+
UnitOfWork.new(*work, wip_queue(work.first)) if work
|
50
|
+
end
|
51
|
+
|
52
|
+
def wip_queue(q)
|
53
|
+
"#{q}_#{Socket.gethostname}_#{@process_index}"
|
54
|
+
end
|
55
|
+
|
56
|
+
def zpopmin_sadd(queue, wip_queue)
|
57
|
+
Sidekiq.redis do |con|
|
58
|
+
@script_sha ||= con.script(:load, Sidekiq::PriorityQueue::Scripts::ZPOPMIN_SADD)
|
59
|
+
con.evalsha(@script_sha, [queue, wip_queue])
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def spop(wip_queue)
|
64
|
+
Sidekiq.redis{ |con| con.spop(wip_queue) }
|
65
|
+
end
|
66
|
+
|
67
|
+
def queues_cmd
|
68
|
+
if @strictly_ordered_queues
|
69
|
+
@queues
|
70
|
+
else
|
71
|
+
@queues.shuffle.uniq
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def bulk_requeue(_inprogress, options)
|
76
|
+
Sidekiq.logger.debug { "Re-queueing terminated jobs" }
|
77
|
+
process_index = options[:index] || ENV['PROCESS_INDEX']
|
78
|
+
self.class.requeue_wip_jobs(options[:queues], process_index)
|
79
|
+
end
|
80
|
+
|
81
|
+
def self.resume_wip_jobs(queues, process_index)
|
82
|
+
Sidekiq.logger.debug { "Re-queueing WIP jobs" }
|
83
|
+
process_index ||= ENV['PROCESS_INDEX']
|
84
|
+
requeue_wip_jobs(queues, process_index)
|
85
|
+
end
|
86
|
+
|
87
|
+
Sidekiq.configure_server do |config|
|
88
|
+
config.on(:startup) do
|
89
|
+
if reliable_fetch_active?(config)
|
90
|
+
Sidekiq::PriorityQueue::ReliableFetch.resume_wip_jobs(config.options[:queues], config.options[:index])
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
private
|
96
|
+
|
97
|
+
def self.reliable_fetch_active?(config)
|
98
|
+
return true if config.options[:fetch].is_a?(Sidekiq::PriorityQueue::ReliableFetch)
|
99
|
+
return config.options[:fetch].is_a?(Sidekiq::PriorityQueue::CombinedFetch) &&
|
100
|
+
config.options[:fetch].fetches.any? { |f| f.is_a?(Sidekiq::PriorityQueue::ReliableFetch) }
|
101
|
+
end
|
102
|
+
|
103
|
+
def self.requeue_wip_jobs(queues, index)
|
104
|
+
jobs_to_requeue = {}
|
105
|
+
Sidekiq.redis do |conn|
|
106
|
+
queues.map { |q| "priority-queue:#{q}" }.each do |q|
|
107
|
+
wip_queue = "#{q}_#{Socket.gethostname}_#{index}"
|
108
|
+
jobs_to_requeue[q] = []
|
109
|
+
while job = conn.spop(wip_queue) do
|
110
|
+
jobs_to_requeue[q] << job
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
conn.pipelined do
|
115
|
+
jobs_to_requeue.each do |queue, jobs|
|
116
|
+
return unless jobs.size > 0
|
117
|
+
conn.zadd(queue, jobs.map{|j| [0,j] })
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
Sidekiq.logger.info("Pushed #{ jobs_to_requeue.map{|q| q.size }.reduce(:+) } jobs back to Redis")
|
122
|
+
rescue => ex
|
123
|
+
Sidekiq.logger.warn("Failed to requeue #{ jobs_to_requeue.map{|q| q.size }.reduce(:+) } jobs: #{ex.message}")
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
|
4
|
+
module Sidekiq
|
5
|
+
module PriorityQueue
|
6
|
+
module Scripts
|
7
|
+
|
8
|
+
ZPOPMIN = %q(
|
9
|
+
local resp = redis.call('zrange', KEYS[1], '0', '0')
|
10
|
+
if (resp[1] ~= nil) then
|
11
|
+
local val = resp[# resp]
|
12
|
+
redis.call('zrem', KEYS[1], val)
|
13
|
+
return val
|
14
|
+
else
|
15
|
+
return false
|
16
|
+
end
|
17
|
+
)
|
18
|
+
|
19
|
+
ZPOPMIN_SADD = %q(
|
20
|
+
local resp = redis.call('zrange', KEYS[1], '0', '0')
|
21
|
+
if (resp[1] ~= nil) then
|
22
|
+
local val = resp[# resp]
|
23
|
+
redis.call('zrem', KEYS[1], val)
|
24
|
+
redis.call('sadd', KEYS[2], val)
|
25
|
+
return val
|
26
|
+
else
|
27
|
+
return false
|
28
|
+
end
|
29
|
+
)
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Don't require in production code.
|
4
|
+
# This disables the middleware and falls back to normal push, meaning in tests it will use inline/fake mode.
|
5
|
+
# Prioritization doesn't make any sense in inline or fake tests anyway.
|
6
|
+
module Sidekiq
|
7
|
+
module PriorityQueue
|
8
|
+
module TestingClient
|
9
|
+
def call(worker_class, item, queue, redis_pool)
|
10
|
+
testing_verify_subqueue(item) if item['subqueue'] && !item['priority']
|
11
|
+
yield # continue pushing the normal Sidekiq way
|
12
|
+
end
|
13
|
+
|
14
|
+
# Help testing the lambda; raise in case it's invalid.
|
15
|
+
def testing_verify_subqueue(item)
|
16
|
+
subqueue = resolve_subqueue(item['subqueue'], item['args'])
|
17
|
+
serialized = "#{subqueue}"
|
18
|
+
|
19
|
+
raise "subqueue shouldn't be nil" if subqueue.nil?
|
20
|
+
raise "subqueue shouldn't be empty" if serialized == ""
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
Sidekiq::PriorityQueue::Client.prepend TestingClient
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'sidekiq/web'
|
2
|
+
|
3
|
+
|
4
|
+
module Sidekiq::PriorityQueue
|
5
|
+
module Web
|
6
|
+
|
7
|
+
ROOT = File.expand_path('../web', __FILE__)
|
8
|
+
|
9
|
+
def self.registered(app)
|
10
|
+
app.tabs['Priority Queues'] = 'priority_queues'
|
11
|
+
|
12
|
+
app.get '/priority_queues' do
|
13
|
+
@queues = Queue.all
|
14
|
+
render(:erb, File.read("#{ROOT}/views/priority_queues.erb"))
|
15
|
+
end
|
16
|
+
|
17
|
+
app.get '/priority_queues/:name' do
|
18
|
+
@name = route_params[:name]
|
19
|
+
halt(404) unless @name
|
20
|
+
|
21
|
+
@count = (params['count'] || 25).to_i
|
22
|
+
@queue = Sidekiq::Queue.new(@name)
|
23
|
+
(@current_page, @total_size, @messages) = page("priority-queue:#{@name}", params['page'], @count)
|
24
|
+
@subqueue_counts = Sidekiq.redis do |con|
|
25
|
+
con.zrevrange("priority-queue-counts:#{@name}", 0, params['subqueue_count'] || 10, withscores: true)
|
26
|
+
end.map { |name, count| SubqueueCount.new(name, count) }
|
27
|
+
|
28
|
+
@messages = @messages.map{ |msg| Job.new(msg.first, @name, msg.last) }
|
29
|
+
render(:erb, File.read("#{ROOT}/views/priority_queue.erb"))
|
30
|
+
end
|
31
|
+
|
32
|
+
app.post "/priority_queues/:name/delete" do
|
33
|
+
name = route_params[:name]
|
34
|
+
Job.new(params['key_val'], name).delete
|
35
|
+
redirect_with_query("#{root_path}priority_queues/#{CGI.escape(name)}")
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
::Sidekiq::Web.register Sidekiq::PriorityQueue::Web
|
@@ -0,0 +1,24 @@
|
|
1
|
+
|
2
|
+
<% if @total_size > @count %>
|
3
|
+
<ul class="pagination pull-right flip">
|
4
|
+
<li class="<%= 'disabled' if @current_page == 1 %>">
|
5
|
+
<a href="<%= url %>?page=1">«</a>
|
6
|
+
</li>
|
7
|
+
<% if @current_page > 1 %>
|
8
|
+
<li>
|
9
|
+
<a href="<%= url %>?<%= qparams(page: @current_page - 1) %>"><%= @current_page - 1 %></a>
|
10
|
+
</li>
|
11
|
+
<% end %>
|
12
|
+
<li class="disabled">
|
13
|
+
<a href="<%= url %>?<%= qparams(page: @current_page) %>"><%= @current_page %></a>
|
14
|
+
</li>
|
15
|
+
<% if @total_size > @current_page * @count %>
|
16
|
+
<li>
|
17
|
+
<a href="<%= url %>?<%= qparams(page: @current_page + 1) %>"><%= @current_page + 1 %></a>
|
18
|
+
</li>
|
19
|
+
<% end %>
|
20
|
+
<li class="<%= 'disabled' if @total_size <= @current_page * @count %>">
|
21
|
+
<a href="<%= url %>?<%= qparams(page: (@total_size.to_f / @count).ceil) %>">»</a>
|
22
|
+
</li>
|
23
|
+
</ul>
|
24
|
+
<% end %>
|
@@ -0,0 +1,74 @@
|
|
1
|
+
<header class="row">
|
2
|
+
<div class="col-sm-5">
|
3
|
+
<h3>
|
4
|
+
<%= t('CurrentMessagesInQueue', :queue => h(@name)) %>
|
5
|
+
|
6
|
+
<span class="badge badge-secondary"><%= number_with_delimiter(@total_size) %></span>
|
7
|
+
</h3>
|
8
|
+
</div>
|
9
|
+
<div class="col-sm-4 pull-right flip">
|
10
|
+
<%= erb :_paging, locals: { url: "#{root_path}priority_queues/#{CGI.escape(@name)}" } %>
|
11
|
+
</div>
|
12
|
+
</header>
|
13
|
+
<div class="row">
|
14
|
+
<div class="col-sm-12">
|
15
|
+
<h3>Biggest subqueues</h3>
|
16
|
+
|
17
|
+
<summary>
|
18
|
+
<details>
|
19
|
+
<div class="table_container">
|
20
|
+
<table class="table table-hover table-bordered table-striped">
|
21
|
+
<thead>
|
22
|
+
<th>Subqueue name</th>
|
23
|
+
<th>Subqueue size</th>
|
24
|
+
</thead>
|
25
|
+
<% @subqueue_counts.each do |subqueue_count| %>
|
26
|
+
<tr>
|
27
|
+
<td><%= subqueue_count.name %></td>
|
28
|
+
<td><%= subqueue_count.size %></td>
|
29
|
+
</tr>
|
30
|
+
<% end %>
|
31
|
+
</table>
|
32
|
+
</div>
|
33
|
+
</details>
|
34
|
+
</summary>
|
35
|
+
|
36
|
+
</div>
|
37
|
+
</div>
|
38
|
+
<hr />
|
39
|
+
<div class="table_container">
|
40
|
+
<table class="queue table table-hover table-bordered table-striped">
|
41
|
+
<thead>
|
42
|
+
<th><%= t('Job') %></th>
|
43
|
+
<th><%= t('Arguments') %></th>
|
44
|
+
<th><%= t('Priority') %></th>
|
45
|
+
<th><%= t('Subqueue') %></th>
|
46
|
+
<th></th>
|
47
|
+
</thead>
|
48
|
+
<% @messages.each_with_index do |msg, index| %>
|
49
|
+
<tr>
|
50
|
+
<td><%= h(msg.display_class) %></td>
|
51
|
+
<td>
|
52
|
+
<% a = msg.display_args %>
|
53
|
+
<% if a.inspect.size > 100 %>
|
54
|
+
<span class="worker_<%= index %>"><%= h(a.inspect[0..100]) + "... " %></span>
|
55
|
+
<button data-toggle="collapse" data-target=".worker_<%= index %>" class="btn btn-default btn-xs"><%= t('ShowAll') %></button>
|
56
|
+
<div class="toggle worker_<%= index %>"><%= display_args(a) %></div>
|
57
|
+
<% else %>
|
58
|
+
<%= display_args(msg.display_args) %>
|
59
|
+
<% end %>
|
60
|
+
</td>
|
61
|
+
<td><%= msg.priority %></td>
|
62
|
+
<td><%= msg.subqueue %></td>
|
63
|
+
<td>
|
64
|
+
<form action="<%= root_path %>priority_queues/<%= CGI.escape(@name) %>/delete" method="post">
|
65
|
+
<%= csrf_tag %>
|
66
|
+
<input name="key_val" value="<%= h msg.value %>" type="hidden" />
|
67
|
+
<input class="btn btn-danger btn-xs" type="submit" name="delete" value="<%= t('Delete') %>" data-confirm="<%= t('AreYouSure') %>" />
|
68
|
+
</form>
|
69
|
+
</td>
|
70
|
+
</tr>
|
71
|
+
<% end %>
|
72
|
+
</table>
|
73
|
+
</div>
|
74
|
+
<%= erb :_paging, locals: { url: "#{root_path}priority_queues/#{@name}" } %>
|
@@ -0,0 +1,27 @@
|
|
1
|
+
<h3><%= t('Queues') %></h3>
|
2
|
+
|
3
|
+
<div class="table_container">
|
4
|
+
<table class="queues table table-hover table-bordered table-striped table-white">
|
5
|
+
<thead>
|
6
|
+
<th><%= t('Queue') %></th>
|
7
|
+
<th><%= t('Size') %></th>
|
8
|
+
<th><%= t('Latency') %></th>
|
9
|
+
<th><%= t('Actions') %></th>
|
10
|
+
</thead>
|
11
|
+
<% @queues.each do |queue| %>
|
12
|
+
<tr>
|
13
|
+
<td>
|
14
|
+
<a href="<%= root_path %>priority_queues/<%= CGI.escape(queue.name) %>"><%= h queue.name %></a>
|
15
|
+
</td>
|
16
|
+
<td><%= queue.size %> </td>
|
17
|
+
<td><%# number_with_delimiter(queue.latency.round(2)) %> </td>
|
18
|
+
<td class="delete-confirm">
|
19
|
+
<form action="<%=root_path %>priority_queues/<%= CGI.escape(queue.name) %>" method="post">
|
20
|
+
<%= csrf_tag %>
|
21
|
+
<input class="btn btn-danger btn-xs" type="submit" name="delete" value="<%= t('Delete') %>" data-confirm="<%= t('AreYouSureDeleteQueue', :queue => h(queue.name)) %>" />
|
22
|
+
</form>
|
23
|
+
</td>
|
24
|
+
</tr>
|
25
|
+
<% end %>
|
26
|
+
</table>
|
27
|
+
</div>
|
@@ -0,0 +1,16 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = 'sidekiq-priority_queue'
|
3
|
+
s.version = '1.0.2'
|
4
|
+
s.date = '2018-07-31'
|
5
|
+
s.summary = "Priority Queuing for Sidekiq"
|
6
|
+
s.description = "An extension for Sidekiq allowing jobs in a single queue to be executed by a priority score rather than FIFO"
|
7
|
+
s.authors = ["Jacob Matthews", "Petr Kopac"]
|
8
|
+
s.email = 'petr@chartmogul.com'
|
9
|
+
s.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features|pkg)/}) }
|
10
|
+
s.homepage = 'https://github.com/chartmogul/sidekiq-priority_queue'
|
11
|
+
s.license = 'MIT'
|
12
|
+
s.required_ruby_version = '>= 2.5.0'
|
13
|
+
|
14
|
+
s.add_dependency 'sidekiq', '>= 6'
|
15
|
+
s.add_development_dependency 'minitest', '~> 5.10', '>= 5.10.1'
|
16
|
+
end
|
metadata
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: sidekiq-priority_queue
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jacob Matthews
|
8
|
+
- Petr Kopac
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2018-07-31 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: sidekiq
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - ">="
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: '6'
|
21
|
+
type: :runtime
|
22
|
+
prerelease: false
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - ">="
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: '6'
|
28
|
+
- !ruby/object:Gem::Dependency
|
29
|
+
name: minitest
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - "~>"
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: '5.10'
|
35
|
+
- - ">="
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: 5.10.1
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
requirements:
|
42
|
+
- - "~>"
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
version: '5.10'
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 5.10.1
|
48
|
+
description: An extension for Sidekiq allowing jobs in a single queue to be executed
|
49
|
+
by a priority score rather than FIFO
|
50
|
+
email: petr@chartmogul.com
|
51
|
+
executables: []
|
52
|
+
extensions: []
|
53
|
+
extra_rdoc_files: []
|
54
|
+
files:
|
55
|
+
- ".github/workflows/tests.yml"
|
56
|
+
- ".gitignore"
|
57
|
+
- Gemfile
|
58
|
+
- Gemfile.lock
|
59
|
+
- LICENSE
|
60
|
+
- README.md
|
61
|
+
- Rakefile
|
62
|
+
- bin/sidekiqload
|
63
|
+
- docker-compose.yml
|
64
|
+
- lib/sidekiq/priority_queue.rb
|
65
|
+
- lib/sidekiq/priority_queue/api.rb
|
66
|
+
- lib/sidekiq/priority_queue/client.rb
|
67
|
+
- lib/sidekiq/priority_queue/combined_fetch.rb
|
68
|
+
- lib/sidekiq/priority_queue/fetch.rb
|
69
|
+
- lib/sidekiq/priority_queue/reliable_fetch.rb
|
70
|
+
- lib/sidekiq/priority_queue/scripts.rb
|
71
|
+
- lib/sidekiq/priority_queue/testing.rb
|
72
|
+
- lib/sidekiq/priority_queue/web.rb
|
73
|
+
- lib/sidekiq/priority_queue/web/views/_paging.erb
|
74
|
+
- lib/sidekiq/priority_queue/web/views/priority_queue.erb
|
75
|
+
- lib/sidekiq/priority_queue/web/views/priority_queues.erb
|
76
|
+
- sidekiq-priority_queue.gemspec
|
77
|
+
homepage: https://github.com/chartmogul/sidekiq-priority_queue
|
78
|
+
licenses:
|
79
|
+
- MIT
|
80
|
+
metadata: {}
|
81
|
+
post_install_message:
|
82
|
+
rdoc_options: []
|
83
|
+
require_paths:
|
84
|
+
- lib
|
85
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 2.5.0
|
90
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
91
|
+
requirements:
|
92
|
+
- - ">="
|
93
|
+
- !ruby/object:Gem::Version
|
94
|
+
version: '0'
|
95
|
+
requirements: []
|
96
|
+
rubygems_version: 3.1.4
|
97
|
+
signing_key:
|
98
|
+
specification_version: 4
|
99
|
+
summary: Priority Queuing for Sidekiq
|
100
|
+
test_files: []
|