Module: Hyperion::LintWrapperPool

Defined in:
lib/hyperion/lint_wrapper_pool.rb

Overview

Phase 2a (1.7.1) — per-worker ‘Rack::Lint::Wrapper` pool.

In dev mode (‘RACK_ENV != ’production’‘), Rack guidance is to wrap the response body with a `Rack::Lint::Wrapper` so spec violations surface immediately. The naive shape is one wrapper allocation per request. On a high-rps dev/staging fleet that’s a measurable allocation tax — every wrapper carries 8 ivars and a non-trivial init.

The pool keeps up to ‘MAX_POOL_SIZE` reusable wrappers per worker fiber scheduler. On request entry, callers `acquire(app, env)` to get a ready-to-go wrapper. On response close, callers `release(wrapper)` to put it back in the free list. The wrapper’s per-request state (‘@app`, `@env`, `@response`, status/headers/body, content-length tracking) is reset before reuse so each request gets clean state.

Safety:

* Production short-circuit: `acquire` always allocates fresh in
  `RACK_ENV=production` so production never carries pool overhead and
  never reuses a wrapper that's mid-iteration on another fiber.
* Pool cap: `MAX_POOL_SIZE` bounds steady-state memory. Excess wrappers
  fall out of scope and the GC reaps them.
* Single-thread safety: each Hyperion worker runs one fiber scheduler on
  one thread, so the underlying `Pool` is contention-free. We don't add
  a Mutex — that would be measurable overhead for zero correctness gain
  in the supported deployment shape. If a host embeds Hyperion in a
  multi-thread context the pool simply won't be reused (each thread
  allocates fresh; no corruption).

Lint semantics are unchanged: every reused wrapper still validates the body each request via ‘check_environment`/`check_headers`/etc. inside `Rack::Lint::Wrapper#response`. The only thing reuse skips is the allocation itself — not the validation work.

Constant Summary collapse

MAX_POOL_SIZE =
32
RESET =

Reset hook — clear all per-request ivars on a wrapper before it goes back into the free list. Mirrors ‘Rack::Lint::Wrapper#initialize` so that the wrapper looks freshly-constructed on the next acquire.

lambda do |wrapper|
  wrapper.instance_variable_set(:@app, nil)
  wrapper.instance_variable_set(:@env, nil)
  wrapper.instance_variable_set(:@response, nil)
  wrapper.instance_variable_set(:@head_request, false)
  wrapper.instance_variable_set(:@status, nil)
  wrapper.instance_variable_set(:@headers, nil)
  wrapper.instance_variable_set(:@body, nil)
  wrapper.instance_variable_set(:@consumed, nil)
  wrapper.instance_variable_set(:@content_length, nil)
  wrapper.instance_variable_set(:@closed, false)
  wrapper.instance_variable_set(:@size, 0)
  wrapper
end

Class Method Summary collapse

Class Method Details

.acquire(app, env) ⇒ Object

Acquire a wrapper for ‘(app, env)`. In production we always allocate fresh (skipping the pool entirely). Outside production we pop a reusable wrapper, rebind it to (app, env) via the reset hook + ivar writes, and return it ready for `#response`.

The returned wrapper behaves identically to ‘Rack::Lint::Wrapper.new(app, env)`.



81
82
83
84
85
86
87
88
89
90
# File 'lib/hyperion/lint_wrapper_pool.rb', line 81

def acquire(app, env)
  if enabled?
    wrapper = pool.acquire
    wrapper.instance_variable_set(:@app, app)
    wrapper.instance_variable_set(:@env, env)
    wrapper
  else
    ::Rack::Lint::Wrapper.new(app, env)
  end
end

.enabled?Boolean

Whether this process should pool Lint wrappers. False in production (Lint is a dev tool; production never inserts it) and false when explicitly disabled via ‘RACK_LINT_DISABLE=1` for operators who want to side-step the pool entirely.

Returns:

  • (Boolean)


64
65
66
67
68
69
# File 'lib/hyperion/lint_wrapper_pool.rb', line 64

def enabled?
  return false if production?
  return false if ENV['RACK_LINT_DISABLE'] == '1'

  true
end

.pool_sizeObject

Read-only accessor for the underlying pool — used by specs to assert reuse without relying on ‘.equal?` identity through `acquire`.



111
112
113
# File 'lib/hyperion/lint_wrapper_pool.rb', line 111

def pool_size
  @pool ? @pool.size : 0
end

.production?Boolean

Returns:

  • (Boolean)


71
72
73
# File 'lib/hyperion/lint_wrapper_pool.rb', line 71

def production?
  ENV['RACK_ENV'] == 'production'
end

.release(wrapper) ⇒ Object

Release a wrapper back to the pool. No-op in production (where ‘acquire` returned a fresh allocation that the GC will reap). The underlying `Hyperion::Pool` enforces MAX_POOL_SIZE; releases past the cap drop the wrapper on the floor.



96
97
98
99
100
101
# File 'lib/hyperion/lint_wrapper_pool.rb', line 96

def release(wrapper)
  return unless enabled?
  return unless wrapper.is_a?(::Rack::Lint::Wrapper)

  pool.release(wrapper)
end

.reset!Object

Test seam: clear the free list so spec runs that toggle RACK_ENV don’t see warm wrappers from a previous example.



105
106
107
# File 'lib/hyperion/lint_wrapper_pool.rb', line 105

def reset!
  @pool = nil
end