nulogy-resque-scheduler 1.10.15

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,202 @@
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
+ # :every can be used in lieu of :cron. see rufus-scheduler's 'every' usage for
22
+ # valid syntax. If :cron is present it will take precedence over :every.
23
+ # :class must be a resque worker class
24
+ # :args can be any yaml which will be converted to a ruby literal and passed
25
+ # in a params. (optional)
26
+ # :rails_envs is the list of envs where the job gets loaded. Envs are comma separated (optional)
27
+ # :description is just that, a description of the job (optional). If params is
28
+ # an array, each element in the array is passed as a separate param,
29
+ # otherwise params is passed in as the only parameter to perform.
30
+ def schedule=(schedule_hash)
31
+ if Resque::Scheduler.dynamic
32
+ schedule_hash.each do |name, job_spec|
33
+ set_schedule(name, job_spec)
34
+ end
35
+ end
36
+ @schedule = schedule_hash
37
+ end
38
+
39
+ # Returns the schedule hash
40
+ def schedule
41
+ @schedule ||= {}
42
+ end
43
+
44
+ # reloads the schedule from redis
45
+ def reload_schedule!
46
+ @schedule = get_schedules
47
+ end
48
+
49
+ # gets the schedule as it exists in redis
50
+ def get_schedules
51
+ if redis.exists(:schedules)
52
+ redis.hgetall(:schedules).tap do |h|
53
+ h.each do |name, config|
54
+ h[name] = decode(config)
55
+ end
56
+ end
57
+ else
58
+ nil
59
+ end
60
+ end
61
+
62
+ # create or update a schedule with the provided name and configuration
63
+ def set_schedule(name, config)
64
+ existing_config = get_schedule(name)
65
+ unless existing_config && existing_config == config
66
+ redis.hset(:schedules, name, encode(config))
67
+ redis.sadd(:schedules_changed, name)
68
+ end
69
+ config
70
+ end
71
+
72
+ # retrive the schedule configuration for the given name
73
+ def get_schedule(name)
74
+ decode(redis.hget(:schedules, name))
75
+ end
76
+
77
+ # remove a given schedule by name
78
+ def remove_schedule(name)
79
+ redis.hdel(:schedules, name)
80
+ redis.sadd(:schedules_changed, name)
81
+ end
82
+
83
+ # This method is nearly identical to +enqueue+ only it also
84
+ # takes a timestamp which will be used to schedule the job
85
+ # for queueing. Until timestamp is in the past, the job will
86
+ # sit in the schedule list.
87
+ def enqueue_at(timestamp, klass, *args)
88
+ delayed_push(timestamp, job_to_hash(klass, args))
89
+ end
90
+
91
+ # Identical to enqueue_at but takes number_of_seconds_from_now
92
+ # instead of a timestamp.
93
+ def enqueue_in(number_of_seconds_from_now, klass, *args)
94
+ enqueue_at(Time.now + number_of_seconds_from_now, klass, *args)
95
+ end
96
+
97
+ # Used internally to stuff the item into the schedule sorted list.
98
+ # +timestamp+ can be either in seconds or a datetime object
99
+ # Insertion if O(log(n)).
100
+ # Returns true if it's the first job to be scheduled at that time, else false
101
+ def delayed_push(timestamp, item)
102
+ # First add this item to the list for this timestamp
103
+ redis.rpush("delayed:#{timestamp.to_i}", encode(item))
104
+
105
+ # Now, add this timestamp to the zsets. The score and the value are
106
+ # the same since we'll be querying by timestamp, and we don't have
107
+ # anything else to store.
108
+ redis.zadd :delayed_queue_schedule, timestamp.to_i, timestamp.to_i
109
+ end
110
+
111
+ # Returns an array of timestamps based on start and count
112
+ def delayed_queue_peek(start, count)
113
+ Array(redis.zrange(:delayed_queue_schedule, start, start+count)).collect{|x| x.to_i}
114
+ end
115
+
116
+ # Returns the size of the delayed queue schedule
117
+ def delayed_queue_schedule_size
118
+ redis.zcard :delayed_queue_schedule
119
+ end
120
+
121
+ # Returns the number of jobs for a given timestamp in the delayed queue schedule
122
+ def delayed_timestamp_size(timestamp)
123
+ redis.llen("delayed:#{timestamp.to_i}").to_i
124
+ end
125
+
126
+ # Returns an array of delayed items for the given timestamp
127
+ def delayed_timestamp_peek(timestamp, start, count)
128
+ if 1 == count
129
+ r = list_range "delayed:#{timestamp.to_i}", start, count
130
+ r.nil? ? [] : [r]
131
+ else
132
+ list_range "delayed:#{timestamp.to_i}", start, count
133
+ end
134
+ end
135
+
136
+ # Returns the next delayed queue timestamp
137
+ # (don't call directly)
138
+ def next_delayed_timestamp(at_time=nil)
139
+ items = redis.zrangebyscore :delayed_queue_schedule, '-inf', (at_time || Time.now).to_i, :limit => [0, 1]
140
+ timestamp = items.nil? ? nil : Array(items).first
141
+ timestamp.to_i unless timestamp.nil?
142
+ end
143
+
144
+ # Returns the next item to be processed for a given timestamp, nil if
145
+ # done. (don't call directly)
146
+ # +timestamp+ can either be in seconds or a datetime
147
+ def next_item_for_timestamp(timestamp)
148
+ key = "delayed:#{timestamp.to_i}"
149
+
150
+ item = decode redis.lpop(key)
151
+
152
+ # If the list is empty, remove it.
153
+ clean_up_timestamp(key, timestamp)
154
+ item
155
+ end
156
+
157
+ # Clears all jobs created with enqueue_at or enqueue_in
158
+ def reset_delayed_queue
159
+ Array(redis.zrange(:delayed_queue_schedule, 0, -1)).each do |item|
160
+ redis.del "delayed:#{item}"
161
+ end
162
+
163
+ redis.del :delayed_queue_schedule
164
+ end
165
+
166
+ # given an encoded item, remove it from the delayed_queue
167
+ def remove_delayed(klass, *args)
168
+ destroyed = 0
169
+ search = encode(job_to_hash(klass, args))
170
+ Array(redis.keys("delayed:*")).each do |key|
171
+ destroyed += redis.lrem key, 0, search
172
+ end
173
+ destroyed
174
+ end
175
+
176
+ def count_all_scheduled_jobs
177
+ total_jobs = 0
178
+ Array(redis.zrange(:delayed_queue_schedule, 0, -1)).each do |timestamp|
179
+ total_jobs += redis.llen("delayed:#{timestamp}").to_i
180
+ end
181
+ total_jobs
182
+ end
183
+
184
+ private
185
+ def job_to_hash(klass, args)
186
+ {:class => klass.to_s, :args => args, :queue => queue_from_class(klass)}
187
+ end
188
+
189
+ def clean_up_timestamp(key, timestamp)
190
+ # If the list is empty, remove it.
191
+ if 0 == redis.llen(key).to_i
192
+ redis.del key
193
+ redis.zrem :delayed_queue_schedule, timestamp.to_i
194
+ end
195
+ end
196
+
197
+ end
198
+
199
+ Resque.extend ResqueScheduler
200
+ Resque::Server.class_eval do
201
+ include ResqueScheduler::Server
202
+ end
@@ -0,0 +1,58 @@
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
+ Resque.reload_schedule! if Resque::Scheduler.dynamic
23
+ # Is there a better way to specify alternate template locations with sinatra?
24
+ erb File.read(File.join(File.dirname(__FILE__), 'server/views/scheduler.erb'))
25
+ end
26
+
27
+ post "/schedule/requeue" do
28
+ config = Resque.schedule[params['job_name']]
29
+ Resque::Scheduler.enqueue_from_config(config)
30
+ redirect url("/overview")
31
+ end
32
+
33
+ get "/delayed" 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.erb'))
36
+ end
37
+
38
+ get "/delayed/:timestamp" do
39
+ # Is there a better way to specify alternate template locations with sinatra?
40
+ erb File.read(File.join(File.dirname(__FILE__), 'server/views/delayed_timestamp.erb'))
41
+ end
42
+
43
+ post "/delayed/queue_now" do
44
+ timestamp = params['timestamp']
45
+ Resque::Scheduler.enqueue_delayed_items_for_timestamp(timestamp.to_i) if timestamp.to_i > 0
46
+ redirect url("/overview")
47
+ end
48
+
49
+ end
50
+
51
+ end
52
+
53
+ Resque::Server.tabs << 'Schedule'
54
+ Resque::Server.tabs << 'Delayed'
55
+
56
+ end
57
+
58
+ end
@@ -0,0 +1,42 @@
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></th>
14
+ <th>Timestamp</th>
15
+ <th>Job count</th>
16
+ <th>Class</th>
17
+ <th>Args</th>
18
+ </tr>
19
+ <% resque.delayed_queue_peek(start, start+20).each do |timestamp| %>
20
+ <tr>
21
+ <td>
22
+ <form action="<%= url "/delayed/queue_now" %>" method="post">
23
+ <input type="hidden" name="timestamp" value="<%= timestamp.to_i %>">
24
+ <input type="submit" value="Queue now">
25
+ </form>
26
+ </td>
27
+ <td><a href="<%= url "delayed/#{timestamp}" %>"><%= format_time(Time.at(timestamp)) %></a></td>
28
+ <td><%= delayed_timestamp_size = resque.delayed_timestamp_size(timestamp) %></td>
29
+ <% job = resque.delayed_timestamp_peek(timestamp, 0, 1).first %>
30
+ <td>
31
+ <% if job && delayed_timestamp_size == 1 %>
32
+ <%= h(job['class']) %>
33
+ <% else %>
34
+ <a href="<%= url "delayed/#{timestamp}" %>">see details</a>
35
+ <% end %>
36
+ </td>
37
+ <td><%= h(job['args'].inspect) if job && delayed_timestamp_size == 1 %></td>
38
+ </tr>
39
+ <% end %>
40
+ </table>
41
+
42
+ <%= 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,37 @@
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>Interval</th>
14
+ <th>Class</th>
15
+ <th>Queue</th>
16
+ <th>Arguments</th>
17
+ </tr>
18
+ <% Resque.schedule.keys.sort.each do |name| %>
19
+ <% config = Resque.schedule[name] %>
20
+ <tr>
21
+ <td>
22
+ <form action="<%= url "/schedule/requeue" %>" method="post">
23
+ <input type="hidden" name="job_name" value="<%= h name %>">
24
+ <input type="submit" value="Queue now">
25
+ </form>
26
+ </td>
27
+ <td><%= h name %></td>
28
+ <td><%= h config['description'] %></td>
29
+ <td style="white-space:nowrap"><%= (config['cron'].nil? && !config['every'].nil?) ?
30
+ h('every: ' + config['every']) :
31
+ h('cron: ' + config['cron']) %></td>
32
+ <td><%= h config['class'] %></td>
33
+ <td><%= h config['queue'] || queue_from_class_name(config['class']) %></td>
34
+ <td><%= h config['args'].inspect %></td>
35
+ </tr>
36
+ <% end %>
37
+ </table>
@@ -0,0 +1,33 @@
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 => :scheduler_setup do
9
+ require 'resque'
10
+ require 'resque_scheduler'
11
+
12
+ Resque::Scheduler.verbose = true if ENV['VERBOSE']
13
+ if File.exists?("config/airbrake.yml")
14
+ Resque::Scheduler.airbrake = true
15
+ Airbrake::configure do |config|
16
+ yaml = YAML.load_file("config/airbrake.yml")
17
+ yaml.each do |key, val|
18
+ config.send(:"#{key}=", val)
19
+ end
20
+ end
21
+ end
22
+ Resque::Scheduler.run
23
+ end
24
+
25
+ task :scheduler_setup do
26
+ if ENV['INITIALIZER_PATH']
27
+ load ENV['INITIALIZER_PATH'].to_s.strip
28
+ else
29
+ Rake::Task['resque:setup'].invoke
30
+ end
31
+ end
32
+
33
+ end
@@ -0,0 +1,3 @@
1
+ module ResqueScheduler
2
+ Version = '1.10.15'
3
+ end
@@ -0,0 +1,86 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{nulogy-resque-scheduler}
8
+ s.version = "1.10.15"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Ben VandenBos", "Brian Landau", "Sean Kirby", "Tanzeeb Khalili", "Justin Fitzsimmons"]
12
+ s.date = %q{2011-09-23}
13
+ s.description = %q{Light weight job scheduling on top of Resque.
14
+ Adds methods enqueue_at/enqueue_in to schedule jobs in the future.
15
+ Also supports queueing jobs on a fixed, cron-like schedule.}
16
+ s.email = %q{sskirby@gmail.com}
17
+ s.extra_rdoc_files = [
18
+ "LICENSE",
19
+ "README.markdown"
20
+ ]
21
+ s.files = [
22
+ ".gitignore",
23
+ "HISTORY.md",
24
+ "LICENSE",
25
+ "README.markdown",
26
+ "Rakefile",
27
+ "lib/resque/scheduler.rb",
28
+ "lib/resque_scheduler.rb",
29
+ "lib/resque_scheduler/server.rb",
30
+ "lib/resque_scheduler/server/views/delayed.erb",
31
+ "lib/resque_scheduler/server/views/delayed_timestamp.erb",
32
+ "lib/resque_scheduler/server/views/scheduler.erb",
33
+ "lib/resque_scheduler/tasks.rb",
34
+ "lib/resque_scheduler/version.rb",
35
+ "sskirby-resque-scheduler.gemspec",
36
+ "tasks/resque_scheduler.rake",
37
+ "test/delayed_queue_test.rb",
38
+ "test/redis-test.conf",
39
+ "test/resque-web_test.rb",
40
+ "test/scheduler_test.rb",
41
+ "test/test_helper.rb"
42
+ ]
43
+ s.homepage = %q{http://github.com/sskirby/resque-scheduler}
44
+ s.rdoc_options = ["--charset=UTF-8"]
45
+ s.require_paths = ["lib"]
46
+ s.rubygems_version = %q{1.3.6}
47
+ s.summary = %q{Light weight job scheduling on top of Resque}
48
+ s.test_files = [
49
+ "test/scheduler_test.rb",
50
+ "test/test_helper.rb",
51
+ "test/resque-web_test.rb",
52
+ "test/delayed_queue_test.rb"
53
+ ]
54
+
55
+ if s.respond_to? :specification_version then
56
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
57
+ s.specification_version = 3
58
+
59
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
60
+ s.add_runtime_dependency(%q<redis>, [">= 2.0.1"])
61
+ s.add_runtime_dependency(%q<resque>, [">= 1.8.0"])
62
+ s.add_runtime_dependency(%q<tanzeeb-rufus-scheduler>, [">= 0"])
63
+ s.add_runtime_dependency(%q<airbrake>, [">= 0"])
64
+ s.add_development_dependency(%q<jeweler>, [">= 0"])
65
+ s.add_development_dependency(%q<mocha>, [">= 0"])
66
+ s.add_development_dependency(%q<rack-test>, [">= 0"])
67
+ else
68
+ s.add_dependency(%q<redis>, [">= 2.0.1"])
69
+ s.add_dependency(%q<resque>, [">= 1.8.0"])
70
+ s.add_dependency(%q<tanzeeb-rufus-scheduler>, [">= 0"])
71
+ s.add_dependency(%q<airbrake>, [">= 0"])
72
+ s.add_dependency(%q<jeweler>, [">= 0"])
73
+ s.add_dependency(%q<mocha>, [">= 0"])
74
+ s.add_dependency(%q<rack-test>, [">= 0"])
75
+ end
76
+ else
77
+ s.add_dependency(%q<redis>, [">= 2.0.1"])
78
+ s.add_dependency(%q<resque>, [">= 1.8.0"])
79
+ s.add_dependency(%q<tanzeeb-rufus-scheduler>, [">= 0"])
80
+ s.add_dependency(%q<airbrake>, [">= 0"])
81
+ s.add_dependency(%q<jeweler>, [">= 0"])
82
+ s.add_dependency(%q<mocha>, [">= 0"])
83
+ s.add_dependency(%q<rack-test>, [">= 0"])
84
+ end
85
+ end
86
+