resque-scheduler 1.9.10 → 2.0.0.a

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of resque-scheduler might be problematic. Click here for more details.

data/.gitignore CHANGED
@@ -1,6 +1,3 @@
1
1
  .bundle/
2
- doc/
3
2
  pkg
4
3
  nbproject
5
- Gemfile.lock
6
- .rvmrc
data/Gemfile CHANGED
@@ -1,3 +1,2 @@
1
- source 'https://rubygems.org'
2
-
1
+ source :rubygems
3
2
  gemspec
@@ -0,0 +1,52 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ resque-scheduler (1.9.7)
5
+ redis (>= 2.0.1)
6
+ resque (>= 1.8.0)
7
+ rufus-scheduler
8
+
9
+ GEM
10
+ remote: http://rubygems.org/
11
+ specs:
12
+ git (1.2.5)
13
+ jeweler (1.5.1)
14
+ bundler (~> 1.0.0)
15
+ git (>= 1.2.5)
16
+ rake
17
+ json (1.4.6)
18
+ mocha (0.9.9)
19
+ rake
20
+ rack (1.2.1)
21
+ rack-test (0.5.6)
22
+ rack (>= 1.0)
23
+ rake (0.8.7)
24
+ redis (2.1.1)
25
+ redis-namespace (0.8.0)
26
+ redis (< 3.0.0)
27
+ resque (1.10.0)
28
+ json (~> 1.4.6)
29
+ redis-namespace (~> 0.8.0)
30
+ sinatra (>= 0.9.2)
31
+ vegas (~> 0.1.2)
32
+ rufus-scheduler (2.0.7)
33
+ tzinfo
34
+ sinatra (1.1.0)
35
+ rack (~> 1.1)
36
+ tilt (~> 1.1)
37
+ tilt (1.1)
38
+ tzinfo (0.3.23)
39
+ vegas (0.1.8)
40
+ rack (>= 1.0.0)
41
+
42
+ PLATFORMS
43
+ ruby
44
+
45
+ DEPENDENCIES
46
+ jeweler
47
+ mocha
48
+ rack-test
49
+ redis (>= 2.0.1)
50
+ resque (>= 1.8.0)
51
+ resque-scheduler!
52
+ rufus-scheduler
data/HISTORY.md CHANGED
@@ -1,18 +1,7 @@
1
- ## 1.9.10 (2013-09-19)
1
+ ## 2.0.0 (???)
2
2
 
3
- * Backported `#enqueue_at_with_queue`
4
- * Locking to resque &lt; 1.25.0
5
- * Mocha setup compatibility
6
- * Ruby 1.8 compatibility in scheduler tab when schedule keys are symbols
7
-
8
- ## 1.9.9 (2011-03-29)
9
-
10
- * Compatibility with resque 1.15.0
11
-
12
- ## 1.9.8 (???)
13
-
14
- * Validates delayed jobs prior to insertion into the delayed queue (bogdan)
15
- * Rescue exceptions that occur during queuing and log them (dgrijalva)
3
+ * Dynamic schedule support (brianjlandau, davidyang)
4
+ * Now depends on redis >=1.3
16
5
 
17
6
  ## 1.9.7 (2010-11-09)
18
7
 
