yahns 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (77) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/Documentation/.gitignore +5 -0
  4. data/Documentation/GNUmakefile +50 -0
  5. data/Documentation/yahns-rackup.txt +152 -0
  6. data/Documentation/yahns.txt +68 -0
  7. data/Documentation/yahns_config.txt +563 -0
  8. data/GIT-VERSION-GEN +1 -1
  9. data/GNUmakefile +14 -7
  10. data/HACKING +56 -0
  11. data/INSTALL +8 -0
  12. data/README +15 -2
  13. data/Rakefile +2 -2
  14. data/bin/yahns +1 -2
  15. data/bin/yahns-rackup +9 -0
  16. data/examples/yahns_multi.conf.rb +14 -4
  17. data/examples/yahns_rack_basic.conf.rb +17 -1
  18. data/extras/README +16 -0
  19. data/extras/autoindex.rb +151 -0
  20. data/extras/exec_cgi.rb +108 -0
  21. data/extras/proxy_pass.rb +210 -0
  22. data/extras/try_gzip_static.rb +208 -0
  23. data/lib/yahns.rb +5 -2
  24. data/lib/yahns/acceptor.rb +64 -22
  25. data/lib/yahns/cap_input.rb +2 -2
  26. data/lib/yahns/{client_expire_portable.rb → client_expire_generic.rb} +12 -11
  27. data/lib/yahns/{client_expire.rb → client_expire_tcpi.rb} +7 -6
  28. data/lib/yahns/config.rb +107 -22
  29. data/lib/yahns/daemon.rb +2 -0
  30. data/lib/yahns/fdmap.rb +28 -9
  31. data/lib/yahns/http_client.rb +123 -37
  32. data/lib/yahns/http_context.rb +21 -3
  33. data/lib/yahns/http_response.rb +80 -19
  34. data/lib/yahns/log.rb +23 -4
  35. data/lib/yahns/queue_epoll.rb +20 -9
  36. data/lib/yahns/queue_quitter.rb +16 -0
  37. data/lib/yahns/queue_quitter_pipe.rb +24 -0
  38. data/lib/yahns/rack.rb +0 -1
  39. data/lib/yahns/rackup_handler.rb +57 -0
  40. data/lib/yahns/server.rb +189 -59
  41. data/lib/yahns/server_mp.rb +43 -35
  42. data/lib/yahns/sigevent_pipe.rb +1 -0
  43. data/lib/yahns/socket_helper.rb +37 -11
  44. data/lib/yahns/stream_file.rb +14 -4
  45. data/lib/yahns/stream_input.rb +13 -7
  46. data/lib/yahns/tcp_server.rb +7 -0
  47. data/lib/yahns/tmpio.rb +10 -3
  48. data/lib/yahns/unix_server.rb +7 -0
  49. data/lib/yahns/wbuf.rb +19 -2
  50. data/lib/yahns/wbuf_common.rb +10 -3
  51. data/lib/yahns/wbuf_str.rb +24 -0
  52. data/lib/yahns/worker.rb +5 -26
  53. data/test/helper.rb +15 -5
  54. data/test/server_helper.rb +37 -1
  55. data/test/test_bin.rb +17 -8
  56. data/test/test_buffer_tmpdir.rb +103 -0
  57. data/test/test_client_expire.rb +71 -35
  58. data/test/test_client_max_body_size.rb +5 -13
  59. data/test/test_config.rb +1 -1
  60. data/test/test_expect_100.rb +176 -0
  61. data/test/test_extras_autoindex.rb +53 -0
  62. data/test/test_extras_exec_cgi.rb +81 -0
  63. data/test/test_extras_exec_cgi.sh +35 -0
  64. data/test/test_extras_try_gzip_static.rb +177 -0
  65. data/test/test_input.rb +128 -0
  66. data/test/test_mt_accept.rb +48 -0
  67. data/test/test_output_buffering.rb +90 -63
  68. data/test/test_rack.rb +1 -1
  69. data/test/test_rack_hijack.rb +2 -6
  70. data/test/test_reopen_logs.rb +2 -8
  71. data/test/test_serve_static.rb +104 -8
  72. data/test/test_server.rb +448 -73
  73. data/test/test_stream_file.rb +1 -1
  74. data/test/test_unix_socket.rb +72 -0
  75. data/test/test_wbuf.rb +20 -17
  76. data/yahns.gemspec +3 -0
  77. metadata +57 -5
