puma-plus 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.
- checksums.yaml +7 -0
- data/exe/puma-plus +12 -0
- data/exe/puma-plus-worker +79 -0
- data/ext/puma_plus_gvl/extconf.rb +16 -0
- data/ext/puma_plus_gvl/puma_plus_gvl.c +160 -0
- data/lib/puma_plus/address.rb +33 -0
- data/lib/puma_plus/app_loader.rb +66 -0
- data/lib/puma_plus/config_file.rb +267 -0
- data/lib/puma_plus/control_channel.rb +138 -0
- data/lib/puma_plus/gvl.rb +76 -0
- data/lib/puma_plus/hooks.rb +88 -0
- data/lib/puma_plus/input.rb +216 -0
- data/lib/puma_plus/launcher.rb +347 -0
- data/lib/puma_plus/rack_env.rb +92 -0
- data/lib/puma_plus/ractor_worker.rb +227 -0
- data/lib/puma_plus/shepherd.rb +234 -0
- data/lib/puma_plus/version.rb +5 -0
- data/lib/puma_plus/wire.rb +236 -0
- data/lib/puma_plus/worker.rb +90 -0
- data/lib/puma_plus/worker_thread.rb +331 -0
- data/lib/puma_plus/ws.rb +233 -0
- data/lib/puma_plus/ws_event.rb +78 -0
- metadata +85 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "socket"
|
|
4
|
+
require "etc"
|
|
5
|
+
require "puma_plus/wire"
|
|
6
|
+
|
|
7
|
+
module PumaPlus
|
|
8
|
+
# The control connection to the Go server, shared by every kind of worker
|
|
9
|
+
# supervisor.
|
|
10
|
+
#
|
|
11
|
+
# Extracted because Shepherd (which forks processes) and RactorWorker (which
|
|
12
|
+
# spawns Ractors) had byte-identical copies of connect, command loop,
|
|
13
|
+
# heartbeat and RSS reading. The only genuinely different parts are what
|
|
14
|
+
# SET_SLOTS means and what goes in the heartbeat -- which is exactly what the
|
|
15
|
+
# handler protocol below covers. Duplicating the rest meant a protocol change
|
|
16
|
+
# had to be made in two files, and nothing would have failed if it were made
|
|
17
|
+
# in one.
|
|
18
|
+
#
|
|
19
|
+
# A supervisor is the handler. It must respond to:
|
|
20
|
+
#
|
|
21
|
+
# running? -- keep looping?
|
|
22
|
+
# set_slots(n) -- SET_SLOTS arrived; n is the desired capacity
|
|
23
|
+
# quiesce -- stop replacing capacity that dies
|
|
24
|
+
# shutdown(ms) -- stop, with this much grace
|
|
25
|
+
# heartbeat_kv -- extra [key, value] pairs for WORKER_STATUS
|
|
26
|
+
#
|
|
27
|
+
# and may respond to:
|
|
28
|
+
#
|
|
29
|
+
# on_idle -- called each pass, whether or not a frame arrived, for
|
|
30
|
+
# work that cannot wait on the socket (the shepherd reaps
|
|
31
|
+
# dead children here)
|
|
32
|
+
class ControlChannel
|
|
33
|
+
# Long enough to be cheap, short enough that signal flags and child deaths
|
|
34
|
+
# are noticed promptly rather than only when Go happens to send something.
|
|
35
|
+
SELECT_TIMEOUT = 0.25
|
|
36
|
+
|
|
37
|
+
def initialize(socket_path:, logger: $stderr)
|
|
38
|
+
@socket_path = socket_path
|
|
39
|
+
@logger = logger
|
|
40
|
+
# Guards the socket. The heartbeat thread writes WORKER_STATUS while the
|
|
41
|
+
# command loop writes PONG, and without this the two can interleave
|
|
42
|
+
# mid-frame and hand Go a corrupt stream -- a race that existed in both
|
|
43
|
+
# copies of this code and had simply never been hit, since PING is only
|
|
44
|
+
# sent when a worker looks suspect.
|
|
45
|
+
@write_mu = Mutex.new
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def connect!
|
|
49
|
+
@conn = UNIXSocket.new(@socket_path)
|
|
50
|
+
@conn.sync = true
|
|
51
|
+
write(Wire::HELLO, Wire.encode_kv([
|
|
52
|
+
["version", Wire::VERSION],
|
|
53
|
+
["role", "control"],
|
|
54
|
+
["pid", Process.pid]
|
|
55
|
+
]))
|
|
56
|
+
self
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Serve commands until the connection closes or the handler stops running.
|
|
60
|
+
def run(handler)
|
|
61
|
+
heartbeat = Thread.new { heartbeat_loop(handler) }
|
|
62
|
+
command_loop(handler)
|
|
63
|
+
ensure
|
|
64
|
+
heartbeat&.kill
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def close
|
|
68
|
+
@conn&.close
|
|
69
|
+
rescue IOError
|
|
70
|
+
nil
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Advisory only. Go reads /proc itself for the memory guard rather than
|
|
74
|
+
# trusting a number reported by a process it may need to kill.
|
|
75
|
+
def self.rss_kb
|
|
76
|
+
File.read("/proc/self/statm").split[1].to_i * (Etc.sysconf(Etc::SC_PAGESIZE) / 1024)
|
|
77
|
+
rescue StandardError
|
|
78
|
+
0
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
def log(msg)
|
|
84
|
+
@logger.puts "[puma-plus control] #{msg}"
|
|
85
|
+
@logger.flush if @logger.respond_to?(:flush)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def write(type, payload = nil)
|
|
89
|
+
@write_mu.synchronize { @conn.write Wire.frame(type, payload) }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def command_loop(handler)
|
|
93
|
+
while handler.running?
|
|
94
|
+
handler.on_idle if handler.respond_to?(:on_idle)
|
|
95
|
+
|
|
96
|
+
ready = IO.select([@conn], nil, nil, SELECT_TIMEOUT)
|
|
97
|
+
next unless ready
|
|
98
|
+
|
|
99
|
+
begin
|
|
100
|
+
type, payload = Wire.read_frame(@conn)
|
|
101
|
+
rescue Wire::Closed
|
|
102
|
+
log "connection closed by server"
|
|
103
|
+
break
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
case type
|
|
107
|
+
when Wire::SET_SLOTS
|
|
108
|
+
handler.set_slots(payload.unpack1("N"))
|
|
109
|
+
when Wire::QUIESCE
|
|
110
|
+
handler.quiesce
|
|
111
|
+
when Wire::SHUTDOWN
|
|
112
|
+
handler.shutdown(payload.unpack1("N") || 5000)
|
|
113
|
+
when Wire::PING
|
|
114
|
+
write(Wire::PONG)
|
|
115
|
+
else
|
|
116
|
+
log "unexpected frame #{Wire.type_name(type)}"
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def heartbeat_loop(handler)
|
|
122
|
+
seq = 0
|
|
123
|
+
while handler.running?
|
|
124
|
+
sleep 1
|
|
125
|
+
seq += 1
|
|
126
|
+
begin
|
|
127
|
+
write(Wire::WORKER_STATUS, Wire.encode_kv(
|
|
128
|
+
[["seq", seq], ["pid", Process.pid]] +
|
|
129
|
+
handler.heartbeat_kv +
|
|
130
|
+
[["rss_hint", self.class.rss_kb * 1024]]
|
|
131
|
+
))
|
|
132
|
+
rescue IOError, Errno::EPIPE
|
|
133
|
+
break
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PumaPlus
|
|
4
|
+
# GVL wait instrumentation.
|
|
5
|
+
#
|
|
6
|
+
# Measures how long threads spend runnable but unable to run because another
|
|
7
|
+
# thread in the same process holds the GVL. That number decides the hardest
|
|
8
|
+
# question the scaling controller asks: add a thread, or fork a process?
|
|
9
|
+
#
|
|
10
|
+
# Without it, the cheap heuristic is io_fraction = 1 - cpu/service, and it is
|
|
11
|
+
# not merely imprecise but backwards in the case that matters. A thread queued
|
|
12
|
+
# behind the GVL accrues no CPU time, so a purely CPU-bound request whose wall
|
|
13
|
+
# time is mostly queueing reads as "60% IO-bound" -- and the controller adds
|
|
14
|
+
# threads to a process where more threads make latency worse. Measured
|
|
15
|
+
# directly, the same request reports a 0.60 GVL fraction and the controller
|
|
16
|
+
# forks instead.
|
|
17
|
+
#
|
|
18
|
+
# The native extension is optional. Without it every method here returns 0 and
|
|
19
|
+
# the controller falls back to io_fraction plus gain probing, which is what it
|
|
20
|
+
# did before this existed.
|
|
21
|
+
module GVL
|
|
22
|
+
@native = begin
|
|
23
|
+
require "puma_plus_gvl"
|
|
24
|
+
true
|
|
25
|
+
rescue LoadError
|
|
26
|
+
false
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Fallbacks, defined only when the extension is absent so the real ones win.
|
|
30
|
+
unless @native
|
|
31
|
+
NATIVE = false
|
|
32
|
+
|
|
33
|
+
class << self
|
|
34
|
+
def native_start! = false
|
|
35
|
+
def native_stop! = false
|
|
36
|
+
def running? = false
|
|
37
|
+
def wait_ns = 0
|
|
38
|
+
def waits = 0
|
|
39
|
+
def thread_wait_ns = 0
|
|
40
|
+
def thread_waits = 0
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
class << self
|
|
45
|
+
# Is the native extension available?
|
|
46
|
+
def native? = const_defined?(:NATIVE) && const_get(:NATIVE)
|
|
47
|
+
|
|
48
|
+
# Begin measuring. Called once per worker process.
|
|
49
|
+
#
|
|
50
|
+
# Returns false when unavailable, which is not an error: the controller
|
|
51
|
+
# simply uses its cheaper signals.
|
|
52
|
+
def start!
|
|
53
|
+
return false unless native?
|
|
54
|
+
|
|
55
|
+
native_start!
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def stop!
|
|
59
|
+
return false unless native?
|
|
60
|
+
|
|
61
|
+
native_stop!
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# What fraction of an interval was spent waiting for the GVL.
|
|
65
|
+
#
|
|
66
|
+
# Above ~0.3 means this process is GVL-bound: its threads are queueing on
|
|
67
|
+
# each other rather than doing work, and adding more of them will deepen
|
|
68
|
+
# the queue rather than raise throughput.
|
|
69
|
+
def fraction(wait_ns, service_ns)
|
|
70
|
+
return 0.0 if service_ns.nil? || service_ns <= 0
|
|
71
|
+
|
|
72
|
+
wait_ns.to_f / service_ns
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "puma_plus/config_file"
|
|
4
|
+
|
|
5
|
+
module PumaPlus
|
|
6
|
+
# Lifecycle hooks from the config file.
|
|
7
|
+
#
|
|
8
|
+
# These are the one part of the configuration that cannot be translated into
|
|
9
|
+
# flags for the Go server, because they are blocks. So the config file is read
|
|
10
|
+
# twice: once by the `puma-plus` launcher, which turns settings into flags and
|
|
11
|
+
# then execs Go, and again here by the Ruby process that actually runs the
|
|
12
|
+
# application, which is the only process where a hook can meaningfully run.
|
|
13
|
+
#
|
|
14
|
+
# Reading it twice means any top-level side effect in the config file happens
|
|
15
|
+
# twice. That is worth knowing and is why config files should declare rather
|
|
16
|
+
# than do -- the same advice puma's own docs give, for the same reason.
|
|
17
|
+
#
|
|
18
|
+
# Semantics follow puma exactly (verified against puma/lib/puma/cluster.rb:438
|
|
19
|
+
# and cluster/worker.rb:58,151):
|
|
20
|
+
#
|
|
21
|
+
# before_fork once in the shepherd, before ANY worker is forked
|
|
22
|
+
# on_worker_boot(idx) in each worker after fork, before it serves
|
|
23
|
+
# on_worker_shutdown in each worker, before it exits
|
|
24
|
+
#
|
|
25
|
+
# The timing of on_worker_boot is unusually clean here. A worker becomes
|
|
26
|
+
# visible to Go by dialing in, so running boot hooks before that dial means Go
|
|
27
|
+
# cannot dispatch a request to a worker whose hooks have not finished. In puma
|
|
28
|
+
# the equivalent guarantee needs the master to track a booted state; here it
|
|
29
|
+
# falls out of the connection being the readiness signal.
|
|
30
|
+
class Hooks
|
|
31
|
+
EMPTY = {}.freeze
|
|
32
|
+
|
|
33
|
+
def self.load(path, logger: $stderr)
|
|
34
|
+
return new(EMPTY, logger: logger) unless path && File.exist?(path)
|
|
35
|
+
|
|
36
|
+
# A throwaway Options: every non-hook directive in the file still runs and
|
|
37
|
+
# still sets values, and all of them are discarded. The launcher already
|
|
38
|
+
# turned those into flags, and honouring them here would let a worker
|
|
39
|
+
# disagree with the server about its own configuration.
|
|
40
|
+
dsl = ConfigFile.load(path, Options.new({}))
|
|
41
|
+
new(dsl.hooks, logger: logger, path: path)
|
|
42
|
+
rescue StandardError => e
|
|
43
|
+
# A config file that raises when read for hooks would otherwise take down
|
|
44
|
+
# a worker with a stack trace and no indication that hooks were the cause.
|
|
45
|
+
raise ConfigError, "loading hooks from #{path}: #{e.class}: #{e.message}"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def initialize(hooks, logger: $stderr, path: nil)
|
|
49
|
+
@hooks = hooks || EMPTY
|
|
50
|
+
@logger = logger
|
|
51
|
+
@path = path
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
attr_reader :path
|
|
55
|
+
|
|
56
|
+
def any?(name) = !(@hooks[name].nil? || @hooks[name].empty?)
|
|
57
|
+
|
|
58
|
+
def names = @hooks.keys.select { |k| any?(k) }
|
|
59
|
+
|
|
60
|
+
# Run every block registered for +name+, in declaration order.
|
|
61
|
+
#
|
|
62
|
+
# fatal: is the difference between "this process cannot do its job" and
|
|
63
|
+
# "this process is leaving anyway". A failed on_worker_boot means the worker
|
|
64
|
+
# would serve requests without whatever the hook was supposed to establish
|
|
65
|
+
# -- a database connection, most often -- so it dies loudly instead of
|
|
66
|
+
# serving wrongly. A failed shutdown hook is logged and the shutdown
|
|
67
|
+
# continues, because refusing to exit helps nobody.
|
|
68
|
+
def run(name, *args, fatal: true)
|
|
69
|
+
blocks = @hooks[name]
|
|
70
|
+
return if blocks.nil? || blocks.empty?
|
|
71
|
+
|
|
72
|
+
blocks.each_with_index do |blk, i|
|
|
73
|
+
blk.call(*args)
|
|
74
|
+
rescue StandardError, ScriptError => e
|
|
75
|
+
where = "#{name} hook #{i + 1}/#{blocks.size}#{@path ? " from #{@path}" : ''}"
|
|
76
|
+
if fatal
|
|
77
|
+
@logger.puts "[puma-plus] #{where} failed: #{e.class}: #{e.message}"
|
|
78
|
+
Array(e.backtrace).first(8).each { |l| @logger.puts "[puma-plus] #{l}" }
|
|
79
|
+
raise HookError, "#{where} failed: #{e.class}: #{e.message}"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
@logger.puts "[puma-plus] #{where} failed (continuing): #{e.class}: #{e.message}"
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
class HookError < StandardError; end
|
|
88
|
+
end
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "stringio"
|
|
4
|
+
require "puma_plus/wire"
|
|
5
|
+
|
|
6
|
+
module PumaPlus
|
|
7
|
+
module Input
|
|
8
|
+
# An empty rack.input.
|
|
9
|
+
#
|
|
10
|
+
# Transcribed from puma/lib/puma/null_io.rb rather than depended upon: the
|
|
11
|
+
# class is a handful of methods, and the gem takes no dependencies.
|
|
12
|
+
class NullIO
|
|
13
|
+
def gets = nil
|
|
14
|
+
def each; end
|
|
15
|
+
def close; end
|
|
16
|
+
def size = 0
|
|
17
|
+
def eof? = true
|
|
18
|
+
def rewind = 0
|
|
19
|
+
def sync = true
|
|
20
|
+
def string = ""
|
|
21
|
+
def closed? = false
|
|
22
|
+
def read_ns = 0
|
|
23
|
+
|
|
24
|
+
def sync=(value)
|
|
25
|
+
value
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def read(count = nil, buffer = nil)
|
|
29
|
+
result = count.nil? || count.zero? ? "" : nil
|
|
30
|
+
if buffer
|
|
31
|
+
buffer.clear
|
|
32
|
+
buffer << result if result
|
|
33
|
+
return count.nil? || count.zero? ? buffer : nil
|
|
34
|
+
end
|
|
35
|
+
result
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def readpartial(_count, buffer = nil)
|
|
39
|
+
raise EOFError if buffer.nil?
|
|
40
|
+
|
|
41
|
+
buffer.clear
|
|
42
|
+
raise EOFError
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Frozen, which NullIO can afford because it holds no state at all. Two
|
|
47
|
+
# reasons, and the first stands on its own: a rack.input shared across every
|
|
48
|
+
# bodyless request must not be mutable, or one request could scribble on
|
|
49
|
+
# another's. The second is that a frozen stateless object is Ractor-shareable,
|
|
50
|
+
# and this constant is on the request path -- unfrozen, it raises
|
|
51
|
+
# IsolationError the first time a Ractor worker serves a GET.
|
|
52
|
+
EMPTY = NullIO.new.freeze
|
|
53
|
+
|
|
54
|
+
# A rack.input backed by BODY_CHUNK frames pulled from the worker connection
|
|
55
|
+
# on demand.
|
|
56
|
+
#
|
|
57
|
+
# Deliberately NOT rewindable. Rack 3 dropped the rewind requirement, and the
|
|
58
|
+
# payoff is that an arbitrarily large or chunked upload never touches a
|
|
59
|
+
# tempfile in either process. Compare puma, which decodes chunked bodies in
|
|
60
|
+
# Ruby and spools every one of them to a Tempfile
|
|
61
|
+
# (puma/lib/puma/client.rb:600), then rewrites CONTENT_LENGTH to the decoded
|
|
62
|
+
# size.
|
|
63
|
+
#
|
|
64
|
+
# Time spent blocked here is accumulated into #read_ns and reported in
|
|
65
|
+
# RESP_END, so the controller can subtract it from service time. Without
|
|
66
|
+
# that, a slow uploader would look exactly like an application that got
|
|
67
|
+
# slower, and the scaler would add capacity to fix a problem capacity cannot
|
|
68
|
+
# fix.
|
|
69
|
+
class FrameStream
|
|
70
|
+
# Sentinel returned by #read at EOF when a length was requested.
|
|
71
|
+
def initialize(conn, prefix = nil)
|
|
72
|
+
@conn = conn
|
|
73
|
+
@buf = +""
|
|
74
|
+
@buf << prefix if prefix && !prefix.empty?
|
|
75
|
+
@eof = false
|
|
76
|
+
@closed = false
|
|
77
|
+
@read_ns = 0
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Nanoseconds blocked reading body frames.
|
|
81
|
+
attr_reader :read_ns
|
|
82
|
+
|
|
83
|
+
def read(length = nil, outbuf = nil)
|
|
84
|
+
if length.nil?
|
|
85
|
+
fill_all
|
|
86
|
+
data = @buf
|
|
87
|
+
@buf = +""
|
|
88
|
+
outbuf ? outbuf.replace(data) : data
|
|
89
|
+
else
|
|
90
|
+
return outbuf ? outbuf.clear : +"" if length.zero?
|
|
91
|
+
|
|
92
|
+
fill_until(length)
|
|
93
|
+
if @buf.empty?
|
|
94
|
+
outbuf&.clear
|
|
95
|
+
return nil # EOF, per IO#read semantics
|
|
96
|
+
end
|
|
97
|
+
data = @buf.byteslice(0, length)
|
|
98
|
+
@buf = @buf.byteslice(length, @buf.bytesize - length) || +""
|
|
99
|
+
outbuf ? outbuf.replace(data) : data
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def gets(sep = $INPUT_RECORD_SEPARATOR || "\n")
|
|
104
|
+
loop do
|
|
105
|
+
if (i = @buf.index(sep))
|
|
106
|
+
line = @buf.byteslice(0, i + sep.bytesize)
|
|
107
|
+
@buf = @buf.byteslice(i + sep.bytesize, @buf.bytesize - i - sep.bytesize) || +""
|
|
108
|
+
return line
|
|
109
|
+
end
|
|
110
|
+
break if @eof
|
|
111
|
+
|
|
112
|
+
pull or break
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
return nil if @buf.empty?
|
|
116
|
+
|
|
117
|
+
line = @buf
|
|
118
|
+
@buf = +""
|
|
119
|
+
line
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def each
|
|
123
|
+
while (line = gets)
|
|
124
|
+
yield line
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def eof?
|
|
129
|
+
@eof && @buf.empty?
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def close
|
|
133
|
+
@closed = true
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def closed? = @closed
|
|
137
|
+
|
|
138
|
+
# rewind is part of the Rack 2 SPEC but not Rack 3's. Raise rather than
|
|
139
|
+
# silently returning 0, so an app that depends on it fails loudly instead
|
|
140
|
+
# of quietly reading an empty body.
|
|
141
|
+
def rewind
|
|
142
|
+
raise Errno::ESPIPE, "puma-plus streams large request bodies; rack.input is not rewindable"
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Consume and discard anything the application did not read.
|
|
146
|
+
#
|
|
147
|
+
# Required before RESP_END: Go writes body frames concurrently with the
|
|
148
|
+
# application running, so if the app returns early (a 413, an auth
|
|
149
|
+
# rejection, a HEAD-like handler) the unread frames are still in flight.
|
|
150
|
+
# Leaving them would desynchronise the connection and the next request on
|
|
151
|
+
# it would read a body chunk where it expected a REQUEST.
|
|
152
|
+
def drain
|
|
153
|
+
pull until @eof
|
|
154
|
+
@buf = +""
|
|
155
|
+
nil
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
private
|
|
159
|
+
|
|
160
|
+
def fill_all
|
|
161
|
+
pull until @eof
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def fill_until(length)
|
|
165
|
+
pull while @buf.bytesize < length && !@eof
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Read one more frame from the connection. Returns false at end of body.
|
|
169
|
+
def pull
|
|
170
|
+
return false if @eof
|
|
171
|
+
|
|
172
|
+
t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond)
|
|
173
|
+
type, payload = Wire.read_frame(@conn)
|
|
174
|
+
@read_ns += Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond) - t0
|
|
175
|
+
|
|
176
|
+
case type
|
|
177
|
+
when Wire::BODY_CHUNK
|
|
178
|
+
@buf << payload
|
|
179
|
+
true
|
|
180
|
+
when Wire::BODY_END
|
|
181
|
+
# Trailers ride on BODY_END. Nothing consumes them yet; decoding here
|
|
182
|
+
# keeps the frame accounted for and gives a place to surface them.
|
|
183
|
+
@trailers, = Wire.decode_kv(payload) unless payload.empty?
|
|
184
|
+
@eof = true
|
|
185
|
+
false
|
|
186
|
+
else
|
|
187
|
+
@eof = true
|
|
188
|
+
raise Wire::Truncated,
|
|
189
|
+
"expected BODY_CHUNK or BODY_END during body read, got #{Wire.type_name(type)}"
|
|
190
|
+
end
|
|
191
|
+
rescue Wire::Closed
|
|
192
|
+
@eof = true
|
|
193
|
+
false
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Build a rack.input for a decoded REQUEST.
|
|
198
|
+
#
|
|
199
|
+
# NONE -> the shared frozen NullIO.
|
|
200
|
+
# INLINE -> a StringIO over a byteslice of the frame payload. Rewindable for
|
|
201
|
+
# free, and covers essentially every form post and JSON API.
|
|
202
|
+
# STREAM -> FrameStream, pulling BODY_CHUNK frames on demand.
|
|
203
|
+
def self.for(mode, body, conn = nil)
|
|
204
|
+
case mode
|
|
205
|
+
when Wire::BODY_NONE then EMPTY
|
|
206
|
+
when Wire::BODY_INLINE then StringIO.new(body)
|
|
207
|
+
when Wire::BODY_STREAM
|
|
208
|
+
raise ArgumentError, "STREAM body requires a connection" unless conn
|
|
209
|
+
|
|
210
|
+
FrameStream.new(conn, body)
|
|
211
|
+
else
|
|
212
|
+
raise ArgumentError, "unknown body_mode #{mode}"
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|