ncr-background_fu 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
+
|