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 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,8 @@
1
+ require 'rubygems'
2
+ require 'hoe'
3
+ require './lib/background_fu.rb'
4
+
5
+ Hoe.new('background-fu', BackgroundFu::VERSION) do |p|
6
+ p.rubyforge_name = 'background-fu'
7
+ p.developer('Jacek Becela', 'jacek.becela@gmail.com')
8
+ end
@@ -0,0 +1,3 @@
1
+ Examples:
2
+ ./script/generate background
3
+
@@ -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,2 @@
1
+ #!/usr/bin/env ruby
2
+ Dir[File.dirname(__FILE__) + "/../lib/daemons/*_ctl"].each {|f| `nice ruby #{f} #{ARGV.first}`}
@@ -0,0 +1,5 @@
1
+ dir_mode: script
2
+ dir: ../../log
3
+ multiple: false
4
+ backtrace: true
5
+ monitor: true
@@ -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
+ <script type="text/javascript">
2
+ //<![CDATA[
3
+ job_state_<%= dom_id(job_deleted) %> = "deleted";
4
+ //]]>
5
+ </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,5 @@
1
+ Dependencies.load_paths << "#{RAILS_ROOT}/lib/workers"
2
+
3
+ if ActiveRecord::Base.allow_concurrency
4
+ Job.send!(:include, Job::BonusFeatures)
5
+ end
@@ -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,5 @@
1
+ module BackgroundFu
2
+
3
+ VERSION = "1.0.2"
4
+
5
+ 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
+