resque-scheduler 0.0.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,13 +1,22 @@
1
1
  resque-scheduler
2
2
  ===============
3
3
 
4
- Resque-scheduler is basically the union of rufus-scheduler and resque. The goal
5
- is to provide simple job scheduling with centralized configuration and
6
- distributed workers.
4
+ Resque-scheduler is an extension to Resque that adds support for queueing items
5
+ 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.
7
16
 
8
17
  The schedule is a list of Resque worker classes with arguments and a
9
18
  schedule frequency (in crontab syntax). The schedule is just a hash, but
10
- is most likely stored in a YAML:
19
+ is most likely stored in a YAML like so:
11
20
 
12
21
  queue_documents_for_indexing:
13
22
  cron: "0 0 * * *"
@@ -32,8 +41,69 @@ And then set the schedule wherever you configure Resque, like so:
32
41
  require 'resque-scheduler'
33
42
  ResqueScheduler.schedule = YAML.load_file(File.join(File.dirname(__FILE__), '../resque_schedule.yml'))
34
43
 
35
- The scheduler process is just a rake task which adds things to resque when they fire
36
- based on the schedule. For obvious reasons, this process never exits.
44
+ Keep in mind, scheduled jobs behave like crons: if your scheduler process (more
45
+ on that later) is not running when a particular job is supposed to be queued,
46
+ it will NOT be ran later when the scheduler process is started back up. In that
47
+ sense, you can sort of think of the scheduler process as crond. Delayed jobs,
48
+ however, are different.
49
+
50
+ A big shout out to [rufus-scheduler](http://github.com/jmettraux/rufus-scheduler) for handling the heavy lifting of the
51
+ actual scheduling engine.
52
+
53
+ ### Delayed jobs
54
+
55
+ Delayed jobs are one-off jobs that you want to be put into a queue at some point
56
+ in the future. The classic example is sending email:
57
+
58
+ Resque.enqueue_at(5.days.from_now, SendFollowUpEmail, :user_id => current_user.id)
59
+
60
+ This will store the job for 5 days in the resque delayed queue at which time the
61
+ scheduler process will pull it from the delayed queue and put it in the
62
+ appropriate work queue for the given job and it will be processed as soon as
63
+ a worker is available.
64
+
65
+ NOTE: The job does not fire **exactly** at the time supplied. Rather, once that
66
+ time is in the past, the job moves from the delayed queue to the actual resque
67
+ work queue and will be completed as workers as free to process it.
68
+
69
+ Also supported is `Resque.enqueue_in` which takes an amount of time in seconds
70
+ in which to queue the job.
71
+
72
+ The delayed queue is stored in redis and is persisted in the same way the
73
+ standard resque jobs are persisted (redis writing to disk). Delayed jobs differ
74
+ from scheduled jobs in that if your scheduler process is down or workers are
75
+ down when a particular job is supposed to be queue, they will simply "catch up"
76
+ once they are started again. Jobs are guaranteed to run (provided they make it
77
+ into the delayed queue) after their given queue_at time has passed.
78
+
79
+ One other thing to note is that insertion into the delayed queue is O(log(n))
80
+ since the jobs are stored in a redis sorted set (zset). I can't imagine this
81
+ being an issue for someone since redis is stupidly fast even at log(n), but full
82
+ disclosure is always best.
83
+
84
+
85
+ Resque-web additions
86
+ --------------------
87
+
88
+ Resque-scheduler also adds to tabs to the resque-web UI. One is for viewing
89
+ (and manually queueing) the schedule and one is for viewing pending jobs in
90
+ the delayed queue.
91
+
92
+ The Schedule tab:
93
+
94
+ ![The Schedule Tab](http://img.skitch.com/20100111-km2f5gmtpbq23enpujbruj6mgk.png)
95
+
96
+ The Delayed tab:
97
+
98
+ ![The Delayed Tab](http://img.skitch.com/20100111-ne4fcqtc5emkcuwc5qtais2kwx.jpg)
99
+
100
+
101
+ The Scheduler process
102
+ ---------------------
103
+
104
+ The scheduler process is just a rake task which is responsible for both queueing
105
+ items from the schedule and polling the delayed queue for items ready to be
106
+ pushed on to the work queues. For obvious reasons, this process never exits.
37
107
 
38
108
  $ rake resque-scheduler
39
109
 
@@ -42,4 +112,22 @@ You'll need to add this to your rakefile:
42
112
  require 'resque_scheduler/tasks'
43
113
  task "resque:setup" => :environment
44
114
 
115
+ Supported environment variables are `VERBOSE` and `MUTE`. If either is set to
116
+ any nonempty value, they will take effect. `VERBOSE` simply dumps more output
117
+ to stdout. `MUTE` does the opposite and silences all output. `MUTE` supercedes
118
+ `VERBOSE`.
119
+
120
+
121
+ Plagurism alert
122
+ ---------------
123
+
124
+ This was intended to be an extension to resque and so resulted in a lot of the
125
+ code looking very similar to resque, particularly in resque-web and the views. I
126
+ wanted it to be similar enough that someone familiar with resque could easily
127
+ work on resque-scheduler.
128
+
129
+
130
+ Contributing
131
+ ------------
45
132
 
133
+ For bugs or suggestions, please just open an issue in github.
data/Rakefile CHANGED
@@ -27,14 +27,16 @@ begin
27
27
 
28
28
  Jeweler::Tasks.new do |gemspec|
29
29
  gemspec.name = "resque-scheduler"
30
- gemspec.summary = ""
31
- gemspec.description = ""
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.}
32
34
  gemspec.email = "bvandenbos@gmail.com"
33
35
  gemspec.homepage = "http://github.com/bvandenbos/resque-scheduler"
34
36
  gemspec.authors = ["Ben VandenBos"]
35
37
  gemspec.version = ResqueScheduler::Version
36
38
 
37
- gemspec.add_dependency "resque"
39
+ gemspec.add_dependency "resque", ">= 1.3.0"
38
40
  gemspec.add_dependency "rufus-scheduler"
39
41
  gemspec.add_development_dependency "jeweler"
40
42
  gemspec.add_development_dependency "mocha"
@@ -49,5 +51,4 @@ task :publish => [ :test, :gemspec, :build ] do
49
51
  system "git push origin master"
50
52
  system "gem push pkg/resque-scheduler-#{ResqueScheduler::Version}.gem"
51
53
  system "git clean -fd"
52
- exec "rake pages"
53
54
  end
@@ -10,24 +10,82 @@ module Resque
10
10
 
11
11
  class << self
12
12
 
13
- # Schedule all jobs and sleep (never returns)
14
- def run(wait = true)
15
- puts "Schedule empty! Set Resque.schedule" if Resque.schedule.empty?
13
+ # If true, logs more stuff...
14
+ attr_accessor :verbose
15
+
16
+ # If set, produces no output
17
+ attr_accessor :mute
18
+
19
+ # Schedule all jobs and continually look for delayed jobs (never returns)
20
+ def run
21
+
22
+ # trap signals
23
+ register_signal_handlers
24
+
25
+ # Load the schedule into rufus
26
+ load_schedule!
27
+
28
+ # Now start the scheduling part of the loop.
29
+ loop do
30
+ handle_delayed_items
31
+ poll_sleep
32
+ end
33
+
34
+ # never gets here.
35
+ end
36
+
37
+ # For all signals, set the shutdown flag and wait for current
38
+ # poll/enqueing to finish (should be almost istant). In the
39
+ # case of sleeping, exit immediately.
40
+ def register_signal_handlers
41
+ trap("TERM") { shutdown }
42
+ trap("INT") { shutdown }
43
+ trap('QUIT') { shutdown } unless defined? JRUBY_VERSION
44
+ end
45
+
46
+ # Pulls the schedule from Resque.schedule and loads it into the
47
+ # rufus scheduler instance
48
+ def load_schedule!
49
+ log! "Schedule empty! Set Resque.schedule" if Resque.schedule.empty?
16
50
 
17
51
  Resque.schedule.each do |name, config|
18
- puts "Scheduling #{name}..."
52
+ log! "Scheduling #{name} "
19
53
  rufus_scheduler.cron config['cron'] do
20
- puts "#{Time.now} queuing #{name}"
54
+ log! "queuing #{config['class']} (#{name})"
21
55
  enqueue_from_config(config)
22
56
  end
23
57
  end
24
- # sleep baby, sleep
25
- ThreadsWait.all_waits(rufus_scheduler.instance_variable_get("@thread")) if wait
26
58
  end
27
59
 
60
+ # Handles queueing delayed items
61
+ def handle_delayed_items
62
+ item = nil
63
+ if timestamp = Resque.next_delayed_timestamp
64
+ item = nil
65
+ begin
66
+ handle_shutdown do
67
+ if item = Resque.next_item_for_timestamp(timestamp)
68
+ log "queuing #{item['class']} [delayed]"
69
+ klass = constantize(item['class'])
70
+ Resque.enqueue(klass, *item['args'])
71
+ end
72
+ end
73
+ end while !item.nil?
74
+ end
75
+ end
76
+
77
+ def handle_shutdown
78
+ exit if @shutdown
79
+ yield
80
+ exit if @shutdown
81
+ end
82
+
83
+ # Enqueues a job based on a config hash
28
84
  def enqueue_from_config(config)
29
- params = config['args'].nil? ? [] : Array(config['args'])
30
- Resque.enqueue(constantize(config['class']), *params)
85
+ args = config['args'] || config[:args]
86
+ klass_name = config['class'] || config[:class]
87
+ params = args.nil? ? [] : Array(args)
88
+ Resque.enqueue(constantize(klass_name), *params)
31
89
  end
32
90
 
33
91
  def rufus_scheduler
@@ -42,6 +100,29 @@ module Resque
42
100
  rufus_scheduler
43
101
  end
44
102
 
103
+ # Sleeps and returns true
104
+ def poll_sleep
105
+ @sleeping = true
106
+ handle_shutdown { sleep 5 }
107
+ @sleeping = false
108
+ true
109
+ end
110
+
111
+ # Sets the shutdown flag, exits if sleeping
112
+ def shutdown
113
+ @shutdown = true
114
+ exit if @sleeping
115
+ end
116
+
117
+ def log!(msg)
118
+ puts "#{Time.now.strftime("%Y-%m-%d %H:%M:%S")} #{msg}" unless mute
119
+ end
120
+
121
+ def log(msg)
122
+ # add "verbose" logic later
123
+ log!(msg) if verbose
124
+ end
125
+
45
126
  end
46
127
 
47
128
  end
@@ -1,7 +1,9 @@
1
1
  require 'rubygems'
2
2
  require 'resque'
3
+ require 'resque/server'
3
4
  require 'resque_scheduler/version'
4
5
  require 'resque/scheduler'
6
+ require 'resque_scheduler/server'
5
7
 
6
8
  module ResqueScheduler
7
9
 
@@ -31,6 +33,84 @@ module ResqueScheduler
31
33
  @schedule ||= {}
32
34
  end
33
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)
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
+ def delayed_push(timestamp, item)
54
+ # First add this item to the list for this timestamp
55
+ redis.rpush("delayed:#{timestamp.to_i}", encode(item))
56
+
57
+ # Now, add this timestamp to the zsets. The score and the value are
58
+ # the same since we'll be querying by timestamp, and we don't have
59
+ # anything else to store.
60
+ redis.zset_add :delayed_queue_schedule, timestamp.to_i, timestamp.to_i
61
+ end
62
+
63
+ # Returns an array of timestamps based on start and count
64
+ def delayed_queue_peek(start, count)
65
+ (redis.zrange :delayed_queue_schedule, start, start+count).collect(&:to_i)
66
+ end
67
+
68
+ # Returns the size of the delayed queue schedule
69
+ def delayed_queue_schedule_size
70
+ redis.zcard :delayed_queue_schedule
71
+ end
72
+
73
+ # Returns the number of jobs for a given timestamp in the delayed queue schedule
74
+ def delayed_timestamp_size(timestamp)
75
+ redis.llen "delayed:#{timestamp.to_i}"
76
+ end
77
+
78
+ # Returns an array of delayed items for the given timestamp
79
+ def delayed_timestamp_peek(timestamp, start, count)
80
+ if 1 == count
81
+ r = list_range "delayed:#{timestamp.to_i}", start, count
82
+ r.nil? ? [] : [r]
83
+ else
84
+ list_range "delayed:#{timestamp.to_i}", start, count
85
+ end
86
+ end
87
+
88
+ # Returns the next delayed queue timestamp
89
+ # (don't call directly)
90
+ def next_delayed_timestamp
91
+ timestamp = redis.zrangebyscore(:delayed_queue_schedule, '-inf', Time.now.to_i, 'limit', 0, 1).first
92
+ timestamp.to_i unless timestamp.nil?
93
+ end
94
+
95
+ # Returns the next item to be processed for a given timestamp, nil if
96
+ # done. (don't call directly)
97
+ # +timestamp+ can either be in seconds or a datetime
98
+ def next_item_for_timestamp(timestamp)
99
+ key = "delayed:#{timestamp.to_i}"
100
+
101
+ item = decode redis.lpop(key)
102
+
103
+ # If the list is empty, remove it.
104
+ if 0 == redis.llen(key).to_i
105
+ redis.del key
106
+ redis.zrem :delayed_queue_schedule, timestamp.to_i
107
+ end
108
+ item
109
+ end
110
+
34
111
  end
35
112
 
36
- Resque.extend ResqueScheduler
113
+ Resque.extend ResqueScheduler
114
+ Resque::Server.class_eval do
115
+ include ResqueScheduler::Server
116
+ end
@@ -0,0 +1,47 @@
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
+ end
16
+
17
+ get "/schedule" do
18
+ # Is there a better way to specify alternate template locations with sinatra?
19
+ erb File.read(File.join(File.dirname(__FILE__), 'server/views/scheduler.erb'))
20
+ end
21
+
22
+ post "/schedule/requeue" do
23
+ config = Resque.schedule[params['job_name']]
24
+ Resque::Scheduler.enqueue_from_config(config)
25
+ redirect url("/queues")
26
+ end
27
+
28
+ get "/delayed" do
29
+ # Is there a better way to specify alternate template locations with sinatra?
30
+ erb File.read(File.join(File.dirname(__FILE__), 'server/views/delayed.erb'))
31
+ end
32
+
33
+ get "/delayed/:timestamp" do
34
+ # Is there a better way to specify alternate template locations with sinatra?
35
+ erb File.read(File.join(File.dirname(__FILE__), 'server/views/delayed_timestamp.erb'))
36
+ end
37
+
38
+ end
39
+
40
+ end
41
+
42
+ Resque::Server.tabs << 'Schedule'
43
+ Resque::Server.tabs << 'Delayed'
44
+
45
+ end
46
+
47
+ end
@@ -0,0 +1,24 @@
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
+ </tr>
16
+ <% resque.delayed_queue_peek(start, start+20).each do |timestamp| %>
17
+ <tr>
18
+ <td><a href="<%= url "delayed/#{timestamp}" %>"><%= format_time(Time.at(timestamp)) %></a></td>
19
+ <td><%= resque.delayed_timestamp_size(timestamp) %></td>
20
+ </tr>
21
+ <% end %>
22
+ </table>
23
+
24
+ <%= 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,32 @@
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>Arguments</th>
16
+ </tr>
17
+ <% Resque.schedule.each do |name, config| %>
18
+ <tr>
19
+ <td>
20
+ <form action="<%= url "/schedule/requeue" %>" method="post">
21
+ <input type="hidden" name="job_name" value="<%= h name %>">
22
+ <input type="submit" value="Queue now">
23
+ </form>
24
+ </td>
25
+ <td><%= h name %></td>
26
+ <td><%= h config['description'] %></td>
27
+ <td style="white-space:nowrap"><%= h config['cron'] %></td>
28
+ <td><%= h config['class'] %></td>
29
+ <td><%= h config['args'].inspect %></td>
30
+ </tr>
31
+ <% end %>
32
+ </table>
@@ -9,6 +9,7 @@ namespace :resque do
9
9
  require 'resque'
10
10
  require 'resque_scheduler'
11
11
 
12
+ Resque::Scheduler.verbose = true if ENV['VERBOSE']
12
13
  Resque::Scheduler.run
13
14
  end
14
15
 
@@ -1,3 +1,3 @@
1
1
  module ResqueScheduler
2
- Version = '0.0.1'
3
- end
2
+ Version = '1.0.0'
3
+ end
@@ -0,0 +1,117 @@
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'}, 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
+ Resque.expects(:enqueue).twice
114
+ Resque::Scheduler.handle_delayed_items
115
+ end
116
+
117
+ 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
@@ -15,7 +15,7 @@ class Resque::SchedulerTest < Test::Unit::TestCase
15
15
  assert_equal(0, Resque::Scheduler.rufus_scheduler.all_jobs.size)
16
16
 
17
17
  Resque.schedule = {:some_ivar_job => {'cron' => "* * * * *", 'class' => 'SomeIvarJob', 'args' => "/tmp"}}
18
- Resque::Scheduler.run(false)
18
+ Resque::Scheduler.load_schedule!
19
19
 
20
20
  assert_equal(1, Resque::Scheduler.rufus_scheduler.all_jobs.size)
21
21
  end
@@ -1,8 +1,54 @@
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
+
1
7
  require 'rubygems'
2
8
  require 'test/unit'
3
9
  require 'mocha'
10
+ require 'resque'
11
+ require File.join(dir, '../lib/resque_scheduler')
4
12
  $LOAD_PATH.unshift File.dirname(File.expand_path(__FILE__)) + '/../lib'
5
- require 'resque_scheduler'
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
+
6
52
 
7
53
  class SomeJob
8
54
  def self.perform(repo_id, path)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: resque-scheduler
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ben VandenBos
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-12-21 00:00:00 -08:00
12
+ date: 2010-01-11 00:00:00 -08:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
@@ -20,7 +20,7 @@ dependencies:
20
20
  requirements:
21
21
  - - ">="
22
22
  - !ruby/object:Gem::Version
23
- version: "0"
23
+ version: 1.3.0
24
24
  version:
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: rufus-scheduler
@@ -52,7 +52,10 @@ dependencies:
52
52
  - !ruby/object:Gem::Version
53
53
  version: "0"
54
54
  version:
55
- description: ""
55
+ description: |-
56
+ Light weight job scheduling on top of Resque.
57
+ Adds methods enqueue_at/enqueue_in to schedule jobs in the future.
58
+ Also supports queueing jobs on a fixed, cron-like schedule.
56
59
  email: bvandenbos@gmail.com
57
60
  executables: []
58
61
 
@@ -66,9 +69,15 @@ files:
66
69
  - Rakefile
67
70
  - lib/resque/scheduler.rb
68
71
  - lib/resque_scheduler.rb
72
+ - lib/resque_scheduler/server.rb
73
+ - lib/resque_scheduler/server/views/delayed.erb
74
+ - lib/resque_scheduler/server/views/delayed_timestamp.erb
75
+ - lib/resque_scheduler/server/views/scheduler.erb
69
76
  - lib/resque_scheduler/tasks.rb
70
77
  - lib/resque_scheduler/version.rb
71
78
  - tasks/resque_scheduler.rake
79
+ - test/delayed_queue_test.rb
80
+ - test/redis-test.conf
72
81
  - test/scheduler_test.rb
73
82
  - test/test_helper.rb
74
83
  has_rdoc: true
@@ -98,7 +107,8 @@ rubyforge_project:
98
107
  rubygems_version: 1.3.5
99
108
  signing_key:
100
109
  specification_version: 3
101
- summary: ""
110
+ summary: Light weight job scheduling on top of Resque
102
111
  test_files:
112
+ - test/delayed_queue_test.rb
103
113
  - test/scheduler_test.rb
104
114
  - test/test_helper.rb