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.
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
-