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,133 @@
|
|
1
|
+
# -*- encoding: binary -*-
|
2
|
+
|
3
|
+
module Pitchfork
|
4
|
+
# Acts like tee(1) on an input input to provide a input-like stream
|
5
|
+
# while providing rewindable semantics through a File/StringIO backing
|
6
|
+
# store. On the first pass, the input is only read on demand so your
|
7
|
+
# Rack application can use input notification (upload progress and
|
8
|
+
# like). This should fully conform to the Rack::Lint::InputWrapper
|
9
|
+
# specification on the public API. This class is intended to be a
|
10
|
+
# strict interpretation of Rack::Lint::InputWrapper functionality and
|
11
|
+
# will not support any deviations from it.
|
12
|
+
#
|
13
|
+
# When processing uploads, pitchfork exposes a TeeInput object under
|
14
|
+
# "rack.input" of the Rack environment by default.
|
15
|
+
class TeeInput < StreamInput
|
16
|
+
# The maximum size (in +bytes+) to buffer in memory before
|
17
|
+
# resorting to a temporary file. Default is 112 kilobytes.
|
18
|
+
@@client_body_buffer_size = Pitchfork::Const::MAX_BODY # :nodoc:
|
19
|
+
|
20
|
+
# sets the maximum size of request bodies to buffer in memory,
|
21
|
+
# amounts larger than this are buffered to the filesystem
|
22
|
+
def self.client_body_buffer_size=(bytes) # :nodoc:
|
23
|
+
@@client_body_buffer_size = bytes
|
24
|
+
end
|
25
|
+
|
26
|
+
# returns the maximum size of request bodies to buffer in memory,
|
27
|
+
# amounts larger than this are buffered to the filesystem
|
28
|
+
def self.client_body_buffer_size # :nodoc:
|
29
|
+
@@client_body_buffer_size
|
30
|
+
end
|
31
|
+
|
32
|
+
# for Rack::TempfileReaper in rack 1.6+
|
33
|
+
def new_tmpio # :nodoc:
|
34
|
+
tmpio = Pitchfork::TmpIO.new
|
35
|
+
(@parser.env['rack.tempfiles'] ||= []) << tmpio
|
36
|
+
tmpio
|
37
|
+
end
|
38
|
+
|
39
|
+
# Initializes a new TeeInput object. You normally do not have to call
|
40
|
+
# this unless you are writing an HTTP server.
|
41
|
+
def initialize(socket, request) # :nodoc:
|
42
|
+
@len = request.content_length
|
43
|
+
super
|
44
|
+
@tmp = @len && @len <= @@client_body_buffer_size ?
|
45
|
+
StringIO.new("") : new_tmpio
|
46
|
+
end
|
47
|
+
|
48
|
+
# :call-seq:
|
49
|
+
# ios.size => Integer
|
50
|
+
#
|
51
|
+
# Returns the size of the input. For requests with a Content-Length
|
52
|
+
# header value, this will not read data off the socket and just return
|
53
|
+
# the value of the Content-Length header as an Integer.
|
54
|
+
#
|
55
|
+
# For Transfer-Encoding:chunked requests, this requires consuming
|
56
|
+
# all of the input stream before returning since there's no other
|
57
|
+
# way to determine the size of the request body beforehand.
|
58
|
+
#
|
59
|
+
# This method is no longer part of the Rack specification as of
|
60
|
+
# Rack 1.2, so its use is not recommended. This method only exists
|
61
|
+
# for compatibility with Rack applications designed for Rack 1.1 and
|
62
|
+
# earlier. Most applications should only need to call +read+ with a
|
63
|
+
# specified +length+ in a loop until it returns +nil+.
|
64
|
+
def size
|
65
|
+
@len and return @len
|
66
|
+
pos = @tmp.pos
|
67
|
+
consume!
|
68
|
+
@tmp.pos = pos
|
69
|
+
@len = @tmp.size
|
70
|
+
end
|
71
|
+
|
72
|
+
# :call-seq:
|
73
|
+
# ios.read([length [, buffer ]]) => string, buffer, or nil
|
74
|
+
#
|
75
|
+
# Reads at most length bytes from the I/O stream, or to the end of
|
76
|
+
# file if length is omitted or is nil. length must be a non-negative
|
77
|
+
# integer or nil. If the optional buffer argument is present, it
|
78
|
+
# must reference a String, which will receive the data.
|
79
|
+
#
|
80
|
+
# At end of file, it returns nil or "" depend on length.
|
81
|
+
# ios.read() and ios.read(nil) returns "".
|
82
|
+
# ios.read(length [, buffer]) returns nil.
|
83
|
+
#
|
84
|
+
# If the Content-Length of the HTTP request is known (as is the common
|
85
|
+
# case for POST requests), then ios.read(length [, buffer]) will block
|
86
|
+
# until the specified length is read (or it is the last chunk).
|
87
|
+
# Otherwise, for uncommon "Transfer-Encoding: chunked" requests,
|
88
|
+
# ios.read(length [, buffer]) will return immediately if there is
|
89
|
+
# any data and only block when nothing is available (providing
|
90
|
+
# IO#readpartial semantics).
|
91
|
+
def read(*args)
|
92
|
+
@socket ? tee(super) : @tmp.read(*args)
|
93
|
+
end
|
94
|
+
|
95
|
+
# :call-seq:
|
96
|
+
# ios.gets => string or nil
|
97
|
+
#
|
98
|
+
# Reads the next ``line'' from the I/O stream; lines are separated
|
99
|
+
# by the global record separator ($/, typically "\n"). A global
|
100
|
+
# record separator of nil reads the entire unread contents of ios.
|
101
|
+
# Returns nil if called at the end of file.
|
102
|
+
# This takes zero arguments for strict Rack::Lint compatibility,
|
103
|
+
# unlike IO#gets.
|
104
|
+
def gets
|
105
|
+
@socket ? tee(super) : @tmp.gets
|
106
|
+
end
|
107
|
+
|
108
|
+
# :call-seq:
|
109
|
+
# ios.rewind => 0
|
110
|
+
#
|
111
|
+
# Positions the *ios* pointer to the beginning of input, returns
|
112
|
+
# the offset (zero) of the +ios+ pointer. Subsequent reads will
|
113
|
+
# start from the beginning of the previously-buffered input.
|
114
|
+
def rewind
|
115
|
+
return 0 if 0 == @tmp.size
|
116
|
+
consume! if @socket
|
117
|
+
@tmp.rewind # Rack does not specify what the return value is here
|
118
|
+
end
|
119
|
+
|
120
|
+
private
|
121
|
+
|
122
|
+
# consumes the stream of the socket
|
123
|
+
def consume!
|
124
|
+
junk = ""
|
125
|
+
nil while read(@@io_chunk_size, junk)
|
126
|
+
end
|
127
|
+
|
128
|
+
def tee(buffer)
|
129
|
+
@tmp.write(buffer) if buffer
|
130
|
+
buffer
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# -*- encoding: binary -*-
|
2
|
+
# :stopdoc:
|
3
|
+
require 'tmpdir'
|
4
|
+
|
5
|
+
module Pitchfork
|
6
|
+
# some versions of Ruby had a broken Tempfile which didn't work
|
7
|
+
# well with unlinked files. This one is much shorter, easier
|
8
|
+
# to understand, and slightly faster.
|
9
|
+
class TmpIO < File
|
10
|
+
|
11
|
+
# creates and returns a new File object. The File is unlinked
|
12
|
+
# immediately, switched to binary mode, and userspace output
|
13
|
+
# buffering is disabled
|
14
|
+
def self.new
|
15
|
+
path = nil
|
16
|
+
|
17
|
+
# workaround File#path being tainted:
|
18
|
+
# https://bugs.ruby-lang.org/issues/14485
|
19
|
+
fp = begin
|
20
|
+
path = "#{Dir::tmpdir}/#{rand}"
|
21
|
+
super(path, RDWR|CREAT|EXCL, 0600)
|
22
|
+
rescue Errno::EEXIST
|
23
|
+
retry
|
24
|
+
end
|
25
|
+
|
26
|
+
unlink(path)
|
27
|
+
fp.binmode
|
28
|
+
fp.sync = true
|
29
|
+
fp
|
30
|
+
end
|
31
|
+
|
32
|
+
# pretend we're Tempfile for Rack::TempfileReaper
|
33
|
+
alias close! close
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,244 @@
|
|
1
|
+
# -*- encoding: binary -*-
|
2
|
+
require "raindrops"
|
3
|
+
|
4
|
+
module Pitchfork
|
5
|
+
# This class and its members can be considered a stable interface
|
6
|
+
# and will not change in a backwards-incompatible fashion between
|
7
|
+
# releases of pitchfork. Knowledge of this class is generally not
|
8
|
+
# not needed for most users of pitchfork.
|
9
|
+
#
|
10
|
+
# Some users may want to access it in the before_fork/after_fork hooks.
|
11
|
+
# See the Pitchfork::Configurator RDoc for examples.
|
12
|
+
class Worker
|
13
|
+
# :stopdoc:
|
14
|
+
EXIT_SIGNALS = [:QUIT, :TERM]
|
15
|
+
@generation = 0
|
16
|
+
attr_accessor :nr, :pid, :generation
|
17
|
+
attr_reader :master
|
18
|
+
|
19
|
+
def initialize(nr, pid: nil, generation: 0)
|
20
|
+
@nr = nr
|
21
|
+
@pid = pid
|
22
|
+
@generation = generation
|
23
|
+
@mold = false
|
24
|
+
@to_io = @master = nil
|
25
|
+
@exiting = false
|
26
|
+
if nr
|
27
|
+
build_raindrops(nr)
|
28
|
+
else
|
29
|
+
promoted!
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def meminfo
|
34
|
+
@meminfo ||= MemInfo.new(pid) if pid
|
35
|
+
end
|
36
|
+
|
37
|
+
def refresh
|
38
|
+
meminfo&.update
|
39
|
+
end
|
40
|
+
|
41
|
+
def exiting?
|
42
|
+
@exiting
|
43
|
+
end
|
44
|
+
|
45
|
+
def update(message)
|
46
|
+
message.class.members.each do |member|
|
47
|
+
send("#{member}=", message.public_send(member))
|
48
|
+
end
|
49
|
+
|
50
|
+
case message
|
51
|
+
when Message::WorkerPromoted, Message::PromoteWorker
|
52
|
+
promoted!
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def register_to_master(control_socket)
|
57
|
+
create_socketpair!
|
58
|
+
message = Message::WorkerSpawned.new(@nr, Process.pid, generation, @master)
|
59
|
+
control_socket.sendmsg(message)
|
60
|
+
@master.close
|
61
|
+
end
|
62
|
+
|
63
|
+
def acknowlege_promotion(control_socket)
|
64
|
+
message = Message::WorkerPromoted.new(@nr, Process.pid, generation)
|
65
|
+
control_socket.sendmsg(message)
|
66
|
+
end
|
67
|
+
|
68
|
+
def promote(generation)
|
69
|
+
send_message_nonblock(Message::PromoteWorker.new(generation))
|
70
|
+
end
|
71
|
+
|
72
|
+
def spawn_worker(new_worker)
|
73
|
+
send_message_nonblock(Message::SpawnWorker.new(new_worker.nr))
|
74
|
+
end
|
75
|
+
|
76
|
+
def promoted!
|
77
|
+
@mold = true
|
78
|
+
@nr = nil
|
79
|
+
@drop_offset = 0
|
80
|
+
@tick_drop = MOLD_DROP
|
81
|
+
end
|
82
|
+
|
83
|
+
def mold?
|
84
|
+
@mold
|
85
|
+
end
|
86
|
+
|
87
|
+
def to_io # IO.select-compatible
|
88
|
+
@to_io.to_io
|
89
|
+
end
|
90
|
+
|
91
|
+
# master fakes SIGQUIT using this
|
92
|
+
def quit # :nodoc:
|
93
|
+
@master = @master.close if @master
|
94
|
+
end
|
95
|
+
|
96
|
+
def master=(socket)
|
97
|
+
@master = MessageSocket.new(socket)
|
98
|
+
end
|
99
|
+
|
100
|
+
# call a signal handler immediately without triggering EINTR
|
101
|
+
# We do not use the more obvious Process.kill(sig, $$) here since
|
102
|
+
# that signal delivery may be deferred. We want to avoid signal delivery
|
103
|
+
# while the Rack app.call is running because some database drivers
|
104
|
+
# (e.g. ruby-pg) may cancel pending requests.
|
105
|
+
def fake_sig(sig) # :nodoc:
|
106
|
+
old_cb = trap(sig, "IGNORE")
|
107
|
+
old_cb.call
|
108
|
+
ensure
|
109
|
+
trap(sig, old_cb)
|
110
|
+
end
|
111
|
+
|
112
|
+
# master sends fake signals to children
|
113
|
+
def soft_kill(sig) # :nodoc:
|
114
|
+
signum = Signal.list[sig.to_s] or raise ArgumentError, "BUG: bad signal: #{sig.inspect}"
|
115
|
+
|
116
|
+
# Do not care in the odd case the buffer is full, here.
|
117
|
+
success = send_message_nonblock(Message::SoftKill.new(signum))
|
118
|
+
if success && EXIT_SIGNALS.include?(sig)
|
119
|
+
@exiting = true
|
120
|
+
end
|
121
|
+
success
|
122
|
+
end
|
123
|
+
|
124
|
+
# this only runs when the Rack app.call is not running
|
125
|
+
# act like a listener
|
126
|
+
def accept_nonblock(exception: nil) # :nodoc:
|
127
|
+
loop do
|
128
|
+
case buf = @to_io.recvmsg_nonblock(exception: false)
|
129
|
+
when :wait_readable # keep waiting
|
130
|
+
return false
|
131
|
+
when nil # EOF master died, but we are at a safe place to exit
|
132
|
+
fake_sig(:QUIT)
|
133
|
+
return false
|
134
|
+
when Message::SoftKill
|
135
|
+
# trigger the signal handler
|
136
|
+
fake_sig(buf.signum)
|
137
|
+
# keep looping, more signals may be queued
|
138
|
+
when Message
|
139
|
+
return buf
|
140
|
+
else
|
141
|
+
raise TypeError, "Unexpected recvmsg_nonblock returns: #{buf.inspect}"
|
142
|
+
end
|
143
|
+
end # loop, as multiple signals may be sent
|
144
|
+
rescue Errno::ECONNRESET
|
145
|
+
nil
|
146
|
+
end
|
147
|
+
|
148
|
+
# worker objects may be compared to just plain Integers
|
149
|
+
def ==(other) # :nodoc:
|
150
|
+
super || (!@nr.nil? && @nr == other)
|
151
|
+
end
|
152
|
+
|
153
|
+
# called in the worker process
|
154
|
+
def tick=(value) # :nodoc:
|
155
|
+
if mold?
|
156
|
+
MOLD_DROP[0] = value
|
157
|
+
else
|
158
|
+
@tick_drop[@drop_offset] = value
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
# called in the master process
|
163
|
+
def tick # :nodoc:
|
164
|
+
if mold?
|
165
|
+
MOLD_DROP[0]
|
166
|
+
else
|
167
|
+
@tick_drop[@drop_offset]
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
def reset
|
172
|
+
@requests_drop[@drop_offset] = 0
|
173
|
+
end
|
174
|
+
|
175
|
+
def requests_count
|
176
|
+
@requests_drop[@drop_offset]
|
177
|
+
end
|
178
|
+
|
179
|
+
def increment_requests_count
|
180
|
+
@requests_drop.incr(@drop_offset)
|
181
|
+
end
|
182
|
+
|
183
|
+
# called in both the master (reaping worker) and worker (SIGQUIT handler)
|
184
|
+
def close # :nodoc:
|
185
|
+
@master.close if @master
|
186
|
+
@to_io.close if @to_io
|
187
|
+
end
|
188
|
+
|
189
|
+
def create_socketpair!
|
190
|
+
@to_io, @master = Pitchfork.socketpair
|
191
|
+
end
|
192
|
+
|
193
|
+
def after_fork_in_child
|
194
|
+
@master.close
|
195
|
+
end
|
196
|
+
|
197
|
+
private
|
198
|
+
|
199
|
+
def pipe=(socket)
|
200
|
+
@master = MessageSocket.new(socket)
|
201
|
+
end
|
202
|
+
|
203
|
+
def send_message_nonblock(message)
|
204
|
+
success = false
|
205
|
+
begin
|
206
|
+
case @master.sendmsg_nonblock(message, exception: false)
|
207
|
+
when :wait_writable
|
208
|
+
else
|
209
|
+
success = true
|
210
|
+
end
|
211
|
+
rescue Errno::EPIPE
|
212
|
+
# worker will be reaped soon
|
213
|
+
end
|
214
|
+
success
|
215
|
+
end
|
216
|
+
|
217
|
+
MOLD_DROP = Raindrops.new(1)
|
218
|
+
PER_DROP = Raindrops::PAGE_SIZE / Raindrops::SIZE
|
219
|
+
TICK_DROPS = []
|
220
|
+
REQUEST_DROPS = []
|
221
|
+
|
222
|
+
class << self
|
223
|
+
# Since workers are created from another process, we have to
|
224
|
+
# pre-allocate the drops so they are shared between everyone.
|
225
|
+
#
|
226
|
+
# However this doesn't account for TTIN signals that increase the
|
227
|
+
# number of workers, but we should probably remove that feature too.
|
228
|
+
def preallocate_drops(workers_count)
|
229
|
+
0.upto(workers_count / PER_DROP) do |i|
|
230
|
+
TICK_DROPS[i] = Raindrops.new(PER_DROP)
|
231
|
+
REQUEST_DROPS[i] = Raindrops.new(PER_DROP)
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
def build_raindrops(drop_nr)
|
237
|
+
drop_index = drop_nr / PER_DROP
|
238
|
+
@drop_offset = drop_nr % PER_DROP
|
239
|
+
@tick_drop = TICK_DROPS[drop_index] ||= Raindrops.new(PER_DROP)
|
240
|
+
@requests_drop = REQUEST_DROPS[drop_index] ||= Raindrops.new(PER_DROP)
|
241
|
+
@tick_drop[@drop_offset] = @requests_drop[@drop_offset] = 0
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
data/lib/pitchfork.rb
ADDED
@@ -0,0 +1,158 @@
|
|
1
|
+
# -*- encoding: binary -*-
|
2
|
+
require 'etc'
|
3
|
+
require 'stringio'
|
4
|
+
require 'raindrops'
|
5
|
+
require 'io/wait'
|
6
|
+
|
7
|
+
begin
|
8
|
+
require 'rack'
|
9
|
+
rescue LoadError
|
10
|
+
warn 'rack not available, functionality reduced'
|
11
|
+
end
|
12
|
+
|
13
|
+
# :stopdoc:
|
14
|
+
# Pitchfork module containing all of the classes (include C extensions) for
|
15
|
+
# running a Pitchfork web server. It contains a minimalist HTTP server with just
|
16
|
+
# enough functionality to service web application requests fast as possible.
|
17
|
+
# :startdoc:
|
18
|
+
|
19
|
+
# pitchfork exposes very little of an user-visible API and most of its
|
20
|
+
# internals are subject to change. pitchfork is designed to host Rack
|
21
|
+
# applications, so applications should be written against the Rack SPEC
|
22
|
+
# and not pitchfork internals.
|
23
|
+
module Pitchfork
|
24
|
+
|
25
|
+
# Raised inside TeeInput when a client closes the socket inside the
|
26
|
+
# application dispatch. This is always raised with an empty backtrace
|
27
|
+
# since there is nothing in the application stack that is responsible
|
28
|
+
# for client shutdowns/disconnects. This exception is visible to Rack
|
29
|
+
# applications unless PrereadInput middleware is loaded. This
|
30
|
+
# is a subclass of the standard EOFError class and applications should
|
31
|
+
# not rescue it explicitly, but rescue EOFError instead.
|
32
|
+
ClientShutdown = Class.new(EOFError)
|
33
|
+
|
34
|
+
BootFailure = Class.new(StandardError)
|
35
|
+
|
36
|
+
# :stopdoc:
|
37
|
+
|
38
|
+
# This returns a lambda to pass in as the app, this does not "build" the
|
39
|
+
# app The returned lambda will be called when it is
|
40
|
+
# time to build the app.
|
41
|
+
def self.builder(ru, op)
|
42
|
+
# allow Configurator to parse cli switches embedded in the ru file
|
43
|
+
op = Pitchfork::Configurator::RACKUP.merge!(:file => ru, :optparse => op)
|
44
|
+
if ru =~ /\.ru$/ && !defined?(Rack::Builder)
|
45
|
+
abort "rack and Rack::Builder must be available for processing #{ru}"
|
46
|
+
end
|
47
|
+
|
48
|
+
# always called after config file parsing, may be called after forking
|
49
|
+
lambda do |_, server|
|
50
|
+
inner_app = case ru
|
51
|
+
when /\.ru$/
|
52
|
+
raw = File.read(ru)
|
53
|
+
raw.sub!(/^__END__\n.*/, '')
|
54
|
+
eval("Rack::Builder.new {(\n#{raw}\n)}.to_app", TOPLEVEL_BINDING, ru)
|
55
|
+
else
|
56
|
+
require ru
|
57
|
+
Object.const_get(File.basename(ru, '.rb').capitalize)
|
58
|
+
end
|
59
|
+
|
60
|
+
if $DEBUG
|
61
|
+
require 'pp'
|
62
|
+
pp({ :inner_app => inner_app })
|
63
|
+
end
|
64
|
+
|
65
|
+
return inner_app unless server.default_middleware
|
66
|
+
|
67
|
+
middleware = { # order matters
|
68
|
+
ContentLength: nil,
|
69
|
+
Chunked: nil,
|
70
|
+
CommonLogger: [ $stderr ],
|
71
|
+
ShowExceptions: nil,
|
72
|
+
Lint: nil,
|
73
|
+
TempfileReaper: nil,
|
74
|
+
}
|
75
|
+
|
76
|
+
# return value, matches rackup defaults based on env
|
77
|
+
# Pitchfork does not support persistent connections, but Rainbows!
|
78
|
+
# and Zbatery both do. Users accustomed to the Rack::Server default
|
79
|
+
# middlewares will need ContentLength/Chunked middlewares.
|
80
|
+
case ENV["RACK_ENV"]
|
81
|
+
when "development"
|
82
|
+
when "deployment"
|
83
|
+
middleware.delete(:ShowExceptions)
|
84
|
+
middleware.delete(:Lint)
|
85
|
+
else
|
86
|
+
return inner_app
|
87
|
+
end
|
88
|
+
Rack::Builder.new do
|
89
|
+
middleware.each do |m, args|
|
90
|
+
use(Rack.const_get(m), *args) if Rack.const_defined?(m)
|
91
|
+
end
|
92
|
+
run inner_app
|
93
|
+
end.to_app
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# returns an array of strings representing TCP listen socket addresses
|
98
|
+
# and Unix domain socket paths. This is useful for use with
|
99
|
+
# Raindrops::Middleware under Linux: https://yhbt.net/raindrops/
|
100
|
+
def self.listener_names
|
101
|
+
Pitchfork::HttpServer::LISTENERS.map do |io|
|
102
|
+
Pitchfork::SocketHelper.sock_name(io)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def self.log_error(logger, prefix, exc)
|
107
|
+
message = exc.message
|
108
|
+
message = message.dump if /[[:cntrl:]]/ =~ message
|
109
|
+
logger.error "#{prefix}: #{message} (#{exc.class})"
|
110
|
+
exc.backtrace.each { |line| logger.error(line) }
|
111
|
+
end
|
112
|
+
|
113
|
+
F_SETPIPE_SZ = 1031 if RUBY_PLATFORM =~ /linux/
|
114
|
+
|
115
|
+
def self.pipe # :nodoc:
|
116
|
+
IO.pipe.each do |io|
|
117
|
+
# shrink pipes to minimize impact on /proc/sys/fs/pipe-user-pages-soft
|
118
|
+
# limits.
|
119
|
+
if defined?(F_SETPIPE_SZ)
|
120
|
+
begin
|
121
|
+
io.fcntl(F_SETPIPE_SZ, Raindrops::PAGE_SIZE)
|
122
|
+
rescue Errno::EINVAL
|
123
|
+
# old kernel
|
124
|
+
rescue Errno::EPERM
|
125
|
+
# resizes fail if Linux is close to the pipe limit for the user
|
126
|
+
# or if the user does not have permissions to resize
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
@socket_type = :SOCK_SEQPACKET
|
133
|
+
def self.socketpair
|
134
|
+
pair = UNIXSocket.socketpair(@socket_type).map { |s| MessageSocket.new(s) }
|
135
|
+
pair[0].close_write
|
136
|
+
pair[1].close_read
|
137
|
+
pair
|
138
|
+
rescue Errno::EPROTONOSUPPORT
|
139
|
+
if @socket_type == :SOCK_SEQPACKET
|
140
|
+
# macOS and very old linuxes don't support SOCK_SEQPACKET (SCTP).
|
141
|
+
# In such case we can fallback to SOCK_STREAM (TCP)
|
142
|
+
warn("SEQPACKET (SCTP) isn't supported, falling back to STREAM")
|
143
|
+
@socket_type = :SOCK_STREAM
|
144
|
+
retry
|
145
|
+
else
|
146
|
+
raise
|
147
|
+
end
|
148
|
+
end
|
149
|
+
# :startdoc:
|
150
|
+
end
|
151
|
+
# :enddoc:
|
152
|
+
|
153
|
+
%w(
|
154
|
+
const socket_helper stream_input tee_input mem_info children message http_parser
|
155
|
+
refork_condition mold_selector configurator tmpio http_response worker http_server
|
156
|
+
).each do |s|
|
157
|
+
require_relative "pitchfork/#{s}"
|
158
|
+
end
|
data/pitchfork.gemspec
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "lib/pitchfork/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = %q{pitchfork}
|
7
|
+
s.version = Pitchfork::VERSION
|
8
|
+
s.authors = ['Jean Boussier']
|
9
|
+
s.email = ["jean.boussier@gmail.com"]
|
10
|
+
s.summary = 'Rack HTTP server for fast clients and Unix'
|
11
|
+
s.description = File.read('README.md').split("\n\n")[1]
|
12
|
+
s.extensions = %w(ext/pitchfork_http/extconf.rb)
|
13
|
+
s.files = Dir.chdir(File.expand_path('..', __FILE__)) do
|
14
|
+
%x(git ls-files -z).split("\x0").reject { |f| f.match(%r{^(test|spec|features|bin|\.)/}) }
|
15
|
+
end
|
16
|
+
s.bindir = "exe"
|
17
|
+
s.executables = s.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
18
|
+
s.homepage = 'https://github.com/Shopify/pitchfork'
|
19
|
+
|
20
|
+
s.required_ruby_version = ">= 2.5.0"
|
21
|
+
|
22
|
+
s.add_dependency(%q<raindrops>, '~> 0.7')
|
23
|
+
s.add_dependency(%q<rack>, '>= 2.0')
|
24
|
+
|
25
|
+
# Note: To avoid ambiguity, we intentionally avoid the SPDX-compatible
|
26
|
+
# 'Ruby' here since Ruby 1.9.3 switched to BSD-2-Clause, but we
|
27
|
+
# inherited our license from Mongrel when Ruby was at 1.8.
|
28
|
+
# We cannot automatically switch licenses when Ruby changes.
|
29
|
+
s.licenses = ['GPL-2.0+', 'Ruby']
|
30
|
+
end
|