Skip to content

Send the light to the browser before writing it to the spooler - #422

Merged
JonJagger merged 2 commits into
mainfrom
send-the-light-before-saving-it
Aug 28, 2026
Merged

Send the light to the browser before writing it to the spooler#422
JonJagger merged 2 commits into
mainfrom
send-the-light-before-saving-it

Conversation

@JonJagger

Copy link
Copy Markdown
Member

A [test] run did three things in order: call runner, write the event,
respond. The write sat on the browser's critical path, so its cost was
added to every traffic-light the user waited for.

That cost is negligible when the spooler is healthy: it acks a buffered
append in about a millisecond, against a runner call of several hundred.
The reason to move it is the bad case. An unreachable or hung spooler
costs Net::HTTP's default 60-second connect and read timeouts, and the
light waited behind that even though the runner had already produced it.

The write now runs as the response body closes. An AfterResponse
middleware wraps the mounted apps; a route stashes work and BodyProxy
runs it once the body has gone out. The work stays inside the request,
so no thread is orphaned and Puma drains it at shutdown. Wrapping
outside Sinatra preserves content-length, which Sinatra computes from
the route's own Array body.

Deferring it first needed the http object to stop being shared.
Requester held one Net::HTTP for the life of the process, and #request
mutates its socket and started flags, so a deferred POST could overlap
the same tab's next one. Each request now builds its own object, which
costs a single allocation: the object is never started, so every
request already opened and closed its own connection.

Measured at 0.9ms saved from an 805ms run. Nobody will feel that; the
tail is what this buys.

Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com

  A [test] run did three things in order: call runner, write the event,
  respond. The write sat on the browser's critical path, so its cost was
  added to every traffic-light the user waited for.

  That cost is negligible when the spooler is healthy: it acks a buffered
  append in about a millisecond, against a runner call of several hundred.
  The reason to move it is the bad case. An unreachable or hung spooler
  costs Net::HTTP's default 60-second connect and read timeouts, and the
  light waited behind that even though the runner had already produced it.

  The write now runs as the response body closes. An AfterResponse
  middleware wraps the mounted apps; a route stashes work and BodyProxy
  runs it once the body has gone out. The work stays inside the request,
  so no thread is orphaned and Puma drains it at shutdown. Wrapping
  outside Sinatra preserves content-length, which Sinatra computes from
  the route's own Array body.

  Deferring it first needed the http object to stop being shared.
  Requester held one Net::HTTP for the life of the process, and #request
  mutates its socket and started flags, so a deferred POST could overlap
  the same tab's next one. Each request now builds its own object, which
  costs a single allocation: the object is never started, so every
  request already opened and closed its own connection.

  Measured at 0.9ms saved from an 805ms run. Nobody will feel that; the
  tail is what this buys.

  Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
  .rubocop.yml grandfathers today's offences file-by-file in
  .rubocop_todo.yml so that a newly added file is linted in full from the
  moment it arrives. after_response.rb arrived without the frozen string
  literal comment every cop-clean file needs, so it is added here rather
  than given a todo entry, which would work against that arrangement.

  Two offences in kata_app.rb go with it. The comment above the deferred
  write ran a character over the 80-column ceiling, and is reflowed. The
  begin/end wrapping the write is dropped in favour of the block's own
  rescue, which reads the same and is what Style/RedundantBegin asks for.

  Only the second of those changes code rather than text, so the server
  suite was rerun: 132 tests, 481 assertions, no failures. It covers the
  rescue path directly, in run_tests_save_error_test.

  Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@JonJagger
JonJagger merged commit d558951 into main Aug 28, 2026
10 checks passed
@JonJagger
JonJagger deleted the send-the-light-before-saving-it branch August 28, 2026 07:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant