pitchfork 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.git-blame-ignore-revs +3 -0
- data/.gitattributes +5 -0
- data/.github/workflows/ci.yml +30 -0
- data/.gitignore +23 -0
- data/COPYING +674 -0
- data/Dockerfile +4 -0
- data/Gemfile +9 -0
- data/Gemfile.lock +30 -0
- data/LICENSE +67 -0
- data/README.md +123 -0
- data/Rakefile +72 -0
- data/docs/Application_Timeouts.md +74 -0
- data/docs/CONFIGURATION.md +388 -0
- data/docs/DESIGN.md +86 -0
- data/docs/FORK_SAFETY.md +80 -0
- data/docs/PHILOSOPHY.md +90 -0
- data/docs/REFORKING.md +113 -0
- data/docs/SIGNALS.md +38 -0
- data/docs/TUNING.md +106 -0
- data/examples/constant_caches.ru +43 -0
- data/examples/echo.ru +25 -0
- data/examples/hello.ru +5 -0
- data/examples/nginx.conf +156 -0
- data/examples/pitchfork.conf.minimal.rb +5 -0
- data/examples/pitchfork.conf.rb +77 -0
- data/examples/unicorn.socket +11 -0
- data/exe/pitchfork +116 -0
- data/ext/pitchfork_http/CFLAGS +13 -0
- data/ext/pitchfork_http/c_util.h +116 -0
- data/ext/pitchfork_http/child_subreaper.h +25 -0
- data/ext/pitchfork_http/common_field_optimization.h +130 -0
- data/ext/pitchfork_http/epollexclusive.h +124 -0
- data/ext/pitchfork_http/ext_help.h +38 -0
- data/ext/pitchfork_http/extconf.rb +14 -0
- data/ext/pitchfork_http/global_variables.h +97 -0
- data/ext/pitchfork_http/httpdate.c +79 -0
- data/ext/pitchfork_http/pitchfork_http.c +4318 -0
- data/ext/pitchfork_http/pitchfork_http.rl +1024 -0
- data/ext/pitchfork_http/pitchfork_http_common.rl +76 -0
- data/lib/pitchfork/app/old_rails/static.rb +59 -0
- data/lib/pitchfork/children.rb +124 -0
- data/lib/pitchfork/configurator.rb +314 -0
- data/lib/pitchfork/const.rb +23 -0
- data/lib/pitchfork/http_parser.rb +206 -0
- data/lib/pitchfork/http_response.rb +63 -0
- data/lib/pitchfork/http_server.rb +822 -0
- data/lib/pitchfork/launcher.rb +9 -0
- data/lib/pitchfork/mem_info.rb +36 -0
- data/lib/pitchfork/message.rb +130 -0
- data/lib/pitchfork/mold_selector.rb +29 -0
- data/lib/pitchfork/preread_input.rb +33 -0
- data/lib/pitchfork/refork_condition.rb +21 -0
- data/lib/pitchfork/select_waiter.rb +9 -0
- data/lib/pitchfork/socket_helper.rb +199 -0
- data/lib/pitchfork/stream_input.rb +152 -0
- data/lib/pitchfork/tee_input.rb +133 -0
- data/lib/pitchfork/tmpio.rb +35 -0
- data/lib/pitchfork/version.rb +8 -0
- data/lib/pitchfork/worker.rb +244 -0
- data/lib/pitchfork.rb +158 -0
- data/pitchfork.gemspec +30 -0
- metadata +137 -0
@@ -0,0 +1,76 @@
|
|
1
|
+
%%{
|
2
|
+
|
3
|
+
machine pitchfork_http_common;
|
4
|
+
|
5
|
+
#### HTTP PROTOCOL GRAMMAR
|
6
|
+
# line endings
|
7
|
+
CRLF = ("\r\n" | "\n");
|
8
|
+
|
9
|
+
# character types
|
10
|
+
CTL = (cntrl | 127);
|
11
|
+
safe = ("$" | "-" | "_" | ".");
|
12
|
+
extra = ("!" | "*" | "'" | "(" | ")" | ",");
|
13
|
+
reserved = (";" | "/" | "?" | ":" | "@" | "&" | "=" | "+");
|
14
|
+
sorta_safe = ("\"" | "<" | ">");
|
15
|
+
unsafe = (CTL | " " | "#" | "%" | sorta_safe);
|
16
|
+
national = any -- (alpha | digit | reserved | extra | safe | unsafe);
|
17
|
+
unreserved = (alpha | digit | safe | extra | national);
|
18
|
+
escape = ("%" xdigit xdigit);
|
19
|
+
uchar = (unreserved | escape | sorta_safe);
|
20
|
+
pchar = (uchar | ":" | "@" | "&" | "=" | "+");
|
21
|
+
tspecials = ("(" | ")" | "<" | ">" | "@" | "," | ";" | ":" | "\\" | "\"" | "/" | "[" | "]" | "?" | "=" | "{" | "}" | " " | "\t");
|
22
|
+
lws = (" " | "\t");
|
23
|
+
content = ((any -- CTL) | lws);
|
24
|
+
|
25
|
+
# elements
|
26
|
+
token = (ascii -- (CTL | tspecials));
|
27
|
+
|
28
|
+
# URI schemes and absolute paths
|
29
|
+
scheme = ( "http"i ("s"i)? ) $downcase_char >mark %scheme;
|
30
|
+
hostname = ((alnum | "-" | "." | "_")+ | ("[" (":" | xdigit)+ "]"));
|
31
|
+
host_with_port = (hostname (":" digit*)?) >mark %host;
|
32
|
+
userinfo = ((unreserved | escape | ";" | ":" | "&" | "=" | "+")+ "@")*;
|
33
|
+
|
34
|
+
path = ( pchar+ ( "/" pchar* )* ) ;
|
35
|
+
query = ( uchar | reserved )* %query_string ;
|
36
|
+
param = ( pchar | "/" )* ;
|
37
|
+
params = ( param ( ";" param )* ) ;
|
38
|
+
rel_path = (path? (";" params)? %request_path) ("?" %start_query query)?;
|
39
|
+
absolute_path = ( "/"+ rel_path );
|
40
|
+
path_uri = absolute_path > mark %request_uri;
|
41
|
+
Absolute_URI = (scheme "://" userinfo host_with_port path_uri);
|
42
|
+
|
43
|
+
Request_URI = ((absolute_path | "*") >mark %request_uri) | Absolute_URI;
|
44
|
+
Fragment = ( uchar | reserved )* >mark %fragment;
|
45
|
+
Method = (token){1,20} >mark %request_method;
|
46
|
+
GetOnly = "GET" >mark %request_method;
|
47
|
+
|
48
|
+
http_number = ( digit+ "." digit+ ) ;
|
49
|
+
HTTP_Version = ( "HTTP/" http_number ) >mark %http_version ;
|
50
|
+
Request_Line = ( Method " " Request_URI ("#" Fragment){0,1} " " HTTP_Version CRLF ) ;
|
51
|
+
|
52
|
+
field_name = ( token -- ":" )+ >start_field $snake_upcase_field %write_field;
|
53
|
+
|
54
|
+
field_value = content* >start_value %write_value;
|
55
|
+
|
56
|
+
value_cont = lws+ content* >start_value %write_cont_value;
|
57
|
+
|
58
|
+
message_header = ((field_name ":" lws* field_value)|value_cont) :> CRLF;
|
59
|
+
chunk_ext_val = token*;
|
60
|
+
chunk_ext_name = token*;
|
61
|
+
chunk_extension = ( ";" " "* chunk_ext_name ("=" chunk_ext_val)? )*;
|
62
|
+
last_chunk = "0"+ chunk_extension CRLF;
|
63
|
+
chunk_size = (xdigit* [1-9a-fA-F] xdigit*) $add_to_chunk_size;
|
64
|
+
chunk_end = CRLF;
|
65
|
+
chunk_body = any >skip_chunk_data;
|
66
|
+
chunk_begin = chunk_size chunk_extension CRLF;
|
67
|
+
chunk = chunk_begin chunk_body chunk_end;
|
68
|
+
ChunkedBody := chunk* last_chunk @end_chunked_body;
|
69
|
+
Trailers := (message_header)* CRLF @end_trailers;
|
70
|
+
|
71
|
+
FullRequest = Request_Line (message_header)* CRLF @header_done;
|
72
|
+
SimpleRequest = GetOnly " " Request_URI ("#"Fragment){0,1} CRLF @header_done;
|
73
|
+
|
74
|
+
main := FullRequest | SimpleRequest;
|
75
|
+
|
76
|
+
}%%
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# -*- encoding: binary -*-
|
2
|
+
# :enddoc:
|
3
|
+
# This code is based on the original Rails handler in Mongrel
|
4
|
+
# Copyright (c) 2005 Zed A. Shaw
|
5
|
+
# Copyright (c) 2009 Eric Wong
|
6
|
+
# You can redistribute it and/or modify it under the same terms as Ruby 1.8 or
|
7
|
+
# the GPLv3
|
8
|
+
|
9
|
+
# Static file handler for Rails < 2.3. This handler is only provided
|
10
|
+
# as a convenience for developers. Performance-minded deployments should
|
11
|
+
# use nginx (or similar) for serving static files.
|
12
|
+
#
|
13
|
+
# This supports page caching directly and will try to resolve a
|
14
|
+
# request in the following order:
|
15
|
+
#
|
16
|
+
# * If the requested exact PATH_INFO exists as a file then serve it.
|
17
|
+
# * If it exists at PATH_INFO+rest_operator+".html" exists
|
18
|
+
# then serve that.
|
19
|
+
#
|
20
|
+
# This means that if you are using page caching it will actually work
|
21
|
+
# with Pitchfork and you should see a decent speed boost (but not as
|
22
|
+
# fast as if you use a static server like nginx).
|
23
|
+
class Pitchfork::App::OldRails::Static < Struct.new(:app, :root, :file_server)
|
24
|
+
FILE_METHODS = { 'GET' => true, 'HEAD' => true }
|
25
|
+
|
26
|
+
# avoid allocating new strings for hash lookups
|
27
|
+
REQUEST_METHOD = 'REQUEST_METHOD'
|
28
|
+
REQUEST_URI = 'REQUEST_URI'
|
29
|
+
PATH_INFO = 'PATH_INFO'
|
30
|
+
|
31
|
+
def initialize(app)
|
32
|
+
self.app = app
|
33
|
+
self.root = "#{::RAILS_ROOT}/public"
|
34
|
+
self.file_server = ::Rack::File.new(root)
|
35
|
+
end
|
36
|
+
|
37
|
+
def call(env)
|
38
|
+
# short circuit this ASAP if serving non-file methods
|
39
|
+
FILE_METHODS.include?(env[REQUEST_METHOD]) or return app.call(env)
|
40
|
+
|
41
|
+
# first try the path as-is
|
42
|
+
path_info = env[PATH_INFO].chomp("/")
|
43
|
+
if File.file?("#{root}/#{::Rack::Utils.unescape(path_info)}")
|
44
|
+
# File exists as-is so serve it up
|
45
|
+
env[PATH_INFO] = path_info
|
46
|
+
return file_server.call(env)
|
47
|
+
end
|
48
|
+
|
49
|
+
# then try the cached version:
|
50
|
+
path_info << ActionController::Base.page_cache_extension
|
51
|
+
|
52
|
+
if File.file?("#{root}/#{::Rack::Utils.unescape(path_info)}")
|
53
|
+
env[PATH_INFO] = path_info
|
54
|
+
return file_server.call(env)
|
55
|
+
end
|
56
|
+
|
57
|
+
app.call(env) # call OldRails
|
58
|
+
end
|
59
|
+
end if defined?(Pitchfork::App::OldRails)
|
@@ -0,0 +1,124 @@
|
|
1
|
+
# -*- encoding: binary -*-
|
2
|
+
|
3
|
+
module Pitchfork
|
4
|
+
# This class keep tracks of the state of all the master children.
|
5
|
+
class Children
|
6
|
+
attr_reader :mold
|
7
|
+
attr_accessor :last_generation
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@last_generation = 0
|
11
|
+
@children = {} # All children, including molds, indexed by PID.
|
12
|
+
@workers = {} # Workers indexed by their `nr`.
|
13
|
+
@molds = {} # Molds, index by PID.
|
14
|
+
@mold = nil # The latest mold, if any.
|
15
|
+
@pending_workers = {} # Pending workers indexed by their `nr`.
|
16
|
+
@pending_molds = {} # Worker promoted to mold, not yet acknowledged
|
17
|
+
end
|
18
|
+
|
19
|
+
def refresh
|
20
|
+
@workers.each_value(&:refresh)
|
21
|
+
@molds.each_value(&:refresh)
|
22
|
+
end
|
23
|
+
|
24
|
+
def register(child)
|
25
|
+
# Children always start as workers, never molds, so we know they have a `#nr`.
|
26
|
+
@pending_workers[child.nr] = @workers[child.nr] = child
|
27
|
+
end
|
28
|
+
|
29
|
+
def register_mold(mold)
|
30
|
+
@pending_molds[mold.pid] = mold
|
31
|
+
@children[mold.pid] = mold
|
32
|
+
@mold = mold
|
33
|
+
end
|
34
|
+
|
35
|
+
def fetch(pid)
|
36
|
+
@children.fetch(pid)
|
37
|
+
end
|
38
|
+
|
39
|
+
def update(message)
|
40
|
+
child = @children[message.pid] || (message.nr && @workers[message.nr])
|
41
|
+
old_nr = child.nr
|
42
|
+
|
43
|
+
child.update(message)
|
44
|
+
|
45
|
+
if child.mold?
|
46
|
+
@workers.delete(old_nr)
|
47
|
+
@pending_molds.delete(child.pid)
|
48
|
+
@molds[child.pid] = child
|
49
|
+
@mold = child
|
50
|
+
end
|
51
|
+
if child.pid
|
52
|
+
@children[child.pid] = child
|
53
|
+
@pending_workers.delete(child.nr)
|
54
|
+
end
|
55
|
+
child
|
56
|
+
end
|
57
|
+
|
58
|
+
def nr_alive?(nr)
|
59
|
+
@workers.key?(nr)
|
60
|
+
end
|
61
|
+
|
62
|
+
def reap(pid)
|
63
|
+
if child = @children.delete(pid)
|
64
|
+
@pending_workers.delete(child.nr)
|
65
|
+
@pending_molds.delete(child.pid)
|
66
|
+
@molds.delete(child.pid)
|
67
|
+
@workers.delete(child.nr)
|
68
|
+
if @mold == child
|
69
|
+
@mold = nil
|
70
|
+
end
|
71
|
+
end
|
72
|
+
child
|
73
|
+
end
|
74
|
+
|
75
|
+
def promote(worker)
|
76
|
+
@pending_molds[worker.pid] = worker
|
77
|
+
worker.promote(self.last_generation += 1)
|
78
|
+
end
|
79
|
+
|
80
|
+
def pending_workers?
|
81
|
+
!(@pending_workers.empty? && @pending_molds.empty?)
|
82
|
+
end
|
83
|
+
|
84
|
+
def pending_promotion?
|
85
|
+
!@pending_molds.empty?
|
86
|
+
end
|
87
|
+
|
88
|
+
def molds
|
89
|
+
@molds.values
|
90
|
+
end
|
91
|
+
|
92
|
+
def each(&block)
|
93
|
+
@children.each_value(&block)
|
94
|
+
end
|
95
|
+
|
96
|
+
def each_worker(&block)
|
97
|
+
@workers.each_value(&block)
|
98
|
+
end
|
99
|
+
|
100
|
+
def workers
|
101
|
+
@workers.values
|
102
|
+
end
|
103
|
+
|
104
|
+
def fresh_workers
|
105
|
+
if @mold
|
106
|
+
workers.select { |w| w.generation >= @mold.generation }
|
107
|
+
else
|
108
|
+
workers
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def workers_count
|
113
|
+
@workers.size
|
114
|
+
end
|
115
|
+
|
116
|
+
def total_pss
|
117
|
+
total_pss = MemInfo.new(Process.pid).pss
|
118
|
+
@children.each do |_, worker|
|
119
|
+
total_pss += worker.meminfo.pss if worker.meminfo
|
120
|
+
end
|
121
|
+
total_pss
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
@@ -0,0 +1,314 @@
|
|
1
|
+
# -*- encoding: binary -*-
|
2
|
+
require 'logger'
|
3
|
+
|
4
|
+
module Pitchfork
|
5
|
+
# Implements a simple DSL for configuring a pitchfork server.
|
6
|
+
#
|
7
|
+
# See https://github.com/Shopify/pitchfork/tree/master/examples/pitchfork.conf.rb and
|
8
|
+
# https://github.com/Shopify/pitchfork/tree/master/examples/pitchfork.conf.minimal.rb
|
9
|
+
# example configuration files.
|
10
|
+
#
|
11
|
+
# See the docs/TUNING.md document for more information on tuning pitchfork.
|
12
|
+
class Configurator
|
13
|
+
include Pitchfork
|
14
|
+
|
15
|
+
# :stopdoc:
|
16
|
+
attr_accessor :set, :config_file, :after_load
|
17
|
+
|
18
|
+
# used to stash stuff for deferred processing of cli options in
|
19
|
+
# config.ru. Do not rely on
|
20
|
+
# this being around later on...
|
21
|
+
RACKUP = {
|
22
|
+
:host => Pitchfork::Const::DEFAULT_HOST,
|
23
|
+
:port => Pitchfork::Const::DEFAULT_PORT,
|
24
|
+
:set_listener => false,
|
25
|
+
:options => { :listeners => [] }
|
26
|
+
}
|
27
|
+
|
28
|
+
# Default settings for Pitchfork
|
29
|
+
DEFAULTS = {
|
30
|
+
:timeout => 20,
|
31
|
+
:logger => Logger.new($stderr),
|
32
|
+
:worker_processes => 1,
|
33
|
+
:after_fork => lambda { |server, worker|
|
34
|
+
server.logger.info("worker=#{worker.nr} gen=#{worker.generation} pid=#{$$} spawned")
|
35
|
+
},
|
36
|
+
:before_fork => lambda { |server, worker|
|
37
|
+
server.logger.info("worker=#{worker.nr} gen=#{worker.generation} spawning...")
|
38
|
+
},
|
39
|
+
:after_worker_exit => lambda { |server, worker, status|
|
40
|
+
m = if worker.nil?
|
41
|
+
"repead unknown process (#{status.inspect})"
|
42
|
+
elsif worker.mold?
|
43
|
+
"mold pid=#{worker.pid rescue 'unknown'} gen=#{worker.generation rescue 'unknown'} reaped (#{status.inspect})"
|
44
|
+
else
|
45
|
+
"worker=#{worker.nr rescue 'unknown'} pid=#{worker.pid rescue 'unknown'} gen=#{worker.generation rescue 'unknown'} reaped (#{status.inspect})"
|
46
|
+
end
|
47
|
+
if status.success?
|
48
|
+
server.logger.info(m)
|
49
|
+
else
|
50
|
+
server.logger.error(m)
|
51
|
+
end
|
52
|
+
},
|
53
|
+
:after_worker_ready => lambda { |server, worker|
|
54
|
+
server.logger.info("worker=#{worker.nr} ready")
|
55
|
+
},
|
56
|
+
:early_hints => false,
|
57
|
+
:mold_selector => MoldSelector::LeastSharedMemory.new,
|
58
|
+
:refork_condition => nil,
|
59
|
+
:check_client_connection => false,
|
60
|
+
:rewindable_input => true,
|
61
|
+
:client_body_buffer_size => Pitchfork::Const::MAX_BODY,
|
62
|
+
}
|
63
|
+
#:startdoc:
|
64
|
+
|
65
|
+
def initialize(defaults = {}) #:nodoc:
|
66
|
+
self.set = Hash.new(:unset)
|
67
|
+
@use_defaults = defaults.delete(:use_defaults)
|
68
|
+
self.config_file = defaults.delete(:config_file)
|
69
|
+
|
70
|
+
set.merge!(DEFAULTS) if @use_defaults
|
71
|
+
defaults.each { |key, value| self.__send__(key, value) }
|
72
|
+
Hash === set[:listener_opts] or
|
73
|
+
set[:listener_opts] = Hash.new { |hash,key| hash[key] = {} }
|
74
|
+
Array === set[:listeners] or set[:listeners] = []
|
75
|
+
load(false)
|
76
|
+
end
|
77
|
+
|
78
|
+
def load(merge_defaults = true) #:nodoc:
|
79
|
+
if merge_defaults && @use_defaults
|
80
|
+
set.merge!(DEFAULTS) if @use_defaults
|
81
|
+
end
|
82
|
+
instance_eval(File.read(config_file), config_file) if config_file
|
83
|
+
|
84
|
+
parse_rackup_file
|
85
|
+
|
86
|
+
RACKUP[:set_listener] and
|
87
|
+
set[:listeners] << "#{RACKUP[:host]}:#{RACKUP[:port]}"
|
88
|
+
|
89
|
+
RACKUP[:no_default_middleware] and
|
90
|
+
set[:default_middleware] = false
|
91
|
+
end
|
92
|
+
|
93
|
+
def commit!(server, options = {}) #:nodoc:
|
94
|
+
skip = options[:skip] || []
|
95
|
+
if ready_pipe = RACKUP.delete(:ready_pipe)
|
96
|
+
server.ready_pipe = ready_pipe
|
97
|
+
end
|
98
|
+
if set[:check_client_connection]
|
99
|
+
set[:listeners].each do |address|
|
100
|
+
if set[:listener_opts][address][:tcp_nopush] == true
|
101
|
+
raise ArgumentError,
|
102
|
+
"check_client_connection is incompatible with tcp_nopush:true"
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
set.each do |key, value|
|
107
|
+
value == :unset and next
|
108
|
+
skip.include?(key) and next
|
109
|
+
server.__send__("#{key}=", value)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def [](key) # :nodoc:
|
114
|
+
set[key]
|
115
|
+
end
|
116
|
+
|
117
|
+
def logger(obj)
|
118
|
+
%w(debug info warn error fatal).each do |m|
|
119
|
+
obj.respond_to?(m) and next
|
120
|
+
raise ArgumentError, "logger=#{obj} does not respond to method=#{m}"
|
121
|
+
end
|
122
|
+
|
123
|
+
set[:logger] = obj
|
124
|
+
end
|
125
|
+
|
126
|
+
def before_fork(*args, &block)
|
127
|
+
set_hook(:before_fork, block_given? ? block : args[0])
|
128
|
+
end
|
129
|
+
|
130
|
+
def after_fork(*args, &block)
|
131
|
+
set_hook(:after_fork, block_given? ? block : args[0])
|
132
|
+
end
|
133
|
+
|
134
|
+
def after_worker_ready(*args, &block)
|
135
|
+
set_hook(:after_worker_ready, block_given? ? block : args[0])
|
136
|
+
end
|
137
|
+
|
138
|
+
def after_worker_exit(*args, &block)
|
139
|
+
set_hook(:after_worker_exit, block_given? ? block : args[0], 3)
|
140
|
+
end
|
141
|
+
|
142
|
+
def mold_selector(*args, &block)
|
143
|
+
set_hook(:mold_selector, block_given? ? block : args[0], 3)
|
144
|
+
end
|
145
|
+
|
146
|
+
def timeout(seconds)
|
147
|
+
set_int(:timeout, seconds, 3)
|
148
|
+
# POSIX says 31 days is the smallest allowed maximum timeout for select()
|
149
|
+
max = 30 * 60 * 60 * 24
|
150
|
+
set[:timeout] = seconds > max ? max : seconds
|
151
|
+
end
|
152
|
+
|
153
|
+
def worker_processes(nr)
|
154
|
+
set_int(:worker_processes, nr, 1)
|
155
|
+
end
|
156
|
+
|
157
|
+
def default_middleware(bool)
|
158
|
+
set_bool(:default_middleware, bool)
|
159
|
+
end
|
160
|
+
|
161
|
+
def early_hints(bool)
|
162
|
+
set_bool(:early_hints, bool)
|
163
|
+
end
|
164
|
+
|
165
|
+
# sets listeners to the given +addresses+, replacing or augmenting the
|
166
|
+
# current set.
|
167
|
+
def listeners(addresses) # :nodoc:
|
168
|
+
Array === addresses or addresses = Array(addresses)
|
169
|
+
addresses.map! { |addr| expand_addr(addr) }
|
170
|
+
set[:listeners] = addresses
|
171
|
+
end
|
172
|
+
|
173
|
+
def listen(address, options = {})
|
174
|
+
address = expand_addr(address)
|
175
|
+
if String === address
|
176
|
+
[ :umask, :backlog, :sndbuf, :rcvbuf, :tries ].each do |key|
|
177
|
+
value = options[key] or next
|
178
|
+
Integer === value or
|
179
|
+
raise ArgumentError, "not an integer: #{key}=#{value.inspect}"
|
180
|
+
end
|
181
|
+
[ :tcp_nodelay, :tcp_nopush, :ipv6only, :reuseport ].each do |key|
|
182
|
+
(value = options[key]).nil? and next
|
183
|
+
TrueClass === value || FalseClass === value or
|
184
|
+
raise ArgumentError, "not boolean: #{key}=#{value.inspect}"
|
185
|
+
end
|
186
|
+
unless (value = options[:delay]).nil?
|
187
|
+
Numeric === value or
|
188
|
+
raise ArgumentError, "not numeric: delay=#{value.inspect}"
|
189
|
+
end
|
190
|
+
set[:listener_opts][address].merge!(options)
|
191
|
+
end
|
192
|
+
|
193
|
+
set[:listeners] << address
|
194
|
+
end
|
195
|
+
|
196
|
+
def rewindable_input(bool)
|
197
|
+
set_bool(:rewindable_input, bool)
|
198
|
+
end
|
199
|
+
|
200
|
+
def client_body_buffer_size(bytes)
|
201
|
+
set_int(:client_body_buffer_size, bytes, 0)
|
202
|
+
end
|
203
|
+
|
204
|
+
def check_client_connection(bool)
|
205
|
+
set_bool(:check_client_connection, bool)
|
206
|
+
end
|
207
|
+
|
208
|
+
# Defines the number of requests per-worker after which a new generation
|
209
|
+
# should be spawned.
|
210
|
+
#
|
211
|
+
# example:
|
212
|
+
#. refork_after [50, 100, 1000]
|
213
|
+
#
|
214
|
+
# Note that reforking is only available on Linux. Other Unix-like systems
|
215
|
+
# don't have this capability.
|
216
|
+
def refork_after(limits)
|
217
|
+
set[:refork_condition] = ReforkCondition::RequestsCount.new(limits)
|
218
|
+
end
|
219
|
+
|
220
|
+
# expands "unix:path/to/foo" to a socket relative to the current path
|
221
|
+
# expands pathnames of sockets if relative to "~" or "~username"
|
222
|
+
# expands "*:port and ":port" to "0.0.0.0:port"
|
223
|
+
def expand_addr(address) #:nodoc:
|
224
|
+
return "0.0.0.0:#{address}" if Integer === address
|
225
|
+
return address unless String === address
|
226
|
+
|
227
|
+
case address
|
228
|
+
when %r{\Aunix:(.*)\z}
|
229
|
+
File.expand_path($1)
|
230
|
+
when %r{\A~}
|
231
|
+
File.expand_path(address)
|
232
|
+
when %r{\A(?:\*:)?(\d+)\z}
|
233
|
+
"0.0.0.0:#$1"
|
234
|
+
when %r{\A\[([a-fA-F0-9:]+)\]\z}, %r/\A((?:\d+\.){3}\d+)\z/
|
235
|
+
canonicalize_tcp($1, 80)
|
236
|
+
when %r{\A\[([a-fA-F0-9:]+)\]:(\d+)\z}, %r{\A(.*):(\d+)\z}
|
237
|
+
canonicalize_tcp($1, $2.to_i)
|
238
|
+
else
|
239
|
+
address
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
private
|
244
|
+
def set_int(var, n, min) #:nodoc:
|
245
|
+
Integer === n or raise ArgumentError, "not an integer: #{var}=#{n.inspect}"
|
246
|
+
n >= min or raise ArgumentError, "too low (< #{min}): #{var}=#{n.inspect}"
|
247
|
+
set[var] = n
|
248
|
+
end
|
249
|
+
|
250
|
+
def canonicalize_tcp(addr, port)
|
251
|
+
packed = Socket.pack_sockaddr_in(port, addr)
|
252
|
+
port, addr = Socket.unpack_sockaddr_in(packed)
|
253
|
+
addr.include?(':') ? "[#{addr}]:#{port}" : "#{addr}:#{port}"
|
254
|
+
end
|
255
|
+
|
256
|
+
def set_path(var, path) #:nodoc:
|
257
|
+
case path
|
258
|
+
when NilClass, String
|
259
|
+
set[var] = path
|
260
|
+
else
|
261
|
+
raise ArgumentError
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
def check_bool(var, bool) # :nodoc:
|
266
|
+
case bool
|
267
|
+
when true, false
|
268
|
+
return bool
|
269
|
+
end
|
270
|
+
raise ArgumentError, "#{var}=#{bool.inspect} not a boolean"
|
271
|
+
end
|
272
|
+
|
273
|
+
def set_bool(var, bool) #:nodoc:
|
274
|
+
set[var] = check_bool(var, bool)
|
275
|
+
end
|
276
|
+
|
277
|
+
def set_hook(var, my_proc, req_arity = 2) #:nodoc:
|
278
|
+
case my_proc
|
279
|
+
when Proc
|
280
|
+
arity = my_proc.arity
|
281
|
+
(arity == req_arity) or \
|
282
|
+
raise ArgumentError,
|
283
|
+
"#{var}=#{my_proc.inspect} has invalid arity: " \
|
284
|
+
"#{arity} (need #{req_arity})"
|
285
|
+
when NilClass
|
286
|
+
my_proc = DEFAULTS[var]
|
287
|
+
else
|
288
|
+
raise ArgumentError, "invalid type: #{var}=#{my_proc.inspect}"
|
289
|
+
end
|
290
|
+
set[var] = my_proc
|
291
|
+
end
|
292
|
+
|
293
|
+
# This only parses the embedded switches in .ru files
|
294
|
+
# (for "rackup" compatibility)
|
295
|
+
def parse_rackup_file # :nodoc:
|
296
|
+
ru = RACKUP[:file] or return # we only return here in unit tests
|
297
|
+
|
298
|
+
# :rails means use (old) Rails autodetect
|
299
|
+
if ru == :rails
|
300
|
+
File.readable?('config.ru') or return
|
301
|
+
ru = 'config.ru'
|
302
|
+
end
|
303
|
+
|
304
|
+
File.readable?(ru) or
|
305
|
+
raise ArgumentError, "rackup file (#{ru}) not readable"
|
306
|
+
|
307
|
+
# it could be a .rb file, too, we don't parse those manually
|
308
|
+
ru.end_with?('.ru') or return
|
309
|
+
|
310
|
+
/^#\\(.*)/ =~ File.read(ru) or return
|
311
|
+
RACKUP[:optparse].parse!($1.split(/\s+/))
|
312
|
+
end
|
313
|
+
end
|
314
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# -*- encoding: binary -*-
|
2
|
+
|
3
|
+
module Pitchfork
|
4
|
+
module Const # :nodoc:
|
5
|
+
# default TCP listen host address (0.0.0.0, all interfaces)
|
6
|
+
DEFAULT_HOST = "0.0.0.0"
|
7
|
+
|
8
|
+
# default TCP listen port (8080)
|
9
|
+
DEFAULT_PORT = 8080
|
10
|
+
|
11
|
+
# default TCP listen address and port (0.0.0.0:8080)
|
12
|
+
DEFAULT_LISTEN = "#{DEFAULT_HOST}:#{DEFAULT_PORT}"
|
13
|
+
|
14
|
+
# The basic request body size we'll try to read at once (16 kilobytes).
|
15
|
+
CHUNK_SIZE = 16 * 1024
|
16
|
+
|
17
|
+
# Maximum request body size before it is moved out of memory and into a
|
18
|
+
# temporary file for reading (112 kilobytes). This is the default
|
19
|
+
# value of client_body_buffer_size.
|
20
|
+
MAX_BODY = 1024 * 112
|
21
|
+
end
|
22
|
+
end
|
23
|
+
require_relative 'version'
|