pastry 0.1.3 → 0.3.0
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/README.rdoc +0 -1
- data/bin/pastry +27 -19
- data/lib/pastry.rb +185 -48
- metadata +39 -49
data/README.rdoc
CHANGED
data/bin/pastry
CHANGED
@@ -3,33 +3,41 @@
|
|
3
3
|
require 'pastry'
|
4
4
|
require 'optparse'
|
5
5
|
|
6
|
-
|
6
|
+
def start_command
|
7
|
+
$start_command ||= "#{$0} #{ARGV.join(' ')}".freeze
|
8
|
+
end
|
9
|
+
|
10
|
+
options = {start_command: start_command}
|
7
11
|
parser = OptionParser.new do |opts|
|
8
12
|
opts.banner = "pastry [options]"
|
9
13
|
|
10
|
-
opts.on('-n', '--servers number', 'worker count') {|value| options[:workers]
|
11
|
-
opts.on('-E', '--environment name', 'rack environment') {|value| options[:env]
|
12
|
-
opts.on('-R', '--rackup file', 'rackup file') {|value| options[:rackup]
|
13
|
-
opts.on('-a', '--address name', 'bind ip/host') {|value| options[:host]
|
14
|
-
opts.on('-p', '--port name', 'bind port') {|value| options[:port]
|
15
|
-
opts.on('-s', '--socket file', 'unix socket') {|value| options[:socket]
|
16
|
-
opts.on('-d', '--[no-]daemon', 'daemonize') {|value| options[:daemonize]
|
17
|
-
opts.on('-l', '--logfile file', 'logfile') {|value| options[:logfile]
|
18
|
-
opts.on('-P', '--pidfile file', 'pidfile') {|value| options[:pidfile]
|
19
|
-
opts.on('-
|
20
|
-
opts.on('-
|
21
|
-
opts.on('-
|
14
|
+
opts.on('-n', '--servers number', 'worker count') {|value| options[:workers] = value }
|
15
|
+
opts.on('-E', '--environment name', 'rack environment') {|value| options[:env] = value }
|
16
|
+
opts.on('-R', '--rackup file', 'rackup file') {|value| options[:rackup] = value }
|
17
|
+
opts.on('-a', '--address name', 'bind ip/host') {|value| options[:host] = value }
|
18
|
+
opts.on('-p', '--port name', 'bind port') {|value| options[:port] = value }
|
19
|
+
opts.on('-s', '--socket file', 'unix socket') {|value| options[:socket] = value }
|
20
|
+
opts.on('-d', '--[no-]daemon', 'daemonize') {|value| options[:daemonize] = value }
|
21
|
+
opts.on('-l', '--logfile file', 'logfile') {|value| options[:logfile] = value }
|
22
|
+
opts.on('-P', '--pidfile file', 'pidfile') {|value| options[:pidfile] = value }
|
23
|
+
opts.on('-q', '--queue size', 'pending requests') {|value| options[:queue] = value }
|
24
|
+
opts.on('-c', '--connections num', 'max connections') {|value| options[:max_connections] = value }
|
25
|
+
opts.on('-t', '--timeout secs', 'read timeout') {|value| options[:timeout] = value }
|
26
|
+
opts.on('-A', '--application name', 'app name') {|value| options[:name] = value }
|
22
27
|
end
|
23
28
|
|
24
29
|
parser.parse!
|
25
30
|
|
26
|
-
%w(workers port
|
31
|
+
%w(workers port max_connections queue timeout).map(&:to_sym).each do |name|
|
27
32
|
options[name] = options[name].to_i if options.key?(name)
|
28
33
|
end
|
29
34
|
|
30
|
-
|
31
|
-
|
32
|
-
|
35
|
+
# setup environment before loading any app library
|
36
|
+
ENV['RACK_ENV'] = options.delete(:env) || 'development'
|
37
|
+
|
38
|
+
app = Rack::Builder.parse_file(options.delete(:rackup) || 'config.ru').first
|
39
|
+
size = options.delete(:workers) || 2
|
40
|
+
master = Pastry.new(size, app, options)
|
33
41
|
|
34
|
-
|
35
|
-
|
42
|
+
master.parse_config(ARGV.shift) if ARGV.first && File.exists?(ARGV.first)
|
43
|
+
master.start
|
data/lib/pastry.rb
CHANGED
@@ -2,45 +2,80 @@ require 'fileutils'
|
|
2
2
|
require 'logger'
|
3
3
|
require 'socket'
|
4
4
|
require 'thin'
|
5
|
+
require 'thin/server'
|
5
6
|
|
6
7
|
class Pastry
|
7
|
-
|
8
|
+
# have defaults
|
9
|
+
attr_accessor :pool, :host, :port, :queue, :max_connections, :timeout, :daemonize, :pidfile
|
10
|
+
|
11
|
+
# no defaults
|
12
|
+
attr_accessor :name, :socket, :logfile, :start_command, :cwd
|
8
13
|
|
9
14
|
def initialize pool, app, options = {}
|
10
|
-
@pool
|
11
|
-
@app
|
12
|
-
@host
|
13
|
-
@
|
14
|
-
@
|
15
|
-
@
|
16
|
-
@
|
17
|
-
@
|
15
|
+
@pool = pool
|
16
|
+
@app = app
|
17
|
+
@host = options.fetch :host, '127.0.0.1'
|
18
|
+
@port = options.fetch :port, 3000
|
19
|
+
@queue = options.fetch :queue, 1024
|
20
|
+
@max_connections = options.fetch :max_connections, 1024
|
21
|
+
@timeout = options.fetch :timeout, 30
|
22
|
+
@daemonize = options.fetch :daemonize, false
|
23
|
+
@pidfile = options.fetch :pidfile, '/tmp/pastry.pid'
|
24
|
+
@name = options.fetch :name, nil
|
25
|
+
@socket = options.fetch :socket, nil
|
26
|
+
@logfile = options.fetch :logfile, nil
|
27
|
+
@start_command = options.fetch :start_command, nil
|
28
|
+
@cwd = File.expand_path(ENV['PWD'] || Dir.pwd)
|
29
|
+
|
30
|
+
@before_fork = nil
|
31
|
+
@after_fork = nil
|
32
|
+
end
|
33
|
+
|
34
|
+
def before_fork &block
|
35
|
+
raise ArgumentError, 'missing callback' unless block
|
36
|
+
@before_fork = block
|
37
|
+
end
|
38
|
+
|
39
|
+
def after_fork &block
|
40
|
+
raise ArgumentError, 'missing callback' unless block
|
41
|
+
@after_fork = block
|
42
|
+
end
|
18
43
|
|
19
|
-
|
20
|
-
|
21
|
-
@maxconn = options.fetch(:maxconn, 1024).to_i
|
22
|
-
@timeout = options.fetch(:timeout, 30).to_i
|
44
|
+
def parse_config file
|
45
|
+
instance_eval File.read(file)
|
23
46
|
end
|
24
47
|
|
25
48
|
def start
|
49
|
+
do_sanity_checks
|
26
50
|
ensure_not_running!
|
27
|
-
Process.daemon if daemon
|
28
51
|
|
29
|
-
|
52
|
+
Process.daemon(true, true) if daemonize
|
53
|
+
|
54
|
+
if daemonize || logfile
|
30
55
|
STDOUT.reopen(logfile || '/tmp/pastry.log', 'a')
|
31
56
|
STDERR.reopen(logfile || '/tmp/pastry.log', 'a')
|
32
57
|
STDOUT.sync = true
|
33
58
|
STDERR.sync = true
|
59
|
+
STDOUT.binmode
|
60
|
+
STDERR.binmode
|
34
61
|
end
|
35
62
|
|
36
63
|
start!
|
37
64
|
end
|
38
65
|
|
66
|
+
private
|
67
|
+
|
68
|
+
attr_accessor :pids
|
69
|
+
|
70
|
+
def do_sanity_checks
|
71
|
+
%w(port queue max_connections timeout).each {|var| send("#{var}=", send(var).to_i)}
|
72
|
+
end
|
73
|
+
|
39
74
|
def ensure_not_running!
|
40
75
|
if File.exists?(pidfile) && pid = File.read(pidfile).to_i
|
41
76
|
running = Process.kill(0, pid) rescue nil
|
42
77
|
raise "already running with pid #{pid}" if running
|
43
|
-
FileUtils.rm_f(pidfile)
|
78
|
+
FileUtils.rm_f([pidfile, socket.to_s])
|
44
79
|
end
|
45
80
|
end
|
46
81
|
|
@@ -48,62 +83,150 @@ class Pastry
|
|
48
83
|
File.open(pidfile, 'w') {|fh| fh.write(Process.pid)}
|
49
84
|
end
|
50
85
|
|
51
|
-
def
|
52
|
-
'%s master' % (
|
86
|
+
def master_name
|
87
|
+
'%s master' % (name || 'pastry')
|
53
88
|
end
|
54
89
|
|
55
90
|
def motd
|
56
|
-
"starting #{
|
91
|
+
"starting #{master_name} with #{pool} minions listening on #{socket ? 'socket %s ' % socket : 'port %d' % port}"
|
92
|
+
end
|
93
|
+
|
94
|
+
def logger
|
95
|
+
@logger ||= Logger.new(logfile || (daemonize ? '/tmp/pastry.log' : $stdout), 0)
|
57
96
|
end
|
58
97
|
|
59
98
|
def start!
|
60
99
|
create_pidfile
|
61
|
-
server
|
100
|
+
server = ENV['PASTRY_FD'] && Socket.for_fd(ENV['PASTRY_FD'].to_i)
|
101
|
+
server ||= socket ? UNIXServer.new(socket) : TCPServer.new(host, port)
|
62
102
|
|
63
|
-
server.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, true) unless
|
103
|
+
server.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, true) unless socket
|
104
|
+
server.fcntl(Fcntl::F_SETFD, server.fcntl(Fcntl::F_GETFD) | Fcntl::FD_CLOEXEC)
|
64
105
|
server.fcntl(Fcntl::F_SETFL, server.fcntl(Fcntl::F_GETFL) | Fcntl::O_NONBLOCK)
|
106
|
+
server.autoclose = false
|
65
107
|
|
66
|
-
server.listen(
|
108
|
+
server.listen(queue)
|
67
109
|
server.extend(PastryServer)
|
68
110
|
|
69
111
|
server.app = @app
|
70
|
-
logger = Logger.new(logfile || (daemon ? '/tmp/pastry.log' : $stdout), 0)
|
71
|
-
options = {timeout: @timeout, maximum_connections: @maxconn}
|
72
|
-
|
73
112
|
logger.info motd
|
74
113
|
|
75
|
-
|
114
|
+
# make this world readable.
|
115
|
+
FileUtils.chmod(0777, socket) if socket
|
116
|
+
|
117
|
+
# pre-fork cleanups, let user cleanup any leaking fds.
|
118
|
+
@before_fork && @before_fork.call
|
119
|
+
|
120
|
+
$0 = "#{name} master (started: #{Time.now})" if name
|
76
121
|
@running = true
|
77
|
-
pids
|
122
|
+
@pids = pool.times.map {|idx| run(server, idx) }
|
78
123
|
|
79
124
|
Signal.trap('CHLD') do
|
80
125
|
if @running
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
126
|
+
Thread.exclusive do
|
127
|
+
died = pids.select {|pid| Process.waitpid(pid, Process::WNOHANG) rescue 0} # find dead pids
|
128
|
+
died = died.reject {|pid| Process.kill(0, pid) rescue nil} # make sure what's dead is dead
|
129
|
+
|
130
|
+
died.each do |pid|
|
131
|
+
logger.info "process #{pid} died, starting a new one"
|
132
|
+
if idx = pids.index(pid)
|
133
|
+
pids[idx] = run(server, idx)
|
134
|
+
end
|
135
|
+
end
|
86
136
|
end
|
87
137
|
end
|
88
138
|
end
|
89
139
|
|
90
|
-
%w(INT TERM
|
140
|
+
signals = %w(INT TERM QUIT)
|
141
|
+
signals << %q(HUP) unless start_command
|
142
|
+
|
143
|
+
signals.each do |signal|
|
91
144
|
Signal.trap(signal) do
|
92
145
|
@running = false
|
93
146
|
logger.info "caught #{signal}, closing time for the bakery -- no more pastries!"
|
94
|
-
|
95
|
-
|
147
|
+
stop_workers(signal)
|
148
|
+
FileUtils.rm_f([pidfile, socket.to_s])
|
149
|
+
Kernel.exit!
|
96
150
|
end
|
97
151
|
end
|
98
152
|
|
99
|
-
|
153
|
+
Signal.trap('HUP') { graceful_restart(server) } if start_command
|
100
154
|
Process.waitall rescue nil
|
101
155
|
end
|
102
156
|
|
103
|
-
def
|
157
|
+
def graceful_restart server
|
158
|
+
@running = false
|
159
|
+
logger.info "caught SIGHUP, restarting gracefully"
|
160
|
+
|
161
|
+
FileUtils.mv pidfile, "#{pidfile}.old"
|
162
|
+
|
163
|
+
pair = UNIXSocket.pair
|
164
|
+
data = Socket::AncillaryData.unix_rights(server)
|
165
|
+
pid = fork do
|
166
|
+
mesg, addr, rflags, *controls = pair[0].recvmsg(scm_rights: true)
|
167
|
+
ENV['PASTRY_FD'] = controls.first.unix_rights[0].fileno.to_s
|
168
|
+
pair.each(&:close)
|
169
|
+
Dir.chdir(cwd) do
|
170
|
+
Kernel.exec(start_command)
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
pair[1].sendmsg "*", 0, nil, data
|
175
|
+
pair.each(&:close)
|
176
|
+
|
177
|
+
# TODO signal parent that all is good and the new master is good to roll on its own ?
|
178
|
+
# 1. the new master failed to start
|
179
|
+
# 2. something in the after_fork bit crapped out
|
180
|
+
# 3. something else in the new code barfs during request
|
181
|
+
#
|
182
|
+
# A way to do this is provide pastry with a test route to hit after spawning the new master.
|
183
|
+
Process.detach(pid)
|
184
|
+
|
185
|
+
begin
|
186
|
+
Timeout.timeout(timeout) { sleep 0.5 until File.exists?(pidfile) }
|
187
|
+
rescue Timeout::Error => e
|
188
|
+
Process.kill('TERM', pid) rescue nil
|
189
|
+
logger.error "new master failed to spawn within #{timeout} secs, check logs"
|
190
|
+
@running = true
|
191
|
+
FileUtils.mv "#{pidfile}.old", pidfile
|
192
|
+
else
|
193
|
+
# finish exiting requests
|
194
|
+
stop_workers('HUP')
|
195
|
+
server.close
|
196
|
+
FileUtils.rm_f "#{pidfile}.old"
|
197
|
+
Kernel.exit
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
def stop_workers signal
|
202
|
+
logger.info "stopping workers"
|
203
|
+
pids.each {|pid| Process.kill(signal, pid) rescue nil}
|
204
|
+
return if signal == 'KILL'
|
205
|
+
|
206
|
+
logger.info "waiting up to #{timeout} seconds"
|
207
|
+
begin
|
208
|
+
Timeout.timeout(timeout) do
|
209
|
+
alive = pids
|
210
|
+
until alive.empty?
|
211
|
+
sleep 0.1
|
212
|
+
alive = pids.reject {|pid| Process.waitpid(pid, Process::WNOHANG) rescue 0}
|
213
|
+
end
|
214
|
+
end
|
215
|
+
rescue Timeout::Error => e
|
216
|
+
logger.info "killing stray pastry chefs with butcher knife (SIGKILL)"
|
217
|
+
pids.each {|pid| Process.kill('KILL', pid) rescue nil}
|
218
|
+
end
|
219
|
+
|
220
|
+
logger.info "all stop - ok"
|
221
|
+
end
|
222
|
+
|
223
|
+
def run server, worker
|
104
224
|
fork do
|
105
|
-
|
106
|
-
|
225
|
+
@after_fork && @after_fork.call(Process.pid, worker)
|
226
|
+
$0 = "#{name ? "%s worker" % name : "pastry chef"} #{worker} (started: #{Time.now})"
|
227
|
+
EM.epoll
|
228
|
+
EM.set_descriptor_table_size(max_connections)
|
229
|
+
EM.run { Backend.new.start(server) }
|
107
230
|
end
|
108
231
|
end
|
109
232
|
|
@@ -112,24 +235,38 @@ class Pastry
|
|
112
235
|
end
|
113
236
|
|
114
237
|
class Backend < Thin::Backends::Base
|
115
|
-
def initialize options = {}
|
116
|
-
super()
|
117
|
-
options.each {|key, value| send("#{key}=", value)}
|
118
|
-
end
|
119
|
-
|
120
238
|
def start server
|
121
239
|
@stopping = false
|
122
240
|
@running = true
|
123
241
|
@server = server
|
124
242
|
|
125
|
-
config
|
126
243
|
trap_signals!
|
127
|
-
|
128
|
-
EM.attach_server_socket(server, Thin::Connection, &method(:initialize_connection))
|
244
|
+
@signature = EM.attach_server_socket(server, Thin::Connection, &method(:initialize_connection))
|
129
245
|
end
|
130
246
|
|
131
247
|
def trap_signals!
|
132
|
-
|
248
|
+
# ignore SIGCHLD, can be overriden by app.
|
249
|
+
Signal.trap('CHLD', 'IGNORE')
|
250
|
+
|
251
|
+
# close connections and stop gracefully
|
252
|
+
%w(HUP QUIT).each do |signal|
|
253
|
+
Signal.trap(signal) do
|
254
|
+
stop
|
255
|
+
EM.add_periodic_timer(1) { Kernel.exit! if @connections.empty? }
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
# hard stop, discard requests and quit
|
260
|
+
%w(INT TERM).each do |signal|
|
261
|
+
Signal.trap(signal) do
|
262
|
+
stop!
|
263
|
+
Kernel.exit!
|
264
|
+
end
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
def disconnect
|
269
|
+
EM.stop_server(@signature)
|
133
270
|
end
|
134
271
|
end # Backend
|
135
272
|
end # Pastry
|
metadata
CHANGED
@@ -1,78 +1,68 @@
|
|
1
|
-
--- !ruby/object:Gem::Specification
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
2
|
name: pastry
|
3
|
-
version: !ruby/object:Gem::Version
|
4
|
-
|
5
|
-
|
6
|
-
- 0
|
7
|
-
- 1
|
8
|
-
- 3
|
9
|
-
version: 0.1.3
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.3.0
|
5
|
+
prerelease:
|
10
6
|
platform: ruby
|
11
|
-
authors:
|
7
|
+
authors:
|
12
8
|
- Bharanee Rathna
|
13
9
|
autorequire:
|
14
10
|
bindir: bin
|
15
11
|
cert_chain: []
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
12
|
+
date: 2013-03-13 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ! '>='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
24
20
|
none: false
|
25
|
-
requirements:
|
26
|
-
- - ">="
|
27
|
-
- !ruby/object:Gem::Version
|
28
|
-
segments:
|
29
|
-
- 0
|
30
|
-
version: "0"
|
31
21
|
type: :runtime
|
32
|
-
|
22
|
+
name: thin
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - ! '>='
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: '0'
|
28
|
+
none: false
|
29
|
+
prerelease: false
|
33
30
|
description: thin runner that forks and supports binding to single socket
|
34
31
|
email: deepfryed@gmail.com
|
35
|
-
executables:
|
32
|
+
executables:
|
36
33
|
- pastry
|
37
34
|
extensions: []
|
38
|
-
|
39
|
-
extra_rdoc_files:
|
35
|
+
extra_rdoc_files:
|
40
36
|
- README.rdoc
|
41
|
-
files:
|
37
|
+
files:
|
42
38
|
- lib/pastry.rb
|
43
39
|
- README.rdoc
|
44
40
|
- bin/pastry
|
45
|
-
has_rdoc: true
|
46
41
|
homepage: http://github.com/deepfryed/pastry
|
47
42
|
licenses: []
|
48
|
-
|
49
43
|
post_install_message:
|
50
44
|
rdoc_options: []
|
51
|
-
|
52
|
-
require_paths:
|
45
|
+
require_paths:
|
53
46
|
- lib
|
54
|
-
required_ruby_version: !ruby/object:Gem::Requirement
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
47
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
48
|
+
requirements:
|
49
|
+
- - ! '>='
|
50
|
+
- !ruby/object:Gem::Version
|
51
|
+
hash: 3318781981847808244
|
52
|
+
version: '0'
|
53
|
+
segments:
|
60
54
|
- 0
|
61
|
-
version: "0"
|
62
|
-
required_rubygems_version: !ruby/object:Gem::Requirement
|
63
55
|
none: false
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
56
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ! '>='
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0'
|
61
|
+
none: false
|
70
62
|
requirements: []
|
71
|
-
|
72
63
|
rubyforge_project:
|
73
|
-
rubygems_version: 1.
|
64
|
+
rubygems_version: 1.8.24
|
74
65
|
signing_key:
|
75
66
|
specification_version: 3
|
76
67
|
summary: thin runner that supports forking
|
77
68
|
test_files: []
|
78
|
-
|