backnob 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt ADDED
@@ -0,0 +1,4 @@
1
+ == 0.0.1 2007-09-18
2
+
3
+ * 1 major enhancement:
4
+ * Initial release
data/License.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2007 Jeremy Wells, Boost New Media Ltd.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Manifest.txt ADDED
@@ -0,0 +1,23 @@
1
+ History.txt
2
+ License.txt
3
+ Manifest.txt
4
+ README.txt
5
+ Rakefile
6
+ bin/backnob
7
+ examples/backnob.yml
8
+ lib/backnob.rb
9
+ lib/backnob/client.rb
10
+ lib/backnob/hash.rb
11
+ lib/backnob/options.rb
12
+ lib/backnob/server.rb
13
+ lib/backnob/version.rb
14
+ lib/backnob/worker.rb
15
+ lib/backnob_control.rb
16
+ scripts/txt2html
17
+ setup.rb
18
+ test/backnob.yml
19
+ test/test_client.rb
20
+ test/test_controller.rb
21
+ test/test_helper.rb
22
+ test/test_server.rb
23
+ test/worker.rb
data/README.txt ADDED
@@ -0,0 +1,3 @@
1
+ README for backnob
2
+ ==================
3
+
data/Rakefile ADDED
@@ -0,0 +1,125 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'rake/clean'
4
+ require 'rake/testtask'
5
+ require 'rake/packagetask'
6
+ require 'rake/gempackagetask'
7
+ require 'rake/rdoctask'
8
+ require 'rake/contrib/rubyforgepublisher'
9
+ require 'fileutils'
10
+ require 'hoe'
11
+
12
+ include FileUtils
13
+ require File.join(File.dirname(__FILE__), 'lib', 'backnob', 'version')
14
+
15
+ AUTHOR = 'Jeremy Wells' # can also be an array of Authors
16
+ EMAIL = "jeremy@boost.co.nz"
17
+ DESCRIPTION = "Background workers"
18
+ GEM_NAME = 'backnob' # what ppl will type to install your gem
19
+
20
+ @config_file = "~/.rubyforge/user-config.yml"
21
+ @config = nil
22
+ def rubyforge_username
23
+ unless @config
24
+ begin
25
+ @config = YAML.load(File.read(File.expand_path(@config_file)))
26
+ rescue
27
+ puts <<-EOS
28
+ ERROR: No rubyforge config file found: #{@config_file}"
29
+ Run 'rubyforge setup' to prepare your env for access to Rubyforge
30
+ - See http://newgem.rubyforge.org/rubyforge.html for more details
31
+ EOS
32
+ exit
33
+ end
34
+ end
35
+ @rubyforge_username ||= @config["username"]
36
+ end
37
+
38
+ RUBYFORGE_PROJECT = 'backnob' # The unix name for your project
39
+ HOMEPATH = "http://#{RUBYFORGE_PROJECT}.rubyforge.org"
40
+ DOWNLOAD_PATH = "http://rubyforge.org/projects/#{RUBYFORGE_PROJECT}"
41
+
42
+ NAME = "backnob"
43
+ REV = nil
44
+ # UNCOMMENT IF REQUIRED:
45
+ # REV = `svn info`.each {|line| if line =~ /^Revision:/ then k,v = line.split(': '); break v.chomp; else next; end} rescue nil
46
+ VERS = Backnob::VERSION::STRING + (REV ? ".#{REV}" : "")
47
+ CLEAN.include ['**/.*.sw?', '*.gem', '.config', '**/.DS_Store']
48
+ RDOC_OPTS = ['--quiet', '--title', 'backnob documentation',
49
+ "--opname", "index.html",
50
+ "--line-numbers",
51
+ "--main", "README",
52
+ "--inline-source"]
53
+
54
+ class Hoe
55
+ def extra_deps
56
+ @extra_deps.reject { |x| Array(x).first == 'hoe' }
57
+ end
58
+ end
59
+
60
+ # Generate all the Rake tasks
61
+ # Run 'rake -T' to see list of generated tasks (from gem root directory)
62
+ hoe = Hoe.new(GEM_NAME, VERS) do |p|
63
+ p.author = AUTHOR
64
+ p.description = DESCRIPTION
65
+ p.email = EMAIL
66
+ p.summary = DESCRIPTION
67
+ p.url = HOMEPATH
68
+ p.rubyforge_name = RUBYFORGE_PROJECT if RUBYFORGE_PROJECT
69
+ p.test_globs = ["test/**/test_*.rb"]
70
+ p.clean_globs |= CLEAN #An array of file patterns to delete on clean.
71
+
72
+ # == Optional
73
+ p.changes = p.paragraphs_of("History.txt", 0..1).join("\n\n")
74
+ #p.extra_deps = [] # An array of rubygem dependencies [name, version], e.g. [ ['active_support', '>= 1.3.1'] ]
75
+ #p.spec_extras = {} # A hash of extra values to set in the gemspec.
76
+ end
77
+
78
+ CHANGES = hoe.paragraphs_of('History.txt', 0..1).join("\n\n")
79
+ PATH = (RUBYFORGE_PROJECT == GEM_NAME) ? RUBYFORGE_PROJECT : "#{RUBYFORGE_PROJECT}/#{GEM_NAME}"
80
+ hoe.remote_rdoc_dir = File.join(PATH.gsub(/^#{RUBYFORGE_PROJECT}\/?/,''), 'rdoc')
81
+
82
+ desc 'Generate website files'
83
+ task :website_generate do
84
+ Dir['website/**/*.txt'].each do |txt|
85
+ sh %{ ruby scripts/txt2html #{txt} > #{txt.gsub(/txt$/,'html')} }
86
+ end
87
+ end
88
+
89
+ desc 'Upload website files to rubyforge'
90
+ task :website_upload do
91
+ host = "#{rubyforge_username}@rubyforge.org"
92
+ remote_dir = "/var/www/gforge-projects/#{PATH}/"
93
+ local_dir = 'website'
94
+ sh %{rsync -aCv #{local_dir}/ #{host}:#{remote_dir}}
95
+ end
96
+
97
+ desc 'Generate and upload website files'
98
+ task :website => [:website_generate, :website_upload, :publish_docs]
99
+
100
+ desc 'Release the website and new gem version'
101
+ task :deploy => [:check_version, :website, :release] do
102
+ puts "Remember to create SVN tag:"
103
+ puts "svn copy svn+ssh://#{rubyforge_username}@rubyforge.org/var/svn/#{PATH}/trunk " +
104
+ "svn+ssh://#{rubyforge_username}@rubyforge.org/var/svn/#{PATH}/tags/REL-#{VERS} "
105
+ puts "Suggested comment:"
106
+ puts "Tagging release #{CHANGES}"
107
+ end
108
+
109
+ desc 'Runs tasks website_generate and install_gem as a local deployment of the gem'
110
+ task :local_deploy => [:website_generate, :install_gem]
111
+
112
+ task :check_version do
113
+ unless ENV['VERSION']
114
+ puts 'Must pass a VERSION=x.y.z release version'
115
+ exit
116
+ end
117
+ unless ENV['VERSION'] == VERS
118
+ puts "Please update your version.rb to match the release version, currently #{VERS}"
119
+ exit
120
+ end
121
+ end
122
+
123
+ task :test do
124
+ system('ruby lib/backnob_control.rb stop')
125
+ end
data/bin/backnob ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # Created by Jeremy Wells on 2007-9-18.
4
+ # Copyright (c) 2007 Boost New Media Ltd. All rights reserved.
5
+
6
+ begin
7
+ require 'rubygems'
8
+ rescue LoadError
9
+ # no rubygems to load, so we fail silently
10
+ end
11
+
12
+ require 'backnob_control'
@@ -0,0 +1,3 @@
1
+ ---
2
+ workers: lib/workers
3
+ listen: 127.0.0.1:6444
@@ -0,0 +1,58 @@
1
+ require 'drb/drb'
2
+ require 'pathname'
3
+
4
+ module Backnob
5
+ SERVER_URI = 'druby://127.0.0.1:6444'
6
+
7
+ class Client
8
+ def initialize(uri = nil)
9
+ @uri = uri || SERVER_URI
10
+ end
11
+
12
+ def create_worker(name, options = {})
13
+ server do |s|
14
+ s.create_worker(name, options)
15
+ end
16
+ end
17
+
18
+ def add_worker(file)
19
+ server do |s|
20
+ s.add_file(Pathname.new(file).realpath.to_s)
21
+ end
22
+ end
23
+
24
+ def available?
25
+ begin
26
+ server do |s|
27
+ s.ping
28
+ end
29
+ rescue
30
+ return false
31
+ end
32
+ end
33
+
34
+ def results(key, hk = nil)
35
+ server do |s|
36
+ s.results(key, hk)
37
+ end
38
+ end
39
+
40
+ def server
41
+ unless block_given?
42
+ DRb.start_service
43
+ return DRbObject.new_with_uri(@uri)
44
+ else
45
+ begin
46
+ DRb.start_service
47
+ s = DRbObject.new_with_uri(@uri)
48
+
49
+ ret = yield(s)
50
+ ensure
51
+ DRb.stop_service
52
+ end
53
+
54
+ ret
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,12 @@
1
+ class Hash
2
+ # Destructively convert all keys to symbols.
3
+ def symbolize_keys!
4
+ keys.each do |key|
5
+ unless key.is_a?(Symbol)
6
+ self[key.to_sym] = self[key]
7
+ delete(key)
8
+ end
9
+ end
10
+ self
11
+ end
12
+ end
@@ -0,0 +1,17 @@
1
+ require 'pathname'
2
+ require File.dirname(__FILE__) + '/hash'
3
+
4
+ module Backnob
5
+ module Options
6
+ def sanitize!
7
+ self.symbolize_keys!
8
+ self[:workers] = [self[:workers]].compact unless self[:workers].is_a?(Array)
9
+ self[:workers].collect!{|f| Pathname.new(f).realpath.to_s } if self[:workers]
10
+
11
+ if self[:listen]
12
+ self[:listen] = 'druby://' + self[:listen] unless self[:listen] =~ /\:\/\//
13
+ self[:listen] << ':6444' unless self[:listen] =~ /\:\d+/
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,208 @@
1
+ require 'rubygems'
2
+ require 'logger'
3
+ require 'drb/drb'
4
+ require 'observer'
5
+ require 'drb/observer'
6
+ require 'digest/sha1'
7
+ require 'singleton'
8
+ require 'slave'
9
+ require 'thread'
10
+ require 'yaml'
11
+ require File.dirname(__FILE__) + '/options'
12
+ require File.dirname(__FILE__) + '/worker'
13
+
14
+ module Backnob
15
+ class Server
16
+ include Singleton
17
+
18
+ # Don't let this object be marshalled
19
+ include DRb::DRbUndumped
20
+
21
+ # Default options
22
+ OPTIONS = {:listen => '127.0.0.1:6444'}
23
+ # Array of worker classes
24
+ WORKER_KLASSES = []
25
+
26
+ # Pass any calls to class to instance
27
+ def self.method_missing(method, *args, &block)
28
+ self.instance.__send__(method, *args)
29
+ end
30
+
31
+ # Stop the server and all workers
32
+ # If receieved twice then stops even if
33
+ # workers are still running
34
+ def stop
35
+ unless true #@stopping
36
+ @stopping = true
37
+ logger.info "Waiting for workers to exit"
38
+ @workers.values.each{|w| w.wait}
39
+ else
40
+ logger.info "Forcing workers to exit"
41
+ @workers.values.each{|w| w.shutdown}
42
+ end
43
+
44
+ logger.info "Stopping DRb service"
45
+ DRb.stop_service
46
+ end
47
+
48
+ # Start the server
49
+ def start(options = {})
50
+ options.symbolize_keys!
51
+
52
+ # Sanitize the options
53
+ @options = OPTIONS.merge(options)
54
+ @options.extend(Backnob::Options)
55
+ @options.sanitize!
56
+
57
+ if @options[:path] && File.exists?(@options[:path])
58
+ Dir.chdir(@options[:path])
59
+ end
60
+
61
+ # Add the workers directory
62
+ add_file File.dirname(__FILE__) + '/workers'
63
+
64
+ # Set default variables
65
+ @workers = {}
66
+ @results = {}
67
+ @rqueue = Queue.new
68
+
69
+ m = self
70
+
71
+ # Trap TERM and INT signals to run the
72
+ # stop method
73
+ Signal.trap("TERM") do
74
+ m.stop
75
+ end
76
+
77
+ Signal.trap("INT") do
78
+ m.stop
79
+ end
80
+
81
+ DRb.start_service(@options[:listen], m)
82
+ logger.info "Waiting on #{@options[:listen]}"
83
+
84
+ # This thread now waits for exit, and also
85
+ # caches results off the results queue.
86
+ # It also cleans up workers that have quit.
87
+ while DRb.thread && DRb.thread.alive?
88
+ sleep 0.25
89
+
90
+ unless @rqueue.empty?
91
+ # Unmarshal results
92
+ data = ::Marshal.load(@rqueue.pop)
93
+ key = data[0]
94
+
95
+ @results[key] = {} unless @results.has_key?(key)
96
+ @results[key][data[1]] = data[2]
97
+
98
+ if data[1] == :error
99
+ logger.debug "Error occurred in worker #{key}"
100
+ logger.debug data[2].to_s
101
+ logger.debug data[2].backtrace if data[2].backtrace
102
+ end
103
+
104
+ if data[1] == :running && data[2] == false
105
+ @workers[key].shutdown(:quiet => true)
106
+ @workers.delete(key)
107
+ end
108
+ end
109
+ end
110
+ end
111
+
112
+ # Get results from a worker. Results are cached
113
+ # if possible
114
+ def results(key, hk = nil)
115
+ results = {}
116
+ if @results[key]
117
+ results = @results[key]
118
+ else
119
+ results = @workers[key].object.results() rescue {}
120
+ end
121
+ (hk ? results[hk] : results)
122
+ end
123
+
124
+ # Just return true. Used by client to test server is responding
125
+ def ping
126
+ return true
127
+ end
128
+
129
+ # Update method for observing workers. This recieved marshalled
130
+ # result data and puts it on the receive queue for caching
131
+ def update(data)
132
+ @rqueue << data
133
+ end
134
+
135
+ # Return a default logger for this process
136
+ def logger
137
+ @logger ||= Logger.new($stdout)
138
+ end
139
+
140
+ # Create a new worker. Given a name it will find the first
141
+ # class that matches that name. Options hash is passed to
142
+ # the worker. A key is returned which can be used to retrieve
143
+ # the worker results.
144
+ def create_worker(name, options = {})
145
+ load_workers
146
+
147
+ klass = WORKER_KLASSES.detect do |w|
148
+ w.name =~ /#{name}/i
149
+ end
150
+
151
+ if klass
152
+ key = Digest::SHA1.hexdigest(Time.now.to_i.to_s + klass.name)
153
+
154
+ logger.debug "Creating worker #{klass.name} with key #{key}"
155
+
156
+ worker = Slave.new :object => klass.new(key, options)
157
+ worker.object.add_observer(self)
158
+ worker.object.start
159
+ @workers[key] = worker
160
+
161
+ key
162
+ end
163
+ end
164
+
165
+ # Register a class as a worker
166
+ def register(klass)
167
+ WORKER_KLASSES.delete(klass) rescue nil
168
+ WORKER_KLASSES << klass
169
+ end
170
+
171
+ # Add a worker file
172
+ def add_file(file)
173
+ @options[:workers] ||= []
174
+
175
+ if File.exists?(file)
176
+ @options[:workers].delete(file)
177
+ @options[:workers] << file
178
+
179
+ logger.debug "Added worker file #{file}"
180
+ end
181
+
182
+ @options[:workers]
183
+ end
184
+
185
+ private
186
+
187
+ # Load each worker file. Worker files should
188
+ # have calls to register their classes.
189
+ def load_workers
190
+ @options[:workers].each do |file|
191
+ if File.exists?(file)
192
+ # If the file is a directory then load all
193
+ # the rb files inside it.
194
+ if File.directory?(file)
195
+ Dir.glob(file + '/*.rb').each do |cfile|
196
+ load cfile
197
+ end
198
+ else
199
+ load file
200
+ end
201
+ end
202
+ end
203
+ end
204
+ end
205
+ end
206
+
207
+ options = defined?(OPTIONS) ? OPTIONS : {}
208
+ Backnob::Server.start(options)
@@ -0,0 +1,9 @@
1
+ module Backnob #:nodoc:
2
+ module VERSION #:nodoc:
3
+ MAJOR = 0
4
+ MINOR = 0
5
+ TINY = 1
6
+
7
+ STRING = [MAJOR, MINOR, TINY].join('.')
8
+ end
9
+ end
@@ -0,0 +1,103 @@
1
+ require 'rubygems'
2
+ require 'slave'
3
+ require 'thread'
4
+ require 'active_support'
5
+ require 'monitor'
6
+ require 'thread'
7
+ require 'drb/observer'
8
+
9
+ module Kernel
10
+ def singleton_class
11
+ class << self; self; end
12
+ end
13
+ end
14
+
15
+ module Backnob
16
+ class Worker
17
+ include DRb::DRbObservable
18
+
19
+ def self.define_attr_method(name, value)
20
+ class_eval do
21
+ define_method(name) do
22
+ value
23
+ end
24
+ end
25
+ end
26
+
27
+ def self.define_class_attr(name, default)
28
+ define_attr_method name, default
29
+ self.singleton_class.send :define_method, name do |value|
30
+ define_attr_method name, value
31
+ end
32
+ end
33
+
34
+ define_class_attr :rails, false
35
+
36
+ def initialize(key, options = {})
37
+ @key = key
38
+ @options = options
39
+ @results = {}
40
+ @results.extend(MonitorMixin)
41
+ end
42
+
43
+ def logger
44
+ @logger ||= Logger.new($stdout)
45
+ end
46
+
47
+ # Start this worker. Starts a new thread and calls execute
48
+ # for work to be performed.
49
+ def start
50
+ @thread = Thread.new do
51
+ Thread.pass
52
+
53
+ logger.debug "Starting #{self.class.name} with key #{@key}"
54
+
55
+ results(:running, true)
56
+
57
+ if rails
58
+ require 'config/environment'
59
+ ActiveRecord::Base.logger = logger
60
+ end
61
+
62
+ # require File.dirname(__FILE__) + "/../../config/environment"
63
+ # ActiveRecord::Base.logger = logger
64
+
65
+ begin
66
+ execute
67
+ rescue Exception => e
68
+ results(:error, e)
69
+ ensure
70
+ results(:running, false)
71
+ logger.debug "Finished #{self.class.name} with key #{@key}"
72
+ end
73
+ end
74
+ end
75
+
76
+ # Get or set results. Workers should access results
77
+ # through this method as it is synchronized
78
+ def results(key = nil,value = nil)
79
+ res = nil
80
+
81
+ @results.synchronize do
82
+ if key.nil?
83
+ res = @results.dup
84
+ elsif value.nil?
85
+ res = @results[key]
86
+ else
87
+ if value != @results[key]
88
+ @results[key] = value
89
+ changed(true) # I've changed
90
+
91
+ # Marshal results into string
92
+ # for some reason marshal doesn't like hash, so
93
+ # convert to array and convert back on server side
94
+ obo = ::Marshal.dump([@key, key, value])
95
+ notify_observers(obo)
96
+ end
97
+ end
98
+ end
99
+
100
+ res
101
+ end
102
+ end
103
+ end
data/lib/backnob.rb ADDED
@@ -0,0 +1,4 @@
1
+ module Backnob
2
+ end
3
+
4
+ require 'backnob/version'
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # Created by Jeremy Wells on 2007-9-18.
4
+ # Copyright (c) 2007 Boost New Media Ltd. All rights reserved.
5
+
6
+ begin
7
+ require 'rubygems'
8
+ rescue LoadError
9
+ # no rubygems to load, so we fail silently
10
+ end
11
+
12
+ require 'pathname'
13
+ require 'optparse'
14
+ require 'rubygems'
15
+ require 'daemons'
16
+ require 'yaml'
17
+ require File.dirname(__FILE__) + '/backnob/options'
18
+ require File.dirname(__FILE__) + '/backnob/version'
19
+
20
+ OPTIONS = {:path => Pathname.new('.').realpath.to_s}
21
+ OPTIONS.extend(Backnob::Options)
22
+
23
+ parser = OptionParser.new do |opts|
24
+ opts.banner = <<BANNER
25
+ Backnob server version #{Backnob::VERSION::STRING}
26
+
27
+ Server Usage: #{File.basename($0)} start|stop|restart|run
28
+ start: Start the server
29
+ stop: Stop the server
30
+ restart: Restart the server
31
+ run: Run the server in the foreground
32
+
33
+ Client Usage: #{File.basename($0)} create|results|generate
34
+ create: Create and start a worker. Returns the worker key
35
+ results: Get the results for a worker key.
36
+ generate: Not sure yet
37
+
38
+ Options are:
39
+ BANNER
40
+ opts.separator ""
41
+ opts.on("-h", "--help",
42
+ "Show this help message.") { puts opts; exit }
43
+ opts.on("-cFILE", "--configuration=FILE", "Load a configuration file") do |c|
44
+ if File.exists?(c)
45
+ file_options = YAML.load(File.read(c)) rescue {}
46
+ OPTIONS.merge!(file_options)
47
+ end
48
+ end
49
+ opts.on("-lADDRESS", "--listen=ADDRESS", "Set the listen address. Defaults to 127.0.0.1:6444") {|l| OPTIONS[:listen] = l }
50
+ opts.on("-v", "--version", "Show the version") { puts "Backnob server version #{Backnob::VERSION::STRING}"; exit }
51
+ opts.parse!(ARGV)
52
+
53
+ OPTIONS.sanitize!
54
+
55
+ case ARGV.first.to_s.downcase
56
+ when 'start', 'stop', 'run', 'restart'
57
+ Daemons.run(File.dirname(__FILE__) + '/backnob/server.rb', :dir_mode => :normal, :dir => '/tmp', :mode => :load, :log_output => true)
58
+ when 'create', 'generate', 'results'
59
+ require 'backnob/client'
60
+ client = Backnob::Client.new
61
+ command = ARGV.pop
62
+
63
+ unless client.available?
64
+ puts <<-EOF
65
+ You must have a server running before using the #{command} command.
66
+ Run a server using `backnob start`
67
+ EOF
68
+ exit
69
+ end
70
+
71
+ case command.downcase
72
+ when 'generate'
73
+
74
+ when 'create'
75
+ begin; puts 'You must specify a worker to create'; exit; end unless ARGV.first
76
+ puts client.create_worker(ARGV.first)
77
+ when 'results'
78
+ begin; puts 'You must specify a worker key to get results'; exit; end unless ARGV.first
79
+ puts client.results(ARGV.first)
80
+ end
81
+ else
82
+ puts opts
83
+ end
84
+ end