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.
- 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
|
+
|