data/lib/yahns.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  # Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
2
2
  # License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
3
+ $stdout.sync = $stderr.sync = true
4
+
3
5
  require 'unicorn' # pulls in raindrops, kgio, fcntl, etc, stringio, and logger
4
6
  require 'sleepy_penguin'
5
7
 
@@ -48,6 +50,9 @@ module Yahns # :nodoc:
48
50
  # for client shutdowns/disconnects.
49
51
  class ClientShutdown < EOFError # :nodoc:
50
52
  end
53
+
54
+ class ClientTimeout < RuntimeError # :nodoc:
55
+ end
51
56
  end
52
57
 
53
58
  # FIXME: require lazily
@@ -56,7 +61,6 @@ require_relative 'yahns/queue_epoll'
56
61
  require_relative 'yahns/stream_input'
57
62
  require_relative 'yahns/tee_input'
58
63
  require_relative 'yahns/queue_egg'
59
- require_relative 'yahns/client_expire'
60
64
  require_relative 'yahns/http_response'
61
65
  require_relative 'yahns/http_client'
62
66
  require_relative 'yahns/http_context'
@@ -65,7 +69,6 @@ require_relative 'yahns/config'
65
69
  require_relative 'yahns/tmpio'
66
70
  require_relative 'yahns/worker'
67
71
  require_relative 'yahns/sigevent'
68
- require_relative 'yahns/daemon'
69
72
  require_relative 'yahns/socket_helper'
70
73
  require_relative 'yahns/server'
71
74
  require_relative 'yahns/fdmap'
@@ -1,30 +1,72 @@
1
+ # -*- encoding: binary -*-
1
2
  # Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> et. al.
2
3
  # License: GPLv3 or later (see COPYING for details)
4
+ require_relative 'client_expire_tcpi'
5
+ require_relative 'client_expire_generic'
3
6
  module Yahns::Acceptor # :nodoc:
4
- def spawn_acceptor(logger, client_class, queue)
5
- Thread.new do
6
- accept_flags = Kgio::SOCK_NONBLOCK | Kgio::SOCK_CLOEXEC
7
- Thread.current.abort_on_exception = true
8
- qev_flags = client_class.superclass::QEV_FLAGS
7
+ def __ac_quit_done?
8
+ @thrs.delete_if do |t|
9
9
  begin
10
- # We want the accept/accept4 syscall to be _blocking_
11
- # so it can distribute work evenly between processes
12
- if client = kgio_accept(client_class, accept_flags)
13
- client.yahns_init
10
+ t.join(0.01)
11
+ rescue
12
+ ! t.alive?
13
+ end
14
+ end
15
+ return false if @thrs[0]
16
+ close
17
+ true
18
+ end
19
+
20
+ # just keep looping this on every acceptor until the associated thread dies
21
+ def ac_quit
22
+ return true unless defined?(@thrs)
23
+ @thrs.each { |t| t[:yahns_quit] = true }
24
+ return true if __ac_quit_done?
25
+
26
+ @thrs.each do
27
+ begin
28
+ # try to connect to kick it out of the blocking accept() syscall
29
+ killer = Kgio::Socket.start(getsockname)
30
+ killer.kgio_write("G") # first byte of "GET / HTTP/1.0\r\n\r\n"
31
+ ensure
32
+ killer.close if killer
33
+ end
34
+ end
35
+ false # now hope __ac_quit_done? is true next time around
36
+ rescue SystemCallError
37
+ return __ac_quit_done?
38
+ end
39
+
40
+ def spawn_acceptor(nr, logger, client_class)
41
+ @thrs = nr.times.map do
42
+ Thread.new do
43
+ queue = client_class.queue
44
+ t = Thread.current
45
+ accept_flags = Kgio::SOCK_NONBLOCK | Kgio::SOCK_CLOEXEC
46
+ qev_flags = client_class.superclass::QEV_FLAGS
47
+ begin
48
+ # We want the accept/accept4 syscall to be _blocking_
49
+ # so it can distribute work evenly between processes
50
+ if client = kgio_accept(client_class, accept_flags)
51
+ client.yahns_init
14
52
 