@@ -4,7 +4,7 @@ resque-scheduler
4
4
  Resque-scheduler is an extension to [Resque](http://github.com/defunkt/resque)
5
5
  that adds support for queueing items in the future.
6
6
 
7
- Requires redis >=1.1.
7
+ Requires redis >=1.3.
8
8
 
9
9
 
10
10
  Job scheduling is supported in two different way:
@@ -30,9 +30,9 @@ is most likely stored in a YAML like so:
30
30
  args: contributors
31
31
  description: "This job resets the weekly leaderboard for contributions"
32
32
 
33
- A queue option can also be specified. When job will go onto the specified queue
34
- if it is available (Even if @queue is specified in the job class). When the
35
- queue is given it is not necessary for the scheduler to load the class.
33
+ A queue option can also be specified. Then the job will go onto the specified
34
+ queue if it is available (Even if @queue is specified in the job class). When
35
+ the queue is given it is not necessary for the scheduler to load the class.
36
36
 
37
37
  clear_leaderboards_moderator:
38
38
  cron: "30 6 * * 1"
@@ -95,6 +95,86 @@ If you have the need to cancel a delayed job, you can do so thusly:
95
95
  # remove the job with exactly the same parameters:
96
96
  Resque.remove_delayed(SendFollowUpEmail, :user_id => current_user.id)
97
97
 
98
+
99
+ ### Dynamic Schedules
100
+
101
+ If needed you can also have recurring jobs (scheduled) that are dynamically
102
+ defined and updated inside of your application. A good example is if you want
103
+ to allow users to configured when a report is automatically generated. This
104
+ can be completed by loading the schedule initially wherever you configure
105
+ Resque and setting `Resque::Scheduler.dynamic` to `true`. Then subsequently
106
+ updating the "`schedules`" key in redis, namespaced to the Resque namespace.
107
+ The "`schedules`" key is expected to be a redis hash data type, where the key
108
+ is the name of the schedule and the value is a JSON encoded hash of the
109
+ schedule configuration. There are methods on Resque to make this easy (see
110
+ below).
111
+
112
+ When the scheduler loops it will look for differences between the existing
113
+ schedule and the current schedule in redis. If there are differences it will
114
+ make the necessary changes to the running schedule. The schedule names that
115
+ need to be changed are stored in the `schedules_changed` set in redis.
116
+
117
+ To force the scheduler to reload the schedule you just send it the `USR2`
118
+ signal. This will force a complete schedule reload (unscheduling and
119
+ rescheduling everything).
120
+
121
+ To add/update, delete, and retrieve individual schedule items you should
122
+ use the provided API methods:
123
+
124
+ * `Resque.set_schedule(name, config)`
125
+ * `Resque.get_schedule(name)`
126
+ * `Resque.remove_schedule(name)`
127
+
128
+ For example:
129
+
130
+ Resque.set_schedule("create_fake_leaderboards", {
131
+ :cron => "30 6 * * 1",
132
+ :class => "CreateFakeLeaderboards",
133
+ :queue => scoring
134
+ })
135
+
136
+ In this way, it's possible to completely configure your scheduled jobs from
137
+ inside your app if you so desire.
138
+
139
+ ### Support for customized Job classes
140
+
141
+ Some Resque extensions like
142
+ [resque-status](http://github.com/quirkey/resque-status) use custom job
143
+ classes with a slightly different API signature. Resque-scheduler isn't
144
+ trying to support all existing and future custom job classes, instead it
145
+ supports a schedule flag so you can extend your custom class and make it
146
+ support scheduled job.
147
+
148
+ Let's pretend we have a JobWithStatus class called FakeLeaderboard
149
+
150
+ class FakeLeaderboard < Resque::JobWithStatus
151
+ def perform
152
+ # do something and keep track of the status
153
+ end
154
+ end
155
+
156
+ create_fake_leaderboards:
157
+ cron: "30 6 * * 1"
158
+ queue: scoring
159
+ custom_job_class: FakeLeaderboard
160
+ args:
161
+ rails_env: demo
162
+ description: "This job will auto-create leaderboards for our online demo and the status will update as the worker makes progress"
163
+
164
+ If your extension doesn't support scheduled job, you would need to extend the
165
+ custom job class to support the #scheduled method:
166
+
167
+ module Resque
168
+ class JobWithStatus
169
+ # Wrapper API to forward a Resque::Job creation API call into
170
+ # a JobWithStatus call.
171
+ def self.scheduled(queue, klass, *args)
172
+ create(args)
173
+ end
174
+ end
175
+ end
176
+
177
+
98
178
  ### Schedule jobs per environment
99
179
 
100
180
  Resque-Scheduler allows to create schedule jobs for specific envs. The arg
@@ -138,39 +218,6 @@ Multiple envs are allowed, separated by commas:
138
218
  NOTE: If you specify the `rails_env` arg without setting RAILS_ENV as an
139
219
  environment variable, the job won't be loaded.
140
220
 
141
- ### Support for customized Job classes
142
-
143
- Some Resque extensions like [resque-status](http://github.com/quirkey/resque-status) use custom job classes with a slightly different API signature.
144
- Resque-scheduler isn't trying to support all existing and future custom job classes, instead it supports a schedule flag so you can extend your custom class
145
- and make it support scheduled job.
146
-
147
- Let's pretend we have a JobWithStatus class called FakeLeaderboard
148
-
149
- class FakeLeaderboard < Resque::JobWithStatus
150
- def perform
151
- # do something and keep track of the status
152
- end
153
- end
154
-
155
- create_fake_leaderboards:
156
- cron: "30 6 * * 1"
157
- queue: scoring
158
- custom_job_class: FakeLeaderboard
159
- args:
160
- rails_env: demo
161
- description: "This job will auto-create leaderboards for our online demo and the status will update as the worker makes progress"
162
-
163
- If your extension doesn't support scheduled job, you would need to extend the custom job class to support the #scheduled method:
164
-
165
- module Resque
166
- class JobWithStatus
167
- # Wrapper API to forward a Resque::Job creation API call into a JobWithStatus call.
168
- def self.scheduled(queue, klass, *args)
169
- create(args)
170
- end
171
- end
172
- end
173
-
174
221
 
175
222
  Resque-web additions
176
223
  --------------------
@@ -188,8 +235,8 @@ The Delayed tab:
188
235
  ![The Delayed Tab](http://img.skitch.com/20100111-ne4fcqtc5emkcuwc5qtais2kwx.jpg)
189
236
 
190
237
  Get get these to show up you need to pass a file to `resque-web` to tell it to
191
- include the `resque-scheduler` plugin. You probably already have a file somewhere
192
- where you configure `resque`. It probably looks something like this:
238
+ include the `resque-scheduler` plugin. You probably already have a file
239
+ somewhere where you configure `resque`. It probably looks something like this:
193
240
 
194
241
  require 'resque' # include resque so we can configure it
195
242
  Resque.redis = "redis_server:6379" # tell Resque where redis lives
@@ -198,7 +245,10 @@ Now, you want to add the following:
198
245
 
199
246
  require 'resque_scheduler' # include the resque_scheduler (this makes the tabs show up)
200
247
 
201
- And if you have a schedule you want to set, add this:
248
+ As of resque-scheduler 2.0, it's no longer necessary to have the resque-web
249
+ process aware of the schedule because it reads it from redis. But prior to
250
+ 2.0, you'll want to make sure you load the schedule in this file as well.
251
+ Something like this:
202
252
 
203
253
  Resque.schedule = YAML.load_file(File.join(RAILS_ROOT, 'config/resque_schedule.yml')) # load the schedule
204
254
 
@@ -209,7 +259,6 @@ Now make sure you're passing that file to resque-web like so:
209
259
  That should make the scheduler tabs show up in `resque-web`.
210
260
 
211
261
 
212
-
213
262
  Installation and the Scheduler process
214
263
  --------------------------------------
215
264
 
@@ -217,10 +266,15 @@ To install:
217
266
 
218
267
  gem install resque-scheduler
219
268
 
220
- You'll need to add this to your rakefile:
269
+ The unless you specify the `queue` for each scheduled job, the scheduler
270
+ needs to know about your job classes (so it can put them into the appropriate
271
+ queue). To do so, extend the "resque:scheduler_setup" to load your app's code.
272
+ In rails, it would look something like this:
221
273
 
222
274
  require 'resque_scheduler/tasks'
223
- task "resque:setup" => :environment
275
+ task "resque:scheduler_setup" => :environment # load the env so we know about the job classes
276
+
277
+ By default, "resque:scheduler_setup" invokes "resque:setup".
224
278
 
225
279
  The scheduler process is just a rake task which is responsible for both queueing
226
280
  items from the schedule and polling the delayed queue for items ready to be
@@ -230,7 +284,7 @@ pushed on to the work queues. For obvious reasons, this process never exits.
230
284
 
231
285
  Supported environment variables are `VERBOSE` and `MUTE`. If either is set to
232
286
  any nonempty value, they will take effect. `VERBOSE` simply dumps more output
233
- to stdout. `MUTE` does the opposite and silences all output. `MUTE` supercedes
287
+ to stdout. `MUTE` does the opposite and silences all output. `MUTE` supersedes
234
288
  `VERBOSE`.
235
289
 
236
290
  NOTE: You DO NOT want to run >1 instance of the scheduler. Doing so will result
data/Rakefile CHANGED
@@ -1,41 +1,13 @@
1
- require 'bundler/gem_tasks'
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
2
3
 
3
4
  $LOAD_PATH.unshift 'lib'
4
5
 
5
6
  task :default => :test
6
7
 
7
- desc 'Run tests'
8
+ desc "Run tests"
8
9
  task :test do
9
- if RUBY_VERSION =~ /^1\.8/
10
- unless ENV['SEED']
11
- srand
12
- ENV['SEED'] = (srand % 0xFFFF).to_s
13
- end
14
-
15
- $stdout.puts "Running with SEED=#{ENV['SEED']}"
16
- srand Integer(ENV['SEED'])
17
- elsif ENV['SEED']
18
- ARGV += %W(--seed #{ENV['SEED']})
19
- end
20
10
  Dir['test/*_test.rb'].each do |f|
21
11
  require File.expand_path(f)
22
12
  end
23
13
  end
24
-
25
- desc 'Run rubocop'
26
- task :rubocop do
27
- unless RUBY_VERSION < '1.9'
28
- sh('rubocop --config .rubocop.yml --format simple') { |r, _| r || abort }
29
- end
30
- end
31
-
32
- begin
33
- require 'rdoc/task'
34
-
35
- Rake::RDocTask.new do |rd|
36
- rd.main = 'README.md'
37
- rd.rdoc_files.include('README.md', 'lib/**/*.rb')
38
- rd.rdoc_dir = 'doc'
39
- end
40
- rescue LoadError
41
- end
@@ -11,22 +11,32 @@ module Resque
11
11
 
12
12
  # If true, logs more stuff...
13
13
  attr_accessor :verbose
14
-
14
+
15
15
  # If set, produces no output
16
16
  attr_accessor :mute
17
+
18
+ # If set, will try to update the schulde in the loop
19
+ attr_accessor :dynamic
20
+
21
+ # the Rufus::Scheduler jobs that are scheduled
22
+ def scheduled_jobs
23
+ @@scheduled_jobs
24
+ end
17
25
 
18
26
  # Schedule all jobs and continually look for delayed jobs (never returns)
19
27
  def run
20
-
28
+ $0 = "resque-scheduler: Starting"
21
29
  # trap signals
22
30
  register_signal_handlers
23
31
 
24
32
  # Load the schedule into rufus
33
+ procline "Loading Schedule"
25
34
  load_schedule!
26
35
 
27
36
  # Now start the scheduling part of the loop.
28
37
  loop do
29
38
  handle_delayed_items
39
+ update_schedule if dynamic
30
40
  poll_sleep
31
41
  end
32
42
 
@@ -39,12 +49,13 @@ module Resque
39
49
  def register_signal_handlers
40
50
  trap("TERM") { shutdown }
41
51
  trap("INT") { shutdown }
42
-
52
+
43
53
  begin
44
54
  trap('QUIT') { shutdown }
45
55
  trap('USR1') { kill_child }
56
+ trap('USR2') { reload_schedule! }
46
57
  rescue ArgumentError
47
- warn "Signals QUIT and USR1 not supported."
58
+ warn "Signals QUIT and USR1 and USR2 not supported."
48
59
  end
49
60
  end
50
61
 
@@ -52,29 +63,42 @@ module Resque
52
63
  # rufus scheduler instance
53
64
  def load_schedule!
54
65
  log! "Schedule empty! Set Resque.schedule" if Resque.schedule.empty?
55
-
66
+
67
+ @@scheduled_jobs = {}
68
+
56
69
  Resque.schedule.each do |name, config|
57
- # If rails_env is set in the config, enforce ENV['RAILS_ENV'] as
58
- # required for the jobs to be scheduled. If rails_env is missing, the
59
- # job should be scheduled regardless of what ENV['RAILS_ENV'] is set
60
- # to.
61
- if config['rails_env'].nil? || rails_env_matches?(config)
62
- log! "Scheduling #{name} "
63
- interval_defined = false
64
- interval_types = %w{cron every}
65
- interval_types.each do |interval_type|
66
- if !config[interval_type].nil? && config[interval_type].length > 0
67
- rufus_scheduler.send(interval_type, config[interval_type]) do
70
+ load_schedule_job(name, config)
71
+ end
72
+ Resque.redis.del(:schedules_changed)
73
+ procline "Schedules Loaded"
74
+ end
75
+
76
+ # Loads a job schedule into the Rufus::Scheduler and stores it in @@scheduled_jobs
77
+ def load_schedule_job(name, config)
78
+ # If rails_env is set in the config, enforce ENV['RAILS_ENV'] as
79
+ # required for the jobs to be scheduled. If rails_env is missing, the
80
+ # job should be scheduled regardless of what ENV['RAILS_ENV'] is set
81
+ # to.
82
+ if config['rails_env'].nil? || rails_env_matches?(config)
83
+ log! "Scheduling #{name} "
84
+ interval_defined = false
85
+ interval_types = %w{cron every}
86
+ interval_types.each do |interval_type|
87
+ if !config[interval_type].nil? && config[interval_type].length > 0
88
+ begin
89
+ @@scheduled_jobs[name] = rufus_scheduler.send(interval_type, config[interval_type]) do
68
90
  log! "queueing #{config['class']} (#{name})"
69
91
  enqueue_from_config(config)
70
92
  end
71
- interval_defined = true
72
- break
93
+ rescue Exception => e
94
+ log! "#{e.class.name}: #{e.message}"
73
95
  end
96
+ interval_defined = true
97
+ break
74
98
  end
75
- unless interval_defined
76
- log! "no #{interval_types.join(' / ')} found for #{config['class']} (#{name}) - skipping"
77
- end
99
+ end
100
+ unless interval_defined
101
+ log! "no #{interval_types.join(' / ')} found for #{config['class']} (#{name}) - skipping"
78
102
  end
79
103
  end
80
104
  end
@@ -88,33 +112,30 @@ module Resque
88
112
  # Handles queueing delayed items
89
113
  # at_time - Time to start scheduling items (default: now).
90
114
  def handle_delayed_items(at_time=nil)
91
- timestamp = nil
92
- begin
93
- if timestamp = Resque.next_delayed_timestamp(at_time)
115
+ item = nil
116
+ if timestamp = Resque.next_delayed_timestamp(at_time)
117
+ procline "Processing Delayed Items"
118
+ while !timestamp.nil?
94
119
  enqueue_delayed_items_for_timestamp(timestamp)
120
+ timestamp = Resque.next_delayed_timestamp(at_time)
95
121
  end
96
- # continue processing until there are no more ready timestamps
97
- end while !timestamp.nil?
122
+ end
98
123
  end
99
-
124
+
100
125
  # Enqueues all delayed jobs for a timestamp
101
126
  def enqueue_delayed_items_for_timestamp(timestamp)
102
127
  item = nil
103
128
  begin
104
129
  handle_shutdown do
105
130
  if item = Resque.next_item_for_timestamp(timestamp)
106
- begin
107
- log "queuing #{item['class']} [delayed]"
108
- queue = item['queue'] || Resque.queue_from_class(constantize(item['class']))
109
- # Support custom job classes like job with status
110
- if (job_klass = item['custom_job_class']) && (job_klass != 'Resque::Job')
111
- # custom job classes not supporting the same API calls must implement the #schedule method
112
- constantize(job_klass).scheduled(queue, item['class'], *item['args'])
113
- else
114
- Resque::Job.create(queue, item['class'], *item['args'])
115
- end
116
- rescue
117
- log! "Failed to enqueue #{klass_name}:\n #{$!}"
131
+ log "queuing #{item['class']} [delayed]"
132
+ queue = item['queue'] || Resque.queue_from_class(constantize(item['class']))
133
+ # Support custom job classes like job with status
134
+ if (job_klass = item['custom_job_class']) && (job_klass != 'Resque::Job')
135
+ # custom job classes not supporting the same API calls must implement the #schedule method
136
+ constantize(job_klass).scheduled(queue, item['class'], *item['args'])
137
+ else
138
+ Resque::Job.create(queue, item['class'], *item['args'])
118
139
  end
119
140
  end
120
141
  end
@@ -140,9 +161,7 @@ module Resque
140
161
  constantize(job_klass).scheduled(queue, klass_name, *params)
141
162
  else
142
163
  Resque::Job.create(queue, klass_name, *params)
143
- end
144
- rescue
145
- log! "Failed to enqueue #{klass_name}:\n #{$!}"
164
+ end
146
165
  end
147
166
 
148
167
  def rufus_scheduler
@@ -154,8 +173,40 @@ module Resque
154
173
  def clear_schedule!
155
174
  rufus_scheduler.stop
156
175
  @rufus_scheduler = nil
176
+ @@scheduled_jobs = {}
157
177
  rufus_scheduler
158
178
  end
179
+
180
+ def reload_schedule!
181
+ procline "Reloading Schedule"
182
+ clear_schedule!
183
+ Resque.reload_schedule!
184
+ load_schedule!
185
+ end
186
+
187
+ def update_schedule
188
+ if Resque.redis.scard(:schedules_changed) > 0
189
+ procline "Updating schedule"
190
+ Resque.reload_schedule!
191
+ while schedule_name = Resque.redis.spop(:schedules_changed)
192
+ if Resque.schedule.keys.include?(schedule_name)
193
+ unschedule_job(schedule_name)
194
+ load_schedule_job(schedule_name, Resque.schedule[schedule_name])
195
+ else
196
+ unschedule_job(schedule_name)
197
+ end
198
+ end
199
+ procline "Schedules Loaded"
200
+ end
201
+ end
202
+
203
+ def unschedule_job(name)
204
+ if scheduled_jobs[name]
205
+ log "Removing schedule #{name}"
206
+ scheduled_jobs[name].unschedule
207
+ @@scheduled_jobs.delete(name)
208
+ end
209
+ end
159
210
 
160
211
  # Sleeps and returns true
161
212
  def poll_sleep
@@ -179,6 +230,11 @@ module Resque
179
230
  # add "verbose" logic later
180
231
  log!(msg) if verbose
181
232
  end
233
+
234
+ def procline(string)
235
+ $0 = "resque-scheduler-#{ResqueScheduler::Version}: #{string}"
236
+ log! $0
237
+ end
182
238
 
183
239
  end
184
240