sidekiq-routing 0.1.0 → 0.1.2
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 +4 -4
- data/README.md +43 -39
- data/lib/sidekiq/routing/queue_composition.rb +105 -0
- data/lib/sidekiq/routing/store.rb +26 -2
- data/lib/sidekiq/routing/version.rb +1 -1
- data/lib/sidekiq/routing.rb +21 -2
- data/lib/sidekiq-routing.rb +1 -0
- metadata +5 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4026ae7773cd59cf7d6f4c47f965fbd6d4f27c2f337d1043a67c6c6ef9a4982c
|
|
4
|
+
data.tar.gz: 57da9b38e4ae73d1ce7664bf54c851941527641cb8d596fa312283172f297983
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fa372018993a3b5ee70f4b19b4ddb26a344a1f1b244063a51ab122632b4808721d3085ebf7de584ec998bb475b32461758172549d92d49c66104939164e941b9
|
|
7
|
+
data.tar.gz: 9a7864642c6695d4313dc69170cc09925e088ee9e72d13239c163010b4c066fb16822bdf5e07ab01a8be8341ffdfd84dce905f2ef714773f586938a280fbd02b
|
data/README.md
CHANGED
|
@@ -4,11 +4,11 @@ Runtime, per-job-class **queue routing for Sidekiq** — park, blackhole, or
|
|
|
4
4
|
auto-reroute a job class **without a deploy**.
|
|
5
5
|
|
|
6
6
|
Background jobs often share a handful of latency-tiered queues
|
|
7
|
-
(`within_5_seconds`, `within_1_minute`, …). The queue name is a contract:
|
|
8
|
-
job on a tier should start within that window. That sharing is efficient
|
|
9
|
-
one class misbehaves — a flood, a runaway argument, a broken downstream
|
|
10
|
-
then the only built-in lever, pausing the whole queue, punishes every
|
|
11
|
-
class on the tier. `sidekiq-routing` gives you a finer lever, applied at
|
|
7
|
+
(`within_5_seconds`, `within_1_minute`, …). The queue name is a contract:
|
|
8
|
+
every job on a tier should start within that window. That sharing is efficient
|
|
9
|
+
until one class misbehaves — a flood, a runaway argument, a broken downstream
|
|
10
|
+
— and then the only built-in lever, pausing the whole queue, punishes every
|
|
11
|
+
other class on the tier. `sidekiq-routing` gives you a finer lever, applied at
|
|
12
12
|
runtime to a single job class:
|
|
13
13
|
|
|
14
14
|
| Kind | Purpose | Driver | Target |
|
|
@@ -33,7 +33,7 @@ bundle install
|
|
|
33
33
|
```
|
|
34
34
|
|
|
35
35
|
Requires Ruby >= 3.1 and Sidekiq >= 7.0 (tested on Sidekiq 7.3.x and 8.x). The
|
|
36
|
-
gem depends only on `sidekiq
|
|
36
|
+
gem depends only on `sidekiq`.
|
|
37
37
|
|
|
38
38
|
## Quick start
|
|
39
39
|
|
|
@@ -52,9 +52,7 @@ Sidekiq::Routing.install! # registers the client + server middleware
|
|
|
52
52
|
```
|
|
53
53
|
|
|
54
54
|
`install!` prepends the client middleware (so enqueues are diverted) and adds
|
|
55
|
-
the server middleware (so in-flight jobs are diverted).
|
|
56
|
-
[sidekiq-unique-jobs](https://github.com/mhenrixon/sidekiq-unique-jobs) is
|
|
57
|
-
present, the routing server middleware is inserted *after* it automatically.
|
|
55
|
+
the server middleware (so in-flight jobs are diverted).
|
|
58
56
|
|
|
59
57
|
Then, from a console during an incident:
|
|
60
58
|
|
|
@@ -76,27 +74,42 @@ runtime, without pausing the rest of its queue or shipping a deploy.
|
|
|
76
74
|
added to the Dead set). Only for classes you can afford to lose.
|
|
77
75
|
|
|
78
76
|
```ruby
|
|
79
|
-
Sidekiq::Routing.park("
|
|
77
|
+
Sidekiq::Routing.park("RunawayImportJob")
|
|
80
78
|
Sidekiq::Routing.blackhole("FireAndForgetJob")
|
|
81
|
-
Sidekiq::Routing.unpark("
|
|
79
|
+
Sidekiq::Routing.unpark("RunawayImportJob")
|
|
82
80
|
|
|
83
|
-
Sidekiq::Routing.routed?("
|
|
84
|
-
Sidekiq::Routing.parked?("
|
|
85
|
-
Sidekiq::Routing.mode("
|
|
86
|
-
Sidekiq::Routing.routes # => { "
|
|
81
|
+
Sidekiq::Routing.routed?("RunawayImportJob") # => true/false
|
|
82
|
+
Sidekiq::Routing.parked?("RunawayImportJob") # => true only when in park mode
|
|
83
|
+
Sidekiq::Routing.mode("RunawayImportJob") # => "park" | "blackhole" | nil
|
|
84
|
+
Sidekiq::Routing.routes # => { "RunawayImportJob" => { "mode" => "park", ... } }
|
|
87
85
|
```
|
|
88
86
|
|
|
89
87
|
A route accepts a Class or a String. ActiveJob jobs are matched by their real
|
|
90
88
|
("wrapped") class, not the adapter's job wrapper.
|
|
91
89
|
|
|
90
|
+
### Identifying the class flooding a live queue
|
|
91
|
+
|
|
92
|
+
During a latency alert, inspect the breached queue with a capped, read-only
|
|
93
|
+
scan:
|
|
94
|
+
|
|
95
|
+
```ruby
|
|
96
|
+
report = Sidekiq::Routing.queue_composition("within_1_minute")
|
|
97
|
+
puts report
|
|
98
|
+
# RunawayImportJob count=12345 oldest=380s ago
|
|
99
|
+
# OtherJob count=12 oldest=45s ago
|
|
100
|
+
# scanned 12357 of 12357 (cap 250000)
|
|
101
|
+
|
|
102
|
+
report.offender["class"] # => "RunawayImportJob"
|
|
103
|
+
```
|
|
104
|
+
|
|
92
105
|
### Clearing an existing backlog into the parking queue
|
|
93
106
|
|
|
94
107
|
`park` only diverts jobs from the moment it's set. To move a class's jobs that
|
|
95
108
|
are *already enqueued* on a live queue into the parking queue, sweep them:
|
|
96
109
|
|
|
97
110
|
```ruby
|
|
98
|
-
Sidekiq::Routing.sweep("
|
|
99
|
-
Sidekiq::Routing.sweep("
|
|
111
|
+
Sidekiq::Routing.sweep("RunawayImportJob", queue: "within_1_minute")
|
|
112
|
+
Sidekiq::Routing.sweep("RunawayImportJob", queue: "within_1_minute", limit: 10_000)
|
|
100
113
|
```
|
|
101
114
|
|
|
102
115
|
A queue must be resolvable — pass `queue:` explicitly (the sweep deliberately
|
|
@@ -108,11 +121,11 @@ never scans every queue, which would hammer Redis during an incident).
|
|
|
108
121
|
# Move parked jobs back to their original queue (stamps them so an active
|
|
109
122
|
# route won't immediately bounce them back).
|
|
110
123
|
Sidekiq::Routing.process_parked
|
|
111
|
-
Sidekiq::Routing.process_parked(klass: "
|
|
124
|
+
Sidekiq::Routing.process_parked(klass: "RunawayImportJob", limit: 1_000)
|
|
112
125
|
|
|
113
126
|
# Introspection
|
|
114
127
|
Sidekiq::Routing.parked_size # O(1) count of the parking queue
|
|
115
|
-
Sidekiq::Routing.parked_breakdown # { "
|
|
128
|
+
Sidekiq::Routing.parked_breakdown # { "RunawayImportJob" => { "count" => 12, "by_original_queue" => {...} } }
|
|
116
129
|
```
|
|
117
130
|
|
|
118
131
|
A processed parked job has its payload rewritten to target its original queue,
|
|
@@ -130,8 +143,8 @@ export SIDEKIQ_ROUTING_AUTO_REROUTE_ENABLED=true
|
|
|
130
143
|
```
|
|
131
144
|
|
|
132
145
|
Auto rerouting only ever moves jobs to the *next* live SLA tier
|
|
133
|
-
(`within_5_seconds` → `within_1_minute` → `within_5_minutes` →
|
|
134
|
-
It never parks or blackholes.
|
|
146
|
+
(`within_5_seconds` → `within_1_minute` → `within_5_minutes` →
|
|
147
|
+
`within_1_hour`). It never parks or blackholes.
|
|
135
148
|
|
|
136
149
|
Wire it up in your initializer:
|
|
137
150
|
|
|
@@ -161,18 +174,18 @@ routing_auto_reroute:
|
|
|
161
174
|
class: "Sidekiq::Routing::Auto::RerouteJob"
|
|
162
175
|
```
|
|
163
176
|
|
|
164
|
-
`RerouteJob` checks each SLA queue's estimated workload against its capacity
|
|
165
|
-
for any tier over `capacity_threshold_percent`, moves the noisiest classes
|
|
166
|
-
the next tier. `RerouteJob` itself is excluded from rerouting by default.
|
|
177
|
+
`RerouteJob` checks each SLA queue's estimated workload against its capacity
|
|
178
|
+
and, for any tier over `capacity_threshold_percent`, moves the noisiest classes
|
|
179
|
+
to the next tier. `RerouteJob` itself is excluded from rerouting by default.
|
|
167
180
|
|
|
168
181
|
## Web tab
|
|
169
182
|
|
|
170
183
|
A read-only "Routing" tab for Sidekiq Web shows active routes and parking-queue
|
|
171
|
-
depth/breakdown. Every mutating action stays on the console API — the tab
|
|
172
|
-
exposes park/blackhole/unpark/sweep, so destructive operations stay
|
|
184
|
+
depth/breakdown. Every mutating action stays on the console API — the tab
|
|
185
|
+
never exposes park/blackhole/unpark/sweep, so destructive operations stay
|
|
186
|
+
deliberate.
|
|
173
187
|
|
|
174
|
-
Require it only where you mount Sidekiq Web
|
|
175
|
-
web framework):
|
|
188
|
+
Require it only where you mount Sidekiq Web:
|
|
176
189
|
|
|
177
190
|
```ruby
|
|
178
191
|
require "sidekiq/web"
|
|
@@ -180,6 +193,8 @@ require "sidekiq/routing/web" # registers the "Routing" tab
|
|
|
180
193
|
mount Sidekiq::Web => "/sidekiq"
|
|
181
194
|
```
|
|
182
195
|
|
|
196
|
+
<img width="2964" height="1550" alt="CleanShot 2026-06-26 at 13 59 49@2x" src="https://github.com/user-attachments/assets/8da1149d-c76d-47d6-8be6-7aef921d54ea" />
|
|
197
|
+
|
|
183
198
|
## Configuration reference
|
|
184
199
|
|
|
185
200
|
`Sidekiq::Routing.setup { |config| ... }`:
|
|
@@ -217,17 +232,6 @@ snapshot, so console changes take effect immediately for the writer and within
|
|
|
217
232
|
one TTL everywhere else. Parking rewrites the `queue` field *inside the job
|
|
218
233
|
payload*, which is what makes recovery and retry-to-original-queue correct.
|
|
219
234
|
|
|
220
|
-
## Development
|
|
221
|
-
|
|
222
|
-
```sh
|
|
223
|
-
bin/setup # or: bundle install
|
|
224
|
-
bundle exec rake test
|
|
225
|
-
```
|
|
226
|
-
|
|
227
|
-
The test suite talks to a real Redis. It uses logical DB 15 of
|
|
228
|
-
`redis://localhost:6379` by default; override with `REDIS_URL` (point it at a
|
|
229
|
-
disposable Redis). No Rails or database is involved.
|
|
230
|
-
|
|
231
235
|
## License
|
|
232
236
|
|
|
233
237
|
Released under the [MIT License](LICENSE).
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sidekiq
|
|
4
|
+
module Routing
|
|
5
|
+
# Read-only, capped scan of a live queue grouped by displayed job class.
|
|
6
|
+
# Used during incidents to answer: "which class is flooding this SLA tier?"
|
|
7
|
+
class QueueComposition
|
|
8
|
+
DEFAULT_SCAN_LIMIT = 250_000
|
|
9
|
+
|
|
10
|
+
Report = Struct.new(:queue, :rows, :scanned, :size, :scan_limit, keyword_init: true) do
|
|
11
|
+
def offender
|
|
12
|
+
rows.first
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def to_h
|
|
16
|
+
{
|
|
17
|
+
"queue" => queue,
|
|
18
|
+
"rows" => rows.map(&:dup),
|
|
19
|
+
"scanned" => scanned,
|
|
20
|
+
"size" => size,
|
|
21
|
+
"scan_limit" => scan_limit
|
|
22
|
+
}
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def to_s
|
|
26
|
+
lines = rows.map do |row|
|
|
27
|
+
age = row["oldest_age_seconds"] ? "#{row["oldest_age_seconds"].round}s ago" : "n/a"
|
|
28
|
+
format("%-55s count=%-9d oldest=%s", row["class"], row["count"], age)
|
|
29
|
+
end
|
|
30
|
+
lines << "scanned #{scanned} of #{size} (cap #{scan_limit})"
|
|
31
|
+
lines.join("\n")
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
alias inspect to_s
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def initialize(queue_name, scan_limit: DEFAULT_SCAN_LIMIT, now: Time.now)
|
|
38
|
+
@queue_name = normalize_queue_name(queue_name)
|
|
39
|
+
@scan_limit = normalize_scan_limit(scan_limit)
|
|
40
|
+
@now = now
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def call
|
|
44
|
+
by_class = Hash.new { |hash, key| hash[key] = { count: 0, oldest_at: nil } }
|
|
45
|
+
queue = Sidekiq::Queue.new(@queue_name)
|
|
46
|
+
scanned = 0
|
|
47
|
+
|
|
48
|
+
queue.each do |job|
|
|
49
|
+
break if scanned >= @scan_limit
|
|
50
|
+
|
|
51
|
+
scanned += 1
|
|
52
|
+
row = by_class[job.display_class]
|
|
53
|
+
row[:count] += 1
|
|
54
|
+
|
|
55
|
+
enqueued_at = job.enqueued_at
|
|
56
|
+
row[:oldest_at] = enqueued_at if older?(enqueued_at, row[:oldest_at])
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
Report.new(
|
|
60
|
+
queue: @queue_name,
|
|
61
|
+
rows: build_rows(by_class),
|
|
62
|
+
scanned: scanned,
|
|
63
|
+
size: queue.size,
|
|
64
|
+
scan_limit: @scan_limit
|
|
65
|
+
)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
def build_rows(by_class)
|
|
71
|
+
by_class.sort_by { |klass, stats| [-stats[:count], klass] }.map do |klass, stats|
|
|
72
|
+
oldest_at = stats[:oldest_at]
|
|
73
|
+
{
|
|
74
|
+
"class" => klass,
|
|
75
|
+
"count" => stats[:count],
|
|
76
|
+
"oldest_at" => oldest_at,
|
|
77
|
+
"oldest_age_seconds" => oldest_at && (@now - oldest_at)
|
|
78
|
+
}
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def normalize_queue_name(queue_name)
|
|
83
|
+
name = queue_name.to_s
|
|
84
|
+
raise ArgumentError, "queue_name must be present" if name.empty?
|
|
85
|
+
|
|
86
|
+
name
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def normalize_scan_limit(scan_limit)
|
|
90
|
+
limit = begin
|
|
91
|
+
Integer(scan_limit)
|
|
92
|
+
rescue ArgumentError, TypeError
|
|
93
|
+
raise ArgumentError, "scan_limit must be a non-negative integer"
|
|
94
|
+
end
|
|
95
|
+
raise ArgumentError, "scan_limit must be non-negative" if limit.negative?
|
|
96
|
+
|
|
97
|
+
limit
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def older?(candidate, current)
|
|
101
|
+
candidate && (current.nil? || candidate < current)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -27,14 +27,38 @@ module Sidekiq
|
|
|
27
27
|
# -> {"mode"=>...} or nil
|
|
28
28
|
def fetch(name)
|
|
29
29
|
raw = Sidekiq.redis { |conn| conn.hget(HASH_KEY, name) }
|
|
30
|
-
raw &&
|
|
30
|
+
raw && parse_entry(name, raw)
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
# -> { "ClassName" => {"mode"=>...}, ... }
|
|
34
34
|
def all
|
|
35
35
|
Sidekiq.redis { |conn| conn.hgetall(HASH_KEY) }
|
|
36
|
-
.
|
|
36
|
+
.each_with_object({}) do |(name, raw), routes|
|
|
37
|
+
entry = parse_entry(name, raw)
|
|
38
|
+
routes[name] = entry if entry
|
|
39
|
+
end
|
|
37
40
|
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
# A route entry must be a JSON object; anything else is skipped with a
|
|
45
|
+
# warning, never raised on. The reads feed the client-middleware hot
|
|
46
|
+
# path inside every perform_async, and the raw value can be foreign
|
|
47
|
+
# data — e.g. a desynced pooled connection handing HGETALL the reply
|
|
48
|
+
# of a RESP HELLO handshake ({"server"=>"redis", "proto"=>"3", ...}).
|
|
49
|
+
def parse_entry(name, raw)
|
|
50
|
+
entry = JSON.parse(raw)
|
|
51
|
+
entry.is_a?(Hash) ? entry : warn_invalid(name, raw)
|
|
52
|
+
rescue JSON::ParserError
|
|
53
|
+
warn_invalid(name, raw)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# -> nil
|
|
57
|
+
def warn_invalid(name, raw)
|
|
58
|
+
Routing.logger.warn(
|
|
59
|
+
"[Routing] ignoring invalid route entry #{name.inspect} => #{raw.inspect.byteslice(0, 200)}")
|
|
60
|
+
nil
|
|
61
|
+
end
|
|
38
62
|
end
|
|
39
63
|
end
|
|
40
64
|
end
|
data/lib/sidekiq/routing.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "sidekiq"
|
|
4
|
+
require "sidekiq/api"
|
|
4
5
|
require "json"
|
|
5
6
|
|
|
6
7
|
# Runtime, per-job-class parking/blackhole mechanism for Sidekiq incident response.
|
|
@@ -25,6 +26,8 @@ module Sidekiq
|
|
|
25
26
|
MODE_BLACKHOLE = "blackhole"
|
|
26
27
|
MODES = [MODE_PARK, MODE_BLACKHOLE].freeze
|
|
27
28
|
|
|
29
|
+
EMPTY_ROUTES = {}.freeze
|
|
30
|
+
|
|
28
31
|
@snapshot = nil
|
|
29
32
|
@snapshot_at = nil
|
|
30
33
|
@snapshot_mutex = Mutex.new
|
|
@@ -121,7 +124,11 @@ module Sidekiq
|
|
|
121
124
|
end
|
|
122
125
|
end
|
|
123
126
|
|
|
124
|
-
# ----
|
|
127
|
+
# ---- queue introspection ----
|
|
128
|
+
|
|
129
|
+
def queue_composition(queue_name, scan_limit: QueueComposition::DEFAULT_SCAN_LIMIT)
|
|
130
|
+
QueueComposition.new(queue_name, scan_limit:).call
|
|
131
|
+
end
|
|
125
132
|
|
|
126
133
|
def parked_size
|
|
127
134
|
Sidekiq::Queue.new(parked_queue).size
|
|
@@ -196,13 +203,25 @@ module Sidekiq
|
|
|
196
203
|
current = @snapshot
|
|
197
204
|
fresh = current && ttl.positive? && (now - @snapshot_at) < ttl
|
|
198
205
|
unless fresh
|
|
199
|
-
@snapshot =
|
|
206
|
+
@snapshot = refresh_snapshot(current)
|
|
200
207
|
@snapshot_at = now
|
|
201
208
|
current = @snapshot
|
|
202
209
|
end
|
|
203
210
|
end
|
|
204
211
|
current
|
|
205
212
|
end
|
|
213
|
+
|
|
214
|
+
# Fail open: the refresh runs inside every perform_async via the client
|
|
215
|
+
# middleware, so a Redis hiccup here must never fail the push. Keep
|
|
216
|
+
# serving the previous snapshot (or route nothing) until the next TTL
|
|
217
|
+
# window instead of raising.
|
|
218
|
+
def refresh_snapshot(current)
|
|
219
|
+
Store.all.freeze
|
|
220
|
+
rescue StandardError => error
|
|
221
|
+
fallback = current ? "keeping stale snapshot" : "routing nothing this TTL window"
|
|
222
|
+
logger.warn("[Routing] snapshot refresh failed, #{fallback} (#{error.class}: #{error.message})")
|
|
223
|
+
current || EMPTY_ROUTES
|
|
224
|
+
end
|
|
206
225
|
end
|
|
207
226
|
end
|
|
208
227
|
end
|
data/lib/sidekiq-routing.rb
CHANGED
|
@@ -13,6 +13,7 @@ require "sidekiq/routing"
|
|
|
13
13
|
require "sidekiq/routing/configuration"
|
|
14
14
|
require "sidekiq/routing/store"
|
|
15
15
|
require "sidekiq/routing/mover"
|
|
16
|
+
require "sidekiq/routing/queue_composition"
|
|
16
17
|
require "sidekiq/routing/sweeper"
|
|
17
18
|
require "sidekiq/routing/parked_processor"
|
|
18
19
|
require "sidekiq/routing/middleware/client"
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: sidekiq-routing
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
|
-
-
|
|
7
|
+
- Neeraj Singh
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-07-17 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: sidekiq
|
|
@@ -33,7 +33,7 @@ description: |
|
|
|
33
33
|
from a process-local snapshot, so the per-job hot path stays an in-memory
|
|
34
34
|
lookup rather than a Redis round-trip.
|
|
35
35
|
email:
|
|
36
|
-
-
|
|
36
|
+
- neeraj@neeto.com
|
|
37
37
|
executables: []
|
|
38
38
|
extensions: []
|
|
39
39
|
extra_rdoc_files: []
|
|
@@ -53,6 +53,7 @@ files:
|
|
|
53
53
|
- lib/sidekiq/routing/middleware/server.rb
|
|
54
54
|
- lib/sidekiq/routing/mover.rb
|
|
55
55
|
- lib/sidekiq/routing/parked_processor.rb
|
|
56
|
+
- lib/sidekiq/routing/queue_composition.rb
|
|
56
57
|
- lib/sidekiq/routing/store.rb
|
|
57
58
|
- lib/sidekiq/routing/sweeper.rb
|
|
58
59
|
- lib/sidekiq/routing/version.rb
|