Record how long a client-terminated request had been in flight - #3287
Open
jitheshtr wants to merge 1 commit into
Open
Record how long a client-terminated request had been in flight#3287jitheshtr wants to merge 1 commit into
jitheshtr wants to merge 1 commit into
Conversation
jitheshtr
marked this pull request as ready for review
August 20, 2026 00:36
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #3287 +/- ##
=============================================
- Coverage 64.24% 50.74% -13.51%
+ Complexity 10398 8683 -1715
=============================================
Files 840 938 +98
Lines 71755 80488 +8733
Branches 8611 9687 +1076
=============================================
- Hits 46099 40840 -5259
- Misses 23004 36252 +13248
- Partials 2652 3396 +744 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
When a client aborts an in-flight request, NettyMessageProcessor logs that the channel became inactive but not how long the request had been running, so a client giving up at a fixed deadline is indistinguishable from one that died early. clientEarlyTerminationCount already counts these aborts, and their duration reaches nioRoundTripTimeInMs, but that histogram is bucketed per request type and mixes aborts with successes, so it cannot show the abort-only distribution. Add a ClientTerminatedRequestTimeInMs histogram, fed from the two client termination paths: channelInactive() and the idle timeout in userEventTriggered(). Which of the two finds the request still open depends on the transport -- a real event loop defers fireChannelInactive to a later task, by which point the request has been closed, whereas EmbeddedChannel runs it inline -- so recording goes through a one shot helper, reset in resetState() alongside the request it belongs to. That keeps the count at one per request on either transport instead of encoding the transport's behaviour. Instrumenting only channelInactive() missed every idle timeout abort in production, which is the longest lived group and the tail this metric exists to show. Read the elapsed time through a new RestRequestMetricsTracker accessor. No new timestamp is introduced: NettyRequest's constructor already marks the request received, and the value simply had no reader. The accessor returns 0 rather than throwing when the request was never marked, unlike its siblings, because its caller is a diagnostic on an error path. Both abort log lines gain the elapsed time. On channelInactive() it is appended, so prefix based log matching keeps working; the idle path previously logged no per-request line at all. testIdleChannelAbortOnRealEventLoopRecordsTimeInFlight drives a LocalChannel on a real event loop, because the EmbeddedChannel tests structurally cannot fail on this defect. The server side and protocol abort paths were verified not to feed the histogram on real TCP sockets, so the metric measures what its name says. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jitheshtr
force-pushed
the
jitheshtr/g1-abort-request-duration
branch
from
August 20, 2026 00:58
9ee0daf to
2bd059b
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
NettyMessageProcessorlogs that a request was aborted because the channel became inactive, but not how long that request had been running. Without an elapsed time there is no way to distinguish a client that gave up after a fixed deadline from one that died early.NettyMetrics.clientEarlyTerminationCountalready counts these aborts, and their duration does eventually reachRestRequestMetrics.nioRoundTripTimeInMs— but that histogram is bucketed per request type and mixes aborts with successes, so it cannot answer "do clients abort at a fixed deadline". Only the abort-only distribution is new here.What changed
RestRequestMetricsTracker.getTimeSinceRequestReceivedInMs()— a new accessor. No new timestamp is introduced:NettyRequest's constructor already callsnioMetricsTracker.markRequestReceived(), so the arrival time was recorded and simply had no reader. It returns0rather than throwing when the request was never marked received, unlike itsmarkFirstByteSent()/markRequestCompleted()siblings, because its caller is a diagnostic on an error path.NettyMessageProcessor.ClientTerminatedRequestTimeInMshistogram, recorded from the two client-termination paths —channelInactive()and the idle timeout inuserEventTriggered()— through a helper that is one shot per request. Which of the two observes the request still open is transport dependent, so recording once is what keeps the count correct rather than an artifact of the transport.channelInactive()it is appended, so any prefix-based log matching keeps working; the idle path previously logged no per-request line at all.Item 1 is the one worth attention — it adds a public method to a shared API module.
Scope of the metric
onRequestAborted()has nine call sites and the histogram is fed from the two that are client terminations. The other seven are server-side or protocol errors and do not reach it, becauseonRequestAborted()closes the request before the deferredchannelInactive()task runs.Measured against real TCP sockets on
NioEventLoopGroup, with a corroborating counter each time to show the path really ran.NettyServerprefers Epoll where available and falls back to NIO, but both deferfireChannelInactivethroughSingleThreadEventLoop, which is the property this turns on:clientEarlyTerminationCount=1idleConnectionCloseCount=1,clientEarlyTerminationCount=1processorExceptionCaughtCount=1requestArrivalRate=1One residual gap:
exceptionCaught()forwards an arbitrary pipelineException, which can include a client-caused I/O failure. Those are not recorded, so the histogram is a lower bound on client terminations. Classifying them is a larger change than this one.Risk Assessment
Durability: no risk. Metrics and logging only. It touches no write, named-blob PUT, TTL, or delete path; no blob metadata storage, read, or indexing; no ordering or atomicity; no callback semantics — nothing is reported to a client earlier or later than before; no
ByteBufrelease,Closeable.close(), or stream lifecycle; no retry or idempotency behaviour; no corruption-detection path. Every checklist item is unchecked.The one-shot flag is reset in
resetState(), which runs immediately before every request is constructed, so a keepalive channel records once per request rather than once per connection.The idle path adds one
errorline per idle abort, alongside theinfoline that already fires there.Testing Done
./gradlew :ambry-rest:test./gradlew :ambry-api:testtestIdleChannelAbortOnRealEventLoopRecordsTimeInFlightdrives aLocalChannelon a real event loop, becauseEmbeddedChannelrunsfireChannelInactiveinline while a real loop defers it — so theEmbeddedChanneltests alone cannot distinguish the correct behaviour from a metric that silently misses every idle abort. It was run 8 times in a row to check it is not timing sensitive.The two
EmbeddedChannelabort tests bound the recorded value below by the time the request was deliberately held open and above by the test's own duration, so a hardcoded zero, a dropped subtraction, or a read of the not-yet-populatedroundTripTimeInMsall fail.testTimeSinceRequestReceivedcovers the new accessor directly, including the unmarked-returns-zero branch.