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.
- data/MIT-LICENSE +2 -1
- data/README.md +191 -0
- data/lib/qu-scheduler.rb +1 -4
- data/lib/qu-scheduler/tasks.rb +35 -0
- data/lib/qu/extensions/scheduler.rb +102 -0
- data/lib/qu/extensions/scheduler/redis.rb +199 -0
- data/lib/qu/scheduler.rb +227 -0
- data/lib/qu/scheduler/version.rb +2 -2
- data/test/delayed_queue_test.rb +277 -0
- data/test/redis-test.conf +115 -0
- data/test/scheduler_args_test.rb +156 -0
- data/test/scheduler_hooks_test.rb +51 -0
- data/test/scheduler_test.rb +181 -0
- data/test/test_helper.rb +91 -7
- metadata +64 -76
- data/README.rdoc +0 -7
- data/lib/tasks/qu-scheduler_tasks.rake +0 -4
- data/test/dummy/Rakefile +0 -7
- data/test/dummy/app/assets/javascripts/application.js +0 -9
- data/test/dummy/app/assets/stylesheets/application.css +0 -7
- data/test/dummy/app/controllers/application_controller.rb +0 -3
- data/test/dummy/app/helpers/application_helper.rb +0 -2
- data/test/dummy/app/views/layouts/application.html.erb +0 -14
- data/test/dummy/config.ru +0 -4
- data/test/dummy/config/application.rb +0 -45
- data/test/dummy/config/boot.rb +0 -10
- data/test/dummy/config/database.yml +0 -25
- data/test/dummy/config/environment.rb +0 -5
- data/test/dummy/config/environments/development.rb +0 -30
- data/test/dummy/config/environments/production.rb +0 -60
- data/test/dummy/config/environments/test.rb +0 -39
- data/test/dummy/config/initializers/backtrace_silencers.rb +0 -7
- data/test/dummy/config/initializers/inflections.rb +0 -10
- data/test/dummy/config/initializers/mime_types.rb +0 -5
- data/test/dummy/config/initializers/secret_token.rb +0 -7
- data/test/dummy/config/initializers/session_store.rb +0 -8
- data/test/dummy/config/initializers/wrap_parameters.rb +0 -14
- data/test/dummy/config/locales/en.yml +0 -5
- data/test/dummy/config/routes.rb +0 -58
- data/test/dummy/db/test.sqlite3 +0 -0
- data/test/dummy/log/test.log +0 -0
- data/test/dummy/public/404.html +0 -26
- data/test/dummy/public/422.html +0 -26
- data/test/dummy/public/500.html +0 -26
- data/test/dummy/public/favicon.ico +0 -0
- data/test/dummy/script/rails +0 -6
data/MIT-LICENSE
CHANGED
data/README.md
ADDED
@@ -0,0 +1,191 @@
|
|
1
|
+
# Qu::Scheduler
|
2
|
+
|
3
|
+
## Description
|
4
|
+
|
5
|
+
qu-scheduler is an extension to [Qu](http://github.com/bkeepers/qu)
|
6
|
+
that adds support for queueing jobs in the future.
|
7
|
+
|
8
|
+
Currently qu-scheduler only works with qu-redis and requires Redis 2.0 or newer.
|
9
|
+
|
10
|
+
Job scheduling is supported in two different ways: Recurring (scheduled) and
|
11
|
+
Delayed.
|
12
|
+
|
13
|
+
Scheduled jobs are like cron jobs, recurring on a regular basis. Delayed
|
14
|
+
jobs are Qu jobs that you want to run at some point in the future.
|
15
|
+
The syntax is pretty explanatory:
|
16
|
+
|
17
|
+
Qu.enqueue_in(5.days, SendFollowupEmail) # run a job in 5 days
|
18
|
+
# or
|
19
|
+
Qu.enqueue_at(5.days.from_now, SomeJob) # run SomeJob at a specific time
|
20
|
+
|
21
|
+
## Installation
|
22
|
+
|
23
|
+
# Rails 3.x: add it to your Gemfile
|
24
|
+
gem 'qu-scheduler'
|
25
|
+
|
26
|
+
There are just a single thing `qu-scheduler` needs to know about in order
|
27
|
+
to do it's jobs: the schedule. The easiest way to configure these things
|
28
|
+
is via the rake task. By default, `qu-scheduler` depends on the "qu:setup"
|
29
|
+
rake task. Since you probably already have this task, lets just put our
|
30
|
+
configuration there. `qu-scheduler` pretty much inherits everything else
|
31
|
+
from Qu.
|
32
|
+
|
33
|
+
# Qu tasks
|
34
|
+
require 'qu/tasks'
|
35
|
+
require 'qu-scheduler/tasks'
|
36
|
+
|
37
|
+
namespace :qu do
|
38
|
+
task :setup do
|
39
|
+
require 'qu'
|
40
|
+
require 'qu-scheduler'
|
41
|
+
|
42
|
+
# If you want to be able to dynamically change the schedule,
|
43
|
+
# uncomment this line. A dynamic schedule can be updated via the
|
44
|
+
# Qu::Scheduler.set_schedule (and remove_schedule) methods.
|
45
|
+
# When dynamic is set to true, the scheduler process looks for
|
46
|
+
# schedule changes and applies them on the fly.
|
47
|
+
# Note: This feature is still under development
|
48
|
+
# Qu::Scheduler.dynamic = true
|
49
|
+
|
50
|
+
# The schedule doesn't need to be stored in a YAML, it just needs to
|
51
|
+
# be a hash. YAML is usually the easiest.
|
52
|
+
Qu.schedule = YAML.load_file(Rails.root.join('config', 'your_resque_schedule.yml'))
|
53
|
+
|
54
|
+
# If your don't depend on your application environment you need to
|
55
|
+
# require your jobs here. Qu determines the queue exclusively from the
|
56
|
+
# class, so we need to have access to them.
|
57
|
+
require 'jobs'
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
The scheduler process is just a rake task which is responsible for both
|
62
|
+
queueing jobs from the schedule and polling the delayed queue for jobs
|
63
|
+
ready to be pushed on to the work queues. For obvious reasons, this process
|
64
|
+
never exits.
|
65
|
+
|
66
|
+
$ bundle exec rake qu:scheduler
|
67
|
+
|
68
|
+
NOTE: You DO NOT want to run more than one instance of the scheduler. Doing
|
69
|
+
so will result in the same job being queued multiple times. You only need one
|
70
|
+
instance of the scheduler running per application, regardless of number of servers.
|
71
|
+
|
72
|
+
If the scheduler process goes down for whatever reason, the delayed items
|
73
|
+
that should have fired during the outage will fire once the scheduler process
|
74
|
+
is started back up again (even if it is on a new machine). Missed scheduled
|
75
|
+
jobs, however, will not fire upon recovery of the scheduler process.
|
76
|
+
|
77
|
+
## Delayed jobs
|
78
|
+
|
79
|
+
Delayed jobs are one-off jobs that you want to be put into a queue at some point
|
80
|
+
in the future. The classic example is sending email:
|
81
|
+
|
82
|
+
Qu.enqueue_in(5.days, SendFollowUpEmail, current_user.id)
|
83
|
+
|
84
|
+
This will store the job for 5 days in the Qu delayed queue at which time
|
85
|
+
the scheduler process will pull it from the delayed queue and put it in the
|
86
|
+
appropriate work queue for the given job. It will then be processed as soon as
|
87
|
+
a worker is available (just like any other Qu job).
|
88
|
+
|
89
|
+
NOTE: The job does not fire **exactly** at the time supplied. Rather, once that
|
90
|
+
time is in the past, the job moves from the delayed queue to the actual work
|
91
|
+
queue and will be completed as workers as free to process it.
|
92
|
+
|
93
|
+
Also supported is `Qu.enqueue_at` which takes a timestamp to queue the job.
|
94
|
+
|
95
|
+
The delayed queue is stored in redis and is persisted in the same way the
|
96
|
+
standard Qu jobs are persisted (redis writing to disk). Delayed jobs differ
|
97
|
+
from scheduled jobs in that if your scheduler process is down or workers are
|
98
|
+
down when a particular job is supposed to be processed, they will simply "catch up"
|
99
|
+
once they are started again. Jobs are guaranteed to run (provided they make it
|
100
|
+
into the delayed queue) after their given queue_at time has passed.
|
101
|
+
|
102
|
+
Your jobs can specify one or more `before_schedule` and `after_schedule` hooks,
|
103
|
+
to be run before or after scheduling. If *any* of your `before_schedule` hooks
|
104
|
+
returns `false`, the job will *not* be scheduled and your `after_schedule` hooks
|
105
|
+
will *not* be run.
|
106
|
+
|
107
|
+
One other thing to note is that insertion into the delayed queue is O(log(n))
|
108
|
+
since the jobs are stored in a redis sorted set (zset). I can't imagine this
|
109
|
+
being an issue for someone since redis is stupidly fast even at log(n), but full
|
110
|
+
disclosure is always best.
|
111
|
+
|
112
|
+
### Removing Delayed jobs
|
113
|
+
|
114
|
+
If you have the need to cancel a delayed job, you can do it like this:
|
115
|
+
|
116
|
+
# after you've enqueued a job like:
|
117
|
+
Qu.enqueue_at(5.days.from_now, SendFollowUpEmail, current_user.id)
|
118
|
+
# remove the job with exactly the same parameters:
|
119
|
+
Qu.remove_delayed(SendFollowUpEmail, current_user.id)
|
120
|
+
|
121
|
+
## Scheduled Jobs (Recurring Jobs)
|
122
|
+
|
123
|
+
Scheduled (or recurring) jobs are logically no different than a standard cron
|
124
|
+
job. They are jobs that run based on a fixed schedule which is set at
|
125
|
+
startup.
|
126
|
+
|
127
|
+
The schedule is a list of job classes with arguments and a schedule frequency
|
128
|
+
(in crontab syntax). The schedule is just a hash, but is most likely stored in
|
129
|
+
a YAML like this:
|
130
|
+
|
131
|
+
queue_documents_for_indexing:
|
132
|
+
cron: "0 0 * * *"
|
133
|
+
# you can use rufus-scheduler "every" syntax in place of cron if you prefer
|
134
|
+
# every: 1hr
|
135
|
+
klass: QueueDocuments
|
136
|
+
args:
|
137
|
+
description: "This job queues all content for indexing in solr"
|
138
|
+
|
139
|
+
clear_leaderboards_contributors:
|
140
|
+
cron: "30 6 * * 1"
|
141
|
+
klass: ClearLeaderboards
|
142
|
+
args: contributors
|
143
|
+
description: "This job resets the weekly leaderboard for contributions"
|
144
|
+
|
145
|
+
NOTE: Six parameter cron's are also supported (as they supported by
|
146
|
+
rufus-scheduler which powers the resque-scheduler process). This allows you
|
147
|
+
to schedule jobs per second (ie: "30 * * * * *" would fire a job every 30
|
148
|
+
seconds past the minute).
|
149
|
+
|
150
|
+
A big shout out to [rufus-scheduler](http://github.com/jmettraux/rufus-scheduler)
|
151
|
+
for handling the heavy lifting of the actual scheduling engine.
|
152
|
+
|
153
|
+
## Running in the background
|
154
|
+
|
155
|
+
(Only supported with ruby >= 1.9). There are scenarios where it's helpful for
|
156
|
+
the resque worker to run itself in the background (usually in combination with
|
157
|
+
PIDFILE). Use the BACKGROUND option so that rake will return as soon as the
|
158
|
+
worker is started.
|
159
|
+
|
160
|
+
$ PIDFILE=./qu-scheduler.pid BACKGROUND=yes bundle exec rake qu:scheduler
|
161
|
+
|
162
|
+
## Additional information
|
163
|
+
|
164
|
+
by Ben VandenBos to the Qu queuing library.
|
165
|
+
|
166
|
+
## Note on Patches / Pull Requests
|
167
|
+
|
168
|
+
* Fork the project.
|
169
|
+
* Make your feature addition or bug fix.
|
170
|
+
* Add tests for it. This is important so I don't break it in a future version unintentionally.
|
171
|
+
* Commit, do not mess with rakefile, version, or history.
|
172
|
+
(if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
|
173
|
+
* Send me a pull request. Bonus points for topic branches.
|
174
|
+
|
175
|
+
## Credits
|
176
|
+
|
177
|
+
This work is a port of [resque-scheduler](https://github.com/bvandenbos/resque-scheduler) by Ben VandenBos.
|
178
|
+
Modified to work with the Qu queueing library by Morton Jonuschat.
|
179
|
+
|
180
|
+
## Maintainers
|
181
|
+
|
182
|
+
* [Morton Jonuschat](https://github.com/yabawock)
|
183
|
+
|
184
|
+
## License
|
185
|
+
|
186
|
+
MIT License
|
187
|
+
|
188
|
+
## Copyright
|
189
|
+
|
190
|
+
Copyright 2011 Morton Jonuschat
|
191
|
+
Some parts copyright 2010 Ben VandenBos
|
data/lib/qu-scheduler.rb
CHANGED
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'qu/tasks'
|
2
|
+
|
3
|
+
namespace :qu do
|
4
|
+
desc "Start Qu Scheduler"
|
5
|
+
task :scheduler => :scheduler_setup do
|
6
|
+
require 'qu'
|
7
|
+
require 'qu-scheduler'
|
8
|
+
|
9
|
+
if ENV['BACKGROUND']
|
10
|
+
unless Process.respond_to?('daemon')
|
11
|
+
abort "Environment variable BACKGROUND is set, which requires ruby >= 1.9"
|
12
|
+
end
|
13
|
+
Process.daemon(true)
|
14
|
+
end
|
15
|
+
|
16
|
+
File.open(ENV['PIDFILE'], 'w') { |f| f << Process.pid.to_s } if ENV['PIDFILE']
|
17
|
+
|
18
|
+
Qu::Scheduler.dynamic = true if ENV['DYNAMIC_SCHEDULE']
|
19
|
+
Qu::Scheduler.run
|
20
|
+
end
|
21
|
+
|
22
|
+
task :scheduler_setup do
|
23
|
+
if ENV['INITIALIZER_PATH']
|
24
|
+
load ENV['INITIALIZER_PATH'].to_s.strip
|
25
|
+
else
|
26
|
+
Rake::Task['qu:setup'].invoke
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# No-op task in case it doesn't already exist
|
31
|
+
task :setup
|
32
|
+
end
|
33
|
+
|
34
|
+
# Convenience tasks compatibility
|
35
|
+
task 'resque:scheduler' => 'qu:scheduler'
|
@@ -0,0 +1,102 @@
|
|
1
|
+
module Qu
|
2
|
+
module Extensions
|
3
|
+
module Scheduler
|
4
|
+
autoload :Redis, 'qu/extensions/scheduler/redis'
|
5
|
+
|
6
|
+
#
|
7
|
+
# Accepts a new schedule configuration of the form:
|
8
|
+
#
|
9
|
+
# {'some_name' => {"cron" => "5/* * * *",
|
10
|
+
# "class" => "DoSomeWork",
|
11
|
+
# "args" => "work on this string",
|
12
|
+
# "description" => "this thing works it"s butter off"},
|
13
|
+
# ...}
|
14
|
+
#
|
15
|
+
# 'some_name' can be anything and is used only to describe and reference
|
16
|
+
# the scheduled job
|
17
|
+
#
|
18
|
+
# :cron can be any cron scheduling string :job can be any resque job class
|
19
|
+
#
|
20
|
+
# :every can be used in lieu of :cron. see rufus-scheduler's 'every' usage
|
21
|
+
# for valid syntax. If :cron is present it will take precedence over :every.
|
22
|
+
#
|
23
|
+
# :class must be a resque worker class
|
24
|
+
#
|
25
|
+
# :args can be any yaml which will be converted to a ruby literal and
|
26
|
+
# passed in a params. (optional)
|
27
|
+
#
|
28
|
+
# :rails_envs is the list of envs where the job gets loaded. Envs are
|
29
|
+
# comma separated (optional)
|
30
|
+
#
|
31
|
+
# :description is just that, a description of the job (optional). If
|
32
|
+
# params is an array, each element in the array is passed as a separate
|
33
|
+
# param, otherwise params is passed in as the only parameter to perform.
|
34
|
+
def schedule=(schedule_hash)
|
35
|
+
if Qu::Scheduler.dynamic
|
36
|
+
schedule_hash.each do |name, job_spec|
|
37
|
+
backend.set_schedule(name, job_spec)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
@schedule = schedule_hash
|
41
|
+
end
|
42
|
+
|
43
|
+
# Returns the schedule hash
|
44
|
+
def schedule
|
45
|
+
@schedule ||= {}
|
46
|
+
end
|
47
|
+
|
48
|
+
# reloads the schedule from redis
|
49
|
+
def reload_schedule!
|
50
|
+
@schedule = backend.get_schedules
|
51
|
+
end
|
52
|
+
|
53
|
+
# This method is nearly identical to +enqueue+ only it also
|
54
|
+
# takes a timestamp which will be used to schedule the job
|
55
|
+
# for queueing. Until timestamp is in the past, the job will
|
56
|
+
# sit in the schedule list.
|
57
|
+
def enqueue_at(timestamp, klass, *args)
|
58
|
+
before_hooks = before_schedule_hooks(klass).collect do |hook|
|
59
|
+
klass.send(hook,*args)
|
60
|
+
end
|
61
|
+
return false if before_hooks.any? { |result| result == false }
|
62
|
+
|
63
|
+
backend.delayed_push(timestamp, Payload.new(:klass => klass, :args => args))
|
64
|
+
|
65
|
+
after_schedule_hooks(klass).collect do |hook|
|
66
|
+
klass.send(hook,*args)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Identical to enqueue_at but takes number_of_seconds_from_now
|
71
|
+
# instead of a timestamp.
|
72
|
+
def enqueue_in(number_of_seconds_from_now, klass, *args)
|
73
|
+
enqueue_at(Time.now + number_of_seconds_from_now, klass, *args)
|
74
|
+
end
|
75
|
+
|
76
|
+
# Returns an array of delayed items for the given timestamp
|
77
|
+
def delayed_timestamp_peek(timestamp, start, count)
|
78
|
+
if 1 == count
|
79
|
+
r = backend.list_range "delayed:#{timestamp.to_i}", start, count
|
80
|
+
r.nil? ? [] : [r]
|
81
|
+
else
|
82
|
+
backend.list_range "delayed:#{timestamp.to_i}", start, count
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
def before_schedule_hooks(klass)
|
89
|
+
klass.methods.grep(/^before_schedule/).sort
|
90
|
+
end
|
91
|
+
|
92
|
+
def after_schedule_hooks(klass)
|
93
|
+
klass.methods.grep(/^after_schedule/).sort
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
Qu.send :extend, Qu::Extensions::Scheduler
|
100
|
+
if defined? Qu::Backend::Redis
|
101
|
+
Qu::Backend::Redis.send :include, Qu::Extensions::Scheduler::Redis
|
102
|
+
end
|
@@ -0,0 +1,199 @@
|
|
1
|
+
module Qu
|
2
|
+
module Extensions
|
3
|
+
module Scheduler
|
4
|
+
module Redis
|
5
|
+
# Does the dirty work of fetching a range of items from a Redis list
|
6
|
+
# and converting them into Ruby objects.
|
7
|
+
def list_range(key, start = 0, count = 1)
|
8
|
+
if count == 1
|
9
|
+
decode redis.lindex(key, start)
|
10
|
+
else
|
11
|
+
Array(redis.lrange(key, start, start+count-1)).map do |item|
|
12
|
+
decode item
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# gets the schedule as it exists in redis
|
18
|
+
def get_schedules
|
19
|
+
if redis.exists(:schedules)
|
20
|
+
redis.hgetall(:schedules).tap do |h|
|
21
|
+
h.each do |name, config|
|
22
|
+
h[name] = decode(config)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
else
|
26
|
+
nil
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Create or update a schedule with the provided name and configuration.
|
31
|
+
#
|
32
|
+
# Note: values for class and custom_job_class need to be strings,
|
33
|
+
# not constants.
|
34
|
+
#
|
35
|
+
# Qu.set_schedule('some_job', { :class => 'SomeJob',
|
36
|
+
# :every => '15mins',
|
37
|
+
# :queue => 'high',
|
38
|
+
# :args => '/tmp/poop' })
|
39
|
+
def set_schedule(name, config)
|
40
|
+
existing_config = get_schedule(name)
|
41
|
+
unless existing_config && existing_config == config
|
42
|
+
redis.hset(:schedules, name, encode(config))
|
43
|
+
redis.sadd(:schedules_changed, name)
|
44
|
+
end
|
45
|
+
config
|
46
|
+
end
|
47
|
+
|
48
|
+
# retrieve the schedule configuration for the given name
|
49
|
+
def get_schedule(name)
|
50
|
+
decode(redis.hget(:schedules, name))
|
51
|
+
end
|
52
|
+
|
53
|
+
# remove a given schedule by name
|
54
|
+
def remove_schedule(name)
|
55
|
+
redis.hdel(:schedules, name)
|
56
|
+
redis.sadd(:schedules_changed, name)
|
57
|
+
end
|
58
|
+
|
59
|
+
def update_schedule
|
60
|
+
if redis.scard(:schedules_changed) > 0
|
61
|
+
Qu::Scheduler.set_process_title "updating schedule"
|
62
|
+
Qu.reload_schedule!
|
63
|
+
while schedule_name = redis.spop(:schedules_changed)
|
64
|
+
if Qu.schedule.keys.include?(schedule_name)
|
65
|
+
Qu::Scheduler.unschedule_job(schedule_name)
|
66
|
+
Qu::Scheduler.load_schedule_job(schedule_name, Qu.schedule[schedule_name])
|
67
|
+
else
|
68
|
+
Qu::Scheduler.unschedule_job(schedule_name)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
Qu::Scheduler.set_process_title "running"
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# Pulls the schedule from Qu.schedule and loads it into the
|
76
|
+
# rufus scheduler instance
|
77
|
+
def load_schedule!
|
78
|
+
Qu::Scheduler.set_process_title "loading schedule"
|
79
|
+
|
80
|
+
# Need to load the schedule from redis for the first time if dynamic
|
81
|
+
Qu.reload_schedule! if Qu::Scheduler.dynamic
|
82
|
+
|
83
|
+
Qu.logger.warning("Schedule empty! Set Qu.schedule") if Qu.schedule.empty?
|
84
|
+
|
85
|
+
@@scheduled_jobs = {}
|
86
|
+
|
87
|
+
Qu.schedule.each do |name, config|
|
88
|
+
Qu::Scheduler.load_schedule_job(name, config)
|
89
|
+
end
|
90
|
+
redis.del(:schedules_changed)
|
91
|
+
Qu::Scheduler.set_process_title "running"
|
92
|
+
end
|
93
|
+
|
94
|
+
# Used internally to stuff the item into the schedule sorted list.
|
95
|
+
# +timestamp+ can be either in seconds or a datetime object
|
96
|
+
# Insertion if O(log(n)).
|
97
|
+
# Returns true if it's the first job to be scheduled at that time, else false
|
98
|
+
def delayed_push(timestamp, payload)
|
99
|
+
# First add this item to the list for this timestamp
|
100
|
+
redis.rpush("delayed:#{timestamp.to_i}", encode('klass' => payload.klass.to_s, 'args' => payload.args))
|
101
|
+
|
102
|
+
# Now, add this timestamp to the zsets. The score and the value are
|
103
|
+
# the same since we'll be querying by timestamp, and we don't have
|
104
|
+
# anything else to store.
|
105
|
+
redis.zadd :delayed_queue_schedule, timestamp.to_i, timestamp.to_i
|
106
|
+
end
|
107
|
+
|
108
|
+
# Returns an array of timestamps based on start and count
|
109
|
+
def delayed_queue_peek(start, count)
|
110
|
+
Array(redis.zrange(:delayed_queue_schedule, start, start+count)).collect{|x| x.to_i}
|
111
|
+
end
|
112
|
+
|
113
|
+
# Returns the size of the delayed queue schedule
|
114
|
+
def delayed_queue_schedule_size
|
115
|
+
redis.zcard :delayed_queue_schedule
|
116
|
+
end
|
117
|
+
|
118
|
+
# Returns the number of jobs for a given timestamp in the delayed queue schedule
|
119
|
+
def delayed_timestamp_size(timestamp)
|
120
|
+
redis.llen("delayed:#{timestamp.to_i}").to_i
|
121
|
+
end
|
122
|
+
|
123
|
+
# Returns the next delayed queue timestamp
|
124
|
+
# (don't call directly)
|
125
|
+
def next_delayed_timestamp(at_time=nil)
|
126
|
+
items = redis.zrangebyscore :delayed_queue_schedule, '-inf', (at_time || Time.now).to_i, :limit => [0, 1]
|
127
|
+
timestamp = items.nil? ? nil : Array(items).first
|
128
|
+
timestamp.to_i unless timestamp.nil?
|
129
|
+
end
|
130
|
+
|
131
|
+
# Returns the next item to be processed for a given timestamp, nil if
|
132
|
+
# done. (don't call directly)
|
133
|
+
# +timestamp+ can either be in seconds or a datetime
|
134
|
+
def next_item_for_timestamp(timestamp)
|
135
|
+
key = "delayed:#{timestamp.to_i}"
|
136
|
+
|
137
|
+
item = decode redis.lpop(key)
|
138
|
+
|
139
|
+
# If the list is empty, remove it.
|
140
|
+
clean_up_timestamp(key, timestamp)
|
141
|
+
item
|
142
|
+
end
|
143
|
+
|
144
|
+
# Clears all jobs created with enqueue_at or enqueue_in
|
145
|
+
def reset_delayed_queue
|
146
|
+
Array(redis.zrange(:delayed_queue_schedule, 0, -1)).each do |item|
|
147
|
+
redis.del "delayed:#{item}"
|
148
|
+
end
|
149
|
+
|
150
|
+
redis.del :delayed_queue_schedule
|
151
|
+
end
|
152
|
+
|
153
|
+
# Given an encoded item, remove it from the delayed_queue
|
154
|
+
#
|
155
|
+
# This method is potentially very expensive since it needs to scan
|
156
|
+
# through the delayed queue for every timestamp.
|
157
|
+
def remove_delayed(klass, *args)
|
158
|
+
destroyed = 0
|
159
|
+
search = encode('klass' => klass.to_s, 'args' => args)
|
160
|
+
Array(redis.keys("delayed:*")).each do |key|
|
161
|
+
destroyed += redis.lrem key, 0, search
|
162
|
+
end
|
163
|
+
destroyed
|
164
|
+
end
|
165
|
+
|
166
|
+
# Given a timestamp and job (klass + args) it removes all instances and
|
167
|
+
# returns the count of jobs removed.
|
168
|
+
#
|
169
|
+
# O(N) where N is the number of jobs scheduled to fire at the given
|
170
|
+
# timestamp
|
171
|
+
def remove_delayed_job_from_timestamp(timestamp, klass, *args)
|
172
|
+
key = "delayed:#{timestamp.to_i}"
|
173
|
+
count = redis.lrem key, 0, encode('klass' => klass.to_s, 'args' => args)
|
174
|
+
clean_up_timestamp(key, timestamp)
|
175
|
+
count
|
176
|
+
end
|
177
|
+
|
178
|
+
def count_all_scheduled_jobs
|
179
|
+
total_jobs = 0
|
180
|
+
Array(redis.zrange(:delayed_queue_schedule, 0, -1)).each do |timestamp|
|
181
|
+
total_jobs += redis.llen("delayed:#{timestamp}").to_i
|
182
|
+
end
|
183
|
+
total_jobs
|
184
|
+
end
|
185
|
+
|
186
|
+
private
|
187
|
+
|
188
|
+
def clean_up_timestamp(key, timestamp)
|
189
|
+
# If the list is empty, remove it.
|
190
|
+
if 0 == redis.llen(key).to_i
|
191
|
+
redis.del key
|
192
|
+
redis.zrem :delayed_queue_schedule, timestamp.to_i
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|