15
- # it is not safe to touch client in this thread after this,
16
- # a worker thread may grab client right away
17
- queue.queue_add(client, qev_flags)
18
- end
19
- rescue Errno::EMFILE, Errno::ENFILE => e
20
- logger.error("#{e.message}, consider raising open file limits")
21
- queue.fdmap.desperate_expire_for(self, 5)
22
- sleep 1 # let other threads do some work
23
- rescue => e
24
- # sleep since this check is racy (and uncommon)
25
- break if closed? || (sleep(0.01) && closed?)
26
- Yahns::Log.exception(logger, "accept loop", e)
27
- end while true
53
+ # it is not safe to touch client in this thread after this,
54
+ # a worker thread may grab client right away
55
+ queue.queue_add(client, qev_flags)
56
+ end
57
+ rescue Errno::EMFILE, Errno::ENFILE => e
58
+ logger.error("#{e.message}, consider raising open file limits")
59
+ queue.fdmap.desperate_expire_for(nil, 5)
60
+ sleep 1 # let other threads do some work
61
+ rescue => e
62
+ Yahns::Log.exception(logger, "accept loop", e)
63
+ end until t[:yahns_quit]
64
+ end
28
65
  end
29
66
  end
67
+
68
+ def expire_mod
69
+ (Yahns::TCPServer === self && Yahns.const_defined?(:ClientExpireTCPI)) ?
70
+ Yahns::ClientExpireTCPI : Yahns::ClientExpireGeneric
71
+ end
30
72
  end
@@ -7,8 +7,8 @@
7
7
  class Yahns::CapInput < Yahns::TmpIO # :nodoc:
8
8
  attr_writer :bytes_left
9
9
 
10
- def self.new(limit)
11
- rv = super()
10
+ def self.new(limit, tmpdir)
11
+ rv = super(tmpdir)
12
12
  rv.bytes_left = limit
13
13
  rv
14
14
  end
@@ -1,38 +1,39 @@
1
1
  # Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
2
2
  # License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
3
- module Yahns::ClientExpire # :nodoc:
3
+ module Yahns::ClientExpireGeneric # :nodoc:
4
4
  def __timestamp
5
5
  Time.now.to_f
6
6
  end
7
7
 
8
+ def yahns_init
9
+ super # Yahns::HttpClient#yahns_init
10
+ @last_io_at = 0
11
+ end
12
+
8
13
  def yahns_expire(timeout)
9
- return 0 if closed? # still racy, but avoid the exception in most cases
14
+ return 0 if closed?
10
15
  if (__timestamp - @last_io_at) > timeout
11
16
  shutdown
12
17
  1
13
18
  else
14
19
  0
15
20
  end
16
- rescue # the IO#closed? check is racy
21
+ # shutdown may race with the shutdown in http_response_done
22
+ rescue
17
23
  0
18
24
  end
19
25
 
20
- def kgio_read(*args)
21
- @last_io_at = __timestamp
22
- super
23
- end
24
-
25
- def kgio_write(*args)
26
+ def kgio_trywrite(*args)
26
27
  @last_io_at = __timestamp
27
28
  super
28
29
  end
29
30
 
30
- def kgio_trywrite(*args)
31
+ def kgio_tryread(*args)
31
32
  @last_io_at = __timestamp
32
33
  super
33
34
  end
34
35
 
35
- def kgio_tryread(*args)
36
+ def trysendfile(*args)
36
37
  @last_io_at = __timestamp
37
38
  super
38
39
  end
@@ -1,5 +1,6 @@
1
1
  # Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
2
2
  # License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
3
+ require 'raindrops'
3
4
 
4
5
  # included in Yahns::HttpClient
5
6
  #
@@ -7,9 +8,9 @@
7
8
  # on idle clients
8
9
  #
9
10
  # we absolutely DO NOT issue IO#close in here, only BasicSocket#shutdown
