yahns 0.0.0TP1

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 (58) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +15 -0
  3. data/COPYING +674 -0
  4. data/GIT-VERSION-GEN +41 -0
  5. data/GNUmakefile +90 -0
  6. data/README +127 -0
  7. data/Rakefile +60 -0
  8. data/bin/yahns +32 -0
  9. data/examples/README +3 -0
  10. data/examples/init.sh +76 -0
  11. data/examples/logger_mp_safe.rb +28 -0
  12. data/examples/logrotate.conf +32 -0
  13. data/examples/yahns_multi.conf.rb +89 -0
  14. data/examples/yahns_rack_basic.conf.rb +27 -0
  15. data/lib/yahns.rb +73 -0
  16. data/lib/yahns/acceptor.rb +28 -0
  17. data/lib/yahns/client_expire.rb +40 -0
  18. data/lib/yahns/client_expire_portable.rb +39 -0
  19. data/lib/yahns/config.rb +344 -0
  20. data/lib/yahns/daemon.rb +51 -0
  21. data/lib/yahns/fdmap.rb +90 -0
  22. data/lib/yahns/http_client.rb +198 -0
  23. data/lib/yahns/http_context.rb +65 -0
  24. data/lib/yahns/http_response.rb +184 -0
  25. data/lib/yahns/log.rb +73 -0
  26. data/lib/yahns/queue.rb +7 -0
  27. data/lib/yahns/queue_egg.rb +23 -0
  28. data/lib/yahns/queue_epoll.rb +57 -0
  29. data/lib/yahns/rack.rb +80 -0
  30. data/lib/yahns/server.rb +336 -0
  31. data/lib/yahns/server_mp.rb +181 -0
  32. data/lib/yahns/sigevent.rb +7 -0
  33. data/lib/yahns/sigevent_efd.rb +18 -0
  34. data/lib/yahns/sigevent_pipe.rb +29 -0
  35. data/lib/yahns/socket_helper.rb +117 -0
  36. data/lib/yahns/stream_file.rb +34 -0
  37. data/lib/yahns/stream_input.rb +150 -0
  38. data/lib/yahns/tee_input.rb +114 -0
  39. data/lib/yahns/tmpio.rb +27 -0
  40. data/lib/yahns/wbuf.rb +36 -0
  41. data/lib/yahns/wbuf_common.rb +32 -0
  42. data/lib/yahns/worker.rb +58 -0
  43. data/test/covshow.rb +29 -0
  44. data/test/helper.rb +115 -0
  45. data/test/server_helper.rb +65 -0
  46. data/test/test_bin.rb +97 -0
  47. data/test/test_client_expire.rb +132 -0
  48. data/test/test_config.rb +56 -0
  49. data/test/test_fdmap.rb +19 -0
  50. data/test/test_output_buffering.rb +291 -0
  51. data/test/test_queue.rb +59 -0
  52. data/test/test_rack.rb +28 -0
  53. data/test/test_serve_static.rb +42 -0
  54. data/test/test_server.rb +415 -0
  55. data/test/test_stream_file.rb +30 -0
  56. data/test/test_wbuf.rb +136 -0
  57. data/yahns.gemspec +19 -0
  58. metadata +165 -0
