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 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
+