qu-scheduler 0.0.1 → 0.1.0

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.
Files changed (46) hide show
  1. data/MIT-LICENSE +2 -1
  2. data/README.md +191 -0
  3. data/lib/qu-scheduler.rb +1 -4
  4. data/lib/qu-scheduler/tasks.rb +35 -0
  5. data/lib/qu/extensions/scheduler.rb +102 -0
  6. data/lib/qu/extensions/scheduler/redis.rb +199 -0
  7. data/lib/qu/scheduler.rb +227 -0
  8. data/lib/qu/scheduler/version.rb +2 -2
  9. data/test/delayed_queue_test.rb +277 -0
  10. data/test/redis-test.conf +115 -0
  11. data/test/scheduler_args_test.rb +156 -0
  12. data/test/scheduler_hooks_test.rb +51 -0
  13. data/test/scheduler_test.rb +181 -0
  14. data/test/test_helper.rb +91 -7
  15. metadata +64 -76
  16. data/README.rdoc +0 -7
  17. data/lib/tasks/qu-scheduler_tasks.rake +0 -4
  18. data/test/dummy/Rakefile +0 -7
  19. data/test/dummy/app/assets/javascripts/application.js +0 -9
  20. data/test/dummy/app/assets/stylesheets/application.css +0 -7
  21. data/test/dummy/app/controllers/application_controller.rb +0 -3
  22. data/test/dummy/app/helpers/application_helper.rb +0 -2
  23. data/test/dummy/app/views/layouts/application.html.erb +0 -14
  24. data/test/dummy/config.ru +0 -4
  25. data/test/dummy/config/application.rb +0 -45
  26. data/test/dummy/config/boot.rb +0 -10
  27. data/test/dummy/config/database.yml +0 -25
  28. data/test/dummy/config/environment.rb +0 -5
  29. data/test/dummy/config/environments/development.rb +0 -30
  30. data/test/dummy/config/environments/production.rb +0 -60
  31. data/test/dummy/config/environments/test.rb +0 -39
  32. data/test/dummy/config/initializers/backtrace_silencers.rb +0 -7
  33. data/test/dummy/config/initializers/inflections.rb +0 -10
  34. data/test/dummy/config/initializers/mime_types.rb +0 -5
  35. data/test/dummy/config/initializers/secret_token.rb +0 -7
  36. data/test/dummy/config/initializers/session_store.rb +0 -8
  37. data/test/dummy/config/initializers/wrap_parameters.rb +0 -14
  38. data/test/dummy/config/locales/en.yml +0 -5
  39. data/test/dummy/config/routes.rb +0 -58
  40. data/test/dummy/db/test.sqlite3 +0 -0
  41. data/test/dummy/log/test.log +0 -0
  42. data/test/dummy/public/404.html +0 -26
  43. data/test/dummy/public/422.html +0 -26
  44. data/test/dummy/public/500.html +0 -26
  45. data/test/dummy/public/favicon.ico +0 -0
  46. data/test/dummy/script/rails +0 -6
