yahns 0.0.1 → 0.0.2

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