forklifter 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/.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: []
|