@@ -0,0 +1,227 @@
1
+ require 'qu'
2
+ require 'qu/scheduler/version'
3
+ require 'qu/extensions/scheduler'
4
+ require 'rufus/scheduler'
5
+ require 'thwait'
6
+
7
+ module Qu
8
+ class Scheduler
9
+ @@scheduled_jobs = {}
10
+
11
+ class << self
12
+ # If set, will try to update the schedule in the loop
13
+ attr_accessor :dynamic
14
+
15
+ # Amount of time in seconds to sleep between polls of the delayed
16
+ # queue. Defaults to 5
17
+ attr_writer :poll_sleep_amount
18
+
19
+ # the Rufus::Scheduler jobs that are scheduled
20
+ def scheduled_jobs
21
+ @@scheduled_jobs
22
+ end
23
+
24
+ def poll_sleep_amount
25
+ @poll_sleep_amount ||= 5 # seconds
26
+ end
27
+
28
+ # Schedule all jobs and continually look for delayed jobs (never returns)
29
+ def run
30
+ set_process_title "starting"
31
+
32
+ # trap signals
33
+ register_signal_handlers
34
+
35
+ # Load the schedule into rufus
36
+ # If dynamic is set, load that schedule otherwise use normal load
37
+ if dynamic
38
+ reload_schedule!
39
+ else
40
+ load_schedule!
41
+ end
42
+
43
+ # Now start the scheduling part of the loop.
44
+ loop do
45
+ begin
46
+ handle_delayed_items
47
+ update_schedule if dynamic
48
+ rescue Errno::EAGAIN, Errno::ECONNRESET => e
49
+ warn e.message
50
+ end
51
+ poll_sleep
52
+ end
53
+
54
+ # never gets here.
55
+ end
56
+
57
+ # For all signals, set the shutdown flag and wait for current
58
+ # poll/enqueing to finish (should be almost istant). In the
59
+ # case of sleeping, exit immediately.
60
+ def register_signal_handlers
61
+ trap("TERM") { shutdown }
62
+ trap("INT") { shutdown }
63
+
64
+ begin
65
+ trap('QUIT') { shutdown }
66
+ trap('USR1') { print_schedule }
67
+ trap('USR2') { reload_schedule! }
68
+ rescue ArgumentError
69
+ warn "Signals QUIT, USR1 and USR2 not supported."
70
+ end
71
+ end
72
+
73
+ def print_schedule
74
+ if rufus_scheduler
75
+ Qu.logger.info("Scheduling Info\tLast Run")
76
+ scheduler_jobs = rufus_scheduler.all_jobs
77
+ scheduler_jobs.each do |k, v|
78
+ Qu.logger.info("#{v.t}\t#{v.last}")
79
+ end
80
+ end
81
+ end
82
+
83
+ # Pulls the schedule from Qu.schedule and loads it into the
84
+ # rufus scheduler instance
85
+ def load_schedule!
86
+ Qu.backend.load_schedule!
87
+ end
88
+
89
+ # Loads a job schedule into the Rufus::Scheduler and stores it in @@scheduled_jobs
90
+ def load_schedule_job(name, config)
91
+ # If rails_env is set in the config, enforce ENV['RAILS_ENV'] as
92
+ # required for the jobs to be scheduled. If rails_env is missing, the
93
+ # job should be scheduled regardless of what ENV['RAILS_ENV'] is set
94
+ # to.
95
+ if config['rails_env'].nil? || rails_env_matches?(config)
96
+ Qu.logger.info("Scheduling #{name}")
97
+ interval_defined = false
98
+ interval_types = %w{cron every}
99
+ interval_types.each do |interval_type|
100
+ if !config[interval_type].nil? && config[interval_type].length > 0
101
+ @@scheduled_jobs[name] = rufus_scheduler.send(interval_type, config[interval_type]) do
102
+ Qu.logger.info("queueing #{config['class']} (#{name})")
103
+ handle_errors { enqueue_from_config(config) }
104
+ end
105
+ interval_defined = true
106
+ break
107
+ end
108
+ end
109
+ unless interval_defined
110
+ Qu.logger.warn("no #{interval_types.join(' / ')} found for #{config['class']} (#{name}) - skipping")
111
+ end
112
+ end
113
+ end
114
+
115
+ # Returns true if the given schedule config hash matches the current
116
+ # ENV['RAILS_ENV']
117
+ def rails_env_matches?(config)
118
+ config['rails_env'] && ENV['RAILS_ENV'] && config['rails_env'].gsub(/\s/,'').split(',').include?(ENV['RAILS_ENV'])
119
+ end
120
+
121
+ # Handles queueing delayed items
122
+ # at_time - Time to start scheduling items (default: now).
123
+ def handle_delayed_items(at_time=nil)
124
+ item = nil
125
+ if timestamp = Qu.backend.next_delayed_timestamp(at_time)
126
+ set_process_title "processing delayed jobs"
127
+ while !timestamp.nil?
128
+ enqueue_delayed_items_for_timestamp(timestamp)
129
+ timestamp = Qu.backend.next_delayed_timestamp(at_time)
130
+ end
131
+ end
132
+ end
133
+
134
+ # Enqueues all delayed jobs for a timestamp
135
+ def enqueue_delayed_items_for_timestamp(timestamp)
136
+ item = nil
137
+ begin
138
+ handle_shutdown do
139
+ if item = Qu.backend.next_item_for_timestamp(timestamp)
140
+ Qu.logger.debug("queuing #{item['klass']} [delayed]")
141
+ handle_errors { enqueue_from_config(item) }
142
+ end
143
+ end
144
+ # continue processing until there are no more ready items in this timestamp
145
+ end while !item.nil?
146
+ end
147
+
148
+ def handle_shutdown
149
+ exit if @shutdown
150
+ yield
151
+ exit if @shutdown
152
+ end
153
+
154
+ def handle_errors
155
+ begin
156
+ yield
157
+ rescue Exception => e
158
+ Qu.logger.fatal("#{e.class.name}: #{e.message}")
159
+ end
160
+ end
161
+
162
+ # Enqueues a job based on a config hash
163
+ def enqueue_from_config(job_config)
164
+ payload = Payload.new(job_config)
165
+ if job_klass = job_config['custom_job_class']
166
+ # The custom job class API must offer a static "scheduled" method. If the custom
167
+ # job class can not be constantized (via a requeue call from the web perhaps), fall
168
+ # back to enqueing normally via Qu::Job.create.
169
+ begin
170
+ Qu::Payload.new.send(:constantize, job_klass).scheduled(payload.queue, job_klass, *payload.args)
171
+ rescue NameError
172
+ # Note that the custom job class (job_config['custom_job_class']) is the one enqueued
173
+ Qu.backend.enqueue(Payload.new(:klass => job_klass, :args => payload.args))
174
+ end
175
+ else
176
+ Qu.backend.enqueue(payload)
177
+ end
178
+ end
179
+
180
+ def rufus_scheduler
181
+ @rufus_scheduler ||= Rufus::Scheduler.start_new
182
+ end
183
+
184
+ # Stops old rufus scheduler and creates a new one. Returns the new
185
+ # rufus scheduler
186
+ def clear_schedule!
187
+ rufus_scheduler.stop
188
+ @rufus_scheduler = nil
189
+ @@scheduled_jobs = {}
190
+ rufus_scheduler
191
+ end
192
+
193
+ def reload_schedule!
194
+ set_process_title "reloading schedule"
195
+ clear_schedule!
196
+ load_schedule!
197
+ end
198
+
199
+ def unschedule_job(name)
200
+ if scheduled_jobs[name]
201
+ Qu.logger.debug("Removing schedule #{name}")
202
+ scheduled_jobs[name].unschedule
203
+ @@scheduled_jobs.delete(name)
204
+ end
205
+ end
206
+
207
+ # Sleeps and returns true
208
+ def poll_sleep
209
+ @sleeping = true
210
+ handle_shutdown { sleep poll_sleep_amount }
211
+ @sleeping = false
212
+ true
213
+ end
214
+
215
+ # Sets the shutdown flag, exits if sleeping
216
+ def shutdown
217
+ @shutdown = true
218
+ exit if @sleeping
219
+ end
220
+
221
+ def set_process_title(string)
222
+ Qu.logger.info(string)
223
+ $0 = "qu-scheduler-#{Qu::Scheduler::VERSION}: #{string}"
224
+ end
225
+ end
226
+ end
227
+ end
@@ -1,5 +1,5 @@
1
1
  module Qu