@@ -0,0 +1,32 @@
1
+ # To the extent possible under law, Eric Wong has waived all copyright and
2
+ # related or neighboring rights to this examples
3
+ #
4
+ # example logrotate config file, I usually keep this in
5
+ # /etc/logrotate.d/yahns_app on my Debian systems
6
+ #
7
+ # See the logrotate(8) manpage for more information:
8
+ # http://linux.die.net/man/8/logrotate
9
+
10
+ # Modify the following glob to match the logfiles your app writes to:
11
+ /var/log/yahns_app/*.log {
12
+ # this first block is mostly just personal preference, though
13
+ # I wish logrotate offered an "hourly" option...
14
+ daily
15
+ missingok
16
+ rotate 180
17
+ compress # must use with delaycompress below
18
+ dateext
19
+
20
+ # this is important if using "compress" since we need to call
21
+ # the "lastaction" script below before compressing:
22
+ delaycompress
23
+
24
+ # note the lack of the evil "copytruncate" option in this
25
+ # config. yahns supports the USR1 signal and we send it
26
+ # as our "lastaction" action:
27
+ lastaction
28
+ # assuming your pid file is in /var/run/yahns_app/pid
29
+ pid=/var/run/yahns_app/pid
30
+ test -s $pid && kill -USR1 "$(cat $pid)"
31
+ endscript
32
+ }
@@ -0,0 +1,89 @@
1
+ # To the extent possible under law, Eric Wong has waived all copyright and
2
+ # related or neighboring rights to this example.
3
+
4
+ # By default, this based on the soft limit of RLIMIT_NOFILE
5
+ # count = Process.getrlimit(:NOFILE)[0]) * 0.5
6
+ # yahns will start expiring idle clients once we hit it
7
+ client_expire_threshold 0.5
8
+
9
+ # each queue definition configures a thread pool and epoll_wait usage
10
+ # The default queue is always present
11
+ queue(:default) do
12
+ worker_threads 7 # this is the default value
13
+ max_events 1 # 1: fairest, best in all multi-threaded cases
14
+ end
15
+
16
+ # This is an example of a small queue with fewer workers and unfair scheduling.
17
+ # It is rarely necessary or even advisable to configure multiple queues.
18
+ queue(:small) do
19
+ worker_threads 2
20
+
21
+ # increase max_events only under one of the following circumstances:
22
+ # 1) worker_threads is 1
23
+ # 2) epoll_wait lock contention inside the kernel is the biggest bottleneck
24
+ # (this is unlikely outside of "hello world" apps)
25
+ max_events 64
26
+ end
27
+
28
+ # This is an example of a Rack application configured in yahns
29
+ # All values below are defaults
30
+ app(:rack, "/path/to/config.ru", preload: false) do
31
+ listen 8080, backlog: 1024, tcp_nodelay: false
32
+ client_max_body_size 1024*1024
33
+ check_client_connection false
34
+ logger Logger.new($stderr)
35
+ client_timeout 15
36
+ input_buffering true
37
+ output_buffering true # output buffering is always lazy if enabled
38
+ persistent_connections true
39
+ errors $stderr
40
+ queue :default
41
+ end
42
+
43
+ # same as first, just listen on different port and small queue
44
+ app(:rack, "/path/to/config.ru") do
45
+ listen "10.0.0.1:10000"
46
+ client_max_body_size 1024*1024*10
47
+ check_client_connection true
48
+ logger Logger.new("/path/to/another/log")
49
+ client_timeout 30
50
+ persistent_connections true
51
+ errors "/path/to/errors.log"
52
+ queue :small
53
+ end
54
+
55
+ # totally different app
56
+ app(:rack, "/path/to/another.ru", preload: true) do
57
+ listen 8081, sndbuf: 1024 * 1024
58
+ listen "/path/to/unix.sock"
59
+ client_max_body_size 1024*1024*1024
60
+ input_buffering :lazy
61
+ output_buffering false
62
+ client_timeout 4
63
+ persistent_connections false
64
+
65
+ # different apps may share the same queue, but listen on different ports.
66
+ queue :default
67
+ end
68
+
69
+ # yet another totally different app, this app is not-thread safe but fast
70
+ # enough for multi-process to not matter.
71
+ # Use unicorn if you need multi-process performance on single-threaded apps
72
+ app(:rack, "/path/to/not_thread_safe.ru") do
73
+ # listeners _always_ get a private thread in yahns
74
+ listen "/path/to/yet_another.sock"
75
+ listen 8082
76
+
77
+ # inline private queue definition here
78
+ queue do
79
+ worker_threads 1 # single-threaded queue
80
+ max_events 64 # very high max_events is perfectly fair for single thread
81
+ end
82
+
83
+ # single (or few)-threaded apps must use full buffering if serving
84
+ # untrusted/slow clients.
85
+ input_buffering true
86
+ output_buffering true
87
+ end
88
+ # Note: this file is used by test_config.rb, be sure to update that
89
+ # if we update this
@@ -0,0 +1,27 @@
1
+ # To the extent possible under law, Eric Wong has waived all copyright and
2
+ # related or neighboring rights to this examples
3
+ # A typical Rack example for hosting a single Rack application with yahns
4
+ # and only frequently-useful config values
5
+
6
+ worker_processes 1
7
+ # working_directory "/path/to/my_app"
8
+ stdout_path "/path/to/my_logs/out.log"
9
+ stderr_path "/path/to/my_logs/err.log"
10
+ pid "/path/to/my_pids/yahns.pid"
11
+ client_expire_threshold 0.5
12
+
13
+ queue do
14
+ worker_threads 50
15
+ end
16
+
17
+ app(:rack, "config.ru", preload: false) do
18
+ listen 8080
19
+ client_max_body_size 1024 * 1024
20
+ input_buffering true
21
+ output_buffering true # this lazy by default
22
+ client_timeout 5
23
+ persistent_connections true
24
+ end
25
+
26
+ # Note: this file is used by test_config.rb, be sure to update that
27
+ # if we update this
@@ -0,0 +1,73 @@
1
+ # Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
2
+ # License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
3
+ require 'unicorn' # pulls in raindrops, kgio, fcntl, etc, stringio, and logger
4
+ require 'sleepy_penguin'
5
+
6
+ # kill off some unicorn internals we don't need
7
+ # we'll probably just make kcar into a server parser so we don't depend
8
+ # on unicorn at all
9
+ [ :ClientShutdown, :Const, :SocketHelper, :StreamInput, :TeeInput,
10
+ :SSLConfigurator, :Configurator, :TmpIO, :Util, :Worker, :SSLServer,
11
+ :HttpServer ].each { |sym| Unicorn.__send__(:remove_const, sym) }
12
+
13
+ # yahns exposes no user-visible API outside of the config file
14
+ # Internals are subject to change.
15
+ module Yahns # :nodoc:
16
+ # We populate this at startup so we can figure out how to reexecute
17
+ # and upgrade the currently running instance of yahns
18
+ # Unlike unicorn, this Hash is NOT a stable/public interface.
19
+ #
20
+ # * 0 - the path to the yahns executable
21
+ # * :argv - a deep copy of the ARGV array the executable originally saw
22
+ # * :cwd - the working directory of the application, this is where
23
+ # you originally started yahns.
24
+ #
25
+ # To change your yahns executable to a different path without downtime,
26
+ # you can set the following in your yahns config file, HUP and then
27
+ # continue with the traditional USR2 + QUIT upgrade steps:
28
+ #
29
+ # Yahns::START[0] = "/home/bofh/2.0.0/bin/yahns"
30
+ START = {
31
+ :argv => ARGV.map { |arg| arg.dup },
32
+ 0 => $0.dup,
33
+ }
34
+
35
+ # We favor ENV['PWD'] since it is (usually) symlink aware for Capistrano
36
+ # and like systems
37
+ START[:cwd] = begin
38
+ a = File.stat(pwd = ENV['PWD'])
39
+ b = File.stat(Dir.pwd)
40
+ a.ino == b.ino && a.dev == b.dev ? pwd : Dir.pwd
41
+ rescue
42
+ Dir.pwd
43
+ end
44
+
45
+ # Raised inside TeeInput when a client closes the socket inside the
46
+ # application dispatch. This is always raised with an empty backtrace
47
+ # since there is nothing in the application stack that is responsible
48
+ # for client shutdowns/disconnects.
49
+ class ClientShutdown < EOFError # :nodoc:
50
+ end
51
+ end
52
+
53
+ # FIXME: require lazily
54
+ require_relative 'yahns/log'
55
+ require_relative 'yahns/queue_epoll'
56
+ require_relative 'yahns/stream_input'
57
+ require_relative 'yahns/tee_input'
58
+ require_relative 'yahns/queue_egg'
59
+ require_relative 'yahns/client_expire'
60
+ require_relative 'yahns/http_response'
61
+ require_relative 'yahns/http_client'
62
+ require_relative 'yahns/http_context'
63
+ require_relative 'yahns/queue'
64
+ require_relative 'yahns/config'
65
+ require_relative 'yahns/tmpio'
66
+ require_relative 'yahns/worker'
67
+ require_relative 'yahns/sigevent'
68
+ require_relative 'yahns/daemon'
69
+ require_relative 'yahns/socket_helper'
70
+ require_relative 'yahns/server'
71
+ require_relative 'yahns/fdmap'
72
+ require_relative 'yahns/acceptor'
73
+ require_relative 'yahns/wbuf'
@@ -0,0 +1,28 @@
1
+ # Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> et. al.
2
+ # License: GPLv3 or later (see COPYING for details)
3
+ module Yahns::Acceptor # :nodoc:
4
+ def spawn_acceptor(logger, client_class, queue)
5
+ accept_flags = Kgio::SOCK_NONBLOCK | Kgio::SOCK_CLOEXEC
6
+ Thread.new do
7
+ Thread.current.abort_on_exception = true
8
+ qev_flags = client_class.superclass::QEV_FLAGS
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
14
+
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
+ Yahns::Log.exception(logger, "accept loop error", e) unless closed?
25
+ end until closed?
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,40 @@
1
+ # Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
2
+ # License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
3
+
4
+ # included in Yahns::HttpClient
5
+ #
6
+ # this provides the ability to expire idle clients once we hit a soft limit
7
+ # on idle clients
8
+ #
9
+ # we absolutely DO NOT issue IO#close in here, only BasicSocket#shutdown
10
+ module Yahns::ClientExpire # :nodoc:
11
+ def yahns_expire(timeout) # rarely called
12
+ return 0 if closed? # still racy, but avoid the exception in most cases
13
+
14
+ info = Raindrops::TCP_Info.new(self)
15
+ return 0 if info.state != 1 # TCP_ESTABLISHED == 1
16
+
17
+ # Linux struct tcp_info timers are in milliseconds
18
+ timeout *= 1000
19
+
20
+ send_timedout = !!(info.last_data_sent > timeout)
21
+
22
+ # tcpi_last_data_recv is not valid unless tcpi_ato (ACK timeout) is set
23
+ if 0 == info.ato
24
+ sd = send_timedout && (info.last_ack_recv > timeout)
25
+ else
26
+ sd = send_timedout && (info.last_data_recv > timeout)
27
+ end
28
+ if sd
29
+ shutdown
30
+ 1
31
+ else
32
+ 0
33
+ 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
37
+ rescue
38
+ 0
39
+ end
40
+ end
@@ -0,0 +1,39 @@
1
+ # Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
2
+ # License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
3
+ module Yahns::ClientExpire # :nodoc:
4
+ def __timestamp
5
+ Time.now.to_f
6
+ end
7
+
8
+ def yahns_expire(timeout)
9
+ return 0 if closed? # still racy, but avoid the exception in most cases
10
+ if (__timestamp - @last_io_at) > timeout
11
+ shutdown
12
+ 1
13
+ else
14
+ 0
15
+ end
16
+ rescue # the IO#closed? check is racy
17
+ 0
18
+ end
19
+
20
+ def kgio_read(*args)
21
+ @last_io_at = __timestamp
22
+ super
23
+ end
24
+
25
+ def kgio_write(*args)
26
+ @last_io_at = __timestamp
27
+ super
28
+ end
29
+
30
+ def kgio_trywrite(*args)
31
+ @last_io_at = __timestamp
32
+ super
33
+ end
34
+
35
+ def kgio_tryread(*args)
36
+ @last_io_at = __timestamp
37
+ super
38
+ end
39
+ end
@@ -0,0 +1,344 @@
1
+ # -*- encoding: binary -*-
2
+ # Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
3
+ # License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
4
+ #
5
+ # Implements a DSL for configuring a yahns server.
6
+ # See http://yahns.yhbt.net/examples/yahns_multi.conf.rb for a full
7
+ # example configuration file.
8
+ class Yahns::Config # :nodoc:
9
+ APP_CLASS = {} # public, see yahns/rack for usage example
10
+ CfgBlock = Struct.new(:type, :ctx) # :nodoc:
11
+ attr_reader :config_file, :config_listeners, :set
12
+ attr_reader :qeggs, :app_ctx
13
+
14
+ def initialize(config_file = nil)
15
+ @config_file = config_file
16
+ @block = nil
17
+ config_reload!
18
+ end
19
+
20
+ def _check_in_block(ctx, var)
21
+ if ctx == nil
22
+ return var if @block == nil
23
+ msg = "#{var} must be called outside of #{@block.type}"
24
+ else
25
+ return var if @block && ctx == @block.type
26
+ msg = @block ? "may not be used inside a #{@block.type} block" :
27
+ "must be used with a #{ctx} block"
28
+ end
29
+ raise ArgumentError, msg
30
+ end
31
+
32
+ def postfork_cleanup
33
+ @app_ctx = @set = @qeggs = @app_instances = @config_file = nil
34
+ end
35
+
36
+ def config_reload! #:nodoc:
37
+ # app_instance:app_ctx is a 1:N relationship
38
+ @config_listeners = {} # name/address -> options
39
+ @app_ctx = []
40
+ @set = Hash.new(:unset)
41
+ @qeggs = {}
42
+ @app_instances = {}
43
+
44
+ # set defaults:
45
+ client_expire_threshold(0.5) # default is half of the open file limit
46
+
47
+ instance_eval(File.read(@config_file), @config_file) if @config_file
48
+
49
+ # working_directory binds immediately (easier error checking that way),
50
+ # now ensure any paths we changed are correctly set.
51
+ [ :pid, :stderr_path, :stdout_path ].each do |var|
52
+ String === (path = @set[var]) or next
53
+ path = File.expand_path(path)
54
+ File.writable?(path) || File.writable?(File.dirname(path)) or \
55
+ raise ArgumentError, "directory for #{var}=#{path} not writable"
56
+ end
57
+ end
58
+
59
+ def logger(obj)
60
+ var = :logger
61
+ %w(debug info warn error fatal).each do |m|
62
+ obj.respond_to?(m) and next
63
+ raise ArgumentError, "#{var}=#{obj} does not respond to method=#{m}"
64
+ end
65
+ if @block
66
+ if @block.ctx.respond_to?(:logger=)
67
+ @block.ctx.logger = obj
68
+ else
69
+ raise ArgumentError, "#{var} not valid inside #{@block.type}"
70
+ end
71
+ else
72
+ @set[var] = obj
73
+ end
74
+ end
75
+
76
+ def worker_processes(nr)
77
+ # TODO: allow zero
78
+ var = _check_in_block(nil, :worker_processes)
79
+ @set[var] = _check_int(var, nr, 1)
80
+ end
81
+
82
+ # sets the +path+ for the PID file of the yahns master process
83
+ def pid(path)
84
+ _set_path(:pid, path)
85
+ end
86
+
87
+ def stderr_path(path)
88
+ _set_path(:stderr_path, path)
89
+ end
90
+
91
+ def stdout_path(path)
92
+ _set_path(:stdout_path, path)
93
+ end
94
+
95
+ def value(var)
96
+ val = @set[var]
97
+ val == :unset ? nil : val
98
+ end
99
+
100
+ # sets the working directory for yahns. This ensures SIGUSR2 will
101
+ # start a new instance of yahns in this directory. This may be
102
+ # a symlink, a common scenario for Capistrano users. Unlike
103
+ # all other yahns configuration directives, this binds immediately
104
+ # for error checking and cannot be undone by unsetting it in the
105
+ # configuration file and reloading.
106
+ def working_directory(path)
107
+ var = :working_directory
108
+ @app_ctx.empty? or
109
+ raise ArgumentError, "#{var} must be declared before any apps"
110
+
111
+ # just let chdir raise errors
112
+ path = File.expand_path(path)
113
+ if @config_file &&
114
+ @config_file[0] != ?/ &&
115
+ ! File.readable?("#{path}/#@config_file")
116
+ raise ArgumentError,
117
+ "config_file=#@config_file would not be accessible in" \
118
+ " #{var}=#{path}"
119
+ end
120
+ Dir.chdir(path)
121
+ @set[var] = ENV["PWD"] = path
122
+ end
123
+
124
+ # Runs worker processes as the specified +user+ and +group+.
125
+ # The master process always stays running as the user who started it.
126
+ # This switch will occur after calling the after_fork hooks, and only
127
+ # if the Worker#user method is not called in the after_fork hooks
128
+ # +group+ is optional and will not change if unspecified.
129
+ def user(user, group = nil)
130
+ var = :user
131
+ @block and raise "#{var} is not valid inside #{@block.type}"
132
+ # raises ArgumentError on invalid user/group
133
+ Etc.getpwnam(user)
134
+ Etc.getgrnam(group) if group
135
+ @set[var] = [ user, group ]
136
+ end
137
+
138
+ def _set_path(var, path) #:nodoc:
139
+ _check_in_block(nil, var)
140
+ case path
141
+ when NilClass, String
142
+ @set[var] = path
143
+ else
144
+ raise ArgumentError
145
+ end
146
+ end
147
+
148
+ def listen(address, options = {})
149
+ options = options.dup
150
+ var = _check_in_block(:app, :listen)
151
+ address = expand_addr(address)
152
+ String === address or
153
+ raise ArgumentError, "address=#{address.inspect} must be a string"
154
+ [ :umask, :backlog, :sndbuf, :rcvbuf ].each do |key|
155
+ value = options[key] or next
156
+ Integer === value or
157
+ raise ArgumentError, "#{var}: not an integer: #{key}=#{value.inspect}"
158
+ end
159
+ [ :ipv6only ].each do |key|
160
+ (value = options[key]).nil? and next
161
+ [ true, false ].include?(value) or
162
+ raise ArgumentError, "#{var}: not boolean: #{key}=#{value.inspect}"
163
+ end
164
+
165
+ options[:yahns_app_ctx] = @block.ctx
166
+ @config_listeners.include?(address) and
167
+ raise ArgumentError, "listen #{address} already in use"
168
+ @config_listeners[address] = options
169
+ end
170
+
171
+ # expands "unix:path/to/foo" to a socket relative to the current path
172
+ # expands pathnames of sockets if relative to "~" or "~username"
173
+ # expands "*:port and ":port" to "0.0.0.0:port"
174
+ def expand_addr(address) #:nodoc:
175
+ return "0.0.0.0:#{address}" if Integer === address
176
+ return address unless String === address
177
+
178
+ case address
179
+ when %r{\Aunix:(.*)\z}
180
+ File.expand_path($1)
181
+ when %r{\A~}
182
+ File.expand_path(address)
183
+ when %r{\A(?:\*:)?(\d+)\z}
184
+ "0.0.0.0:#$1"
185
+ when %r{\A\[([a-fA-F0-9:]+)\]\z}, %r/\A((?:\d+\.){3}\d+)\z/
186
+ canonicalize_tcp($1, 80)
187
+ when %r{\A\[([a-fA-F0-9:]+)\]:(\d+)\z}, %r{\A(.*):(\d+)\z}
188
+ canonicalize_tcp($1, $2.to_i)
189
+ else
190
+ address
191
+ end
192
+ end
193
+
194
+ def canonicalize_tcp(addr, port)
195
+ packed = Socket.pack_sockaddr_in(port, addr)
196
+ port, addr = Socket.unpack_sockaddr_in(packed)
197
+ /:/ =~ addr ? "[#{addr}]:#{port}" : "#{addr}:#{port}"
198
+ end
199
+
200
+ def queue(name = :default, &block)
201
+ var = :queue
202
+ qegg = @qeggs[name] ||= Yahns::QueueEgg.new
203
+ prev_block = @block
204
+ _check_in_block(:app, var) if prev_block
205
+ if block_given?
206
+ @block = CfgBlock.new(:queue, qegg)
207
+ instance_eval(&block)
208
+ @block = prev_block
209
+ end
210
+ prev_block.ctx.qegg = qegg if prev_block
211
+ end
212
+
213
+ # queue parameters (Yahns::QueueEgg)
214
+ %w(max_events worker_threads).each do |_v|
215
+ eval(
216
+ %Q(def #{_v}(val);) <<
217
+ %Q( _check_in_block(:queue, :#{_v});) <<
218
+ %Q( @block.ctx.__send__("#{_v}=", _check_int(:#{_v}, val, 1));) <<
219
+ %Q(end)
220
+ )
221
+ end
222
+
223
+ def _check_int(var, n, min)
224
+ Integer === n or raise ArgumentError, "not an integer: #{var}=#{n.inspect}"
225
+ n >= min or raise ArgumentError, "too low (< #{min}): #{var}=#{n.inspect}"
226
+ n
227
+ end
228
+
229
+ # global
230
+ def client_expire_threshold(val)
231
+ var = _check_in_block(nil, :client_expire_threshold)
232
+ case val
233
+ when Float
234
+ val <= 1.0 or raise ArgumentError, "#{var} must be <= 1.0 if a ratio"
235
+ when Integer
236
+ else
237
+ raise ArgumentError, "#{var} must be a float or integer"
238
+ end
239
+ @set[var] = val
240
+ end
241
+
242
+ # type = :rack
243
+ def app(type, *args, &block)
244
+ var = _check_in_block(nil, :app)
245
+ file = "yahns/#{type.to_s}"
246
+ begin
247
+ require file
248
+ rescue LoadError => e
249
+ raise ArgumentError, "#{type.inspect} is not a supported app type",
250
+ e.backtrace
251
+ end
252
+ klass = APP_CLASS[type] or
253
+ raise TypeError,
254
+ "#{var}: #{file} did not register #{type} in #{self.class}::APP_CLASS"
255
+
256
+ # apps may have multiple configurator contexts
257
+ app = @app_instances[klass.instance_key(*args)] = klass.new(*args)
258
+ ctx = app.config_context
259
+ if block_given?
260
+ @block = CfgBlock.new(:app, ctx)
261
+ instance_eval(&block)
262
+ @block = nil
263
+ end
264
+ @app_ctx << ctx
265
+ end
266
+
267
+ def _check_bool(var, val)
268
+ return val if [ true, false ].include?(val)
269
+ raise ArgumentError, "#{var} must be boolean"
270
+ end
271
+
272
+ # boolean config directives for app
273
+ %w(check_client_connection
274
+ output_buffering
275
+ persistent_connections).each do |_v|
276
+ eval(
277
+ %Q(def #{_v}(bool);) <<
278
+ %Q( _check_in_block(:app, :#{_v});) <<
279
+ %Q( @block.ctx.__send__("#{_v}=", _check_bool(:#{_v}, bool));) <<
280
+ %Q(end)
281
+ )
282
+ end
283
+
284
+ # integer config directives for app
285
+ {
286
+ # config name, minimum value
287
+ client_body_buffer_size: 1,
288
+ client_max_body_size: 0,
289
+ client_header_buffer_size: 1,
290
+ client_max_header_size: 1,
291
+ client_timeout: 0,
292
+ }.each do |_v,minval|
293
+ eval(
294
+ %Q(def #{_v}(val);) <<
295
+ %Q( _check_in_block(:app, :#{_v});) <<
296
+ %Q( @block.ctx.__send__("#{_v}=", _check_int(:#{_v}, val, #{minval}));) <<
297
+ %Q(end)
298
+ )
299
+ end
300
+
301
+ def input_buffering(val)
302
+ var = _check_in_block(:app, :input_buffering)
303
+ ok = [ :lazy, true, false ]
304
+ ok.include?(val) or
305
+ raise ArgumentError, "`#{var}' must be one of: #{ok.inspect}"
306
+ @block.ctx.__send__("#{var}=", val)
307
+ end
308
+
309
+ # used to configure rack.errors destination
310
+ def errors(val)
311
+ var = _check_in_block(:app, :errors)
312
+ if String === val
313
+ # we've already bound working_directory by the time we get here
314
+ val = File.open(File.expand_path(val), "a")
315
+ val.binmode
316
+ val.sync = true
317
+ else
318
+ rt = %w(puts write flush).map(&:to_sym) # match Rack::Lint
319
+ rt.all? { |m| val.respond_to?(m) } or raise ArgumentError,
320
+ "`#{var}' destination must respond to all of: #{rt.inspect}"
321
+ end
322
+ @block.ctx.__send__("#{var}=", val)
323
+ end
324
+
325
+ def commit!(server)
326
+ # redirect IOs
327
+ { stdout_path: $stdout, stderr_path: $stderr }.each do |key, io|
328
+ path = @set[key]
329
+ if path == :unset && server.daemon_pipe
330
+ @set[key] = path = "/dev/null"
331
+ end
332
+ File.open(path, 'a') { |fp| io.reopen(fp) } if String === path
333
+ io.sync = true
334
+ end
335
+
336
+ [ :logger, :pid, :worker_processes ].each do |var|
337
+ val = @set[var]
338
+ server.__send__("#{var}=", val) if val != :unset
339
+ end
340
+ queue(:default) if @qeggs.empty?
341
+ @qeggs.each_value { |qegg| qegg.logger ||= server.logger }
342
+ @app_ctx.each { |app| app.logger ||= server.logger }
343
+ end
344
+ end