10
- module Yahns::ClientExpire # :nodoc:
11
+ module Yahns::ClientExpireTCPI # :nodoc:
11
12
  def yahns_expire(timeout) # rarely called
12
- return 0 if closed? # still racy, but avoid the exception in most cases
13
+ return 0 if closed?
13
14
 
14
15
  info = Raindrops::TCP_Info.new(self)
15
16
  return 0 if info.state != 1 # TCP_ESTABLISHED == 1
@@ -31,10 +32,10 @@ module Yahns::ClientExpire # :nodoc:
31
32
  else
32
33
  0
33
34
  end
34
- # we also do not expire UNIX domain sockets
35
- # (since those are the most trusted of local clients)
36
- # the IO#closed? check is racy
35
+ # shutdown may race with the shutdown in http_response_done
37
36
  rescue
38
37
  0
39
38
  end
40
- end
39
+ # FreeBSD has "struct tcp_info", too, but does not support all the fields
40
+ # Linux does as of FreeBSD 9 (haven't checked FreeBSD 10, yet).
41
+ end if RUBY_PLATFORM =~ /linux/
data/lib/yahns/config.rb CHANGED
@@ -22,9 +22,10 @@ class Yahns::Config # :nodoc:
22
22
  return var if @block == nil
23
23
  msg = "#{var} must be called outside of #{@block.type}"
24
24
  else
25
- return var if @block && ctx == @block.type
25
+ ctx = Array(ctx)
26
+ return var if @block && ctx.include?(@block.type)
26
27
  msg = @block ? "may not be used inside a #{@block.type} block" :
27
- "must be used with a #{ctx} block"
28
+ "must be used with a #{ctx.join(' or ')} block"
28
29
  end
29
30
  raise ArgumentError, msg
30
31
  end
@@ -38,7 +39,7 @@ class Yahns::Config # :nodoc:
38
39
  @config_listeners = {} # name/address -> options
39
40
  @app_ctx = []
40
41
  @set = Hash.new(:unset)
41
- @qeggs = {}
42
+ @qeggs = Hash.new { |h,k| h[k] = Yahns::QueueEgg.new }
42
43
  @app_instances = {}
43
44
 
44
45
  # set defaults:
@@ -73,10 +74,44 @@ class Yahns::Config # :nodoc:
73
74
  end
74
75
  end
75
76
 
76
- def worker_processes(nr)
77
- # TODO: allow zero
78
- var = _check_in_block(nil, :worker_processes)
77
+ def shutdown_timeout(sec)
78
+ var = _check_in_block(nil, :shutdown_timeout)
79
+ @set[var] = _check_num(var, sec, 0)
80
+ end
81
+
82
+ def worker_processes(nr, &blk)
83
+ var =_check_in_block(nil, :worker_processes)
79
84
  @set[var] = _check_int(var, nr, 1)
