resque-scheduler 1.9.10 → 2.0.0.a

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.

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