Class: PumaPlus::RackEnv

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

Overview

Assembles the Rack env from a REQUEST frame.

Go emits the complete, final env in CGI form, so this class performs zero string transformation -- no dash/underscore mangling, no Host splitting, no case folding. All of that lives in internal/frontend/env.go. What remains here is a hash dup plus byteslice merges, which is the cheapest per-request env construction available without a C extension.

Compare puma, which builds the env in five layers (Binder's @proto_env, the Ragel C parser writing straight into the hash, normalize_env, req_env_post_parse, then handle_request). Moving parsing to Go collapses that to one pass.

Constant Summary collapse

COMMON_KEYS =

Keys that appear on essentially every request, pre-frozen so the common path never allocates a key string. Same optimization as puma's 35 pre-interned header names in ext/puma_http11/puma_http11.c:78-118.

%w[
  REQUEST_METHOD PATH_INFO SCRIPT_NAME QUERY_STRING SERVER_PROTOCOL
  SERVER_NAME SERVER_PORT REMOTE_ADDR SERVER_SOFTWARE
  CONTENT_LENGTH CONTENT_TYPE
  rack.url_scheme
  HTTP_HOST HTTP_ACCEPT HTTP_ACCEPT_CHARSET HTTP_ACCEPT_ENCODING
  HTTP_ACCEPT_LANGUAGE HTTP_AUTHORIZATION HTTP_CACHE_CONTROL HTTP_CONNECTION
  HTTP_COOKIE HTTP_IF_MODIFIED_SINCE HTTP_IF_NONE_MATCH HTTP_ORIGIN
  HTTP_PRAGMA HTTP_REFERER HTTP_USER_AGENT HTTP_X_FORWARDED_FOR
  HTTP_X_FORWARDED_PROTO HTTP_X_REQUEST_ID HTTP_X_REQUEST_START
].each_with_object({}) { |k, h| h[k.b.freeze] = k.freeze }.freeze

Instance Method Summary collapse

Constructor Details

#initialize(errors: $stderr, multithread: true, multiprocess: false) ⇒ RackEnv

Uncommon keys are deduplicated into this cache on first sight, so a long tail of custom headers costs one allocation per distinct name for the lifetime of the process rather than one per request. multithread and multiprocess describe THIS process, and are computed by the caller from the real configuration rather than assumed.

Rack 3 dropped these from the spec, along with rack.version and rack.run_once. They are kept because apps still read them -- sizing a connection pool is the usual reason -- and because puma still sets them (puma/lib/puma/binder.rb:33-35). rack.version is NOT kept: Rack 3 removed it and any value would be a claim about a spec version that no longer numbers itself that way.

Hardcoding them, as this used to, meant a single-threaded worker still announced rack.multithread, and an app sizing a pool from it over-allocated by exactly the factor it was trying to compute.



50
51
52
53
54
55
56
57
58
59
# File 'lib/puma_plus/rack_env.rb', line 50

def initialize(errors: $stderr, multithread: true, multiprocess: false)
  @key_cache = Hash.new { |h, k| h[k] = -k.dup.force_encoding(Encoding::UTF_8) }
  @prototype = {
    "rack.errors" => errors,
    "rack.hijack?" => true,
    "rack.multithread" => multithread,
    "rack.multiprocess" => multiprocess,
    "rack.run_once" => false
  }.freeze
end

Instance Method Details

#build(pairs, meta) ⇒ Object

Build an env from a decoded env kv blob.

pairs is the array of [key, value] byteslices from Wire.decode_kv. Values are handed through as-is: they are byteslices of the single frame payload, so the whole env costs one payload string plus N slice headers.



66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/puma_plus/rack_env.rb', line 66

def build(pairs, meta)
  env = @prototype.dup

  pairs.each do |key, value|
    env[COMMON_KEYS[key] || @key_cache[key]] = value
  end

  # Timing, in the three shapes callers expect.
  #
  # puma.request_queue_time is ours and authoritative: nanoseconds measured
  # entirely inside Go from a monotonic clock, so no cross-process skew.
  #
  # puma.request_body_wait keeps puma's exact key and millisecond unit
  # (lib/puma/client.rb:746) so middleware written against puma works
  # unchanged.
  env["puma.request_queue_time"] = meta[:queue_ns] / 1_000_000_000.0
  env["puma.request_body_wait"] = meta[:body_wait_ns] / 1_000_000.0

  # X-Request-Start in the exact `t=<unix micros>` format New Relic, Scout
  # and Skylight already parse, injected only if absent so an upstream load
  # balancer's value always wins. Existing APM gems light up with no changes.
  env["HTTP_X_REQUEST_START"] ||= "t=#{meta[:runnable_wall_us]}"

  env
end