pitchfork 0.1.0
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.
Potentially problematic release.
This version of pitchfork might be problematic. Click here for more details.
- 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
|