thehenster-resque-scheduler 1.0.6

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,3 @@
1
+ *.gemspec
2
+ pkg
3
+ nbproject
data/HISTORY.md ADDED
@@ -0,0 +1,30 @@
1
+ ## 1.0.5 (2010-03-01)
2
+
3
+ * Fixed support for overriding queue from schedule config.
4
+ * Removed resque-web dependency on loading the job classes for "Queue Now",
5
+ provided "queue" is specified in the schedule.
6
+ * The queue is now stored with the job and arguments in the delayed queue so
7
+ there is no longer a need for the scheduler to load job classes to introspect
8
+ the queue.
9
+
10
+ ## 1.0.4 (2010-02-26)
11
+
12
+ * Added support for specifying the queue to put the job onto. This allows for
13
+ you to have one job that can go onto multiple queues and be able to schedule
14
+ jobs without having to load the job classes.
15
+
16
+ ## 1.0.3 (2010-02-11)
17
+
18
+ * Added support for scheduled jobs with empty crons. This is helpful to have
19
+ jobs that you don't want on a schedule, but do want to be able to queue by
20
+ clicking a button.
21
+
22
+ ## 1.0.2 (2010-02-?)
23
+
24
+ * Change Delayed Job tab to display job details if only 1 job exists
25
+ for a given timestamp
26
+
27
+ ## 1.0.1 (2010-01-?)
28
+
29
+ * Bugfix: delayed jobs close together resulted in a 5 second sleep
30
+
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ Copyright (c) 2010 Ben VandenBos
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
data/README.markdown ADDED
@@ -0,0 +1,142 @@
1
+ resque-scheduler
2
+ ===============
3
+
4
+ Resque-scheduler is an extension to [Resque](http://github.com/defunkt/resque)
5
+ that adds support for queueing items in the future.
6
+
7
+ Requires redis >=1.1.
8
+
9
+
10
+ Job scheduling is supported in two different way:
11
+
12
+ ### Recurring (scheduled)
13
+
14
+ Recurring (or scheduled) jobs are logically no different than a standard cron
15
+ job. They are jobs that run based on a fixed schedule which is set at startup.
16
+
17
+ The schedule is a list of Resque worker classes with arguments and a
18
+ schedule frequency (in crontab syntax). The schedule is just a hash, but
19
+ is most likely stored in a YAML like so:
20
+
21
+ queue_documents_for_indexing:
22
+ cron: "0 0 * * *"
23
+ class: QueueDocuments
24
+ args:
25
+ description: "This job queues all content for indexing in solr"
26
+
27
+ clear_leaderboards_contributors:
28
+ cron: "30 6 * * 1"
29
+ class: ClearLeaderboards
30
+ args: contributors
31
+ description: "This job resets the weekly leaderboard for contributions"
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.
36
+
37
+ clear_leaderboards_moderator:
38
+ cron: "30 6 * * 1"
39
+ class: ClearLeaderboards
40
+ queue: scoring
41
+ args: moderators
42
+ description: "This job resets the weekly leaderboard for moderators"
43
+
44
+ And then set the schedule wherever you configure Resque, like so:
45
+
46
+ require 'resque_scheduler'
47
+ Resque.schedule = YAML.load_file(File.join(File.dirname(__FILE__), '../resque_schedule.yml'))
48
+
49
+ Keep in mind, scheduled jobs behave like crons: if your scheduler process (more
50
+ on that later) is not running when a particular job is supposed to be queued,
51
+ it will NOT be ran later when the scheduler process is started back up. In that
52
+ sense, you can sort of think of the scheduler process as crond. Delayed jobs,
53
+ however, are different.
54
+
55
+ A big shout out to [rufus-scheduler](http://github.com/jmettraux/rufus-scheduler)
56
+ for handling the heavy lifting of the actual scheduling engine.
57
+
58
+ ### Delayed jobs
59
+
60
+ Delayed jobs are one-off jobs that you want to be put into a queue at some point
61
+ in the future. The classic example is sending email:
62
+
63
+ Resque.enqueue_at(5.days.from_now, SendFollowUpEmail, :user_id => current_user.id)
64
+
65
+ This will store the job for 5 days in the resque delayed queue at which time the
66
+ scheduler process will pull it from the delayed queue and put it in the
67
+ appropriate work queue for the given job and it will be processed as soon as
68
+ a worker is available.
69
+
70
+ NOTE: The job does not fire **exactly** at the time supplied. Rather, once that
71
+ time is in the past, the job moves from the delayed queue to the actual resque
72
+ work queue and will be completed as workers as free to process it.
73
+
74
+ Also supported is `Resque.enqueue_in` which takes an amount of time in seconds
75
+ in which to queue the job.
76
+
77
+ The delayed queue is stored in redis and is persisted in the same way the
78
+ standard resque jobs are persisted (redis writing to disk). Delayed jobs differ
79
+ from scheduled jobs in that if your scheduler process is down or workers are
80
+ down when a particular job is supposed to be queue, they will simply "catch up"
81
+ once they are started again. Jobs are guaranteed to run (provided they make it
82
+ into the delayed queue) after their given queue_at time has passed.
83
+
84
+ One other thing to note is that insertion into the delayed queue is O(log(n))
85
+ since the jobs are stored in a redis sorted set (zset). I can't imagine this
86
+ being an issue for someone since redis is stupidly fast even at log(n), but full
87
+ disclosure is always best.
88
+
89
+
90
+ Resque-web additions
91
+ --------------------
92
+
93
+ Resque-scheduler also adds to tabs to the resque-web UI. One is for viewing
94
+ (and manually queueing) the schedule and one is for viewing pending jobs in
95
+ the delayed queue.
96
+
97
+ The Schedule tab:
98
+
99
+ ![The Schedule Tab](http://img.skitch.com/20100111-km2f5gmtpbq23enpujbruj6mgk.png)
100
+
101
+ The Delayed tab:
102
+
103
+ ![The Delayed Tab](http://img.skitch.com/20100111-ne4fcqtc5emkcuwc5qtais2kwx.jpg)
104
+
105
+
106
+ Installation and the Scheduler process
107
+ --------------------------------------
108
+
109
+ To install:
110
+
111
+ gem install resque-scheduler
112
+
113
+ You'll need to add this to your rakefile:
114
+
115
+ require 'resque_scheduler/tasks'
116
+ task "resque:setup" => :environment
117
+
118
+ The scheduler process is just a rake task which is responsible for both queueing
119
+ items from the schedule and polling the delayed queue for items ready to be
120
+ pushed on to the work queues. For obvious reasons, this process never exits.
121
+
122
+ $ rake resque:scheduler
123
+
124
+ Supported environment variables are `VERBOSE` and `MUTE`. If either is set to
125
+ any nonempty value, they will take effect. `VERBOSE` simply dumps more output
126
+ to stdout. `MUTE` does the opposite and silences all output. `MUTE` supercedes
127
+ `VERBOSE`.
128
+
129
+
130
+ Plagurism alert
131
+ ---------------
132
+
133
+ This was intended to be an extension to resque and so resulted in a lot of the
134
+ code looking very similar to resque, particularly in resque-web and the views. I
135
+ wanted it to be similar enough that someone familiar with resque could easily
136
+ work on resque-scheduler.
137
+
138
+
139
+ Contributing
140
+ ------------
141
+
142
+ For bugs or suggestions, please just open an issue in github.
data/Rakefile ADDED
@@ -0,0 +1,54 @@
1
+ load 'tasks/resque_scheduler.rake'
2
+
3
+ $LOAD_PATH.unshift 'lib'
4
+
5
+ task :default => :test
6
+
7
+ desc "Run tests"
8
+ task :test do
9
+ Dir['test/*_test.rb'].each do |f|
10
+ require f
11
+ end
12
+ end
13
+
14
+
15
+ desc "Build a gem"
16
+ task :gem => [ :test, :gemspec, :build ]
17
+
18
+ begin
19
+ begin
20
+ require 'jeweler'
21
+ rescue LoadError
22
+ puts "Jeweler not available. Install it with: "
23
+ puts "gem install jeweler"
24
+ end
25
+
26
+ require 'resque_scheduler/version'
27
+
28
+ Jeweler::Tasks.new do |gemspec|
29
+ gemspec.name = "thehenster-resque-scheduler"
30
+ gemspec.summary = "Light weight job scheduling on top of Resque"
31
+ gemspec.description = %{Light weight job scheduling on top of Resque.
32
+ Adds methods enqueue_at/enqueue_in to schedule jobs in the future.
33
+ Also supports queueing jobs on a fixed, cron-like schedule.}
34
+ gemspec.email = "bvandenbos@gmail.com"
35
+ gemspec.homepage = "http://github.com/bvandenbos/resque-scheduler"
36
+ gemspec.authors = ["Ben VandenBos"]
37
+ gemspec.version = ResqueScheduler::Version
38
+
39
+ gemspec.add_dependency "resque", ">= 1.5.0"
40
+ gemspec.add_dependency "rufus-scheduler"
41
+ gemspec.add_development_dependency "jeweler"
42
+ gemspec.add_development_dependency "mocha"
43
+ end
44
+ end
45
+
46
+
47
+ desc "Push a new version to Gemcutter"
48
+ task :publish => [ :test, :gemspec, :build ] do
49
+ system "git tag v#{ResqueScheduler::Version}"
50
+ system "git push origin v#{ResqueScheduler::Version}"
51
+ system "git push origin master"
52
+ system "gem push pkg/resque-scheduler-#{ResqueScheduler::Version}.gem"
53
+ system "git clean -fd"
54
+ end
@@ -0,0 +1,141 @@
1
+ require 'rufus/scheduler'
2
+ require 'thwait'
3
+
4
+ module Resque
5
+
6
+ class Scheduler
7
+
8
+ extend Resque::Helpers
9
+
10
+ class << self
11
+
12
+ # If true, logs more stuff...
13
+ attr_accessor :verbose
14
+
15
+ # If set, produces no output
16
+ attr_accessor :mute
17
+
18
+ # Schedule all jobs and continually look for delayed jobs (never returns)
19
+ def run
20
+
21
+ # trap signals
22
+ register_signal_handlers
23
+
24
+ # Load the schedule into rufus
25
+ load_schedule!
26
+
27
+ # Now start the scheduling part of the loop.
28
+ loop do
29
+ handle_delayed_items
30
+ poll_sleep
31
+ end
32
+
33
+ # never gets here.
34
+ end
35
+
36
+ # For all signals, set the shutdown flag and wait for current
37
+ # poll/enqueing to finish (should be almost istant). In the
38
+ # case of sleeping, exit immediately.
39
+ def register_signal_handlers
40
+ trap("TERM") { shutdown }
41
+ trap("INT") { shutdown }
42
+ trap('QUIT') { shutdown } unless defined? JRUBY_VERSION
43
+ end
44
+
45
+ # Pulls the schedule from Resque.schedule and loads it into the
46
+ # rufus scheduler instance
47
+ def load_schedule!
48
+ log! "Schedule empty! Set Resque.schedule" if Resque.schedule.empty?
49
+
50
+ Resque.schedule.each do |name, config|
51
+ log! "Scheduling #{name} "
52
+ if !config['cron'].nil? && config['cron'].length > 0
53
+ rufus_scheduler.cron config['cron'] do
54
+ log! "queuing #{config['class']} (#{name})"
55
+ enqueue_from_config(config)
56
+ end
57
+ else
58
+ log! "no cron found for #{config['class']} (#{name}) - skipping"
59
+ end
60
+ end
61
+ end
62
+
63
+ # Handles queueing delayed items
64
+ def handle_delayed_items
65
+ item = nil
66
+ begin
67
+ if timestamp = Resque.next_delayed_timestamp
68
+ item = nil
69
+ begin
70
+ handle_shutdown do
71
+ if item = Resque.next_item_for_timestamp(timestamp)
72
+ log "queuing #{item['class']} [delayed]"
73
+ queue = item['queue'] || Resque.queue_from_class(constantize(item['class']))
74
+ Job.create(queue, item['class'], *item['args'])
75
+ end
76
+ end
77
+ # continue processing until there are no more ready items in this timestamp
78
+ end while !item.nil?
79
+ end
80
+ # continue processing until there are no more ready timestamps
81
+ end while !timestamp.nil?
82
+ end
83
+
84
+ def handle_shutdown
85
+ exit if @shutdown
86
+ yield
87
+ exit if @shutdown
88
+ end
89
+
90
+ # Enqueues a job based on a config hash
91
+ def enqueue_from_config(config)
92
+ args = config['args'] || config[:args]
93
+ klass_name = config['class'] || config[:class]
94
+ params = args.nil? ? [] : Array(args)
95
+ queue = config['queue'] || config[:queue] || Resque.queue_from_class(constantize(klass_name))
96
+ if config[:just_once]
97
+ Resque::Job.destroy(queue, klass_name, *params)
98
+ end
99
+ Resque::Job.create(queue, klass_name, *params)
100
+ end
101
+
102
+ def rufus_scheduler
103
+ @rufus_scheduler ||= Rufus::Scheduler.start_new
104
+ end
105
+
106
+ # Stops old rufus scheduler and creates a new one. Returns the new
107
+ # rufus scheduler
108
+ def clear_schedule!
109
+ rufus_scheduler.stop
110
+ @rufus_scheduler = nil
111
+ rufus_scheduler
112
+ end
113
+
114
+ # Sleeps and returns true
115
+ def poll_sleep
116
+ @sleeping = true
117
+ handle_shutdown { sleep 5 }
118
+ @sleeping = false
119
+ true
120
+ end
121
+
122
+ # Sets the shutdown flag, exits if sleeping
123
+ def shutdown
124
+ @shutdown = true
125
+ exit if @sleeping
126
+ end
127
+
128
+ def log!(msg)
129
+ puts "#{Time.now.strftime("%Y-%m-%d %H:%M:%S")} #{msg}" unless mute
130
+ end
131
+
132
+ def log(msg)
133
+ # add "verbose" logic later
134
+ log!(msg) if verbose
135
+ end
136
+
137
+ end
138
+
139
+ end
140
+
141
+ end
@@ -0,0 +1,117 @@
1
+ require 'rubygems'
2
+ require 'resque'
3
+ require 'resque/server'
4
+ require 'resque_scheduler/version'
5
+ require 'resque/scheduler'
6
+ require 'resque_scheduler/server'
7
+
8
+ module ResqueScheduler
9
+
10
+ #
11
+ # Accepts a new schedule configuration of the form:
12
+ #
13
+ # {some_name => {"cron" => "5/* * * *",
14
+ # "class" => DoSomeWork,
15
+ # "args" => "work on this string",
16
+ # "description" => "this thing works it"s butter off"},
17
+ # ...}
18
+ #
19
+ # :name can be anything and is used only to describe the scheduled job
20
+ # :cron can be any cron scheduling string :job can be any resque job class
21
+ # :class must be a resque worker class
22
+ # :args can be any yaml which will be converted to a ruby literal and passed
23
+ # in a params. (optional)
24
+ # :description is just that, a description of the job (optional). If params is
25
+ # an array, each element in the array is passed as a separate param,
26
+ # otherwise params is passed in as the only parameter to perform.
27
+ def schedule=(schedule_hash)
28
+ redis[:resque_schedule_hash] = schedule_hash.to_yaml
29
+ end
30
+
31
+ # Returns the schedule hash
32
+ def schedule
33
+ YAML.load(redis[:resque_schedule_hash])
34
+ end
35
+
36
+ # This method is nearly identical to +enqueue+ only it also
37
+ # takes a timestamp which will be used to schedule the job
38
+ # for queueing. Until timestamp is in the past, the job will
39
+ # sit in the schedule list.
40
+ def enqueue_at(timestamp, klass, *args)
41
+ delayed_push(timestamp, :class => klass.to_s, :args => args, :queue => queue_from_class(klass))
42
+ end
43
+
44
+ # Identical to enqueue_at but takes number_of_seconds_from_now
45
+ # instead of a timestamp.
46
+ def enqueue_in(number_of_seconds_from_now, klass, *args)
47
+ enqueue_at(Time.now + number_of_seconds_from_now, klass, *args)
48
+ end
49
+
50
+ # Used internally to stuff the item into the schedule sorted list.
51
+ # +timestamp+ can be either in seconds or a datetime object
52
+ # Insertion if O(log(n)).
53
+ # Returns true if it's the first job to be scheduled at that time, else false
54
+ def delayed_push(timestamp, item)
55
+ # First add this item to the list for this timestamp
56
+ redis.rpush("delayed:#{timestamp.to_i}", encode(item))
57
+
58
+ # Now, add this timestamp to the zsets. The score and the value are
59
+ # the same since we'll be querying by timestamp, and we don't have
60
+ # anything else to store.
61
+ redis.zadd :delayed_queue_schedule, timestamp.to_i, timestamp.to_i
62
+ end
63
+
64
+ # Returns an array of timestamps based on start and count
65
+ def delayed_queue_peek(start, count)
66
+ redis.zrange(:delayed_queue_schedule, start, start+count).collect(&:to_i)
67
+ end
68
+
69
+ # Returns the size of the delayed queue schedule
70
+ def delayed_queue_schedule_size
71
+ redis.zcard :delayed_queue_schedule
72
+ end
73
+
74
+ # Returns the number of jobs for a given timestamp in the delayed queue schedule
75
+ def delayed_timestamp_size(timestamp)
76
+ redis.llen("delayed:#{timestamp.to_i}").to_i
77
+ end
78
+
79
+ # Returns an array of delayed items for the given timestamp
80
+ def delayed_timestamp_peek(timestamp, start, count)
81
+ if 1 == count
82
+ r = list_range "delayed:#{timestamp.to_i}", start, count
83
+ r.nil? ? [] : [r]
84
+ else
85
+ list_range "delayed:#{timestamp.to_i}", start, count
86
+ end
87
+ end
88
+
89
+ # Returns the next delayed queue timestamp
90
+ # (don't call directly)
91
+ def next_delayed_timestamp
92
+ timestamp = redis.zrangebyscore(:delayed_queue_schedule, '-inf', Time.now.to_i, 'limit', 0, 1).first
93
+ timestamp.to_i unless timestamp.nil?
94
+ end
95
+
96
+ # Returns the next item to be processed for a given timestamp, nil if
97
+ # done. (don't call directly)
98
+ # +timestamp+ can either be in seconds or a datetime
99
+ def next_item_for_timestamp(timestamp)
100
+ key = "delayed:#{timestamp.to_i}"
101
+
102
+ item = decode redis.lpop(key)
103
+
104
+ # If the list is empty, remove it.
105
+ if 0 == redis.llen(key).to_i
106
+ redis.del key
107
+ redis.zrem :delayed_queue_schedule, timestamp.to_i
108
+ end
109
+ item
110
+ end
111
+
112
+ end
113
+
114
+ Resque.extend ResqueScheduler
115
+ Resque::Server.class_eval do
116
+ include ResqueScheduler::Server
117
+ end
@@ -0,0 +1,51 @@
1
+
2
+ # Extend Resque::Server to add tabs
3
+ module ResqueScheduler
4
+
5
+ module Server
6
+
7
+ def self.included(base)
8
+
9
+ base.class_eval do
10
+
11
+ helpers do
12
+ def format_time(t)
13
+ t.strftime("%Y-%m-%d %H:%M:%S")
14
+ end
15
+
16
+ def queue_from_class_name(class_name)
17
+ Resque.queue_from_class(Resque.constantize(class_name))
18
+ end
19
+ end
20
+
21
+ get "/schedule" do
22
+ # Is there a better way to specify alternate template locations with sinatra?
23
+ erb File.read(File.join(File.dirname(__FILE__), 'server/views/scheduler.erb'))
24
+ end
25
+
26
+ post "/schedule/requeue" do
27
+ config = Resque.schedule[params['job_name']]
28
+ Resque::Scheduler.enqueue_from_config(config)
29
+ redirect url("/overview")
30
+ end
31
+
32
+ get "/delayed" do
33
+ # Is there a better way to specify alternate template locations with sinatra?
34
+ erb File.read(File.join(File.dirname(__FILE__), 'server/views/delayed.erb'))
35
+ end
36
+
37
+ get "/delayed/:timestamp" do
38
+ # Is there a better way to specify alternate template locations with sinatra?
39
+ erb File.read(File.join(File.dirname(__FILE__), 'server/views/delayed_timestamp.erb'))
40
+ end
41
+
42
+ end
43
+
44
+ end
45
+
46
+ Resque::Server.tabs << 'Schedule'
47
+ Resque::Server.tabs << 'Delayed'
48
+
49
+ end
50
+
51
+ end
@@ -0,0 +1,35 @@
1
+ <h1>Delayed Jobs</h1>
2
+
3
+ <p class='intro'>
4
+ This list below contains the timestamps for scheduled delayed jobs.
5
+ </p>
6
+
7
+ <p class='sub'>
8
+ Showing <%= start = params[:start].to_i %> to <%= start + 20 %> of <b><%=size = resque.delayed_queue_schedule_size %></b> timestamps
9
+ </p>
10
+
11
+ <table>
12
+ <tr>
13
+ <th>Timestamp</th>
14
+ <th>Job count</th>
15
+ <th>Class</th>
16
+ <th>Args</th>
17
+ </tr>
18
+ <% resque.delayed_queue_peek(start, start+20).each do |timestamp| %>
19
+ <tr>
20
+ <td><a href="<%= url "delayed/#{timestamp}" %>"><%= format_time(Time.at(timestamp)) %></a></td>
21
+ <td><%= delayed_timestamp_size = resque.delayed_timestamp_size(timestamp) %></td>
22
+ <% job = resque.delayed_timestamp_peek(timestamp, 0, 1).first %>
23
+ <td>
24
+ <% if job && delayed_timestamp_size == 1 %>
25
+ <%= h(job['class']) %>
26
+ <% else %>
27
+ <a href="<%= url "delayed/#{timestamp}" %>">see details</a>
28
+ <% end %>
29
+ </td>
30
+ <td><%= h(job['args'].inspect) if job && delayed_timestamp_size == 1 %></td>
31
+ </tr>
32
+ <% end %>
33
+ </table>
34
+
35
+ <%= partial :next_more, :start => start, :size => size %>
@@ -0,0 +1,26 @@
1
+ <% timestamp = params[:timestamp].to_i %>
2
+
3
+ <h1>Delayed jobs scheduled for <%= format_time(Time.at(timestamp)) %></h1>
4
+
5
+ <p class='sub'>Showing <%= start = params[:start].to_i %> to <%= start + 20 %> of <b><%=size = resque.delayed_timestamp_size(timestamp)%></b> jobs</p>
6
+
7
+ <table class='jobs'>
8
+ <tr>
9
+ <th>Class</th>
10
+ <th>Args</th>
11
+ </tr>
12
+ <% jobs = resque.delayed_timestamp_peek(timestamp, start, 20) %>
13
+ <% jobs.each do |job| %>
14
+ <tr>
15
+ <td class='class'><%= job['class'] %></td>
16
+ <td class='args'><%=h job['args'].inspect %></td>
17
+ </tr>
18
+ <% end %>
19
+ <% if jobs.empty? %>
20
+ <tr>
21
+ <td class='no-data' colspan='2'>There are no pending jobs scheduled for this time.</td>
22
+ </tr>
23
+ <% end %>
24
+ </table>
25
+
26
+ <%= partial :next_more, :start => start, :size => size %>
@@ -0,0 +1,34 @@
1
+ <h1>Schedule</h1>
2
+
3
+ <p class='intro'>
4
+ The list below contains all scheduled jobs. Click &quot;Queue now&quot; to queue
5
+ a job immediately.
6
+ </p>
7
+
8
+ <table>
9
+ <tr>
10
+ <th></th>
11
+ <th>Name</th>
12
+ <th>Description</th>
13
+ <th>Cron</th>
14
+ <th>Class</th>
15
+ <th>Queue</th>
16
+ <th>Arguments</th>
17
+ </tr>
18
+ <% Resque.schedule.each do |name, config| %>
19
+ <tr>
20
+ <td>
21
+ <form action="<%= url "/schedule/requeue" %>" method="post">
22
+ <input type="hidden" name="job_name" value="<%= h name %>">
23
+ <input type="submit" value="Queue now">
24
+ </form>
25
+ </td>
26
+ <td><%= h name %></td>
27
+ <td><%= h config['description'] %></td>
28
+ <td style="white-space:nowrap"><%= h config['cron'] %></td>
29
+ <td><%= h config['class'] %></td>
30
+ <td><%= h config['queue'] || queue_from_class_name(config['class']) %></td>
31
+ <td><%= h config['args'].inspect %></td>
32
+ </tr>
33
+ <% end %>
34
+ </table>
@@ -0,0 +1,16 @@
1
+ # require 'resque/tasks'
2
+ # will give you the resque tasks
3
+
4
+ namespace :resque do
5
+ task :setup
6
+
7
+ desc "Start Resque Scheduler"
8
+ task :scheduler => :setup do
9
+ require 'resque'
10
+ require 'resque_scheduler'
11
+
12
+ Resque::Scheduler.verbose = true if ENV['VERBOSE']
13
+ Resque::Scheduler.run
14
+ end
15
+
16
+ end
@@ -0,0 +1,3 @@
1
+ module ResqueScheduler
2
+ Version = '1.0.6'
3
+ end
@@ -0,0 +1,2 @@
1
+ $LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
2
+ require 'resque_scheduler/tasks'
@@ -0,0 +1,132 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+
3
+ class Resque::DelayedQueueTest < Test::Unit::TestCase
4
+
5
+ def setup
6
+ Resque::Scheduler.mute = true
7
+ Resque.redis.flush_all
8
+ end
9
+
10
+ def test_enqueue_at_adds_correct_list_and_zset
11
+
12
+ timestamp = Time.now - 1 # 1 second ago (in the past, should come out right away)
13
+
14
+ assert_equal(0, Resque.redis.llen("delayed:#{timestamp.to_i}").to_i, "delayed queue should be empty to start")
15
+
16
+ Resque.enqueue_at(timestamp, SomeIvarJob, "path")
17
+
18
+ # Confirm the correct keys were added
19
+ assert_equal(1, Resque.redis.llen("delayed:#{timestamp.to_i}").to_i, "delayed queue should have one entry now")
20
+ assert_equal(1, Resque.redis.zcard(:delayed_queue_schedule), "The delayed_queue_schedule should have 1 entry now")
21
+
22
+ read_timestamp = Resque.next_delayed_timestamp
23
+
24
+ # Confirm the timestamp came out correctly
25
+ assert_equal(timestamp.to_i, read_timestamp, "The timestamp we pull out of redis should match the one we put in")
26
+ item = Resque.next_item_for_timestamp(read_timestamp)
27
+
28
+ # Confirm the item came out correctly
29
+ assert_equal('SomeIvarJob', item['class'], "Should be the same class that we queued")
30
+ assert_equal(["path"], item['args'], "Should have the same arguments that we queued")
31
+
32
+ # And now confirm the keys are gone
33
+ assert(!Resque.redis.exists("delayed:#{timestamp.to_i}"))
34
+ assert_equal(0, Resque.redis.zcard(:delayed_queue_schedule), "delayed queue should be empty")
35
+ end
36
+
37
+ def test_something_in_the_future_doesnt_come_out
38
+ timestamp = Time.now + 600 # 10 minutes from now (in the future, shouldn't come out)
39
+
40
+ assert_equal(0, Resque.redis.llen("delayed:#{timestamp.to_i}").to_i, "delayed queue should be empty to start")
41
+
42
+ Resque.enqueue_at(timestamp, SomeIvarJob, "path")
43
+
44
+ # Confirm the correct keys were added
45
+ assert_equal(1, Resque.redis.llen("delayed:#{timestamp.to_i}").to_i, "delayed queue should have one entry now")
46
+ assert_equal(1, Resque.redis.zcard(:delayed_queue_schedule), "The delayed_queue_schedule should have 1 entry now")
47
+
48
+ read_timestamp = Resque.next_delayed_timestamp
49
+
50
+ assert_nil(read_timestamp, "No timestamps should be ready for queueing")
51
+ end
52
+
53
+ def test_enqueue_at_and_enqueue_in_are_equivelent
54
+ timestamp = Time.now + 60
55
+
56
+ Resque.enqueue_at(timestamp, SomeIvarJob, "path")
57
+ Resque.enqueue_in(timestamp - Time.now, SomeIvarJob, "path")
58
+
59
+ assert_equal(1, Resque.redis.zcard(:delayed_queue_schedule), "should have one timestamp in the delayed queue")
60
+ assert_equal(2, Resque.redis.llen("delayed:#{timestamp.to_i}"), "should have 2 items in the timestamp queue")
61
+ end
62
+
63
+ def test_delayed_queue_peek
64
+ t = Time.now
65
+ expected_timestamps = (1..5).to_a.map do |i|
66
+ (t + 60 + i).to_i
67
+ end
68
+
69
+ expected_timestamps.each do |timestamp|
70
+ Resque.delayed_push(timestamp, {:class => SomeIvarJob, :args => 'blah1'})
71
+ end
72
+
73
+ timestamps = Resque.delayed_queue_peek(2,3)
74
+
75
+ assert_equal(expected_timestamps[2,3], timestamps)
76
+ end
77
+
78
+ def test_delayed_queue_schedule_size
79
+ assert_equal(0, Resque.delayed_queue_schedule_size)
80
+ Resque.enqueue_at(Time.now+60, SomeIvarJob)
81
+ assert_equal(1, Resque.delayed_queue_schedule_size)
82
+ end
83
+
84
+ def test_delayed_timestamp_size
85
+ t = Time.now + 60
86
+ assert_equal(0, Resque.delayed_timestamp_size(t))
87
+ Resque.enqueue_at(t, SomeIvarJob)
88
+ assert_equal(1, Resque.delayed_timestamp_size(t))
89
+ assert_equal(0, Resque.delayed_timestamp_size(t.to_i+1))
90
+ end
91
+
92
+ def test_delayed_timestamp_peek
93
+ t = Time.now + 60
94
+ assert_equal([], Resque.delayed_timestamp_peek(t, 0, 1), "make sure it's an empty array, not nil")
95
+ Resque.enqueue_at(t, SomeIvarJob)
96
+ assert_equal(1, Resque.delayed_timestamp_peek(t, 0, 1).length)
97
+ Resque.enqueue_at(t, SomeIvarJob)
98
+ assert_equal(1, Resque.delayed_timestamp_peek(t, 0, 1).length)
99
+ assert_equal(2, Resque.delayed_timestamp_peek(t, 0, 3).length)
100
+
101
+ assert_equal({'args' => [], 'class' => 'SomeIvarJob', 'queue' => 'ivar'}, Resque.delayed_timestamp_peek(t, 0, 1).first)
102
+ end
103
+
104
+ def test_handle_delayed_items_with_no_items
105
+ Resque::Scheduler.expects(:enqueue).never
106
+ Resque::Scheduler.handle_delayed_items
107
+ end
108
+
109
+ def test_handle_delayed_items_with_items
110
+ t = Time.now - 60 # in the past
111
+ Resque.enqueue_at(t, SomeIvarJob)
112
+ Resque.enqueue_at(t, SomeIvarJob)
113
+
114
+ # 2 SomeIvarJob jobs should be created in the "ivar" queue
115
+ Resque::Job.expects(:create).twice.with('ivar', 'SomeIvarJob', nil)
116
+ Resque.expects(:queue_from_class).never # Should NOT need to load the class
117
+ Resque::Scheduler.handle_delayed_items
118
+ end
119
+
120
+ def test_works_with_out_specifying_queue__upgrade_case
121
+ t = Time.now - 60
122
+ Resque.delayed_push(t, :class => 'SomeIvarJob')
123
+
124
+ # Since we didn't specify :queue when calling delayed_push, it will be forced
125
+ # to load the class to figure out the queue. This is the upgrade case from 1.0.4
126
+ # to 1.0.5.
127
+ Resque::Job.expects(:create).once.with(:ivar, 'SomeIvarJob', nil)
128
+
129
+ Resque::Scheduler.handle_delayed_items
130
+ end
131
+
132
+ end
@@ -0,0 +1,132 @@
1
+ # Redis configuration file example
2
+
3
+ # By default Redis does not run as a daemon. Use 'yes' if you need it.
4
+ # Note that Redis will write a pid file in /var/run/redis.pid when daemonized.
5
+ daemonize yes
6
+
7
+ # When run as a daemon, Redis write a pid file in /var/run/redis.pid by default.
8
+ # You can specify a custom pid file location here.
9
+ pidfile ./test/redis-test.pid
10
+
11
+ # Accept connections on the specified port, default is 6379
12
+ port 9736
13
+
14
+ # If you want you can bind a single interface, if the bind option is not
15
+ # specified all the interfaces will listen for connections.
16
+ #
17
+ # bind 127.0.0.1
18
+
19
+ # Close the connection after a client is idle for N seconds (0 to disable)
20
+ timeout 300
21
+
22
+ # Save the DB on disk:
23
+ #
24
+ # save <seconds> <changes>
25
+ #
26
+ # Will save the DB if both the given number of seconds and the given
27
+ # number of write operations against the DB occurred.
28
+ #
29
+ # In the example below the behaviour will be to save:
30
+ # after 900 sec (15 min) if at least 1 key changed
31
+ # after 300 sec (5 min) if at least 10 keys changed
32
+ # after 60 sec if at least 10000 keys changed
33
+ save 900 1
34
+ save 300 10
35
+ save 60 10000
36
+
37
+ # The filename where to dump the DB
38
+ dbfilename dump.rdb
39
+
40
+ # For default save/load DB in/from the working directory
41
+ # Note that you must specify a directory not a file name.
42
+ dir ./test/
43
+
44
+ # Set server verbosity to 'debug'
45
+ # it can be one of:
46
+ # debug (a lot of information, useful for development/testing)
47
+ # notice (moderately verbose, what you want in production probably)
48
+ # warning (only very important / critical messages are logged)
49
+ loglevel debug
50
+
51
+ # Specify the log file name. Also 'stdout' can be used to force
52
+ # the demon to log on the standard output. Note that if you use standard
53
+ # output for logging but daemonize, logs will be sent to /dev/null
54
+ logfile stdout
55
+
56
+ # Set the number of databases. The default database is DB 0, you can select
57
+ # a different one on a per-connection basis using SELECT <dbid> where
58
+ # dbid is a number between 0 and 'databases'-1
59
+ databases 16
60
+
61
+ ################################# REPLICATION #################################
62
+
63
+ # Master-Slave replication. Use slaveof to make a Redis instance a copy of
64
+ # another Redis server. Note that the configuration is local to the slave
65
+ # so for example it is possible to configure the slave to save the DB with a
66
+ # different interval, or to listen to another port, and so on.
67
+
68
+ # slaveof <masterip> <masterport>
69
+
70
+ ################################## SECURITY ###################################
71
+
72
+ # Require clients to issue AUTH <PASSWORD> before processing any other
73
+ # commands. This might be useful in environments in which you do not trust
74
+ # others with access to the host running redis-server.
75
+ #
76
+ # This should stay commented out for backward compatibility and because most
77
+ # people do not need auth (e.g. they run their own servers).
78
+
79
+ # requirepass foobared
80
+
81
+ ################################### LIMITS ####################################
82
+
83
+ # Set the max number of connected clients at the same time. By default there
84
+ # is no limit, and it's up to the number of file descriptors the Redis process
85
+ # is able to open. The special value '0' means no limts.
86
+ # Once the limit is reached Redis will close all the new connections sending
87
+ # an error 'max number of clients reached'.
88
+
89
+ # maxclients 128
90
+
91
+ # Don't use more memory than the specified amount of bytes.
92
+ # When the memory limit is reached Redis will try to remove keys with an
93
+ # EXPIRE set. It will try to start freeing keys that are going to expire
94
+ # in little time and preserve keys with a longer time to live.
95
+ # Redis will also try to remove objects from free lists if possible.
96
+ #
97
+ # If all this fails, Redis will start to reply with errors to commands
98
+ # that will use more memory, like SET, LPUSH, and so on, and will continue
99
+ # to reply to most read-only commands like GET.
100
+ #
101
+ # WARNING: maxmemory can be a good idea mainly if you want to use Redis as a
102
+ # 'state' server or cache, not as a real DB. When Redis is used as a real
103
+ # database the memory usage will grow over the weeks, it will be obvious if
104
+ # it is going to use too much memory in the long run, and you'll have the time
105
+ # to upgrade. With maxmemory after the limit is reached you'll start to get
106
+ # errors for write operations, and this may even lead to DB inconsistency.
107
+
108
+ # maxmemory <bytes>
109
+
110
+ ############################### ADVANCED CONFIG ###############################
111
+
112
+ # Glue small output buffers together in order to send small replies in a
113
+ # single TCP packet. Uses a bit more CPU but most of the times it is a win
114
+ # in terms of number of queries per second. Use 'yes' if unsure.
115
+ glueoutputbuf yes
116
+
117
+ # Use object sharing. Can save a lot of memory if you have many common
118
+ # string in your dataset, but performs lookups against the shared objects
119
+ # pool so it uses more CPU and can be a bit slower. Usually it's a good
120
+ # idea.
121
+ #
122
+ # When object sharing is enabled (shareobjects yes) you can use
123
+ # shareobjectspoolsize to control the size of the pool used in order to try
124
+ # object sharing. A bigger pool size will lead to better sharing capabilities.
125
+ # In general you want this value to be at least the double of the number of
126
+ # very common strings you have in your dataset.
127
+ #
128
+ # WARNING: object sharing is experimental, don't enable this feature
129
+ # in production before of Redis 1.0-stable. Still please try this feature in
130
+ # your development environment so that we can test it better.
131
+ shareobjects no
132
+ shareobjectspoolsize 1024
@@ -0,0 +1,28 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+
3
+ class Resque::SchedulerTest < Test::Unit::TestCase
4
+
5
+ def setup
6
+ Resque::Scheduler.clear_schedule!
7
+ end
8
+
9
+ def test_enqueue_from_config_puts_stuff_in_the_resque_queue_without_class_loaded
10
+ Resque::Job.stubs(:create).once.returns(true).with('joes_queue', 'BigJoesJob', '/tmp')
11
+ Resque::Scheduler.enqueue_from_config('cron' => "* * * * *", 'class' => 'BigJoesJob', 'args' => "/tmp", 'queue' => 'joes_queue')
12
+ end
13
+
14
+ def test_enqueue_from_config_puts_stuff_in_the_resque_queue
15
+ Resque::Job.stubs(:create).once.returns(true).with(:ivar, 'SomeIvarJob', '/tmp')
16
+ Resque::Scheduler.enqueue_from_config('cron' => "* * * * *", 'class' => 'SomeIvarJob', 'args' => "/tmp")
17
+ end
18
+
19
+ def test_config_makes_it_into_the_rufus_scheduler
20
+ assert_equal(0, Resque::Scheduler.rufus_scheduler.all_jobs.size)
21
+
22
+ Resque.schedule = {:some_ivar_job => {'cron' => "* * * * *", 'class' => 'SomeIvarJob', 'args' => "/tmp"}}
23
+ Resque::Scheduler.load_schedule!
24
+
25
+ assert_equal(1, Resque::Scheduler.rufus_scheduler.all_jobs.size)
26
+ end
27
+
28
+ end
@@ -0,0 +1,60 @@
1
+
2
+ # Pretty much copied this file from the resque test_helper since we want
3
+ # to do all the same stuff
4
+
5
+ dir = File.dirname(File.expand_path(__FILE__))
6
+
7
+ require 'rubygems'
8
+ require 'test/unit'
9
+ require 'mocha'
10
+ require 'resque'
11
+ require File.join(dir, '../lib/resque_scheduler')
12
+ $LOAD_PATH.unshift File.dirname(File.expand_path(__FILE__)) + '/../lib'
13
+
14
+
15
+ #
16
+ # make sure we can run redis
17
+ #
18
+
19
+ if !system("which redis-server")
20
+ puts '', "** can't find `redis-server` in your path"
21
+ puts "** try running `sudo rake install`"
22
+ abort ''
23
+ end
24
+
25
+
26
+ #
27
+ # start our own redis when the tests start,
28
+ # kill it when they end
29
+ #
30
+
31
+ at_exit do
32
+ next if $!
33
+
34
+ if defined?(MiniTest)
35
+ exit_code = MiniTest::Unit.new.run(ARGV)
36
+ else
37
+ exit_code = Test::Unit::AutoRunner.run
38
+ end
39
+
40
+ pid = `ps -e -o pid,command | grep [r]edis-test`.split(" ")[0]
41
+ puts "Killing test redis server..."
42
+ `rm -f #{dir}/dump.rdb`
43
+ Process.kill("KILL", pid.to_i)
44
+ exit exit_code
45
+ end
46
+
47
+ puts "Starting redis for testing at localhost:9736..."
48
+ `redis-server #{dir}/redis-test.conf`
49
+ Resque.redis = 'localhost:9736'
50
+
51
+
52
+
53
+ class SomeJob
54
+ def self.perform(repo_id, path)
55
+ end
56
+ end
57
+
58
+ class SomeIvarJob < SomeJob
59
+ @queue = :ivar
60
+ end
metadata ADDED
@@ -0,0 +1,134 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: thehenster-resque-scheduler
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 1
7
+ - 0
8
+ - 6
9
+ version: 1.0.6
10
+ platform: ruby
11
+ authors:
12
+ - Ben VandenBos
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-03-29 00:00:00 +01:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: resque
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ segments:
28
+ - 1
29
+ - 5
30
+ - 0
31
+ version: 1.5.0
32
+ type: :runtime
33
+ version_requirements: *id001
34
+ - !ruby/object:Gem::Dependency
35
+ name: rufus-scheduler
36
+ prerelease: false
37
+ requirement: &id002 !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ segments:
42
+ - 0
43
+ version: "0"
44
+ type: :runtime
45
+ version_requirements: *id002
46
+ - !ruby/object:Gem::Dependency
47
+ name: jeweler
48
+ prerelease: false
49
+ requirement: &id003 !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ segments:
54
+ - 0
55
+ version: "0"
56
+ type: :development
57
+ version_requirements: *id003
58
+ - !ruby/object:Gem::Dependency
59
+ name: mocha
60
+ prerelease: false
61
+ requirement: &id004 !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ segments:
66
+ - 0
67
+ version: "0"
68
+ type: :development
69
+ version_requirements: *id004
70
+ description: |-
71
+ Light weight job scheduling on top of Resque.
72
+ Adds methods enqueue_at/enqueue_in to schedule jobs in the future.
73
+ Also supports queueing jobs on a fixed, cron-like schedule.
74
+ email: bvandenbos@gmail.com
75
+ executables: []
76
+
77
+ extensions: []
78
+
79
+ extra_rdoc_files:
80
+ - LICENSE
81
+ - README.markdown
82
+ files:
83
+ - .gitignore
84
+ - HISTORY.md
85
+ - LICENSE
86
+ - README.markdown
87
+ - Rakefile
88
+ - lib/resque/scheduler.rb
89
+ - lib/resque_scheduler.rb
90
+ - lib/resque_scheduler/server.rb
91
+ - lib/resque_scheduler/server/views/delayed.erb
92
+ - lib/resque_scheduler/server/views/delayed_timestamp.erb
93
+ - lib/resque_scheduler/server/views/scheduler.erb
94
+ - lib/resque_scheduler/tasks.rb
95
+ - lib/resque_scheduler/version.rb
96
+ - tasks/resque_scheduler.rake
97
+ - test/delayed_queue_test.rb
98
+ - test/redis-test.conf
99
+ - test/scheduler_test.rb
100
+ - test/test_helper.rb
101
+ has_rdoc: true
102
+ homepage: http://github.com/bvandenbos/resque-scheduler
103
+ licenses: []
104
+
105
+ post_install_message:
106
+ rdoc_options:
107
+ - --charset=UTF-8
108
+ require_paths:
109
+ - lib
110
+ required_ruby_version: !ruby/object:Gem::Requirement
111
+ requirements:
112
+ - - ">="
113
+ - !ruby/object:Gem::Version
114
+ segments:
115
+ - 0
116
+ version: "0"
117
+ required_rubygems_version: !ruby/object:Gem::Requirement
118
+ requirements:
119
+ - - ">="
120
+ - !ruby/object:Gem::Version
121
+ segments:
122
+ - 0
123
+ version: "0"
124
+ requirements: []
125
+
126
+ rubyforge_project:
127
+ rubygems_version: 1.3.6
128
+ signing_key:
129
+ specification_version: 3
130
+ summary: Light weight job scheduling on top of Resque
131
+ test_files:
132
+ - test/delayed_queue_test.rb
133
+ - test/scheduler_test.rb
134
+ - test/test_helper.rb