workhorse 1.5.2 → 2.0.0.rc0
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/CHANGELOG.md +102 -0
- data/README.md +303 -74
- data/Rakefile +1 -0
- data/VERSION +1 -1
- data/bin/rubocop +5 -1
- data/lib/generators/workhorse/install_generator.rb +10 -1
- data/lib/generators/workhorse/templates/config/initializers/workhorse.rb +55 -0
- data/lib/generators/workhorse/templates/create_table_jobs.rb +11 -13
- data/lib/generators/workhorse/templates/create_table_workhorse_schedules.rb +29 -0
- data/lib/workhorse/daemon.rb +52 -6
- data/lib/workhorse/db_job.rb +58 -7
- data/lib/workhorse/enqueuer.rb +51 -8
- data/lib/workhorse/jobs/cleanup_succeeded_jobs.rb +26 -8
- data/lib/workhorse/jobs/detect_late_schedules_job.rb +59 -0
- data/lib/workhorse/notifiers/base.rb +55 -0
- data/lib/workhorse/notifiers/file_system.rb +66 -0
- data/lib/workhorse/notifiers/none.rb +8 -0
- data/lib/workhorse/notifiers/redis.rb +227 -0
- data/lib/workhorse/performer.rb +28 -0
- data/lib/workhorse/poller.rb +289 -47
- data/lib/workhorse/pool.rb +12 -6
- data/lib/workhorse/schedule.rb +288 -0
- data/lib/workhorse/schedules.rb +197 -0
- data/lib/workhorse/worker.rb +77 -27
- data/lib/workhorse.rb +136 -0
- data/test/lib/db_schema.rb +21 -1
- data/test/lib/jobs.rb +29 -0
- data/test/lib/test_helper.rb +9 -14
- data/test/workhorse/daemon_test.rb +33 -0
- data/test/workhorse/db_job_test.rb +1 -1
- data/test/workhorse/notifier_test.rb +500 -0
- data/test/workhorse/poller_test.rb +8 -4
- data/test/workhorse/schedule_test.rb +967 -0
- data/test/workhorse/worker_test.rb +92 -0
- data/workhorse.gemspec +6 -5
- metadata +29 -3
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
require 'test_helper'
|
|
2
|
+
require 'tempfile'
|
|
3
|
+
|
|
4
|
+
class Workhorse::NotifierTest < WorkhorseTest
|
|
5
|
+
def setup
|
|
6
|
+
super
|
|
7
|
+
@notifier = Workhorse.notifier
|
|
8
|
+
FileUtils.rm_f(wake_path)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def teardown
|
|
12
|
+
Workhorse.notifier = @notifier
|
|
13
|
+
FileUtils.rm_f(wake_path)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def test_default_notifier_is_none
|
|
17
|
+
assert_instance_of Workhorse::Notifiers::None, Workhorse.notifier
|
|
18
|
+
assert_nil Workhorse.notifier.token
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def test_notifier_can_be_selected_by_symbol
|
|
22
|
+
Workhorse.notifier = :file
|
|
23
|
+
assert_instance_of Workhorse::Notifiers::FileSystem, Workhorse.notifier
|
|
24
|
+
|
|
25
|
+
Workhorse.notifier = :redis
|
|
26
|
+
assert_instance_of Workhorse::Notifiers::Redis, Workhorse.notifier
|
|
27
|
+
|
|
28
|
+
Workhorse.notifier = :none
|
|
29
|
+
assert_instance_of Workhorse::Notifiers::None, Workhorse.notifier
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def test_notifier_can_be_set_to_nil
|
|
33
|
+
Workhorse.notifier = :file
|
|
34
|
+
Workhorse.notifier = nil
|
|
35
|
+
|
|
36
|
+
# Asserted on the stored value: the reader falls back to a None of its
|
|
37
|
+
# own, so it cannot tell a cleared notifier from an unset one.
|
|
38
|
+
assert_instance_of Workhorse::Notifiers::None, Workhorse.instance_variable_get(:@notifier)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def test_the_file_notifier_defaults_below_the_rails_root
|
|
42
|
+
assert_equal Rails.root.join('tmp', 'pids', 'workhorse.wake').to_s,
|
|
43
|
+
Workhorse::Notifiers::FileSystem.new.path
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def test_the_redis_notifier_takes_the_configured_client
|
|
47
|
+
redis = FakeRedis.new
|
|
48
|
+
Workhorse.notification_redis = redis
|
|
49
|
+
notifier = Workhorse::Notifiers::Redis.new
|
|
50
|
+
|
|
51
|
+
notifier.notify(queue: :mailer)
|
|
52
|
+
|
|
53
|
+
assert_equal [[Workhorse::Notifiers::Redis::DEFAULT_CHANNEL, 'mailer']], redis.published
|
|
54
|
+
ensure
|
|
55
|
+
Workhorse.notification_redis = nil
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def test_unknown_notifier_is_rejected
|
|
59
|
+
assert_raises ArgumentError do
|
|
60
|
+
Workhorse.notifier = :carrier_pigeon
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def test_file_notifier_token_changes_on_notify
|
|
65
|
+
notifier = file_notifier
|
|
66
|
+
|
|
67
|
+
assert_nil notifier.token
|
|
68
|
+
|
|
69
|
+
notifier.notify
|
|
70
|
+
first = notifier.token
|
|
71
|
+
|
|
72
|
+
refute_nil first
|
|
73
|
+
assert_equal first, notifier.token
|
|
74
|
+
|
|
75
|
+
sleep 0.01
|
|
76
|
+
notifier.notify
|
|
77
|
+
|
|
78
|
+
refute_equal first, notifier.token
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def test_file_notifier_creates_its_directory
|
|
82
|
+
path = File.join(Dir.mktmpdir, 'deeply', 'nested', 'workhorse.wake')
|
|
83
|
+
notifier = Workhorse::Notifiers::FileSystem.new(path: path)
|
|
84
|
+
|
|
85
|
+
notifier.notify
|
|
86
|
+
|
|
87
|
+
assert File.exist?(path)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def test_file_notifier_swallows_errors
|
|
91
|
+
Tempfile.create('workhorse') do |file|
|
|
92
|
+
# A regular file cannot be a directory, on any platform.
|
|
93
|
+
notifier = Workhorse::Notifiers::FileSystem.new(path: File.join(file.path, 'workhorse.wake'))
|
|
94
|
+
|
|
95
|
+
assert_nothing_raised { notifier.notify }
|
|
96
|
+
assert_nil notifier.token
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def test_enqueueing_notifies
|
|
101
|
+
Workhorse.notifier = file_notifier
|
|
102
|
+
|
|
103
|
+
assert_nil Workhorse.notifier.token
|
|
104
|
+
|
|
105
|
+
Workhorse.enqueue BasicJob.new(sleep_time: 0)
|
|
106
|
+
|
|
107
|
+
refute_nil Workhorse.notifier.token
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def test_enqueueing_does_not_notify_for_jobs_that_are_not_due
|
|
111
|
+
Workhorse.notifier = file_notifier
|
|
112
|
+
|
|
113
|
+
Workhorse.enqueue BasicJob.new(sleep_time: 0), perform_at: Time.now + 60
|
|
114
|
+
|
|
115
|
+
assert_nil Workhorse.notifier.token
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def test_notification_happens_after_commit
|
|
119
|
+
Workhorse.notifier = file_notifier
|
|
120
|
+
|
|
121
|
+
ActiveRecord::Base.transaction do
|
|
122
|
+
Workhorse.enqueue BasicJob.new(sleep_time: 0)
|
|
123
|
+
|
|
124
|
+
assert_nil Workhorse.notifier.token, 'must not notify before the commit'
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
refute_nil Workhorse.notifier.token, 'must notify after the commit'
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def test_worker_starts_an_announced_job_ahead_of_its_polling_interval
|
|
131
|
+
Workhorse.notifier = file_notifier
|
|
132
|
+
|
|
133
|
+
log = capture_log do |logger|
|
|
134
|
+
with_worker(polling_interval: 60, pool_size: 1, auto_terminate: false, logger: logger) do
|
|
135
|
+
wait_for_first_poll(logger)
|
|
136
|
+
|
|
137
|
+
Workhorse.enqueue BasicJob.new(sleep_time: 0)
|
|
138
|
+
|
|
139
|
+
with_retries(30) do
|
|
140
|
+
# Re-announced on every attempt: a single announcement whose poll
|
|
141
|
+
# loses the global lock race would otherwise wait out the interval.
|
|
142
|
+
Workhorse.notifier.notify
|
|
143
|
+
assert_equal 1, Workhorse::DbJob.succeeded.count
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
assert_match(/Job was announced/, log)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def test_worker_without_a_notifier_waits_out_its_polling_interval
|
|
152
|
+
capture_log do |logger|
|
|
153
|
+
with_worker(polling_interval: 60, pool_size: 1, auto_terminate: false, logger: logger) do
|
|
154
|
+
wait_for_first_poll(logger)
|
|
155
|
+
|
|
156
|
+
Workhorse.enqueue BasicJob.new(sleep_time: 0)
|
|
157
|
+
sleep 1
|
|
158
|
+
|
|
159
|
+
assert_equal 1, Workhorse::DbJob.waiting.count
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Without this, marking every poll as brought forward - which would
|
|
165
|
+
# silently disable the max_global_lock_fails alarm - goes undetected.
|
|
166
|
+
def test_a_poll_that_waited_out_its_interval_is_not_marked_as_brought_forward
|
|
167
|
+
poller = Workhorse::Worker.new(polling_interval: 0.2, pool_size: 1).poller
|
|
168
|
+
poller.instance_variable_set(:@running, true)
|
|
169
|
+
|
|
170
|
+
poller.send(:sleep)
|
|
171
|
+
|
|
172
|
+
refute poller.instance_variable_get(:@poll_brought_forward),
|
|
173
|
+
'a poll that waited out its interval is the scheduled one'
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def test_redis_notifier_publishes_and_counts
|
|
177
|
+
redis = FakeRedis.new
|
|
178
|
+
notifier = Workhorse::Notifiers::Redis.new(client: redis, channel: 'test:jobs')
|
|
179
|
+
|
|
180
|
+
notifier.notify(queue: :mailer)
|
|
181
|
+
|
|
182
|
+
assert_equal [['test:jobs', 'mailer']], redis.published
|
|
183
|
+
|
|
184
|
+
notifier.start
|
|
185
|
+
begin
|
|
186
|
+
redis.deliver('test:jobs', 'mailer')
|
|
187
|
+
|
|
188
|
+
with_retries(50, interval: 0.02) do
|
|
189
|
+
assert_equal 1, notifier.token
|
|
190
|
+
end
|
|
191
|
+
ensure
|
|
192
|
+
notifier.stop
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# A reporter that is itself unreachable must not leave the process without
|
|
197
|
+
# a subscriber for the rest of its life.
|
|
198
|
+
def test_a_raising_exception_handler_does_not_kill_the_subscriber
|
|
199
|
+
redis = FakeRedis.new(fail_subscribe: 1)
|
|
200
|
+
notifier = Workhorse::Notifiers::Redis.new(client: redis, channel: 'test:jobs')
|
|
201
|
+
|
|
202
|
+
with_exception_handler ->(_e) { fail 'reporter is down' } do
|
|
203
|
+
notifier.start
|
|
204
|
+
|
|
205
|
+
begin
|
|
206
|
+
redis.deliver('test:jobs', 'mailer')
|
|
207
|
+
|
|
208
|
+
with_retries(100, interval: 0.05) { assert_equal 1, notifier.token }
|
|
209
|
+
ensure
|
|
210
|
+
notifier.stop
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# The once-only guard has to re-arm, or only the first outage in a
|
|
216
|
+
# process's whole life is ever reported.
|
|
217
|
+
def test_a_second_outage_is_reported_again
|
|
218
|
+
redis = FakeRedis.new
|
|
219
|
+
notifier = Workhorse::Notifiers::Redis.new(client: redis, channel: 'test:jobs')
|
|
220
|
+
reported = []
|
|
221
|
+
|
|
222
|
+
with_exception_handler ->(e) { reported << e.message } do
|
|
223
|
+
notifier.start
|
|
224
|
+
|
|
225
|
+
begin
|
|
226
|
+
redis.drop!
|
|
227
|
+
with_retries(100, interval: 0.05) { assert_equal 1, reported.size }
|
|
228
|
+
|
|
229
|
+
redis.deliver('test:jobs', 'mailer')
|
|
230
|
+
with_retries(100, interval: 0.05) { assert_equal 1, notifier.token }
|
|
231
|
+
|
|
232
|
+
redis.drop!
|
|
233
|
+
with_retries(100, interval: 0.05) { assert_equal 2, reported.size }
|
|
234
|
+
ensure
|
|
235
|
+
notifier.stop
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def test_redis_notifier_swallows_publish_errors
|
|
241
|
+
notifier = Workhorse::Notifiers::Redis.new(client: FakeRedis.new(fail_publish: true), channel: 'test:jobs')
|
|
242
|
+
|
|
243
|
+
assert_nothing_raised { notifier.notify(queue: :mailer) }
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# A broken notifier must cost latency, not the worker.
|
|
247
|
+
def test_a_raising_notifier_does_not_take_the_worker_down
|
|
248
|
+
Workhorse.notifier = RaisingNotifier.new
|
|
249
|
+
|
|
250
|
+
with_worker(polling_interval: 0.2, pool_size: 1, auto_terminate: false) do |w|
|
|
251
|
+
Workhorse.enqueue BasicJob.new(sleep_time: 0)
|
|
252
|
+
|
|
253
|
+
with_retries(30) do
|
|
254
|
+
assert_equal 1, Workhorse::DbJob.succeeded.count
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
assert_equal :running, w.state
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# Losing that race is expected when several workers are woken at once.
|
|
262
|
+
def test_lock_failures_of_brought_forward_polls_are_not_counted
|
|
263
|
+
Workhorse.notifier = file_notifier
|
|
264
|
+
w = Workhorse::Worker.new(polling_interval: 60, pool_size: 1)
|
|
265
|
+
poller = w.poller
|
|
266
|
+
|
|
267
|
+
poller.instance_variable_set(:@running, true)
|
|
268
|
+
poller.instance_variable_set(:@last_notification, Workhorse.notifier.token)
|
|
269
|
+
|
|
270
|
+
Workhorse.notifier.notify
|
|
271
|
+
poller.send(:sleep)
|
|
272
|
+
|
|
273
|
+
assert poller.instance_variable_get(:@poll_brought_forward),
|
|
274
|
+
'a poll following an announcement must be marked as brought forward'
|
|
275
|
+
|
|
276
|
+
with_global_lock_held do
|
|
277
|
+
poller.send(:poll)
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
assert_equal 0, poller.instance_variable_get(:@global_lock_fails)
|
|
281
|
+
|
|
282
|
+
poller.instance_variable_set(:@poll_brought_forward, false)
|
|
283
|
+
|
|
284
|
+
with_global_lock_held do
|
|
285
|
+
poller.send(:poll)
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
assert_equal 1, poller.instance_variable_get(:@global_lock_fails)
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def test_redis_notifier_requires_a_client
|
|
292
|
+
Workhorse.notification_redis = nil
|
|
293
|
+
notifier = Workhorse::Notifiers::Redis.new
|
|
294
|
+
|
|
295
|
+
assert_raises RuntimeError do
|
|
296
|
+
notifier.client
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
# A worker with no free thread must leave the announcement pending, or the
|
|
301
|
+
# job waits out the whole polling interval once capacity frees up.
|
|
302
|
+
def test_an_announcement_arriving_while_busy_is_not_lost
|
|
303
|
+
Workhorse.notifier = file_notifier
|
|
304
|
+
|
|
305
|
+
with_worker(polling_interval: 60, pool_size: 1, auto_terminate: false) do
|
|
306
|
+
blocker = Workhorse.enqueue BasicJob.new(sleep_time: 1)
|
|
307
|
+
with_retries(60, interval: 0.05) { assert_equal 'started', blocker.reload.state }
|
|
308
|
+
|
|
309
|
+
job = Workhorse.enqueue BasicJob.new(sleep_time: 0)
|
|
310
|
+
|
|
311
|
+
with_retries(60, interval: 0.1) { assert_equal 'succeeded', job.reload.state }
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
# Several workers in one process share one subscriber thread; the first to
|
|
316
|
+
# stop must not take it away from the others.
|
|
317
|
+
def test_the_redis_subscriber_survives_until_the_last_worker_stops
|
|
318
|
+
redis = FakeRedis.new
|
|
319
|
+
notifier = Workhorse::Notifiers::Redis.new(client: redis, channel: 'test:jobs')
|
|
320
|
+
|
|
321
|
+
notifier.start
|
|
322
|
+
notifier.start
|
|
323
|
+
notifier.stop
|
|
324
|
+
|
|
325
|
+
begin
|
|
326
|
+
redis.deliver('test:jobs', 'mailer')
|
|
327
|
+
|
|
328
|
+
with_retries(50, interval: 0.02) { assert_equal 1, notifier.token }
|
|
329
|
+
ensure
|
|
330
|
+
notifier.stop
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def test_the_file_notifier_follows_the_configured_path
|
|
335
|
+
Workhorse.notifier = :file
|
|
336
|
+
Workhorse.notification_path = wake_path
|
|
337
|
+
|
|
338
|
+
assert_equal wake_path, Workhorse.notifier.path
|
|
339
|
+
ensure
|
|
340
|
+
Workhorse.notification_path = nil
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
def test_the_redis_notifier_follows_the_configured_channel
|
|
344
|
+
Workhorse.notifier = :redis
|
|
345
|
+
Workhorse.notification_channel = 'custom:jobs'
|
|
346
|
+
|
|
347
|
+
assert_equal 'custom:jobs', Workhorse.notifier.channel
|
|
348
|
+
ensure
|
|
349
|
+
Workhorse.notification_channel = nil
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
# A subscribed connection cannot also publish, so the callable has to be
|
|
353
|
+
# asked again for the subscriber rather than the publisher being reused.
|
|
354
|
+
def test_the_redis_notifier_subscribes_on_a_client_of_its_own
|
|
355
|
+
built = []
|
|
356
|
+
notifier = Workhorse::Notifiers::Redis.new(
|
|
357
|
+
client: -> { FakeRedis.new.tap { |r| built << r } }, channel: 'test:jobs'
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
notifier.notify(queue: :mailer)
|
|
361
|
+
notifier.start
|
|
362
|
+
|
|
363
|
+
begin
|
|
364
|
+
with_retries(50, interval: 0.02) { assert_equal 2, built.size }
|
|
365
|
+
|
|
366
|
+
refute_equal built.first.object_id, built.last.object_id
|
|
367
|
+
ensure
|
|
368
|
+
notifier.stop
|
|
369
|
+
end
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
def test_the_redis_notifier_builds_a_client_from_a_callable
|
|
373
|
+
built = []
|
|
374
|
+
Workhorse.notification_redis = lambda do
|
|
375
|
+
built << :built
|
|
376
|
+
FakeRedis.new
|
|
377
|
+
end
|
|
378
|
+
notifier = Workhorse::Notifiers::Redis.new
|
|
379
|
+
|
|
380
|
+
notifier.notify(queue: :mailer)
|
|
381
|
+
|
|
382
|
+
assert_equal [:built], built
|
|
383
|
+
ensure
|
|
384
|
+
Workhorse.notification_redis = nil
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
private
|
|
388
|
+
|
|
389
|
+
def with_exception_handler(handler)
|
|
390
|
+
previous = Workhorse.on_exception
|
|
391
|
+
Workhorse.on_exception = handler
|
|
392
|
+
yield
|
|
393
|
+
ensure
|
|
394
|
+
Workhorse.on_exception = previous
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
def wake_path
|
|
398
|
+
return File.join(Dir.tmpdir, 'workhorse_test.wake')
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
# Holds workhorse's global lock on a connection of its own, so that the code
|
|
402
|
+
# under test sees it as taken by another worker.
|
|
403
|
+
def with_global_lock_held
|
|
404
|
+
connection = ActiveRecord::Base.connection_pool.checkout
|
|
405
|
+
connection.select_value("SELECT GET_LOCK(CONCAT(DATABASE(), '_workhorse'), 1)")
|
|
406
|
+
yield
|
|
407
|
+
ensure
|
|
408
|
+
if connection
|
|
409
|
+
connection.select_value("SELECT RELEASE_LOCK(CONCAT(DATABASE(), '_workhorse'))")
|
|
410
|
+
ActiveRecord::Base.connection_pool.checkin(connection)
|
|
411
|
+
end
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
def file_notifier
|
|
415
|
+
return Workhorse::Notifiers::FileSystem.new(path: wake_path)
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
# Waits until the worker has completed the poll it performs on startup, so
|
|
419
|
+
# that a job enqueued afterwards can only be found through a notification.
|
|
420
|
+
def wait_for_first_poll(logger)
|
|
421
|
+
with_retries(50, interval: 0.05) do
|
|
422
|
+
assert_match(/Polling DB for jobs/, logger.instance_variable_get(:@logdev).dev.string)
|
|
423
|
+
end
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
# Notifier whose token cannot be read, standing in for a broken custom one.
|
|
427
|
+
class RaisingNotifier < Workhorse::Notifiers::Base
|
|
428
|
+
def token
|
|
429
|
+
fail 'notifier is broken'
|
|
430
|
+
end
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
# Minimal stand-in for a Redis client, so that the notifier's logic can be
|
|
434
|
+
# tested without a server. `subscribe` blocks the way the real one does.
|
|
435
|
+
class FakeRedis
|
|
436
|
+
attr_reader :published
|
|
437
|
+
|
|
438
|
+
def initialize(fail_publish: false, fail_subscribe: 0)
|
|
439
|
+
@published = []
|
|
440
|
+
@fail_publish = fail_publish
|
|
441
|
+
@fail_subscribe = fail_subscribe
|
|
442
|
+
@queue = Queue.new
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
def publish(channel, message)
|
|
446
|
+
fail 'Connection refused' if @fail_publish
|
|
447
|
+
|
|
448
|
+
@published << [channel, message]
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
def deliver(channel, message)
|
|
452
|
+
@queue << [channel, message]
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
# Makes the current subscription fail, as a dropped connection would.
|
|
456
|
+
def drop!
|
|
457
|
+
@queue << :drop
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
def dup
|
|
461
|
+
return self
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
def subscribe(_channel)
|
|
465
|
+
if @fail_subscribe > 0
|
|
466
|
+
@fail_subscribe -= 1
|
|
467
|
+
fail 'Connection refused'
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
on = Callbacks.new
|
|
471
|
+
yield on
|
|
472
|
+
on.subscribed!
|
|
473
|
+
|
|
474
|
+
loop do
|
|
475
|
+
item = @queue.pop
|
|
476
|
+
fail 'Connection reset' if item == :drop
|
|
477
|
+
|
|
478
|
+
on.call(*item)
|
|
479
|
+
end
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
class Callbacks
|
|
483
|
+
def message(&block)
|
|
484
|
+
@message = block
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
def subscribe(&block)
|
|
488
|
+
@subscribe = block
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
def subscribed!
|
|
492
|
+
@subscribe&.call
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
def call(channel, message)
|
|
496
|
+
@message&.call(channel, message)
|
|
497
|
+
end
|
|
498
|
+
end
|
|
499
|
+
end
|
|
500
|
+
end
|
|
@@ -71,10 +71,8 @@ class Workhorse::PollerTest < WorkhorseTest
|
|
|
71
71
|
assert_equal [], w.poller.send(:valid_queues)
|
|
72
72
|
end
|
|
73
73
|
|
|
74
|
-
# Not every adapter returns a result set from `execute
|
|
75
|
-
#
|
|
76
|
-
# version 7.0.0. Querying the valid queues must therefore not rely on the
|
|
77
|
-
# return value of `execute`.
|
|
74
|
+
# Not every adapter returns a result set from `execute`, so querying the
|
|
75
|
+
# valid queues must not rely on its return value.
|
|
78
76
|
def test_valid_queues_without_usable_execute
|
|
79
77
|
w = Workhorse::Worker.new(polling_interval: 60)
|
|
80
78
|
|
|
@@ -139,6 +137,12 @@ class Workhorse::PollerTest < WorkhorseTest
|
|
|
139
137
|
10.times do
|
|
140
138
|
Process.fork do
|
|
141
139
|
work 3, pool_size: 1, polling_interval: 0.1
|
|
140
|
+
ensure
|
|
141
|
+
# Exit without running the at_exit handlers of the test process: one
|
|
142
|
+
# of them is Minitest's, which joins threads this fork did not
|
|
143
|
+
# inherit and hangs the child, leaving waitall below waiting forever.
|
|
144
|
+
# In an ensure, as a child that raised must not run them either.
|
|
145
|
+
exit!(0)
|
|
142
146
|
end
|
|
143
147
|
end
|
|
144
148
|
|