qu-scheduler 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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