ncr-background_fu 1.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +16 -0
- data/Manifest.txt +27 -0
- data/README.txt +166 -0
- data/Rakefile +8 -0
- data/generators/background/USAGE +3 -0
- data/generators/background/background_generator.rb +58 -0
- data/generators/background/templates/background.rb +22 -0
- data/generators/background/templates/background_ctl +20 -0
- data/generators/background/templates/daemons +2 -0
- data/generators/background/templates/daemons.yml +5 -0
- data/generators/background/templates/example_monitored_worker.rb +20 -0
- data/generators/background/templates/migration.rb +30 -0
- data/generators/background/templates/scaffold/_job.html.erb +39 -0
- data/generators/background/templates/scaffold/_job_deleted.html.erb +5 -0
- data/generators/background/templates/scaffold/_progress_indicator.html.erb +5 -0
- data/generators/background/templates/scaffold/background_fu.css +23 -0
- data/generators/background/templates/scaffold/index.html.erb +27 -0
- data/generators/background/templates/scaffold/jobs.html.erb +22 -0
- data/generators/background/templates/scaffold/jobs_controller.rb +60 -0
- data/generators/background/templates/scaffold/jobs_helper.rb +32 -0
- data/init.rb +5 -0
- data/lib/background_fu/worker_monitoring.rb +19 -0
- data/lib/background_fu.rb +5 -0
- data/lib/job/bonus_features.rb +99 -0
- data/lib/job.rb +110 -0
- metadata +87 -0
data/History.txt
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
=== 1.0.2 / 2008-05-27
|
2
|
+
|
3
|
+
* Fiddling with gemspec to make it work on GitHub. [ncr]
|
4
|
+
|
5
|
+
=== 1.0.1 / 2008-05-27
|
6
|
+
|
7
|
+
* Updaed README with install instructions. [ncr]
|
8
|
+
|
9
|
+
=== 1.0.0 / 2008-05-27
|
10
|
+
|
11
|
+
* BackgroundFu is now also a gem. You should check Rails 2.1 and the super sexy gems vendoring features!
|
12
|
+
|
13
|
+
* Updated README.txt so now it looks like those from gems. [ncr]
|
14
|
+
* Previous changes before becoming a gem:
|
15
|
+
* 2008-05-12 Added excessive logging and changed default priority to 0 (Negative priorities allowed). [ncr]
|
16
|
+
* 2008-05-11 Added three columns to Job model: priority (execute jobs in priority descending order), start_at (execute jobs after start_at), lock_version (ensure job is executed by a single daemon). [ncr]
|
data/Manifest.txt
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
History.txt
|
2
|
+
Manifest.txt
|
3
|
+
README.txt
|
4
|
+
Rakefile
|
5
|
+
background-fu.gemspec
|
6
|
+
generators/background/USAGE
|
7
|
+
generators/background/background_generator.rb
|
8
|
+
generators/background/templates/background.rb
|
9
|
+
generators/background/templates/background_ctl
|
10
|
+
generators/background/templates/daemons
|
11
|
+
generators/background/templates/daemons.yml
|
12
|
+
generators/background/templates/example_monitored_worker.rb
|
13
|
+
generators/background/templates/example_worker.rb
|
14
|
+
generators/background/templates/migration.rb
|
15
|
+
generators/background/templates/scaffold/_job.html.erb
|
16
|
+
generators/background/templates/scaffold/_job_deleted.html.erb
|
17
|
+
generators/background/templates/scaffold/_progress_indicator.html.erb
|
18
|
+
generators/background/templates/scaffold/background_fu.css
|
19
|
+
generators/background/templates/scaffold/index.html.erb
|
20
|
+
generators/background/templates/scaffold/jobs.html.erb
|
21
|
+
generators/background/templates/scaffold/jobs_controller.rb
|
22
|
+
generators/background/templates/scaffold/jobs_helper.rb
|
23
|
+
init.rb
|
24
|
+
lib/background_fu.rb
|
25
|
+
lib/background_fu/worker_monitoring.rb
|
26
|
+
lib/job.rb
|
27
|
+
lib/job/bonus_features.rb
|
data/README.txt
ADDED
@@ -0,0 +1,166 @@
|
|
1
|
+
= BackgroundFu
|
2
|
+
|
3
|
+
* http://github.com/ncr/background-fu
|
4
|
+
* git://github.com/ncr/background-fu.git
|
5
|
+
* http://trix.lighthouseapp.com/projects/9809-backgroundfu
|
6
|
+
* ncr@trix.pl
|
7
|
+
* http://trix.pl
|
8
|
+
|
9
|
+
== DESCRIPTION:
|
10
|
+
|
11
|
+
Background tasks in Ruby On Rails made dead simple.
|
12
|
+
|
13
|
+
== FEATURES/PROBLEMS:
|
14
|
+
|
15
|
+
* Running long background tasks outside of request-response cycle.
|
16
|
+
* Very easy to setup and fun to use (See examples below).
|
17
|
+
* Clean and straightforward approach (database-based priority queue).
|
18
|
+
* Uses database table (migration included) to store jobs reliably.
|
19
|
+
* Capistrano tasks included.
|
20
|
+
* Generators with migrations and example views included.
|
21
|
+
* Multiple worker daemons available.
|
22
|
+
* Easy to deploy in distributed environments.
|
23
|
+
* Enables prioritizing and simple scheduling.
|
24
|
+
* Optional worker monitoring (good for AJAX progress bars).
|
25
|
+
* Proven its stability and reliability in production use.
|
26
|
+
|
27
|
+
== SYNOPSIS:
|
28
|
+
|
29
|
+
ruby ./script/generate background
|
30
|
+
rake db:migrate
|
31
|
+
|
32
|
+
# to run in production mode use RAILS_ENV=production ruby ./script/daemons start
|
33
|
+
ruby ./script/daemons start
|
34
|
+
|
35
|
+
# then try in console:
|
36
|
+
|
37
|
+
job_id = Job.enqueue!(ExampleWorker, :add, 1, 2).id
|
38
|
+
|
39
|
+
# after few seconds when background daemon completes the job
|
40
|
+
|
41
|
+
Job.find(job_id).result # result of the job should equal 3
|
42
|
+
|
43
|
+
== EXAMPLES:
|
44
|
+
|
45
|
+
# In lib/workers/example_worker.rb:
|
46
|
+
|
47
|
+
# Simple, non-monitored worker.
|
48
|
+
class ExampleWorker
|
49
|
+
|
50
|
+
def add(a, b)
|
51
|
+
a + b
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
|
56
|
+
# In lib/workers/example_monitored_worker.rb:
|
57
|
+
|
58
|
+
# Remeber to include BackgroundFu::WorkerMonitoring.
|
59
|
+
class ExampleMonitoredWorker
|
60
|
+
|
61
|
+
include BackgroundFu::WorkerMonitoring
|
62
|
+
|
63
|
+
def long_and_monitored
|
64
|
+
my_progress = 0
|
65
|
+
|
66
|
+
record_progress(my_progress)
|
67
|
+
|
68
|
+
while(my_progress < 100)
|
69
|
+
my_progress += 1
|
70
|
+
record_progress(my_progress)
|
71
|
+
sleep 1
|
72
|
+
end
|
73
|
+
|
74
|
+
record_progress(100)
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
|
79
|
+
# In a controller:
|
80
|
+
|
81
|
+
def create
|
82
|
+
session[:job_id] = Job.enqueue!(ExampleWorker, :add, 1, 2).id
|
83
|
+
# or try the monitored worker: session[:job_id] = Job.enqueue!(ExampleMonitoredWorker, :long_and_monitored)
|
84
|
+
end
|
85
|
+
|
86
|
+
def show
|
87
|
+
@job = Job.find(session[:job_id])
|
88
|
+
@result = @job.result if @job.finished?
|
89
|
+
# or check progress if your worker is monitored: @progress = @job.progress
|
90
|
+
end
|
91
|
+
|
92
|
+
def index
|
93
|
+
@jobs = Job.find(:all)
|
94
|
+
end
|
95
|
+
|
96
|
+
def destroy
|
97
|
+
Job.find(session[:job_id]).destroy
|
98
|
+
end
|
99
|
+
|
100
|
+
== HANDY CAPISTRANO TASKS:
|
101
|
+
|
102
|
+
namespace :deploy do
|
103
|
+
|
104
|
+
desc "Run this after every successful deployment"
|
105
|
+
task :after_default do
|
106
|
+
restart_background_fu
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|
110
|
+
|
111
|
+
desc "Restart BackgroundFu daemon"
|
112
|
+
task :restart_background_fu do
|
113
|
+
run "RAILS_ENV=production ruby #{current_path}/script/daemons stop"
|
114
|
+
run "RAILS_ENV=production ruby #{current_path}/script/daemons start"
|
115
|
+
end
|
116
|
+
|
117
|
+
== BONUS FEATURES:
|
118
|
+
|
119
|
+
There are bonus features available if you set
|
120
|
+
ActiveRecord::Base.allow_concurrency = true
|
121
|
+
in your environment.
|
122
|
+
|
123
|
+
These features are:
|
124
|
+
* monitoring progress (perfect for ajax progress bars)
|
125
|
+
* stopping a running worker in a merciful way.
|
126
|
+
|
127
|
+
Read the code (short and easy) to discover them.
|
128
|
+
|
129
|
+
== REQUIREMENTS:
|
130
|
+
|
131
|
+
* rails
|
132
|
+
* daemons
|
133
|
+
|
134
|
+
== INSTALL:
|
135
|
+
|
136
|
+
* As a Rails plugin: ./script/plugin install git://github.com/ncr/background-fu.git
|
137
|
+
* As as a gem to be 'vendorized' starting from Rails 2.1: refer to documentation on rake gems:unpack:dependencies.
|
138
|
+
|
139
|
+
== CONTRIBUTING:
|
140
|
+
|
141
|
+
If you want to help improve this plugin, feel free to contact me. Fork the project on GitHub, implement a feature, send me a pull request.
|
142
|
+
|
143
|
+
== LICENSE:
|
144
|
+
|
145
|
+
(The MIT License)
|
146
|
+
|
147
|
+
Copyright (c) Jacek Becela
|
148
|
+
|
149
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
150
|
+
a copy of this software and associated documentation files (the
|
151
|
+
'Software'), to deal in the Software without restriction, including
|
152
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
153
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
154
|
+
permit persons to whom the Software is furnished to do so, subject to
|
155
|
+
the following conditions:
|
156
|
+
|
157
|
+
The above copyright notice and this permission notice shall be
|
158
|
+
included in all copies or substantial portions of the Software.
|
159
|
+
|
160
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
161
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
162
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
163
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
164
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
165
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
166
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
class BackgroundGenerator < Rails::Generator::Base
|
2
|
+
|
3
|
+
default_options :skip_migration => false, :skip_scaffold => false, :skip_examples => false
|
4
|
+
|
5
|
+
def manifest
|
6
|
+
record do |m|
|
7
|
+
unless options[:skip_migration]
|
8
|
+
m.migration_template 'migration.rb', 'db/migrate', :migration_file_name => "create_jobs"
|
9
|
+
end
|
10
|
+
|
11
|
+
m.directory "lib/daemons"
|
12
|
+
m.file 'background.rb', 'lib/daemons/background.rb'
|
13
|
+
m.file 'background_ctl', 'lib/daemons/background_ctl'
|
14
|
+
|
15
|
+
m.directory "lib/workers"
|
16
|
+
|
17
|
+
unless options[:skip_examples]
|
18
|
+
m.file 'example_worker.rb', 'lib/workers/example_worker.rb'
|
19
|
+
m.file 'example_monitored_worker.rb', 'lib/workers/example_monitored_worker.rb'
|
20
|
+
end
|
21
|
+
|
22
|
+
m.file 'daemons.yml', 'config/daemons.yml'
|
23
|
+
m.file 'daemons', 'script/daemons'
|
24
|
+
|
25
|
+
unless options[:skip_scaffold]
|
26
|
+
m.directory 'app/controllers/admin'
|
27
|
+
m.directory 'app/helpers/admin'
|
28
|
+
m.directory 'app/views/admin/jobs'
|
29
|
+
m.directory 'app/views/layouts/admin'
|
30
|
+
|
31
|
+
m.file 'scaffold/jobs_controller.rb', 'app/controllers/admin/jobs_controller.rb'
|
32
|
+
m.file 'scaffold/jobs_helper.rb', 'app/helpers/admin/jobs_helper.rb'
|
33
|
+
|
34
|
+
m.file 'scaffold/index.html.erb', 'app/views/admin/jobs/index.html.erb'
|
35
|
+
m.file 'scaffold/_job.html.erb', 'app/views/admin/jobs/_job.html.erb'
|
36
|
+
m.file 'scaffold/_job_deleted.html.erb', 'app/views/admin/jobs/_job_deleted.html.erb'
|
37
|
+
m.file 'scaffold/_progress_indicator.html.erb', 'app/views/admin/jobs/_progress_indicator.html.erb'
|
38
|
+
|
39
|
+
m.file 'scaffold/jobs.html.erb', 'app/views/layouts/admin/jobs.html.erb'
|
40
|
+
|
41
|
+
m.file "scaffold/background_fu.css", "public/stylesheets/background_fu.css"
|
42
|
+
|
43
|
+
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
protected
|
49
|
+
|
50
|
+
def add_options!(opt)
|
51
|
+
opt.separator ''
|
52
|
+
opt.separator 'Options:'
|
53
|
+
opt.on("--skip-migration", "Don't generate migration file for this model") { |v| options[:skip_migration] = v }
|
54
|
+
opt.on("--skip-scaffold", "Don't generate scaffold controller and views for this model") { |v| options[:skip_scaffold] = v }
|
55
|
+
opt.on("--skip-examples", "Don't generate example workers") { |v| options[:skip_examples] = v }
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require File.dirname(__FILE__) + "/../../config/environment"
|
4
|
+
|
5
|
+
Signal.trap("TERM") { exit }
|
6
|
+
|
7
|
+
if Job.included_modules.include?(Job::BonusFeatures)
|
8
|
+
RAILS_DEFAULT_LOGGER.info("BackgroundFu: Starting daemon (bonus features enabled).")
|
9
|
+
else
|
10
|
+
RAILS_DEFAULT_LOGGER.info("BackgroundFu: Starting daemon (bonus features disabled).")
|
11
|
+
end
|
12
|
+
|
13
|
+
loop do
|
14
|
+
if job = Job.find(:first, :conditions => ["state='pending' and start_at <= ?", Time.now], :order => "priority desc, start_at asc")
|
15
|
+
job.get_done!
|
16
|
+
else
|
17
|
+
RAILS_DEFAULT_LOGGER.info("BackgroundFu: Waiting for jobs...")
|
18
|
+
sleep 5
|
19
|
+
end
|
20
|
+
|
21
|
+
Job.destroy_all(["state='finished' and updated_at < ?", 1.week.ago])
|
22
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'rubygems'
|
3
|
+
require 'daemons'
|
4
|
+
require 'yaml'
|
5
|
+
require 'erb'
|
6
|
+
|
7
|
+
class Hash
|
8
|
+
def with_symbols!
|
9
|
+
self.keys.each{|key| self[key.to_s.to_sym] = self[key] }; self
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
options = YAML.load(
|
14
|
+
ERB.new(
|
15
|
+
IO.read(
|
16
|
+
File.dirname(__FILE__) + "/../../config/daemons.yml"
|
17
|
+
)).result).with_symbols!
|
18
|
+
options[:dir_mode] = options[:dir_mode].to_sym
|
19
|
+
|
20
|
+
Daemons.run File.dirname(__FILE__) + '/background.rb', options
|
@@ -0,0 +1,20 @@
|
|
1
|
+
class ExampleMonitoredWorker
|
2
|
+
|
3
|
+
# After including worker monitoring you can invoke record_progress() method.
|
4
|
+
include BackgroundFu::WorkerMonitoring
|
5
|
+
|
6
|
+
def long_and_monitored
|
7
|
+
my_progress = 0
|
8
|
+
|
9
|
+
record_progress(my_progress)
|
10
|
+
|
11
|
+
while(my_progress < 100)
|
12
|
+
my_progress += 1
|
13
|
+
record_progress(my_progress)
|
14
|
+
sleep 1
|
15
|
+
end
|
16
|
+
|
17
|
+
record_progress(100)
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
class CreateJobs < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
create_table :jobs do |t|
|
4
|
+
t.string :worker_class
|
5
|
+
t.string :worker_method
|
6
|
+
|
7
|
+
t.text :args
|
8
|
+
t.integer :priority
|
9
|
+
|
10
|
+
t.integer :progress
|
11
|
+
t.string :state
|
12
|
+
|
13
|
+
t.integer :lock_version, :default => 0
|
14
|
+
|
15
|
+
t.timestamp :start_at
|
16
|
+
t.timestamp :started_at
|
17
|
+
t.timestamps
|
18
|
+
|
19
|
+
t.columns << 'result longtext' # text can store 65kb only, it's often too short
|
20
|
+
end
|
21
|
+
|
22
|
+
add_index :jobs, :state
|
23
|
+
add_index :jobs, :start_at
|
24
|
+
add_index :jobs, :priority
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.down
|
28
|
+
drop_table :jobs
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
<td class="checkbox">
|
2
|
+
<% case job.state
|
3
|
+
when "running" %>
|
4
|
+
<%= link_to_remote("Stop", :url => admin_job_path(job, :command => "stop"), :method => :put, :confirm => "Are you sure?") %>
|
5
|
+
<% when "failed", "stopped" %>
|
6
|
+
<%= link_to_remote("Restart", :url => admin_job_path(job, :command => "restart"), :method => :put, :confirm => "Are you sure?") %> <br />
|
7
|
+
<%= link_to_remote("Delete", :url => admin_job_path(job), :method => :delete, :confirm => "Are you sure?") %>
|
8
|
+
<% when "finished", "pending" %>
|
9
|
+
<%= link_to_remote("Delete", :url => admin_job_path(job), :method => :delete, :confirm => "Are you sure?") %><br />
|
10
|
+
<%= link_to("Download result", formatted_admin_job_path(job, :txt)) if downloadable?(job) %>
|
11
|
+
<% end %>
|
12
|
+
<br />
|
13
|
+
</td>
|
14
|
+
<td class="progress">
|
15
|
+
<%= render :partial => "progress_indicator",
|
16
|
+
:locals => {
|
17
|
+
:progress => job.progress,
|
18
|
+
:elapsed => job.elapsed,
|
19
|
+
:estimated => job.estimated,
|
20
|
+
:state => job.state
|
21
|
+
}
|
22
|
+
%>
|
23
|
+
</td>
|
24
|
+
<td class="info">
|
25
|
+
<h2><%= job.worker_class %>.new.<%= job.worker_method %>( <strong><%= h truncate(job.args.map(&:inspect).join(", "), 255) %></strong> )</h2>
|
26
|
+
<p>
|
27
|
+
started at: <strong><%= time_ago_in_words_with_customization(job.started_at) %></strong>,
|
28
|
+
created at: <strong><%= time_ago_in_words_with_customization(job.created_at) %></strong>,
|
29
|
+
updated at: <strong><%= time_ago_in_words_with_customization(job.updated_at) %></strong>,<br />
|
30
|
+
state: <strong><%= job.state %></strong>,
|
31
|
+
progress: <strong><%= job.progress || "n/a" %></strong>,
|
32
|
+
result: <strong><%=h truncate(job.result.inspect, 255) %></strong>
|
33
|
+
</p>
|
34
|
+
</td>
|
35
|
+
<script type="text/javascript">
|
36
|
+
//<![CDATA[
|
37
|
+
job_state_<%= dom_id(job) %> = "<%= job.state %>";
|
38
|
+
//]]>
|
39
|
+
</script>
|
@@ -0,0 +1,5 @@
|
|
1
|
+
<div class="progress-indicator">
|
2
|
+
<span class="bar <%= state %>" style="width: <%= progress %>%;"></span>
|
3
|
+
<span class="label elapsed"><%= seconds_in_short(elapsed) if elapsed.to_i > 0 %></span>
|
4
|
+
<span class="label estimated"><%= seconds_in_short(estimated) if estimated.to_i > 0 %></span>
|
5
|
+
</div>
|
@@ -0,0 +1,23 @@
|
|
1
|
+
.progress-indicator { height: 6em; width: 30em; position: relative; border: 1px solid #536070; background-color: #A8B1BC; }
|
2
|
+
.progress-indicator span { position: absolute; height: 100%;}
|
3
|
+
.progress-indicator .bar.running { background-color: #1687C2; }
|
4
|
+
.progress-indicator .bar.finished { background-color: #00AF00; }
|
5
|
+
.progress-indicator .bar.stopping { background-color: orange; }
|
6
|
+
.progress-indicator .bar.stopped { background-color: #E90000; }
|
7
|
+
.progress-indicator .bar.failed { background-color: #C90000; }
|
8
|
+
|
9
|
+
.progress-indicator .label { width: 100%; font-size: 1.5em; line-height: 1.5em; font-weight: bold; font-family: "Myriad Pro", sans-serif; color: white; }
|
10
|
+
.progress-indicator .elapsed { margin-left: 2%; }
|
11
|
+
.progress-indicator .estimated { margin-left: 85%; }
|
12
|
+
|
13
|
+
table.jobs td { border: none; vertical-align: top; color: #555; border-bottom: 1px solid white; }
|
14
|
+
table.jobs tr.even td{ background-color: #f6f6f6; }
|
15
|
+
table.jobs tr.odd td { background-color: #efefef; }
|
16
|
+
table.jobs td h2 { margin-top: 0; }
|
17
|
+
table.jobs strong { color: #000; font-weight: bold; }
|
18
|
+
|
19
|
+
form.job input { margin: 0 0 1em 0; }
|
20
|
+
form.job textarea { width: 50%; }
|
21
|
+
form.job input.worker-class { font-size: 2em; font-weight: bold; width: 33%; }
|
22
|
+
form.job input.worker-method { font-size: 1.5em; font-weight: bold; width: 33%; }
|
23
|
+
form.job input.button { font-size: 1.2em; font-weight: bold; }
|
@@ -0,0 +1,27 @@
|
|
1
|
+
<div>
|
2
|
+
<% form_for(:job, :html => {:class => "job"}) do |f| %>
|
3
|
+
<%= f.label :worker_class %><br />
|
4
|
+
<%= f.text_field :worker_class, :class => "worker-class" %><br />
|
5
|
+
<%= f.label :worker_method %><br />
|
6
|
+
<%= f.text_field :worker_method, :class => "worker-method" %><br />
|
7
|
+
<%= f.label :args, "Args - each line (including empty) is interpreted as a string argument" %><br />
|
8
|
+
<%= f.text_area :args %><br />
|
9
|
+
<%= f.submit "Enqueue", :class => "button" %><br />
|
10
|
+
<% end %>
|
11
|
+
</div>
|
12
|
+
|
13
|
+
<hr style="visibility:hidden; margin: 20px auto" />
|
14
|
+
|
15
|
+
<table class="jobs">
|
16
|
+
<% @jobs.each do |job| %>
|
17
|
+
<tr class="job <%= cycle(:odd, :even) %>" id="<%= dom_id(job) %>">
|
18
|
+
<%= render :partial => "job", :locals => {:job => job} %>
|
19
|
+
</tr>
|
20
|
+
<%= periodically_call_remote(
|
21
|
+
:url => admin_job_path(job),
|
22
|
+
:method => "get",
|
23
|
+
:interval => 5,
|
24
|
+
:condition => "/^(running|pending|stopping)$/.match(job_state_#{dom_id(job)})")
|
25
|
+
%>
|
26
|
+
<% end %>
|
27
|
+
</table>
|
@@ -0,0 +1,22 @@
|
|
1
|
+
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
2
|
+
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
3
|
+
<head>
|
4
|
+
<title>Background Jobs - BackgroundFu</title>
|
5
|
+
<link rel="stylesheet" type="text/css" href="http://yui.yahooapis.com/2.5.0/build/reset-fonts-grids/reset-fonts-grids.css">
|
6
|
+
<link rel="stylesheet" type="text/css" href="http://yui.yahooapis.com/2.5.0/build/base/base-min.css">
|
7
|
+
<style type="text/css">
|
8
|
+
#custom-doc { width: 90%; min-width: 250px; }
|
9
|
+
</style>
|
10
|
+
<%= javascript_include_tag :defaults %>
|
11
|
+
<%= stylesheet_link_tag 'background_fu' %>
|
12
|
+
</head>
|
13
|
+
<body>
|
14
|
+
<div id="custom-doc" class="yui-t7">
|
15
|
+
<div id="hd"><h1>BackgroundFu Jobs</h1></div>
|
16
|
+
<div id="bd"><div class="yui-g">
|
17
|
+
<%= yield %>
|
18
|
+
</div></div>
|
19
|
+
<div id="ft" style="text-align: center;"><a href="http://trix.pl/background_fu">BackgroundFu</a> Running Background Jobs Made Dead Simple <a href="http://trix.pl">Jacek Becela</a> 2008 </div>
|
20
|
+
</div>
|
21
|
+
</body>
|
22
|
+
</html>
|
@@ -0,0 +1,60 @@
|
|
1
|
+
class Admin::JobsController < Admin::ApplicationController
|
2
|
+
|
3
|
+
def index
|
4
|
+
@jobs = Job.find(:all, :order => "id desc")
|
5
|
+
end
|
6
|
+
|
7
|
+
def show
|
8
|
+
@job = Job.find(params[:id])
|
9
|
+
|
10
|
+
respond_to do |format|
|
11
|
+
format.text do
|
12
|
+
if @job.result.respond_to?(:join)
|
13
|
+
send_data @job.result.join("\n"), :type => "text/plain", :disposition => "attachment"
|
14
|
+
else
|
15
|
+
render :nothing => true, :status => 404
|
16
|
+
end
|
17
|
+
end
|
18
|
+
format.js do
|
19
|
+
render :update do |page|
|
20
|
+
page[@job].replace_html :partial => "job"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def create
|
27
|
+
params[:job][:args] = params[:job][:args].split("\n").map(&:strip)
|
28
|
+
@job = Job.new(params[:job])
|
29
|
+
|
30
|
+
if @job.save
|
31
|
+
redirect_to admin_jobs_path
|
32
|
+
else
|
33
|
+
render :action => "new"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def update
|
38
|
+
@job = Job.find(params[:id])
|
39
|
+
|
40
|
+
if params[:command] == "stop"
|
41
|
+
@job.stop!
|
42
|
+
elsif params[:command] == "restart"
|
43
|
+
@job.restart!
|
44
|
+
end
|
45
|
+
|
46
|
+
render :update do |page|
|
47
|
+
page[@job].replace_html :partial => "job"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def destroy
|
52
|
+
@job = Job.find(params[:id])
|
53
|
+
@job.destroy
|
54
|
+
|
55
|
+
render :update do |page|
|
56
|
+
page[@job].replace_html :partial => "job_deleted", :object => @job
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Admin::JobsHelper
|
2
|
+
|
3
|
+
def seconds_in_short(seconds)
|
4
|
+
seconds = seconds.to_i
|
5
|
+
minutes = seconds / 60
|
6
|
+
hours = seconds / (60 * 60)
|
7
|
+
days = seconds / (60 * 60 * 24)
|
8
|
+
|
9
|
+
if days > 0
|
10
|
+
return "#{days} d"
|
11
|
+
elsif hours > 0
|
12
|
+
return "#{hours} h"
|
13
|
+
elsif minutes > 0
|
14
|
+
return "#{minutes} m"
|
15
|
+
else
|
16
|
+
return "#{seconds} s"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def time_ago_in_words_with_customization(time)
|
21
|
+
if time
|
22
|
+
"#{time_ago_in_words(time)} ago"
|
23
|
+
else
|
24
|
+
"n/a"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def downloadable?(job)
|
29
|
+
job.result.respond_to?(:join)
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
data/init.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
# Include it in your workers to enable progress monitoring and stopping jobs.
|
2
|
+
module BackgroundFu::WorkerMonitoring
|
3
|
+
|
4
|
+
# In most cases you will have some loop which will execute known (m) times.
|
5
|
+
# Every time the loop iterates you increment a counter (n).
|
6
|
+
# The formula to get progress in percents is: 100 * n / m.
|
7
|
+
# If you invoke this method with second argument, then this is calculated for you.
|
8
|
+
# You also can omit second argument and progress will be passed directly to db.
|
9
|
+
def record_progress(progress_or_iteration, iterations_count = nil)
|
10
|
+
if iterations_count.to_i > 0
|
11
|
+
@progress = ((progress_or_iteration.to_f / iterations_count) * 100).to_i
|
12
|
+
else
|
13
|
+
@progress = progress_or_iteration.to_i
|
14
|
+
end
|
15
|
+
|
16
|
+
throw(:stopping, true) if @stopping
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
module Job::BonusFeatures
|
2
|
+
|
3
|
+
def self.included(base)
|
4
|
+
base.states += %w(stopping stopped)
|
5
|
+
base.generate_state_helpers
|
6
|
+
|
7
|
+
base.alias_method_chain :invoke_worker, :threads
|
8
|
+
base.alias_method_chain :ensure_worker, :threads
|
9
|
+
base.alias_method_chain :restart!, :threads
|
10
|
+
end
|
11
|
+
|
12
|
+
def invoke_worker_with_threads
|
13
|
+
monitor_worker
|
14
|
+
|
15
|
+
res = catch(:stopping) do
|
16
|
+
invoke_worker_without_threads; nil
|
17
|
+
end
|
18
|
+
|
19
|
+
self.state = res ? "stopped" : "finished"
|
20
|
+
end
|
21
|
+
|
22
|
+
def ensure_worker_with_threads
|
23
|
+
ensure_worker_without_threads
|
24
|
+
cleanup_after_threads
|
25
|
+
end
|
26
|
+
|
27
|
+
# The record_progress() method becomes available when your worker class includes
|
28
|
+
# Background::WorkerMonitoring.
|
29
|
+
#
|
30
|
+
# Every time worker invokes record_progress() is a possible stopping place.
|
31
|
+
#
|
32
|
+
# How it works:
|
33
|
+
# 1. invoke job.stop! to set a state (stopping) in a db
|
34
|
+
# 2. Monitoring thread picks up the state change from db
|
35
|
+
# and sets @stopping to true in the worker.
|
36
|
+
# 3. The worker invokes a register_progress() somewhere during execution.
|
37
|
+
# 4. The record_progress() method throws :stopping symbol if @stopping == true
|
38
|
+
# 5. The job catches the :stopping symbol and reacts upon it.
|
39
|
+
# 6. The job is stopped in a merciful way. No one gets harmed.
|
40
|
+
def stop!
|
41
|
+
if running?
|
42
|
+
update_attribute(:state, "stopping")
|
43
|
+
logger.info("BackgroundFu: Stopping job. Job(id: #{id}).")
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Overwritten because of new "stopped" state.
|
48
|
+
def restart_with_threads!
|
49
|
+
if stopped? || failed?
|
50
|
+
update_attributes!(
|
51
|
+
:result => nil,
|
52
|
+
:progress => nil,
|
53
|
+
:started_at => nil,
|
54
|
+
:state => "pending"
|
55
|
+
)
|
56
|
+
logger.info("BackgroundFu: Restarting job. Job(id: #{id}).")
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# This is the only place where multi-threading
|
61
|
+
# is used in the plugin and is completely optional.
|
62
|
+
def monitor_worker
|
63
|
+
Thread.new do
|
64
|
+
# 1. running? - check if not failed or finished.
|
65
|
+
# 2. !Job.find(id).stopping? - check if someone ordered stopping the job.
|
66
|
+
while(running? && !Job.find(id).stopping?)
|
67
|
+
current_progress = @worker.instance_variable_get("@progress")
|
68
|
+
|
69
|
+
if current_progress == progress
|
70
|
+
sleep 5
|
71
|
+
else
|
72
|
+
update_attribute(:progress, current_progress)
|
73
|
+
sleep 1
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# If someone ordered stopping a job we infrom the worker that it should stop.
|
78
|
+
if(Job.find(id).stopping?)
|
79
|
+
@worker.instance_variable_set("@stopping", true)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
logger.info("BackgroundFu: Job monitoring started. Job(id: #{id}).")
|
83
|
+
end
|
84
|
+
|
85
|
+
# Closes database connections left after finished threads.
|
86
|
+
def cleanup_after_threads
|
87
|
+
ActiveRecord::Base.verify_active_connections!
|
88
|
+
end
|
89
|
+
|
90
|
+
def elapsed
|
91
|
+
(updated_at.to_f - started_at.to_f).to_i if !pending?
|
92
|
+
end
|
93
|
+
|
94
|
+
# seconds to go, based on estimated and progress
|
95
|
+
def estimated
|
96
|
+
((elapsed * 100) / progress) - elapsed if running? && (1..99).include?(progress.to_i)
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
data/lib/job.rb
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
# Example:
|
2
|
+
#
|
3
|
+
# job = Job.enqueue!(MyWorker, :my_method, "my_arg_1", "my_arg_2")
|
4
|
+
class Job < ActiveRecord::Base
|
5
|
+
|
6
|
+
cattr_accessor :states
|
7
|
+
self.states = %w(pending running finished failed)
|
8
|
+
|
9
|
+
serialize :args, Array
|
10
|
+
serialize :result
|
11
|
+
|
12
|
+
before_create :setup_state, :setup_priority, :setup_start_at
|
13
|
+
validates_presence_of :worker_class, :worker_method
|
14
|
+
|
15
|
+
attr_readonly :worker_class, :worker_method, :args
|
16
|
+
|
17
|
+
def self.enqueue!(worker_class, worker_method, *args)
|
18
|
+
job = create!(
|
19
|
+
:worker_class => worker_class.to_s,
|
20
|
+
:worker_method => worker_method.to_s,
|
21
|
+
:args => args
|
22
|
+
)
|
23
|
+
logger.info("BackgroundFu: Job enqueued. Job(id: #{job.id}, worker: #{worker_class}, method: #{worker_method}, argc: #{args.size}).")
|
24
|
+
end
|
25
|
+
|
26
|
+
# Invoked by a background daemon.
|
27
|
+
def get_done!
|
28
|
+
initialize_worker
|
29
|
+
invoke_worker
|
30
|
+
rescue Exception => e
|
31
|
+
rescue_worker(e)
|
32
|
+
ensure
|
33
|
+
ensure_worker
|
34
|
+
end
|
35
|
+
|
36
|
+
# Restart a failed job.
|
37
|
+
def restart!
|
38
|
+
if failed?
|
39
|
+
update_attributes!(
|
40
|
+
:result => nil,
|
41
|
+
:progress => nil,
|
42
|
+
:started_at => nil,
|
43
|
+
:state => "pending"
|
44
|
+
)
|
45
|
+
logger.info("BackgroundFu: Job restarted. Job(id: #{id}).")
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def initialize_worker
|
50
|
+
update_attributes!(:started_at => Time.now, :state => "running")
|
51
|
+
@worker = worker_class.constantize.new
|
52
|
+
logger.info("BackgroundFu: Job initialized. Job(id: #{id}).")
|
53
|
+
end
|
54
|
+
|
55
|
+
def invoke_worker
|
56
|
+
self.result = @worker.send!(worker_method, *args)
|
57
|
+
self.state = "finished"
|
58
|
+
logger.info("BackgroundFu: Job finished. Job(id: #{id}).")
|
59
|
+
end
|
60
|
+
|
61
|
+
def rescue_worker(exception)
|
62
|
+
self.result = [exception.message, exception.backtrace.join("\n")].join("\n\n")
|
63
|
+
self.state = "failed"
|
64
|
+
logger.info("BackgroundFu: Job failed. Job(id: #{id}).")
|
65
|
+
end
|
66
|
+
|
67
|
+
def ensure_worker
|
68
|
+
self.progress = @worker.instance_variable_get("@progress")
|
69
|
+
save!
|
70
|
+
rescue StaleObjectError
|
71
|
+
# Ignore this exception as its only purpose is
|
72
|
+
# not allowing multiple daemons execute the same job.
|
73
|
+
logger.info("BackgroundFu: Race condition handled (It's OK). Job(id: #{id}).")
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.generate_state_helpers
|
77
|
+
states.each do |state_name|
|
78
|
+
define_method("#{state_name}?") do
|
79
|
+
state == state_name
|
80
|
+
end
|
81
|
+
|
82
|
+
# Job.running => array of running jobs, etc.
|
83
|
+
self.class.send!(:define_method, state_name) do
|
84
|
+
find_all_by_state(state_name, :order => "id desc")
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
generate_state_helpers
|
89
|
+
|
90
|
+
def setup_state
|
91
|
+
return unless state.blank?
|
92
|
+
|
93
|
+
self.state = "pending"
|
94
|
+
end
|
95
|
+
|
96
|
+
# Default priority is 0. Jobs will be executed in descending priority order (negative priorities allowed).
|
97
|
+
def setup_priority
|
98
|
+
return unless priority.blank?
|
99
|
+
|
100
|
+
self.priority = 0
|
101
|
+
end
|
102
|
+
|
103
|
+
# Job will be executed after this timestamp.
|
104
|
+
def setup_start_at
|
105
|
+
return unless start_at.blank?
|
106
|
+
|
107
|
+
self.start_at = Time.now
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
metadata
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ncr-background_fu
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jacek Becela
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2008-05-27 00:00:00 -07:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: daemons
|
17
|
+
version_requirement:
|
18
|
+
version_requirements: !ruby/object:Gem::Requirement
|
19
|
+
requirements:
|
20
|
+
- - ">="
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 1.0.9
|
23
|
+
version:
|
24
|
+
description: Background tasks in Ruby On Rails made dead simple.
|
25
|
+
email:
|
26
|
+
- jacek.becela@gmail.com
|
27
|
+
executables: []
|
28
|
+
|
29
|
+
extensions: []
|
30
|
+
|
31
|
+
extra_rdoc_files: []
|
32
|
+
|
33
|
+
files:
|
34
|
+
- History.txt
|
35
|
+
- Manifest.txt
|
36
|
+
- README.txt
|
37
|
+
- Rakefile
|
38
|
+
- generators/background/USAGE
|
39
|
+
- generators/background/background_generator.rb
|
40
|
+
- generators/background/templates/background.rb
|
41
|
+
- generators/background/templates/background_ctl
|
42
|
+
- generators/background/templates/daemons
|
43
|
+
- generators/background/templates/daemons.yml
|
44
|
+
- generators/background/templates/example_monitored_worker.rb
|
45
|
+
- generators/background/templates/example_worker.rb
|
46
|
+
- generators/background/templates/migration.rb
|
47
|
+
- generators/background/templates/scaffold/_job.html.erb
|
48
|
+
- generators/background/templates/scaffold/_job_deleted.html.erb
|
49
|
+
- generators/background/templates/scaffold/_progress_indicator.html.erb
|
50
|
+
- generators/background/templates/scaffold/background_fu.css
|
51
|
+
- generators/background/templates/scaffold/index.html.erb
|
52
|
+
- generators/background/templates/scaffold/jobs.html.erb
|
53
|
+
- generators/background/templates/scaffold/jobs_controller.rb
|
54
|
+
- generators/background/templates/scaffold/jobs_helper.rb
|
55
|
+
- init.rb
|
56
|
+
- lib/background_fu.rb
|
57
|
+
- lib/background_fu/worker_monitoring.rb
|
58
|
+
- lib/job.rb
|
59
|
+
- lib/job/bonus_features.rb
|
60
|
+
has_rdoc: false
|
61
|
+
homepage: http://github.com/ncr/background-fu
|
62
|
+
post_install_message:
|
63
|
+
rdoc_options: []
|
64
|
+
|
65
|
+
require_paths:
|
66
|
+
- lib
|
67
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
68
|
+
requirements:
|
69
|
+
- - ">="
|
70
|
+
- !ruby/object:Gem::Version
|
71
|
+
version: "0"
|
72
|
+
version:
|
73
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
74
|
+
requirements:
|
75
|
+
- - ">="
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: "0"
|
78
|
+
version:
|
79
|
+
requirements: []
|
80
|
+
|
81
|
+
rubyforge_project: background-fu
|
82
|
+
rubygems_version: 1.0.1
|
83
|
+
signing_key:
|
84
|
+
specification_version: 2
|
85
|
+
summary: Background tasks in Ruby On Rails made dead simple.
|
86
|
+
test_files: []
|
87
|
+
|