backnob 0.0.1

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