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