workforce 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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