85
+ if block_given?
86
+ @block = CfgBlock.new(var, nil)
87
+ instance_eval(&blk)
88
+ @block = nil
89
+ end
90
+ end
91
+
92
+ %w(atfork_prepare atfork_parent atfork_child).each do |fn|
93
+ eval(
94
+ %Q(def #{fn}(*args, &blk);) <<
95
+ %Q( _check_in_block([:worker_processes,:app], :#{fn});) <<
96
+ %Q( _add_hook(:#{fn}, block_given? ? blk : args[0]);) <<
97
+ %Q(end)
98
+ )
99
+ end
100
+
101
+ def before_exec(&blk)
102
+ var = _check_in_block(nil, :before_exec)
103
+ @set[var] = (block_given? ? blk : args[0])
104
+ end
105
+
106
+ def _add_hook(var, my_proc)
107
+ Proc === my_proc or
108
+ raise ArgumentError, "invalid type: #{var}=#{my_proc.inspect}"
109
+
110
+ # this sets:
111
+ # :atfork_prepare, :atfork_parent, :atfork_child
112
+ key = var.to_sym
113
+ @set[key] = [] unless @set.include?(key)
114
+ @set[key] << my_proc
80
115
  end
81
116
 
82
117
  # sets the +path+ for the PID file of the yahns master process
@@ -104,7 +139,7 @@ class Yahns::Config # :nodoc:
104
139
  # for error checking and cannot be undone by unsetting it in the
105
140
  # configuration file and reloading.
106
141
  def working_directory(path)
107
- var = :working_directory
142
+ var = _check_in_block(nil, :working_directory)
108
143
  @app_ctx.empty? or
109
144
  raise ArgumentError, "#{var} must be declared before any apps"
110
145
 
@@ -127,7 +162,7 @@ class Yahns::Config # :nodoc:
127
162
  # if the Worker#user method is not called in the after_fork hooks
128
163
  # +group+ is optional and will not change if unspecified.
129
164
  def user(user, group = nil)
130
- var = :user
165
+ var = _check_in_block(nil, :user)
131
166
  @block and raise "#{var} is not valid inside #{@block.type}"
132
167
  # raises ArgumentError on invalid user/group
133
168
  Etc.getpwnam(user)
@@ -151,12 +186,17 @@ class Yahns::Config # :nodoc:
151
186
  address = expand_addr(address)
152
187
  String === address or
153
188
  raise ArgumentError, "address=#{address.inspect} must be a string"
154
- [ :umask, :backlog, :sndbuf, :rcvbuf ].each do |key|
189
+ [ :umask, :backlog ].each do |key|
190
+ # :backlog may be negative on some OSes
155
191
  value = options[key] or next
156
192
  Integer === value or
157
193
  raise ArgumentError, "#{var}: not an integer: #{key}=#{value.inspect}"
158
194
  end
159
- [ :ipv6only ].each do |key|
195
+ [ :sndbuf, :rcvbuf, :threads ].each do |key|
196
+ value = options[key] and _check_int(key, value, 1)
197
+ end
198
+
199
+ [ :ipv6only, :reuseport ].each do |key|
160
200
  (value = options[key]).nil? and next
161
201
  [ true, false ].include?(value) or
162
202
  raise ArgumentError, "#{var}: not boolean: #{key}=#{value.inspect}"
@@ -197,16 +237,33 @@ class Yahns::Config # :nodoc:
197
237
  /:/ =~ addr ? "[#{addr}]:#{port}" : "#{addr}:#{port}"
198
238
  end
199
239
 
200
- def queue(name = :default, &block)
240
+ def queue(*args, &block)
201
241
  var = :queue
202
- qegg = @qeggs[name] ||= Yahns::QueueEgg.new
203
242
  prev_block = @block
204
- _check_in_block(:app, var) if prev_block
243
+ if prev_block
244
+ _check_in_block(:app, var)
245
+ if block_given?
246
+ args.size == 0 or
247
+ raise ArgumentError,
248
+ "queues defined with a block inside app must not have names"
249
+ name = @block
250
+ else
251
+ name = args[0] or
252
+ raise ArgumentError, "queue must be given a name if no block given"
253
+ end
254
+ else
255
+ name = args[0] || :default
256
+ end
257
+ args.size > 1 and
258
+ raise ArgumentError, "queue only takes one name argument"
259
+ qegg = @qeggs[name]
205
260
  if block_given?
206
261
  @block = CfgBlock.new(:queue, qegg)
207
262
  instance_eval(&block)
208
263
  @block = prev_block
209
264
  end
265
+
266
+ # associate the queue if we're inside an app
210
267
  prev_block.ctx.qegg = qegg if prev_block
211
268
  end
212
269
 
@@ -226,12 +283,19 @@ class Yahns::Config # :nodoc:
226
283
  n
227
284
  end
228
285
 
286
+ def _check_num(var, n, min)
287
+ Numeric === n or raise ArgumentError, "not a number: #{var}=#{n.inspect}"
288
+ n >= min or raise ArgumentError, "too low (< #{min}): #{var}=#{n.inspect}"
289
+ n
290
+ end
291
+
229
292
  # global
230
293
  def client_expire_threshold(val)
231
294
  var = _check_in_block(nil, :client_expire_threshold)
232
295
  case val
233
296
  when Float
234
- val <= 1.0 or raise ArgumentError, "#{var} must be <= 1.0 if a ratio"
297
+ (val > 0 && val <= 1.0) or
298
+ raise ArgumentError, "#{var} must be > 0 and <= 1.0 if a ratio"
235
299
  when Integer
236
300
  else
237
301
  raise ArgumentError, "#{var} must be a float or integer"
@@ -270,9 +334,7 @@ class Yahns::Config # :nodoc:
270
334
  end
271
335
 
272
336
  # boolean config directives for app
273
- %w(check_client_connection
274
- output_buffering
275
- persistent_connections).each do |_v|
337
+ %w(check_client_connection persistent_connections).each do |_v|
276
338
  eval(
277
339
  %Q(def #{_v}(bool);) <<
278
340
  %Q( _check_in_block(:app, :#{_v});) <<
@@ -281,13 +343,27 @@ class Yahns::Config # :nodoc:
281
343
  )
282
344
  end
283
345
 
346
+ def output_buffering(bool, opts = {})
347
+ var = _check_in_block(:app, :output_buffering)
348
+ @block.ctx.__send__("#{var}=", _check_bool(var, bool))
349
+ tmpdir = opts[:tmpdir] and
350
+ @block.ctx.output_buffer_tmpdir = _check_tmpdir(var, tmpdir)
351
+ end
352
+
353
+ def _check_tmpdir(var, path)
354
+ File.directory?(path) or
355
+ raise ArgumentError, "#{var} tmpdir: #{path} is not a directory"
356
+ File.writable?(path) or
357
+ raise ArgumentError, "#{var} tmpdir: #{path} is not writable"
358
+ path
359
+ end
360
+
284
361
  # integer config directives for app
285
362
  {
286
363
  # config name, minimum value
287
364
  client_body_buffer_size: 1,
288
365
  client_header_buffer_size: 1,
289
366
  client_max_header_size: 1,
290
- client_timeout: 0,
291
367
  }.each do |_v,minval|
292
368
  eval(
293
369
  %Q(def #{_v}(val);) <<
@@ -297,18 +373,25 @@ class Yahns::Config # :nodoc:
297
373
  )
298
374
  end
299
375
 
376
+ def client_timeout(val)
377
+ var = _check_in_block(:app, :client_timeout)
378
+ @block.ctx.__send__("#{var}=", _check_num(var, val, 0))
379
+ end
380
+
300
381
  def client_max_body_size(val)
301
382
  var = _check_in_block(:app, :client_max_body_size)
302
383
  val = _check_int(var, val, 0) if val != nil
303
384
  @block.ctx.__send__("#{var}=", val)
304
385
  end
305
386
 
306
- def input_buffering(val)
387
+ def input_buffering(val, opts = {})
307
388
  var = _check_in_block(:app, :input_buffering)
308
389
  ok = [ :lazy, true, false ]
309
390
  ok.include?(val) or
310
391
  raise ArgumentError, "`#{var}' must be one of: #{ok.inspect}"
311
392
  @block.ctx.__send__("#{var}=", val)
393
+ tmpdir = opts[:tmpdir] and
394
+ @block.ctx.input_buffer_tmpdir = _check_tmpdir(var, tmpdir)
312
395
  end
313
396
 
314
397
  # used to configure rack.errors destination
@@ -335,14 +418,16 @@ class Yahns::Config # :nodoc:
335
418
  @set[key] = path = "/dev/null"
336
419
  end
337
420
  File.open(path, 'a') { |fp| io.reopen(fp) } if String === path
338
- io.close_on_exec = io.sync = true
421
+ io.sync = true
339
422
  end
340
423
 
341
- [ :logger, :pid, :worker_processes ].each do |var|
424
+ [ :logger, :pid, :worker_processes, :user, :shutdown_timeout, :before_exec,
425
+ :atfork_prepare, :atfork_parent, :atfork_child
426
+ ].each do |var|
342
427
  val = @set[var]
343
428
  server.__send__("#{var}=", val) if val != :unset
344
429
  end
345
- queue(:default) if @qeggs.empty?
430
+
346
431
  @app_ctx.each { |app| app.logger ||= server.logger }
347
432
  end
348
433
  end