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.
- checksums.yaml +7 -0
- data/.gitignore +15 -0
- data/COPYING +674 -0
- data/GIT-VERSION-GEN +41 -0
- data/GNUmakefile +90 -0
- data/README +127 -0
- data/Rakefile +60 -0
- data/bin/yahns +32 -0
- data/examples/README +3 -0
- data/examples/init.sh +76 -0
- data/examples/logger_mp_safe.rb +28 -0
- data/examples/logrotate.conf +32 -0
- data/examples/yahns_multi.conf.rb +89 -0
- data/examples/yahns_rack_basic.conf.rb +27 -0
- data/lib/yahns.rb +73 -0
- data/lib/yahns/acceptor.rb +28 -0
- data/lib/yahns/client_expire.rb +40 -0
- data/lib/yahns/client_expire_portable.rb +39 -0
- data/lib/yahns/config.rb +344 -0
- data/lib/yahns/daemon.rb +51 -0
- data/lib/yahns/fdmap.rb +90 -0
- data/lib/yahns/http_client.rb +198 -0
- data/lib/yahns/http_context.rb +65 -0
- data/lib/yahns/http_response.rb +184 -0
- data/lib/yahns/log.rb +73 -0
- data/lib/yahns/queue.rb +7 -0
- data/lib/yahns/queue_egg.rb +23 -0
- data/lib/yahns/queue_epoll.rb +57 -0
- data/lib/yahns/rack.rb +80 -0
- data/lib/yahns/server.rb +336 -0
- data/lib/yahns/server_mp.rb +181 -0
- data/lib/yahns/sigevent.rb +7 -0
- data/lib/yahns/sigevent_efd.rb +18 -0
- data/lib/yahns/sigevent_pipe.rb +29 -0
- data/lib/yahns/socket_helper.rb +117 -0
- data/lib/yahns/stream_file.rb +34 -0
- data/lib/yahns/stream_input.rb +150 -0
- data/lib/yahns/tee_input.rb +114 -0
- data/lib/yahns/tmpio.rb +27 -0
- data/lib/yahns/wbuf.rb +36 -0
- data/lib/yahns/wbuf_common.rb +32 -0
- data/lib/yahns/worker.rb +58 -0
- data/test/covshow.rb +29 -0
- data/test/helper.rb +115 -0
- data/test/server_helper.rb +65 -0
- data/test/test_bin.rb +97 -0
- data/test/test_client_expire.rb +132 -0
- data/test/test_config.rb +56 -0
- data/test/test_fdmap.rb +19 -0
- data/test/test_output_buffering.rb +291 -0
- data/test/test_queue.rb +59 -0
- data/test/test_rack.rb +28 -0
- data/test/test_serve_static.rb +42 -0
- data/test/test_server.rb +415 -0
- data/test/test_stream_file.rb +30 -0
- data/test/test_wbuf.rb +136 -0
- data/yahns.gemspec +19 -0
- 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
|
data/lib/yahns.rb
ADDED
@@ -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
|
data/lib/yahns/config.rb
ADDED
@@ -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
|