Class: PumaPlus::Input::FrameStream

Inherits:
Object
  • Object
show all
Defined in:
lib/puma_plus/input.rb

Overview

A rack.input backed by BODY_CHUNK frames pulled from the worker connection on demand.

Deliberately NOT rewindable. Rack 3 dropped the rewind requirement, and the payoff is that an arbitrarily large or chunked upload never touches a tempfile in either process. Compare puma, which decodes chunked bodies in Ruby and spools every one of them to a Tempfile (puma/lib/puma/client.rb:600), then rewrites CONTENT_LENGTH to the decoded size.

Time spent blocked here is accumulated into #read_ns and reported in RESP_END, so the controller can subtract it from service time. Without that, a slow uploader would look exactly like an application that got slower, and the scaler would add capacity to fix a problem capacity cannot fix.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(conn, prefix = nil) ⇒ FrameStream

Sentinel returned by #read at EOF when a length was requested.



71
72
73
74
75
76
77
78
# File 'lib/puma_plus/input.rb', line 71

def initialize(conn, prefix = nil)
  @conn = conn
  @buf = +""
  @buf << prefix if prefix && !prefix.empty?
  @eof = false
  @closed = false
  @read_ns = 0
end

Instance Attribute Details

#read_nsObject (readonly)

Nanoseconds blocked reading body frames.



81
82
83
# File 'lib/puma_plus/input.rb', line 81

def read_ns
  @read_ns
end

Instance Method Details

#closeObject



132
133
134
# File 'lib/puma_plus/input.rb', line 132

def close
  @closed = true
end

#closed?Boolean

Returns:

  • (Boolean)


136
# File 'lib/puma_plus/input.rb', line 136

def closed? = @closed

#drainObject

Consume and discard anything the application did not read.

Required before RESP_END: Go writes body frames concurrently with the application running, so if the app returns early (a 413, an auth rejection, a HEAD-like handler) the unread frames are still in flight. Leaving them would desynchronise the connection and the next request on it would read a body chunk where it expected a REQUEST.



152
153
154
155
156
# File 'lib/puma_plus/input.rb', line 152

def drain
  pull until @eof
  @buf = +""
  nil
end

#eachObject



122
123
124
125
126
# File 'lib/puma_plus/input.rb', line 122

def each
  while (line = gets)
    yield line
  end
end

#eof?Boolean

Returns:

  • (Boolean)


128
129
130
# File 'lib/puma_plus/input.rb', line 128

def eof?
  @eof && @buf.empty?
end

#gets(sep = $INPUT_RECORD_SEPARATOR || "\n") ⇒ Object



103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
# File 'lib/puma_plus/input.rb', line 103

def gets(sep = $INPUT_RECORD_SEPARATOR || "\n")
  loop do
    if (i = @buf.index(sep))
      line = @buf.byteslice(0, i + sep.bytesize)
      @buf = @buf.byteslice(i + sep.bytesize, @buf.bytesize - i - sep.bytesize) || +""
      return line
    end
    break if @eof

    pull or break
  end

  return nil if @buf.empty?

  line = @buf
  @buf = +""
  line
end

#read(length = nil, outbuf = nil) ⇒ Object



83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/puma_plus/input.rb', line 83

def read(length = nil, outbuf = nil)
  if length.nil?
    fill_all
    data = @buf
    @buf = +""
    outbuf ? outbuf.replace(data) : data
  else
    return outbuf ? outbuf.clear : +"" if length.zero?

    fill_until(length)
    if @buf.empty?
      outbuf&.clear
      return nil # EOF, per IO#read semantics
    end
    data = @buf.byteslice(0, length)
    @buf = @buf.byteslice(length, @buf.bytesize - length) || +""
    outbuf ? outbuf.replace(data) : data
  end
end

#rewindObject

rewind is part of the Rack 2 SPEC but not Rack 3's. Raise rather than silently returning 0, so an app that depends on it fails loudly instead of quietly reading an empty body.

Raises:

  • (Errno::ESPIPE)


141
142
143
# File 'lib/puma_plus/input.rb', line 141

def rewind
  raise Errno::ESPIPE, "puma-plus streams large request bodies; rack.input is not rewindable"
end