workforce 0.1.0

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/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,23 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+
21
+ ## PROJECT::SPECIFIC
22
+ log
23
+ pid
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Rodrigo Kochenburger
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/README.md ADDED
@@ -0,0 +1,26 @@
1
+ Summary
2
+ =======
3
+
4
+ Workforce is an attempt to create a framework to develop background processes and controlling the execution of these processes in a distributed environment.
5
+
6
+ Project Description
7
+ ===================
8
+
9
+ Coming soon.
10
+
11
+ Contributing
12
+ ============
13
+
14
+ Note on Patches/Pull Requests
15
+ -----------------------------
16
+
17
+ * Fork the project.
18
+ * Make your feature addition or bug fix.
19
+ * Add tests for it. This is important so I don't break it in a future version unintentionally.
20
+ * Commit, do not mess with Rakefile, version, or history.
21
+ * Send me a pull request. Bonus points for topic branches.
22
+
23
+ Copyright & Licensing
24
+ =====================
25
+
26
+ Copyright (c) 2010 Rodrigo Kochenburger. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,45 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "workforce"
8
+ gem.summary = "Framework and a distributed runtime manager to creating and running background processes"
9
+ gem.description = "Workforce is an attempt to create a framework to develop background processes and controlling the execution of these processes in a distributed environment"
10
+ gem.email = "divoxx@gmail.com"
11
+ gem.homepage = "http://github.com/divoxx/workforce"
12
+ gem.authors = ["Rodrigo Kochenburger"]
13
+ gem.add_development_dependency "rspec", ">= 1.2.9"
14
+ gem.add_development_dependency "yard", ">= 0.5.5"
15
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
16
+ end
17
+ Jeweler::GemcutterTasks.new
18
+ rescue LoadError
19
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
20
+ end
21
+
22
+ require 'spec/rake/spectask'
23
+ Spec::Rake::SpecTask.new(:spec) do |spec|
24
+ spec.libs << 'lib' << 'spec'
25
+ spec.spec_files = FileList['spec/**/*_spec.rb']
26
+ end
27
+
28
+ Spec::Rake::SpecTask.new(:rcov) do |spec|
29
+ spec.libs << 'lib' << 'spec'
30
+ spec.pattern = 'spec/**/*_spec.rb'
31
+ spec.rcov = true
32
+ end
33
+
34
+ task :spec => :check_dependencies
35
+
36
+ task :default => :spec
37
+
38
+ begin
39
+ require 'yard'
40
+ YARD::Rake::YardocTask.new
41
+ rescue LoadError
42
+ task :yardoc do
43
+ abort "YARD is not available. In order to run yardoc, you must: sudo gem install yard"
44
+ end
45
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
data/bin/workforce ADDED
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env ruby
2
+ require "ostruct"
3
+ require "optparse"
4
+ require "workforce"
5
+
6
+ config = Workforce::Config.instance
7
+ files_to_require = []
8
+
9
+ optparser = OptionParser.new do |opts|
10
+ opts.banner = "Usage: #{$0} [options] path/to/worker.rb #{Workforce::Controller::ACTIONS.join("|")}"
11
+
12
+ opts.separator ""
13
+ opts.separator "Options:"
14
+
15
+ opts.on("-c", "--config [CONFIG_PATH]", "Uses configurations in config_file") do |config_path|
16
+ config.load_file(config_path)
17
+ end
18
+
19
+ opts.on("--pid-dir PID_DIR", "Store pids on directory PID_DIR", "Default: #{config.pid_dir}") do |pid_dir|
20
+ config.pid_dir = pid_dir
21
+ end
22
+
23
+ opts.on("--log-dir LOG_DIR", "Store logs on directory LOG_DIR", "Default: #{config.log_dir}") do |log_dir|
24
+ config.log_dir = log_dir
25
+ end
26
+
27
+ opts.on("--require FILE", "Require FILE before executing action") do |file_path|
28
+ files_to_require << file_path
29
+ end
30
+ end
31
+
32
+ optparser.parse!
33
+ worker_path, action = ARGV
34
+
35
+ unless worker_path && action
36
+ puts optparser
37
+ exit(1)
38
+ end
39
+
40
+ files_to_require.each { |path| require(path) }
41
+ require worker_path
42
+ worker_class = File.basename(worker_path, ".rb").camelcase
43
+ Workforce::Controller.instance.dispatch(worker_class, action)
data/config_sample.rb ADDED
@@ -0,0 +1,4 @@
1
+ base_path = File.dirname(__FILE__)
2
+
3
+ pid_dir "#{base_path}/pid"
4
+ log_dir "#{base_path}/log"
data/lib/core_ext.rb ADDED
@@ -0,0 +1,9 @@
1
+ class String
2
+ def underscore
3
+ self.sub(/\A([A-Z])/){ $1.downcase }.gsub(/([a-z])([A-Z])/){ "#{$1}_#{$2.downcase}" }
4
+ end
5
+
6
+ def camelcase
7
+ self.sub(/\A([a-z])/){ $1.upcase }.gsub(/([a-z])_([a-z])/){ "#{$1}#{$2.upcase}"}
8
+ end
9
+ end
@@ -0,0 +1,39 @@
1
+ require "singleton"
2
+
3
+ module Workforce
4
+ class Config
5
+ include Singleton
6
+
7
+ def self.config_attr(attr_name)
8
+ define_method(attr_name) do |*args|
9
+ raise ArgumentError, "wrong number of arguments (#{args.size} for 1)" if args.size > 1
10
+
11
+ if instance_variable_get("@#{attr_name}").nil? || args[0]
12
+ instance_variable_set("@#{attr_name}", args[0] || yield)
13
+ end
14
+
15
+ instance_variable_get("@#{attr_name}")
16
+ end
17
+ end
18
+
19
+ def self.get(attr_name)
20
+ self.instance.send(attr_name)
21
+ end
22
+
23
+ def initialize
24
+ yield self if block_given?
25
+ end
26
+
27
+ def load_file(file_path)
28
+ instance_eval(File.read(file_path))
29
+ end
30
+
31
+ config_attr :pid_dir do
32
+ "./pid"
33
+ end
34
+
35
+ config_attr :log_dir do
36
+ "./log"
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,107 @@
1
+ require "singleton"
2
+ require "workforce/manager"
3
+
4
+ module Workforce
5
+ class Controller
6
+ ACTIONS = %w(run start stop restart status schedule dispose)
7
+ include Singleton
8
+
9
+ def dispatch(worker_klass, action)
10
+ unless ACTIONS.include?(action.to_s)
11
+ puts "Invalid action #{action}"
12
+ exit(1)
13
+ end
14
+
15
+ send(action, worker_klass)
16
+ end
17
+
18
+ def run(worker_klass)
19
+ if running?(worker_klass)
20
+ puts "#{worker_klass}'s manager already running"
21
+ exit(1)
22
+ end
23
+
24
+ manager = Workforce::Manager.new(get_klass(worker_klass))
25
+ store_pid(worker_klass, Process.pid)
26
+ manager.run
27
+ remove_pid_file(worker_klass)
28
+ end
29
+
30
+ def start(worker_klass)
31
+ if running?(worker_klass)
32
+ puts "#{worker_klass}'s manager already running"
33
+ exit(1)
34
+ end
35
+
36
+ manager = Workforce::Manager.new(get_klass(worker_klass))
37
+ store_pid(worker_klass, manager.launch)
38
+ end
39
+
40
+ def stop(worker_klass)
41
+ Process.kill(:QUIT, running_pid(worker_klass))
42
+ remove_pid_file(worker_klass)
43
+ end
44
+
45
+ def restart(worker_klass)
46
+ stop(worker_klass) if running?(worker_klass)
47
+ start(worker_klass)
48
+ end
49
+
50
+ def status(worker_klass)
51
+ if running?(worker_klass)
52
+ puts "Running (#{running_pid(worker_klass)})"
53
+ else
54
+ puts "Not running"
55
+ end
56
+ end
57
+
58
+ def schedule(worker_klass)
59
+ unless running?(worker_klass)
60
+ puts "#{worker_klass}'s manager not running yet"
61
+ exit(1)
62
+ end
63
+
64
+ Process.kill(:USR1, running_pid(worker_klass))
65
+ end
66
+
67
+ def dispose(worker_klass)
68
+ unless running?(worker_klass)
69
+ puts "#{worker_klass}'s manager not running yet"
70
+ exit(1)
71
+ end
72
+
73
+ Process.kill(:USR2, running_pid(worker_klass))
74
+ end
75
+
76
+ protected
77
+ def get_klass(klass)
78
+ return klass unless klass.is_a?(String) || klass.is_a?(Symbol)
79
+
80
+ klass.to_s.split(/::/).inject(Object) do |namespace, const_name|
81
+ namespace.const_get(const_name)
82
+ end
83
+ end
84
+
85
+ def pid_file(worker_klass)
86
+ "#{Workforce::Config.get(:pid_dir)}/#{worker_klass.to_s.underscore}.pid"
87
+ end
88
+
89
+ def running?(worker_klass)
90
+ File.exist?(pid_file(worker_klass))
91
+ end
92
+
93
+ def running_pid(worker_klass)
94
+ Integer(File.read(pid_file(worker_klass)))
95
+ end
96
+
97
+ def store_pid(worker_klass, pid)
98
+ File.open(pid_file(worker_klass), "w") do |fp|
99
+ fp.write(pid)
100
+ end
101
+ end
102
+
103
+ def remove_pid_file(worker_klass)
104
+ File.unlink(pid_file(worker_klass))
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,156 @@
1
+ module Workforce
2
+ # A manager is responisble for managing instance of workers of a certain class.
3
+ # It allows to dinamically schedule a new worker or dispose an existing one by trapping signals.
4
+ #
5
+ # A worker is any class that defines the method _run_.
6
+ #
7
+ # For example:
8
+ #
9
+ # class ContinuousRun
10
+ # def initialize
11
+ # Signal.trap(:QUIT) { @shutdown = true }
12
+ # end
13
+ #
14
+ # def run
15
+ # loop do
16
+ # puts "Running worker #{Process.pid}"
17
+ # sleep 1
18
+ # Process.exit if @shutdown
19
+ # end
20
+ # end
21
+ # end
22
+ #
23
+ # manager = Workforce::Manager.new(ContinuousRun)
24
+ # manager.run
25
+ #
26
+ # Now the current process will be managing instances of the ContinuousRun task:
27
+ # - If you send a SIGUSR1 to it, it will schedule a new instance of ContinuousRun to run.
28
+ # - If you send a SIGUSR2 to it, it will dispose one of the running instance of it.
29
+ # - If you send a SIGQUIT or SIGINT to it, it will stop all the instances and then exit.
30
+ #
31
+ # When a instance is disposed, a SIGQUIT is send to the worker process, so if you might want to
32
+ # trap the signal and perform a graceful shutdown on the worker.
33
+ class Manager
34
+ include Services
35
+
36
+ # Creates a new Manager instance to the given worker class.
37
+ def initialize(worker)
38
+ @worker = worker
39
+ @pids = []
40
+ @old_signals = {}
41
+ @pipe_in, @pipe_out = IO.pipe
42
+ end
43
+
44
+ # Run the manager main loop, which controls the running instances.
45
+ def run
46
+ log_errors do
47
+ logger.info "Starting manager for #{@worker}"
48
+
49
+ @running = true
50
+ $0 = "Workforce: #{@worker.name} master"
51
+
52
+ trap_signal(:INT, :shutdown)
53
+ trap_signal(:QUIT, :shutdown)
54
+ trap_signal(:USR1, :schedule)
55
+ trap_signal(:USR2, :dispose)
56
+
57
+ trap_signal(:CLD) do
58
+ pid = Process.wait
59
+ logger.info "Removing process (#{pid}) from worker pool"
60
+ @pids.delete(pid)
61
+ end
62
+
63
+ # Block process until something is written to @pipe_out
64
+ IO.select([@pipe_in], nil, nil, nil)
65
+ logger.info "Shutting down manager"
66
+ end
67
+ ensure
68
+ @pipe_in.close
69
+ @pipe_out.close
70
+ end
71
+
72
+ # Launch a manager as a daemon. Creates a new fork running the manager and returns the
73
+ # manager pid to the parent process (the one who called this method).
74
+ def launch
75
+ logger.info "Launching a new manager for #{@worker.name}"
76
+ child_id = fork and return child_id
77
+ Process.setsid
78
+ Dir.chdir '/'
79
+ File.umask 0000
80
+ STDIN.reopen '/dev/null'
81
+ STDOUT.reopen '/dev/null', 'a'
82
+ STDERR.reopen STDOUT
83
+ run
84
+ end
85
+
86
+ # Sends a SIGQUIT to all worker processes and signals the main loop that it is safe to exit.
87
+ def shutdown
88
+ log_errors do
89
+ ensure_running!
90
+
91
+ logger.info "Shutting down child processes"
92
+ @pids.each do |pid|
93
+ Process.kill(:QUIT, pid)
94
+ end
95
+
96
+ @pipe_out.write(1)
97
+ end
98
+ end
99
+
100
+ # schedule a new worker instance.
101
+ # This will fork the master process and start running the worker as a child process.
102
+ def schedule
103
+ log_errors do
104
+ ensure_running!
105
+ logger.info "Scheduling new worker"
106
+
107
+ if pid = fork
108
+ logger.info "New worker scheduled: #{pid}"
109
+ @pids << pid
110
+ else
111
+ $0 = "Workforce: #{@worker.name} worker"
112
+ restore_signals
113
+ @worker.new.run
114
+ logger.info "Worker finished processing: #{Process.pid}"
115
+ Process.exit
116
+ end
117
+ end
118
+ end
119
+
120
+ # Sends a SIGQUIT to one of the workers.
121
+ def dispose
122
+ log_errors do
123
+ ensure_running!
124
+ logger.info "Disposing one of the workers"
125
+
126
+ Process.kill(:QUIT, @pids.shift) unless @pids.empty?
127
+ end
128
+ end
129
+
130
+ protected
131
+ def ensure_running!
132
+ raise RuntimeError, "manager is not running" unless @running
133
+ end
134
+
135
+ def log_errors
136
+ yield
137
+ rescue => e
138
+ logger.error("#{e.message}\n#{e.backtrace.join("\n")}")
139
+ raise(e)
140
+ end
141
+
142
+ def trap_signal(signal, method = nil, &block)
143
+ current_pid = Process.pid
144
+
145
+ @old_signals[signal] = Signal.trap(signal) do
146
+ if Process.pid == current_pid
147
+ method ? send(method) : block.call
148
+ end
149
+ end
150
+ end
151
+
152
+ def restore_signals
153
+ @old_signals.each { |sig, cmd| Signal.trap(sig, cmd) }
154
+ end
155
+ end
156
+ end
data/lib/workforce.rb ADDED
@@ -0,0 +1,13 @@
1
+ module Workforce
2
+ module Services
3
+ def logger
4
+ @logger ||= Logger.new("#{Config.get(:log_dir)}/#{@worker.name.underscore}.log")
5
+ end
6
+ end
7
+ end
8
+
9
+ require "logger"
10
+ require "core_ext"
11
+ require "workforce/configuration"
12
+ require "workforce/manager"
13
+ require "workforce/controller"
data/sleep_daemon.rb ADDED
@@ -0,0 +1,5 @@
1
+ class SleepDaemon
2
+ def run
3
+ loop { sleep 1 }
4
+ end
5
+ end
data/spec/spec.opts ADDED
@@ -0,0 +1 @@
1
+ --color
@@ -0,0 +1,9 @@
1
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
2
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
3
+ require 'workforce'
4
+ require 'spec'
5
+ require 'spec/autorun'
6
+
7
+ Spec::Runner.configure do |config|
8
+
9
+ end
metadata ADDED
@@ -0,0 +1,105 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: workforce
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 1
8
+ - 0
9
+ version: 0.1.0
10
+ platform: ruby
11
+ authors:
12
+ - Rodrigo Kochenburger
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-05-31 00:00:00 -03:00
18
+ default_executable: workforce
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: rspec
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ segments:
28
+ - 1
29
+ - 2
30
+ - 9
31
+ version: 1.2.9
32
+ type: :development
33
+ version_requirements: *id001
34
+ - !ruby/object:Gem::Dependency
35
+ name: yard
36
+ prerelease: false
37
+ requirement: &id002 !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ segments:
42
+ - 0
43
+ - 5
44
+ - 5
45
+ version: 0.5.5
46
+ type: :development
47
+ version_requirements: *id002
48
+ description: Workforce is an attempt to create a framework to develop background processes and controlling the execution of these processes in a distributed environment
49
+ email: divoxx@gmail.com
50
+ executables:
51
+ - workforce
52
+ extensions: []
53
+
54
+ extra_rdoc_files:
55
+ - LICENSE
56
+ - README.md
57
+ files:
58
+ - .document
59
+ - .gitignore
60
+ - LICENSE
61
+ - README.md
62
+ - Rakefile
63
+ - VERSION
64
+ - bin/workforce
65
+ - config_sample.rb
66
+ - lib/core_ext.rb
67
+ - lib/workforce.rb
68
+ - lib/workforce/configuration.rb
69
+ - lib/workforce/controller.rb
70
+ - lib/workforce/manager.rb
71
+ - sleep_daemon.rb
72
+ - spec/spec.opts
73
+ - spec/spec_helper.rb
74
+ has_rdoc: true
75
+ homepage: http://github.com/divoxx/workforce
76
+ licenses: []
77
+
78
+ post_install_message:
79
+ rdoc_options:
80
+ - --charset=UTF-8
81
+ require_paths:
82
+ - lib
83
+ required_ruby_version: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - ">="
86
+ - !ruby/object:Gem::Version
87
+ segments:
88
+ - 0
89
+ version: "0"
90
+ required_rubygems_version: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - ">="
93
+ - !ruby/object:Gem::Version
94
+ segments:
95
+ - 0
96
+ version: "0"
97
+ requirements: []
98
+
99
+ rubyforge_project:
100
+ rubygems_version: 1.3.6
101
+ signing_key:
102
+ specification_version: 3
103
+ summary: Framework and a distributed runtime manager to creating and running background processes
104
+ test_files:
105
+ - spec/spec_helper.rb