pastry 0.1.3 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (4) hide show
  1. data/README.rdoc +0 -1
  2. data/bin/pastry +27 -19
  3. data/lib/pastry.rb +185 -48
  4. metadata +39 -49
@@ -21,7 +21,6 @@ It's fairly pre-alpha, so use at your own peril
21
21
 
22
22
  == TODO
23
23
 
24
- * graceful stop / restart
25
24
  * increase or decrease worker count
26
25
 
27
26
  == License
data/bin/pastry CHANGED
@@ -3,33 +3,41 @@
3
3
  require 'pastry'
4
4
  require 'optparse'
5
5
 
6
- options = {}
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] = value }
11
- opts.on('-E', '--environment name', 'rack environment') {|value| options[:env] = value }
12
- opts.on('-R', '--rackup file', 'rackup file') {|value| options[:rackup] = value }
13
- opts.on('-a', '--address name', 'bind ip/host') {|value| options[:host] = value }
14
- opts.on('-p', '--port name', 'bind port') {|value| options[:port] = value }
15
- opts.on('-s', '--socket file', 'unix socket') {|value| options[:socket] = value }
16
- opts.on('-d', '--[no-]daemon', 'daemonize') {|value| options[:daemonize] = value }
17
- opts.on('-l', '--logfile file', 'logfile') {|value| options[:logfile] = value }
18
- opts.on('-P', '--pidfile file', 'pidfile') {|value| options[:pidfile] = value }
19
- opts.on('-c', '--connections num', 'max connections') {|value| options[:maxconn] = value }
20
- opts.on('-t', '--timeout secs', 'read timeout') {|value| options[:timeout] = value }
21
- opts.on('-A', '--application name', 'app name') {|value| options[:name] = value }
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 maxconn timeout).map(&:to_sym).each do |name|
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
- app = Rack::Builder.parse_file(options.delete(:rackup) || 'config.ru').first
31
- size = options.delete(:workers) || 2
32
- env = options.delete(:env) || 'development'
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
- ENV['RACK_ENV'] = env
35
- Pastry.new(size, app, options).start
42
+ master.parse_config(ARGV.shift) if ARGV.first && File.exists?(ARGV.first)
43
+ master.start
@@ -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
- attr_reader :pool, :unix, :host, :port, :pidfile, :logfile, :daemon
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 = pool
11
- @app = app
12
- @host = options.fetch :host, '127.0.0.1'
13
- @unix = options.fetch :socket, nil
14
- @logfile = options.fetch :logfile, nil
15
- @daemon = options.fetch :daemonize, false
16
- @pidfile = options.fetch :pidfile, '/tmp/pastry.pid'
17
- @name = options.fetch :name, nil
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
- @port = options.fetch(:port, 3000).to_i
20
- @queue = options.fetch(:queue, 1024).to_i
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
- if daemon || logfile
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 name
52
- '%s master' % (@name || 'pastry')
86
+ def master_name
87
+ '%s master' % (name || 'pastry')
53
88
  end
54
89
 
55
90
  def motd
56
- "starting #{name} with #{pool} minions listening on #{unix ? 'socket %s ' % unix : 'port %d' % port}"
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 = unix ? UNIXServer.new(unix) : TCPServer.new(host, port)
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 unix
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(@queue)
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
- $0 = name if @name
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 = pool.times.map {|n| run(server, options, n) }
122
+ @pids = pool.times.map {|idx| run(server, idx) }
78
123
 
79
124
  Signal.trap('CHLD') do
80
125
  if @running
81
- died = pids.reject {|pid| Process.kill(0, pid) rescue nil}
82
- died.each do |pid|
83
- logger.info "process #{pid} died, starting a new one"
84
- idx = pids.index(pid)
85
- pids[idx] = run(server, options, idx)
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 HUP).each do |signal|
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
- pids.each {|pid| Process.kill(signal, pid) rescue nil}
95
- exit
147
+ stop_workers(signal)
148
+ FileUtils.rm_f([pidfile, socket.to_s])
149
+ Kernel.exit!
96
150
  end
97
151
  end
98
152
 
99
- at_exit { FileUtils.rm_f(pidfile); FileUtils.rm_f(unix.to_s) }
153
+ Signal.trap('HUP') { graceful_restart(server) } if start_command
100
154
  Process.waitall rescue nil
101
155
  end
102
156
 
103
- def run server, options, worker
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
- $0 = "#{@name ? "%s worker" % @name : "pastry chef"} #{worker} (started: #{Time.now})"
106
- EM.run { Backend.new(options).start(server) }
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
- %w(INT TERM HUP CHLD).each {|signal| Signal.trap(signal) { exit }}
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
- prerelease: false
5
- segments:
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
- date: 2011-11-27 00:00:00 +11:00
18
- default_executable:
19
- dependencies:
20
- - !ruby/object:Gem::Dependency
21
- name: thin
22
- prerelease: false
23
- requirement: &id001 !ruby/object:Gem::Requirement
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
- version_requirements: *id001
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
- none: false
56
- requirements:
57
- - - ">="
58
- - !ruby/object:Gem::Version
59
- segments:
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
- requirements:
65
- - - ">="
66
- - !ruby/object:Gem::Version
67
- segments:
68
- - 0
69
- version: "0"
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.3.7
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
-