forklifter 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/Gemfile +5 -0
- data/LICENSE +22 -0
- data/README.md +39 -0
- data/Rakefile +2 -0
- data/bin/forklift +17 -0
- data/forklift.gemspec +22 -0
- data/lib/forklift.rb +18 -0
- data/lib/forklift/master.rb +296 -0
- data/lib/forklift/master_old.rb +257 -0
- data/lib/forklift/ui.rb +40 -0
- data/lib/forklift/version.rb +3 -0
- data/lib/forklift/worker.rb +41 -0
- data/play/pipes.rb +12 -0
- metadata +106 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
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.
|
data/README.md
ADDED
@@ -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
|
+
|
data/Rakefile
ADDED
data/bin/forklift
ADDED
@@ -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
|
data/forklift.gemspec
ADDED
@@ -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
|
data/lib/forklift.rb
ADDED
@@ -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
|
data/lib/forklift/ui.rb
ADDED
@@ -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,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
|
data/play/pipes.rb
ADDED
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: []
|