2
- module Scheduler
3
- VERSION = "0.0.1"
2
+ class Scheduler
3
+ VERSION = "0.1.0"
4
4
  end
5
5
  end
@@ -0,0 +1,277 @@
1
+ require 'test_helper'
2
+
3
+ context "DelayedQueue" do
4
+
5
+ setup do
6
+ Qu.backend.redis.flushall
7
+ end
8
+
9
+ test "enqueue_at adds correct list and zset" do
10
+ timestamp = Time.now - 1 # 1 second ago (in the past, should come out right away)
11
+
12
+ assert_equal(0, Qu.backend.redis.llen("delayed:#{timestamp.to_i}").to_i, "delayed queue should be empty to start")
13
+
14
+ Qu.enqueue_at(timestamp, SomeIvarJob, "path")
15
+
16
+ # Confirm the correct keys were added
17
+ assert_equal(1, Qu.backend.redis.llen("delayed:#{timestamp.to_i}").to_i, "delayed queue should have one entry now")
18
+ assert_equal(1, Qu.backend.redis.zcard(:delayed_queue_schedule), "The delayed_queue_schedule should have 1 entry now")
19
+
20
+ read_timestamp = Qu.backend.next_delayed_timestamp
21
+
22
+ # Confirm the timestamp came out correctly
23
+ assert_equal(timestamp.to_i, read_timestamp, "The timestamp we pull out of redis should match the one we put in")
24
+ item = Qu.backend.next_item_for_timestamp(read_timestamp)
25
+
26
+ # Confirm the item came out correctly
27
+ assert_equal('SomeIvarJob', item['klass'], "Should be the same class that we queued")
28
+ assert_equal(["path"], item['args'], "Should have the same arguments that we queued")
29
+
30
+ # And now confirm the keys are gone
31
+ assert(!Qu.backend.redis.exists("delayed:#{timestamp.to_i}"))
32
+ assert_equal(0, Qu.backend.redis.zcard(:delayed_queue_schedule), "delayed queue should be empty")
33
+ end
34
+
35
+ test "a job in the future doesn't come out" do
36
+ timestamp = Time.now + 600 # 10 minutes from now (in the future, shouldn't come out)
37
+
38
+ assert_equal(0, Qu.backend.redis.llen("delayed:#{timestamp.to_i}").to_i, "delayed queue should be empty to start")
39
+
40
+ Qu.enqueue_at(timestamp, SomeIvarJob, "path")
41
+
42
+ # Confirm the correct keys were added
43
+ assert_equal(1, Qu.backend.redis.llen("delayed:#{timestamp.to_i}").to_i, "delayed queue should have one entry now")
44
+ assert_equal(1, Qu.backend.redis.zcard(:delayed_queue_schedule), "The delayed_queue_schedule should have 1 entry now")
45
+
46
+ read_timestamp = Qu.backend.next_delayed_timestamp
47
+
48
+ assert_nil(read_timestamp, "No timestamps should be ready for queueing")
49
+ end
50
+
51
+ test "a job in the future comes out if you want it to" do
52
+ timestamp = Time.now + 600 # 10 minutes from now
53
+
54
+ Qu.enqueue_at(timestamp, SomeIvarJob, "path")
55
+
56
+ read_timestamp = Qu.backend.next_delayed_timestamp(timestamp)
57
+
58
+ assert_equal(timestamp.to_i, read_timestamp, "The timestamp we pull out of redis should match the one we put in")
59
+ end
60
+
61
+ test "enqueue_at and enqueue_in are equivelent" do
62
+ timestamp = Time.now + 60
63
+
64
+ Qu.enqueue_at(timestamp, SomeIvarJob, "path")
65
+ Qu.enqueue_in(timestamp - Time.now, SomeIvarJob, "path")
66
+
67
+ assert_equal(1, Qu.backend.redis.zcard(:delayed_queue_schedule), "should have one timestamp in the delayed queue")
68
+ assert_equal(2, Qu.backend.redis.llen("delayed:#{timestamp.to_i}"), "should have 2 items in the timestamp queue")
69
+ end
70
+
71
+ test "empty delayed_queue_peek returns empty array" do
72
+ assert_equal([], Qu.backend.delayed_queue_peek(0,20))
73
+ end
74
+
75
+ test "delayed_queue_peek returns stuff" do
76
+ t = Time.now
77
+ expected_timestamps = (1..5).to_a.map do |i|
78
+ (t + 60 + i).to_i
79
+ end
80
+
81
+ expected_timestamps.each do |timestamp|
82
+ Qu.backend.delayed_push(timestamp, Qu::Payload.new({:class => SomeIvarJob, :args => 'blah1'}))
83
+ end
84
+
85
+ timestamps = Qu.backend.delayed_queue_peek(2,3)
86
+
87
+ assert_equal(expected_timestamps[2,3], timestamps)
88
+ end
89
+
90
+ test "delayed_queue_schedule_size returns correct size" do
91
+ assert_equal(0, Qu.backend.delayed_queue_schedule_size)
92
+ Qu.enqueue_at(Time.now+60, SomeIvarJob)
93
+ assert_equal(1, Qu.backend.delayed_queue_schedule_size)
94
+ end
95
+
96
+ test "delayed_timestamp_size returns 0 when nothing is queue" do
97
+ t = Time.now + 60
98
+ assert_equal(0, Qu.backend.delayed_timestamp_size(t))
99
+ end
100
+
101
+ test "delayed_timestamp_size returns 1 when one thing is queued" do
102
+ t = Time.now + 60
103
+ Qu.enqueue_at(t, SomeIvarJob)
104
+ assert_equal(1, Qu.backend.delayed_timestamp_size(t))
105
+ end
106
+
107
+ test "delayed_timestamp_peek returns empty array when nothings in it" do
108
+ t = Time.now + 60
109
+ assert_equal([], Qu.delayed_timestamp_peek(t, 0, 1), "make sure it's an empty array, not nil")
110
+ end
111
+
112
+ test "delayed_timestamp_peek returns an array containing one job when one thing is queued" do
113
+ t = Time.now + 60
114
+ Qu.enqueue_at(t, SomeIvarJob)
115
+ assert_equal [{'args' => [], 'klass' => 'SomeIvarJob'}], Qu.delayed_timestamp_peek(t, 0, 1)
116
+ end
117
+
118
+ test "delayed_timestamp_peek returns an array of multiple jobs when more than one job is queued" do
119
+ t = Time.now + 60
120
+ Qu.enqueue_at(t, SomeIvarJob)
121
+ Qu.enqueue_at(t, SomeIvarJob)
122
+ job = {'args' => [], 'klass' => 'SomeIvarJob'}
123
+ assert_equal([job, job], Qu.delayed_timestamp_peek(t, 0, 2))
124
+ end
125
+
126
+ test "delayed_timestamp_peek only returns an array of one job if only asked for 1" do
127
+ t = Time.now + 60
128
+ Qu.enqueue_at(t, SomeIvarJob)
129
+ Qu.enqueue_at(t, SomeIvarJob)
130
+ job = {'args' => [], 'klass' => 'SomeIvarJob'}
131
+ assert_equal([job], Qu.delayed_timestamp_peek(t, 0, 1))
132
+ end
133
+
134
+ test "handle_delayed_items with no items" do
135
+ Qu::Scheduler.expects(:enqueue).never
136
+ Qu::Scheduler.handle_delayed_items
137
+ end
138
+
139
+ test "handle_delayed_item with items" do
140
+ t = Time.now - 60 # in the past
141
+ Qu.enqueue_at(t, SomeIvarJob)
142
+ Qu.enqueue_at(t, SomeIvarJob)
143
+
144
+ # 2 SomeIvarJob jobs should be created in the "ivar" queue
145
+ Qu.backend.expects(:enqueue).twice.with(all_of(instance_of(Qu::Payload), responds_with(:klass, SomeIvarJob), responds_with(:args, []), responds_with(:queue, 'ivar')))
146
+ Qu::Scheduler.handle_delayed_items
147
+ end
148
+
149
+ test "handle_delayed_items with items in the future" do
150
+ t = Time.now + 60 # in the future
151
+ Qu.enqueue_at(t, SomeIvarJob)
152
+ Qu.enqueue_at(t, SomeIvarJob)
153
+
154
+ # 2 SomeIvarJob jobs should be created in the "ivar" queue
155
+ Qu.backend.expects(:enqueue).twice.with(all_of(instance_of(Qu::Payload), responds_with(:klass, SomeIvarJob), responds_with(:args, []), responds_with(:queue, 'ivar')))
156
+ Qu::Scheduler.handle_delayed_items(t)
157
+ end
158
+
159
+ test "enqueue_delayed_items_for_timestamp creates jobs and empties the delayed queue" do
160
+ t = Time.now + 60
161
+
162
+ Qu.enqueue_at(t, SomeIvarJob)
163
+ Qu.enqueue_at(t, SomeIvarJob)
164
+
165
+ # 2 SomeIvarJob jobs should be created in the "ivar" queue
166
+ Qu.backend.expects(:enqueue).twice.with(all_of(instance_of(Qu::Payload), responds_with(:klass, SomeIvarJob), responds_with(:args, []), responds_with(:queue, 'ivar')))
167
+
168
+ Qu::Scheduler.enqueue_delayed_items_for_timestamp(t)
169
+
170
+ # delayed queue for timestamp should be empty
171
+ assert_equal(0, Qu.delayed_timestamp_peek(t, 0, 3).length)
172
+ end
173
+
174
+ test "handle_delayed_items works with out specifying queue (upgrade case)" do
175
+ t = Time.now - 60
176
+ Qu.backend.delayed_push(t, Qu::Payload.new(:klass => 'SomeIvarJob'))
177
+
178
+ # Since we didn't specify :queue when calling delayed_push, it will be forced
179
+ # to load the class to figure out the queue. This is the upgrade case from 1.0.4
180
+ # to 1.0.5.
181
+ Qu.backend.expects(:enqueue).once.with(all_of(instance_of(Qu::Payload), responds_with(:klass, SomeIvarJob), responds_with(:args, []), responds_with(:queue, 'ivar')))
182
+
183
+ Qu::Scheduler.handle_delayed_items
184
+ end
185
+
186
+ test "reset_delayed_queue clears the queue" do
187
+ t = Time.now + 120
188
+ 4.times { Qu.enqueue_at(t, SomeIvarJob) }
189
+ 4.times { Qu.enqueue_at(Time.now + rand(100), SomeIvarJob) }
190
+
191
+ Qu.backend.reset_delayed_queue
192
+ assert_equal(0, Qu.backend.delayed_queue_schedule_size)
193
+ end
194
+
195
+ test "remove_delayed removes job and returns the count" do
196
+ t = Time.now + 120
197
+ Qu.enqueue_at(t, SomeIvarJob)
198
+
199
+ assert_equal(1, Qu.backend.remove_delayed(SomeIvarJob))
200
+ end
201
+
202
+ test "remove_delayed doesn't remove things it shouldn't" do
203
+ t = Time.now + 120
204
+ Qu.enqueue_at(t, SomeIvarJob, "foo")
205
+ Qu.enqueue_at(t, SomeIvarJob, "bar")
206
+ Qu.enqueue_at(t, SomeIvarJob, "bar")
207
+ Qu.enqueue_at(t, SomeIvarJob, "baz")
208
+
209
+ assert_equal(0, Qu.backend.remove_delayed(SomeIvarJob))
210
+ end
211
+
212
+ test "remove_delayed respected param" do
213
+ t = Time.now + 120
214
+ Qu.enqueue_at(t, SomeIvarJob, "foo")
215
+ Qu.enqueue_at(t, SomeIvarJob, "bar")
216
+ Qu.enqueue_at(t, SomeIvarJob, "bar")
217
+ Qu.enqueue_at(t, SomeIvarJob, "baz")
218
+
219
+ assert_equal(2, Qu.backend.remove_delayed(SomeIvarJob, "bar"))
220
+ assert_equal(1, Qu.backend.delayed_queue_schedule_size)
221
+ end
222
+
223
+ test "remove_delayed removes items in different timestamps" do
224
+ t = Time.now + 120
225
+ Qu.enqueue_at(t, SomeIvarJob, "foo")
226
+ Qu.enqueue_at(t + 1, SomeIvarJob, "bar")
227
+ Qu.enqueue_at(t + 2, SomeIvarJob, "bar")
228
+ Qu.enqueue_at(t + 3, SomeIvarJob, "baz")
229
+
230
+ assert_equal(2, Qu.backend.remove_delayed(SomeIvarJob, "bar"))
231
+ assert_equal(2, Qu.backend.count_all_scheduled_jobs)
232
+ end
233
+
234
+ test "remove_delayed_job_from_timestamp removes instances of jobs at a given timestamp" do
235
+ t = Time.now + 120
236
+ Qu.enqueue_at(t, SomeIvarJob, "foo")
237
+ assert_equal 1, Qu.backend.remove_delayed_job_from_timestamp(t, SomeIvarJob, "foo")
238
+ assert_equal 0, Qu.backend.delayed_timestamp_size(t)
239
+ end
240
+
241
+ test "remove_delayed_job_from_timestamp doesn't remove items from other timestamps" do
242
+ t1 = Time.now + 120
243
+ t2 = t1 + 1
244
+ Qu.enqueue_at(t1, SomeIvarJob, "foo")
245
+ Qu.enqueue_at(t2, SomeIvarJob, "foo")
246
+ assert_equal 1, Qu.backend.remove_delayed_job_from_timestamp(t2, SomeIvarJob, "foo")
247
+ assert_equal 1, Qu.backend.delayed_timestamp_size(t1)
248
+ assert_equal 0, Qu.backend.delayed_timestamp_size(t2)
249
+ end
250
+
251
+ test "remove_delayed_job_from_timestamp removes nothing if there are no matches" do
252
+ t = Time.now + 120
253
+ assert_equal 0, Qu.backend.remove_delayed_job_from_timestamp(t, SomeIvarJob, "foo")
254
+ end
255
+
256
+ test "remove_delayed_job_from_timestamp only removes items that match args" do
257
+ t = Time.now + 120
258
+ Qu.enqueue_at(t, SomeIvarJob, "foo")
259
+ Qu.enqueue_at(t, SomeIvarJob, "bar")
260
+ assert_equal 1, Qu.backend.remove_delayed_job_from_timestamp(t, SomeIvarJob, "foo")
261
+ assert_equal 1, Qu.backend.delayed_timestamp_size(t)
262
+ end
263
+
264
+ test "remove_delayed_job_from_timestamp returns the number of items removed" do
265
+ t = Time.now + 120
266
+ Qu.enqueue_at(t, SomeIvarJob, "foo")
267
+ assert_equal 1, Qu.backend.remove_delayed_job_from_timestamp(t, SomeIvarJob, "foo")
268
+ end
269
+
270
+ test "remove_delayed_job_from_timestamp should cleanup the delayed timestamp list if not jobs are left" do
271
+ t = Time.now + 120
272
+ Qu.enqueue_at(t, SomeIvarJob, "foo")
273
+ assert_equal 1, Qu.backend.remove_delayed_job_from_timestamp(t, SomeIvarJob, "foo")
274
+ assert !Qu.backend.redis.exists("delayed:#{t.to_i}")
275
+ assert Qu.backend.delayed_queue_peek(0, 100).empty?
276
+ end
277
+ end