forklifter 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ source :rubygems
2
+
3
+ # Specify your gem's dependencies in forklift.gemspec
4
+ gemspec
5
+
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Ivan Vanderbyl
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,39 @@
1
+ # Forklift
2
+
3
+ Forklift is a process manager with middleware allowing it to be used for building a multitude of background
4
+ processing applications. It can be controlled using AMQP, has process managing and reporting over AMQP,
5
+ autoscaling based on machine usage, pure unix forking model, socket based IPC, and an auto-healing highly redundent design.
6
+
7
+ _The design goal is to be the perfect replacement for Resque, Sidekiq, and the like._
8
+
9
+ ## Installation
10
+
11
+ Add this line to your application's Gemfile:
12
+
13
+ gem 'forklift'
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install forklift
22
+
23
+ ## Usage
24
+
25
+ $ forklift help
26
+
27
+ ## Contributing
28
+
29
+ 1. Fork it
30
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
31
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
32
+ 4. Push to the branch (`git push origin my-new-feature`)
33
+ 5. Create new Pull Request
34
+
35
+ ## Author
36
+
37
+ - [Ivan Vanderbyl](http://twitter.com/ivanvanderbyl)
38
+
39
+
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: UTF-8
3
+
4
+ # resolve bin path, ignoring symlinks
5
+ require "pathname"
6
+ bin_file = Pathname.new(__FILE__).realpath
7
+
8
+ # add self to libpath
9
+ $:.unshift File.expand_path("../../lib", bin_file)
10
+
11
+ # require "bundler"
12
+ # Bundler.setup
13
+
14
+ require "forklift/ui"
15
+
16
+ # start up the CLI
17
+ Forklift::UI.start
@@ -0,0 +1,22 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/forklift/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Ivan Vanderbyl"]
6
+ gem.email = ["ivanvanderbyl@me.com"]
7
+ gem.description = %q{Forklift is a preforking, autoscaling process manager for building background processing applications}
8
+ gem.summary = %q{"Unicorn for background processing"}
9
+ gem.homepage = ""
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
+ gem.name = "forklifter"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = Forklift::VERSION
17
+
18
+ gem.add_dependency 'activesupport', '~> 3.2.0'
19
+ gem.add_dependency 'thor'
20
+ gem.add_dependency 'eventmachine'
21
+ gem.add_dependency 'kgio'
22
+ end
@@ -0,0 +1,18 @@
1
+ require 'fcntl'
2
+ require 'etc'
3
+ require 'stringio'
4
+ require 'kgio'
5
+ require 'eventmachine'
6
+
7
+ module Forklift
8
+ autoload :VERSION, 'forklift/version'
9
+ autoload :Master, 'forklift/master'
10
+ autoload :Worker, 'forklift/worker'
11
+
12
+ def self.log_error(logger, prefix, exc)
13
+ message = exc.message
14
+ message = message.dump if /[[:cntrl:]]/ =~ message
15
+ logger.error "#{prefix}: #{message} (#{exc.class})"
16
+ exc.backtrace.each { |line| logger.error(line) }
17
+ end
18
+ end
@@ -0,0 +1,296 @@
1
+ require "logger"
2
+ require "socket"
3
+
4
+ module Forklift
5
+ class Master
6
+
7
+ # This hash maps PIDs to Workers
8
+ WORKERS = {}
9
+
10
+ attr_reader :pid, :logger
11
+ attr_accessor :worker_processes, :master_pid, :reexec_pid, :timeout
12
+
13
+ SELF_PIPE = []
14
+
15
+ # signal queue used for self-piping
16
+ SIG_QUEUE = []
17
+
18
+ # list of signals we care about and trap in master.
19
+ QUEUE_SIGS = [ :WINCH, :QUIT, :INT, :TERM, :USR1, :USR2, :HUP, :TTIN, :TTOU ]
20
+
21
+ START_CTX = {
22
+ :argv => ARGV.map { |arg| arg.dup },
23
+ 0 => $0.dup,
24
+ }
25
+ # We favor ENV['PWD'] since it is (usually) symlink aware for Capistrano
26
+ # and like systems
27
+ START_CTX[:cwd] = begin
28
+ a = File.stat(pwd = ENV['PWD'])
29
+ b = File.stat(Dir.pwd)
30
+ a.ino == b.ino && a.dev == b.dev ? pwd : Dir.pwd
31
+ rescue
32
+ Dir.pwd
33
+ end
34
+
35
+ def initialize
36
+ self.reexec_pid = 0
37
+ self.worker_processes = 2
38
+ @logger = Logger.new($stdout)
39
+ @timeout = 100
40
+ end
41
+
42
+ def start
43
+ init_self_pipe!
44
+ # setup signal handlers before writing pid file in case people get
45
+ # trigger happy and send signals as soon as the pid file exists.
46
+ # Note that signals don't actually get handled until the #join method
47
+ QUEUE_SIGS.each { |sig| trap(sig) { SIG_QUEUE << sig; awaken_master } }
48
+ trap(:CHLD) { awaken_master }
49
+
50
+ self.master_pid = $$
51
+
52
+ logger.info "Starting master"
53
+
54
+ spawn_missing_workers
55
+
56
+ self
57
+ end
58
+
59
+ def join
60
+ respawn = true
61
+ last_check = Time.now
62
+
63
+ proc_name 'master'
64
+ logger.info "master process ready" # test_exec.rb relies on this message
65
+ if @ready_pipe
66
+ @ready_pipe.syswrite($$.to_s)
67
+ @ready_pipe = @ready_pipe.close rescue nil
68
+ end
69
+
70
+ begin
71
+ reap_all_workers
72
+
73
+ case SIG_QUEUE.shift
74
+ when nil
75
+ # avoid murdering workers after our master process (or the
76
+ # machine) comes out of suspend/hibernation
77
+ if (last_check + @timeout) >= (last_check = Time.now)
78
+ sleep_time = murder_lazy_workers
79
+ else
80
+ sleep_time = @timeout/2.0 + 1
81
+ @logger.debug("waiting #{sleep_time}s after suspend/hibernation")
82
+ end
83
+ maintain_worker_count if respawn
84
+ master_sleep(sleep_time)
85
+
86
+ when :QUIT # graceful shutdown
87
+ break
88
+ when :TERM, :INT # immediate shutdown
89
+ stop(false)
90
+ break
91
+ when :USR1 # rotate logs
92
+ logger.info "master done reopening logs"
93
+ kill_each_worker(:USR1)
94
+ when :USR2 # exec binary, stay alive in case something went wrong
95
+ # reexec
96
+ end
97
+
98
+ rescue => e
99
+ Forklift.log_error(@logger, "master loop error", e)
100
+ end while true
101
+
102
+ stop # gracefully shutdown all workers on our way out
103
+ logger.info "master complete"
104
+ unlink_pid_safe(pid) if pid
105
+ end
106
+
107
+ private
108
+
109
+ # wait for a signal hander to wake us up and then consume the pipe
110
+ def master_sleep(sec)
111
+ IO.select([ SELF_PIPE[0] ], nil, nil, sec) or return
112
+ SELF_PIPE[0].kgio_tryread(11)
113
+ end
114
+
115
+ def awaken_master
116
+ SELF_PIPE[1].kgio_trywrite('.') # wakeup master process from select
117
+ end
118
+
119
+ def worker_loop(worker)
120
+ ppid = master_pid
121
+ init_worker_process(worker)
122
+
123
+ logger.info "worker=#{worker.number} ready"
124
+
125
+ begin
126
+ nr = 0
127
+ worker.tick = Time.now.to_i
128
+
129
+ worker.perform
130
+
131
+ ppid == Process.ppid or return
132
+
133
+ # timeout used so we can detect parent death:
134
+ worker.tick = Time.now.to_i
135
+ # ret = IO.select(l, nil, SELF_PIPE, @timeout) and ready = ret[0]
136
+ rescue => e
137
+ redo if nr < 0 && (Errno::EBADF === e || IOError === e) # reopen logs
138
+ Forklift.log_error(@logger, "listen loop error", e) if worker
139
+ exit
140
+ end while worker
141
+ end
142
+
143
+ EXIT_SIGS = [ :QUIT, :TERM, :INT ]
144
+ WORKER_QUEUE_SIGS = QUEUE_SIGS - EXIT_SIGS
145
+
146
+ # gets rid of stuff the worker has no business keeping track of
147
+ # to free some resources and drops all sig handlers.
148
+ # traps for USR1, USR2, and HUP may be set in the after_fork Proc
149
+ # by the user.
150
+ def init_worker_process(worker)
151
+ # we'll re-trap :QUIT later for graceful shutdown iff we accept clients
152
+ EXIT_SIGS.each { |sig| trap(sig) { exit!(0) } }
153
+ exit!(0) if (SIG_QUEUE & EXIT_SIGS)[0]
154
+ WORKER_QUEUE_SIGS.each { |sig| trap(sig, nil) }
155
+ trap(:CHLD, 'DEFAULT')
156
+ SIG_QUEUE.clear
157
+ proc_name "worker[#{worker.number}]"
158
+ START_CTX.clear
159
+ init_self_pipe!
160
+ WORKERS.clear
161
+ # after_fork.call(self, worker) # can drop perms
162
+ # worker.user(*user) if user.kind_of?(Array) && ! worker.switched
163
+ self.timeout /= 2.0 # halve it for select()
164
+ @config = nil
165
+ @after_fork = @listener_opts = @orig_app = nil
166
+ end
167
+
168
+ def spawn_missing_workers
169
+ worker_nr = -1
170
+ until (worker_nr += 1) == self.worker_processes
171
+ WORKERS.value?(worker_nr) and next
172
+ worker = Worker.new(logger, worker_nr)
173
+ # before_fork.call(self, worker)
174
+ if pid = fork
175
+ WORKERS[pid] = worker
176
+ else
177
+ worker_loop(worker)
178
+ exit
179
+ end
180
+ end
181
+ rescue => e
182
+ @logger.error(e) rescue nil
183
+ exit!
184
+ end
185
+
186
+ def maintain_worker_count
187
+ (off = WORKERS.size - worker_processes) == 0 and return
188
+ off < 0 and return spawn_missing_workers
189
+ WORKERS.dup.each_pair { |wpid,w|
190
+ w.number >= worker_processes and kill_worker(:QUIT, wpid) rescue nil
191
+ }
192
+ end
193
+
194
+ # forcibly terminate all workers that haven't checked in in timeout seconds. The timeout is implemented using an unlinked File
195
+ def murder_lazy_workers
196
+ next_sleep = @timeout - 1
197
+ now = Time.now.to_i
198
+ WORKERS.dup.each_pair do |wpid, worker|
199
+ tick = worker.tick
200
+ 0 == tick and next # skip workers that haven't processed any clients
201
+ diff = now - tick
202
+ tmp = @timeout - diff
203
+ if tmp >= 0
204
+ next_sleep > tmp and next_sleep = tmp
205
+ next
206
+ end
207
+ next_sleep = 0
208
+ logger.error "worker=#{worker.number} PID:#{wpid} timeout " \
209
+ "(#{diff}s > #{@timeout}s), killing"
210
+ kill_worker(:KILL, wpid) # take no prisoners for timeout violations
211
+ end
212
+ next_sleep <= 0 ? 1 : next_sleep
213
+ end
214
+
215
+ def proc_name(tag)
216
+ $0 = tag
217
+ # $0 = ([ File.basename(START_CTX[0]), tag
218
+ # ]).concat(START_CTX[:argv]).join(' ')
219
+ end
220
+
221
+ def redirect_io(io, path)
222
+ File.open(path, 'ab') { |fp| io.reopen(fp) } if path
223
+ io.sync = true
224
+ end
225
+
226
+ # Terminates all workers, but does not exit master process
227
+ def stop(graceful = true)
228
+ limit = Time.now + timeout
229
+ until WORKERS.empty? || Time.now > limit
230
+ kill_each_worker(graceful ? :QUIT : :TERM)
231
+ sleep(0.1)
232
+ reap_all_workers
233
+ end
234
+ kill_each_worker(:KILL)
235
+ end
236
+
237
+ # delivers a signal to a worker and fails gracefully if the worker
238
+ # is no longer running.
239
+ def kill_worker(signal, wpid)
240
+ Process.kill(signal, wpid)
241
+ rescue Errno::ESRCH
242
+ worker = WORKERS.delete(wpid) and worker.close rescue nil
243
+ end
244
+
245
+ # delivers a signal to each worker
246
+ def kill_each_worker(signal)
247
+ WORKERS.keys.each { |wpid| kill_worker(signal, wpid) }
248
+ end
249
+
250
+ def init_self_pipe!
251
+ SELF_PIPE.each { |io| io.close rescue nil }
252
+ SELF_PIPE.replace(Kgio::Pipe.new)
253
+ SELF_PIPE.each { |io| io.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC) }
254
+ end
255
+
256
+ # reaps all unreaped workers
257
+ def reap_all_workers
258
+ begin
259
+ wpid, status = Process.waitpid2(-1, Process::WNOHANG)
260
+ wpid or return
261
+ if reexec_pid == wpid
262
+ logger.error "reaped #{status.inspect} exec()-ed"
263
+ self.reexec_pid = 0
264
+ self.pid = pid.chomp('.oldbin') if pid
265
+ proc_name 'master'
266
+ else
267
+ worker = WORKERS.delete(wpid) and worker.close rescue nil
268
+ m = "reaped #{status.inspect} worker=#{worker.number rescue 'unknown'}"
269
+ status.success? ? logger.info(m) : logger.error(m)
270
+ end
271
+ rescue Errno::ECHILD
272
+ break
273
+ end while true
274
+ end
275
+
276
+ # unlinks a PID file at given +path+ if it contains the current PID
277
+ # still potentially racy without locking the directory (which is
278
+ # non-portable and may interact badly with other programs), but the
279
+ # window for hitting the race condition is small
280
+ def unlink_pid_safe(path)
281
+ (File.read(path).to_i == $$ and File.unlink(path)) rescue nil
282
+ end
283
+
284
+ # returns a PID if a given path contains a non-stale PID file,
285
+ # nil otherwise.
286
+ def valid_pid?(path)
287
+ wpid = File.read(path).to_i
288
+ wpid <= 0 and return
289
+ Process.kill(0, wpid)
290
+ wpid
291
+ rescue Errno::ESRCH, Errno::ENOENT
292
+ # don't unlink stale pid files, racy without non-portable locking...
293
+ end
294
+
295
+ end
296
+ end
@@ -0,0 +1,257 @@
1
+ require "logger"
2
+
3
+ module Forklift
4
+ class Master
5
+ attr_accessor :app, :timeout, :worker_processes,
6
+ :before_fork, :after_fork, :before_exec,
7
+ :listener_opts, :preload_app,
8
+ :reexec_pid, :orig_app, :init_listeners,
9
+ :master_pid, :config, :ready_pipe, :user
10
+
11
+ attr_reader :pid, :logger
12
+
13
+ # This hash maps PIDs to Workers
14
+ WORKERS = {}
15
+
16
+ SELF_PIPE = []
17
+
18
+ # signal queue used for self-piping
19
+ SIG_QUEUE = []
20
+
21
+ # list of signals we care about and trap in master.
22
+ QUEUE_SIGS = [ :WINCH, :QUIT, :INT, :TERM, :USR1, :USR2, :HUP, :TTIN, :TTOU ]
23
+
24
+ START_CTX = {
25
+ :argv => ARGV.map { |arg| arg.dup },
26
+ 0 => $0.dup,
27
+ }
28
+ # We favor ENV['PWD'] since it is (usually) symlink aware for Capistrano
29
+ # and like systems
30
+ START_CTX[:cwd] = begin
31
+ a = File.stat(pwd = ENV['PWD'])
32
+ b = File.stat(Dir.pwd)
33
+ a.ino == b.ino && a.dev == b.dev ? pwd : Dir.pwd
34
+ rescue
35
+ Dir.pwd
36
+ end
37
+
38
+ def initialize
39
+ self.reexec_pid = 0
40
+ end
41
+
42
+ def start
43
+ # this pipe is used to wake us up from select(2) in #join when signals
44
+ # are trapped. See trap_deferred.
45
+ init_self_pipe!
46
+
47
+ # setup signal handlers before writing pid file in case people get
48
+ # trigger happy and send signals as soon as the pid file exists.
49
+ # Note that signals don't actually get handled until the #join method
50
+ QUEUE_SIGS.each { |sig| trap(sig) { SIG_QUEUE << sig; awaken_master } }
51
+ trap(:CHLD) { awaken_master }
52
+
53
+ self.master_pid = $$
54
+
55
+ @logger = Logger.new($stdout)
56
+ @timeout = 10
57
+
58
+ logger.info "Starting master"
59
+
60
+ @worker_processes = 2
61
+
62
+ spawn_missing_workers
63
+
64
+ self
65
+ end
66
+
67
+ # monitors children and receives signals forever
68
+ # (or until a termination signal is sent). This handles signals
69
+ # one-at-a-time time and we'll happily drop signals in case somebody
70
+ # is signalling us too often.
71
+ def join
72
+ respawn = true
73
+ last_check = Time.now
74
+
75
+ proc_name 'master'
76
+ logger.info "master process ready" # test_exec.rb relies on this message
77
+
78
+ if @ready_pipe
79
+ @ready_pipe.syswrite($$.to_s)
80
+ @ready_pipe = @ready_pipe.close rescue nil
81
+ end
82
+
83
+ begin
84
+ reap_all_workers
85
+
86
+ case SIG_QUEUE.shift
87
+ when nil
88
+
89
+ when :QUIT # graceful shutdown
90
+ break
91
+ when :TERM, :INT # immediate shutdown
92
+ stop(false)
93
+ break
94
+ when :USR1 # rotate logs
95
+ logger.info "master done reopening logs"
96
+ kill_each_worker(:USR1)
97
+ when :USR2 # exec binary, stay alive in case something went wrong
98
+ reexec
99
+ end
100
+
101
+ rescue => e
102
+ Forklift.log_error(@logger, "master loop error", e)
103
+ end while true
104
+ end
105
+
106
+ private
107
+
108
+ def worker_loop(worker)
109
+ ppid = master_pid
110
+ ppid = master_pid
111
+ init_worker_process(worker)
112
+ nr = 0 # this becomes negative if we need to reopen logs
113
+ # ready = l.dup
114
+
115
+ # closing anything we IO.select on will raise EBADF
116
+ trap(:USR1) { nr = -65536; SELF_PIPE[0].close rescue nil }
117
+ trap(:QUIT) { worker = nil; LISTENERS.each { |s| s.close rescue nil }.clear }
118
+ logger.info "worker=#{worker.nr} ready"
119
+
120
+ begin
121
+ nr < 0 and reopen_worker_logs(worker.nr)
122
+ nr = 0
123
+
124
+ logger.info "Working"
125
+
126
+ EM.run {
127
+
128
+ }
129
+
130
+ ppid == Process.ppid or return
131
+
132
+ # timeout used so we can detect parent death:
133
+ # ret = IO.select(l, nil, SELF_PIPE, @timeout) and ready = ret[0]
134
+
135
+ rescue => e
136
+ redo if nr < 0 && (Errno::EBADF === e || IOError === e) # reopen logs
137
+ Forklift.log_error(@logger, "listen loop error", e) if worker
138
+ exit
139
+ end while worker
140
+ end
141
+
142
+ EXIT_SIGS = [ :QUIT, :TERM, :INT ]
143
+ WORKER_QUEUE_SIGS = QUEUE_SIGS - EXIT_SIGS
144
+
145
+ # gets rid of stuff the worker has no business keeping track of
146
+ # to free some resources and drops all sig handlers.
147
+ # traps for USR1, USR2, and HUP may be set in the after_fork Proc
148
+ # by the user.
149
+ def init_worker_process(worker)
150
+ # we'll re-trap :QUIT later for graceful shutdown iff we accept clients
151
+ EXIT_SIGS.each { |sig| trap(sig) { exit!(0) } }
152
+ exit!(0) if (SIG_QUEUE & EXIT_SIGS)[0]
153
+ WORKER_QUEUE_SIGS.each { |sig| trap(sig, nil) }
154
+ trap(:CHLD, 'DEFAULT')
155
+ SIG_QUEUE.clear
156
+ proc_name "worker[#{worker.nr}]"
157
+ START_CTX.clear
158
+ init_self_pipe!
159
+ WORKERS.clear
160
+ # after_fork.call(self, worker) # can drop perms
161
+ # worker.user(*user) if user.kind_of?(Array) && ! worker.switched
162
+ self.timeout /= 2.0 # halve it for select()
163
+ @config = nil
164
+ @after_fork = @listener_opts = @orig_app = nil
165
+ end
166
+
167
+ def spawn_missing_workers
168
+ worker_nr = -1
169
+ until (worker_nr += 1) == @worker_processes
170
+ WORKERS.value?(worker_nr) and next
171
+ worker = Worker.new(worker_nr)
172
+ # before_fork.call(self, worker)
173
+ if pid = fork
174
+ WORKERS[pid] = worker
175
+ else
176
+ after_fork_internal
177
+ worker_loop(worker)
178
+ exit
179
+ end
180
+ end
181
+ rescue => e
182
+ @logger.error(e) rescue nil
183
+ exit!
184
+ end
185
+
186
+
187
+ def after_fork_internal
188
+ @ready_pipe.close if @ready_pipe
189
+ @ready_pipe = @init_listeners = @before_exec = @before_fork = nil
190
+
191
+ srand # http://redmine.ruby-lang.org/issues/4338
192
+
193
+ # The OpenSSL PRNG is seeded with only the pid, and apps with frequently
194
+ # dying workers can recycle pids
195
+ OpenSSL::Random.seed(rand.to_s) if defined?(OpenSSL::Random)
196
+ end
197
+
198
+ # wait for a signal hander to wake us up and then consume the pipe
199
+ def master_sleep(sec)
200
+ IO.select([ SELF_PIPE[0] ], nil, nil, sec) or return
201
+ SELF_PIPE[0].kgio_tryread(11)
202
+ end
203
+
204
+ def awaken_master
205
+ SELF_PIPE[1].kgio_trywrite('.') # wakeup master process from select
206
+ end
207
+
208
+ def proc_name(tag)
209
+ $0 = ([ File.basename(START_CTX[0]), tag
210
+ ]).concat(START_CTX[:argv]).join(' ')
211
+ end
212
+
213
+ def redirect_io(io, path)
214
+ File.open(path, 'ab') { |fp| io.reopen(fp) } if path
215
+ io.sync = true
216
+ end
217
+
218
+ def init_self_pipe!
219
+ SELF_PIPE.each { |io| io.close rescue nil }
220
+ SELF_PIPE.replace(Kgio::Pipe.new)
221
+ SELF_PIPE.each { |io| io.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC) }
222
+ end
223
+
224
+ # delivers a signal to a worker and fails gracefully if the worker
225
+ # is no longer running.
226
+ def kill_worker(signal, wpid)
227
+ Process.kill(signal, wpid)
228
+ rescue Errno::ESRCH
229
+ worker = WORKERS.delete(wpid) and worker.close rescue nil
230
+ end
231
+
232
+ # delivers a signal to each worker
233
+ def kill_each_worker(signal)
234
+ WORKERS.keys.each { |wpid| kill_worker(signal, wpid) }
235
+ end
236
+
237
+ # unlinks a PID file at given +path+ if it contains the current PID
238
+ # still potentially racy without locking the directory (which is
239
+ # non-portable and may interact badly with other programs), but the
240
+ # window for hitting the race condition is small
241
+ def unlink_pid_safe(path)
242
+ (File.read(path).to_i == $$ and File.unlink(path)) rescue nil
243
+ end
244
+
245
+ # returns a PID if a given path contains a non-stale PID file,
246
+ # nil otherwise.
247
+ def valid_pid?(path)
248
+ wpid = File.read(path).to_i
249
+ wpid <= 0 and return
250
+ Process.kill(0, wpid)
251
+ wpid
252
+ rescue Errno::ESRCH, Errno::ENOENT
253
+ # don't unlink stale pid files, racy without non-portable locking...
254
+ end
255
+
256
+ end
257
+ end
@@ -0,0 +1,40 @@
1
+ require 'thor'
2
+ require 'thor/group'
3
+ require 'fileutils'
4
+ require 'pathname'
5
+
6
+ require "forklift"
7
+
8
+ module Forklift
9
+ class UI < ::Thor
10
+ map "-T" => :list, "-v" => :version
11
+
12
+ # If a task is not found on Thor::Runner, method missing is invoked and
13
+ # Thor::Runner is then responsable for finding the task in all classes.
14
+ #
15
+ def method_missing(meth, *args)
16
+ meth = meth.to_s
17
+ klass, task = Thor::Util.find_class_and_task_by_namespace(meth)
18
+ args.unshift(task) if task
19
+ klass.start(args, :shell => self.shell)
20
+ end
21
+
22
+ def self.banner(task, all = false, subcommand = false)
23
+ "forklift " + task.formatted_usage(self, all, subcommand)
24
+ end
25
+
26
+ desc :start, "Start master"
27
+ def start
28
+ Forklift::Master.new.start.join
29
+ end
30
+
31
+ class Remote < Thor
32
+ namespace :remote
33
+
34
+ desc "#{namespace}:start", "Start"
35
+ def start
36
+ puts "Start"
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,3 @@
1
+ module Forklift
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,41 @@
1
+ module Forklift
2
+ class Worker
3
+
4
+ attr_reader :number, :logger
5
+
6
+ def initialize(logger, number)
7
+ @i = 0
8
+ @number = number
9
+ @tick = Time.now.to_i
10
+ @logger = logger
11
+ logger.debug "Worker #{number} spawned"
12
+ end
13
+
14
+ # worker objects may be compared to just plain Integers
15
+ def ==(other_nr) # :nodoc:
16
+ @number == other_nr
17
+ end
18
+
19
+ def tick=(int)
20
+ @tick = int
21
+ end
22
+
23
+ def tick
24
+ @tick
25
+ end
26
+
27
+ def nr
28
+ @number
29
+ end
30
+
31
+ def close # :nodoc:
32
+ @tmp.close if @tmp
33
+ end
34
+
35
+ def perform
36
+ @i += 1
37
+ logger.info "Working #{@i}"
38
+ sleep 5
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,12 @@
1
+ reader, writer = IO.pipe
2
+ fork do
3
+ reader.close
4
+ 10.times do
5
+ # heavy lifting
6
+ writer.puts "Another one bites the dust"
7
+ end
8
+ end
9
+ writer.close
10
+ while message = reader.gets
11
+ $stdout.puts message
12
+ end
metadata ADDED
@@ -0,0 +1,106 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: forklifter
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Ivan Vanderbyl
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-05-13 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: activesupport
16
+ requirement: &70179603660300 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 3.2.0
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *70179603660300
25
+ - !ruby/object:Gem::Dependency
26
+ name: thor
27
+ requirement: &70179603659880 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *70179603659880
36
+ - !ruby/object:Gem::Dependency
37
+ name: eventmachine
38
+ requirement: &70179603659420 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ type: :runtime
45
+ prerelease: false
46
+ version_requirements: *70179603659420
47
+ - !ruby/object:Gem::Dependency
48
+ name: kgio
49
+ requirement: &70179603659000 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ type: :runtime
56
+ prerelease: false
57
+ version_requirements: *70179603659000
58
+ description: Forklift is a preforking, autoscaling process manager for building background
59
+ processing applications
60
+ email:
61
+ - ivanvanderbyl@me.com
62
+ executables:
63
+ - forklift
64
+ extensions: []
65
+ extra_rdoc_files: []
66
+ files:
67
+ - .gitignore
68
+ - Gemfile
69
+ - Gemfile.lock
70
+ - LICENSE
71
+ - README.md
72
+ - Rakefile
73
+ - bin/forklift
74
+ - forklift.gemspec
75
+ - lib/forklift.rb
76
+ - lib/forklift/master.rb
77
+ - lib/forklift/master_old.rb
78
+ - lib/forklift/ui.rb
79
+ - lib/forklift/version.rb
80
+ - lib/forklift/worker.rb
81
+ - play/pipes.rb
82
+ homepage: ''
83
+ licenses: []
84
+ post_install_message:
85
+ rdoc_options: []
86
+ require_paths:
87
+ - lib
88
+ required_ruby_version: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ required_rubygems_version: !ruby/object:Gem::Requirement
95
+ none: false
96
+ requirements:
97
+ - - ! '>='
98
+ - !ruby/object:Gem::Version
99
+ version: '0'
100
+ requirements: []
101
+ rubyforge_project:
102
+ rubygems_version: 1.8.15
103
+ signing_key:
104
+ specification_version: 3
105
+ summary: ! '"Unicorn for background processing"'
106
+ test_files: []