yahns 0.0.0TP1

Sign up to get free protection for your applications and to get access to all the features.
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