Repository navigation
Conversation
Compressed batch-upload refusals (corrupt gzip/zstd, unsupported formats,
non-multipart bodies) were only visible as anonymous 400s and 415s on the
files route: they never pass through the inference rejection recorder.
`dwctl_file_upload_rejections_total{code}` now counts every upload refusal
under a bounded code, registered at zero at startup so the first of a kind
is visible to increase().
Realtime decoding waits for one of a fixed set of slots rather than
failing, so slot saturation only showed as slower compressed requests.
`dwctl_inference_decode_slot_wait_seconds` records each wait and
`dwctl_inference_decodes_waiting` the decodes waiting now.
DW-994
There was a problem hiding this comment.
All reported issues were addressed across 3 files
Reply with feedback, questions, or to request a fix.
View guided diff | Turn on auto-fix | Re-trigger cubic
There was a problem hiding this comment.
Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.
An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing. Review the Copilot code review billing settings.
The upload limiter's 429 and the batch-disabled 403 bypassed the new upload rejection counter, so an upload overload would leave every series flat. Count them as `upload_capacity` and `batch_disabled`, and rename the per-file line-limit code to `too_many_lines` so it can't be read as the 429. DW-994
Deploying control-layer with
|
| Latest commit: |
64924fc
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://a07e7907.control-layer.pages.dev |
| Branch Preview URL: | https://steffen-dw-994-make-compress.control-layer.pages.dev |
There was a problem hiding this comment.
0 issues found across 3 files (changes from recent commits).
Auto-approved: Adds per-code upload-refusal counters and decode-slot wait histogram/gauge; observability-only, no request-handling behavior change, codes zero-registered and tests cover the counting.
Tip: Get a Slack DM when cubic finishes reviewing your PRs. Connect Slack
View guided diff | Turn on auto-fix | Re-trigger cubic
Closes DW-994. Follow-up to DW-985 (#2082, #2083).
Compressed request bodies and compressed batch uploads shipped with good coverage for realtime refusals:
unsupported_content_encoding,body_decode_failedandrequest_too_largego intoonwards_rejections_total, and the gateway rejection alerts pick them up. Two failure modes from that work had no signal. This PR adds one for each.Gap 1: file-upload refusals weren't counted by reason
Batch uploads don't go through the inference body layer.
upload_filerefuses them through dwctl's ownError: a corrupt.jsonl.gzgets a 400, an xz/bzip2/zip/lz4 file gets a 415, and so does a body that isn'tmultipart/form-data. None of these reachonwards_rejections_total, anddwctl_http_requests_totalonly gives the status, so they were mixed in with every other 400 and 415 on the files route.New counter:
dwctl_file_upload_rejections_total{code}. It covers everyFileUploadErrorvariant, plus the refusalsupload_filemakes before it reads the file:not_multipartmultipart/form-dataupload_capacitybatch_disabledinvalid_multipartstream_interruptedfile_too_largelimits.files.max_file_size, from eitherContent-Lengthor the streamtoo_many_linesmax_requests_per_fileinvalid_json/invalid_utf8invalid_compressionunsupported_compressionno_file/empty_file/invalid_purposemodel_access_deniedvalidation_error/unprocessable_lineimage_unfetchableimage_transient/image_store_failed/image_access_unavailable/image_fetch_failedEvery code is registered at 0 when the Prometheus recorder is installed, so the first refusal of a kind is a 0 → 1 step that
increase()can see.Why a dedicated counter rather than
rejections::refuse. I looked at recording these through the existing rejection machinery so they'd land inonwards_rejections_total. It doesn't fit well:Recorderextension isn't installed there.refusealso works on aResponse, but the handler returns dwctl'sError.modelwould always be empty, because one upload spans many models.trafficonly distinguishes realtime from dispatched inference, so an upload would be labelledrealtimeinference traffic.onwards_rejections_totalcounts client errors. Several upload failures are 503s caused by our image store or an image origin. Mixing them in would make a server problem look like a client one.GatewayRejectionNewandGatewayRejectionSpikeare tuned for inference traffic. A separate series lets upload alerts get their own thresholds.The cost is that the existing dashboard and alerts don't pick the new series up on their own, so they need adding there.
Gap 2: decode-slot waits weren't measured
Decoding a compressed realtime body takes one of
DECODE_SLOTS, one per CPU. Requests over that bound wait rather than fail, so if the slots saturate (a burst of compressed traffic, or abuse, since decoding runs before authentication) compressed requests just get slower, and nothing measured that.in_slotnow records:dwctl_inference_decode_slot_wait_seconds(histogram): time from asking for a slot to getting one. Buckets are 1 ms to 5 s with an edge at 0.25 s, so a 250 ms threshold reads an exact bucket. A free slot is taken at once, so almost every observation lands in the first bucket.dwctl_inference_decodes_waiting(gauge): decodes waiting for a slot right now. It's set to 0 at startup. A drop guard decrements it, so a request dropped while waiting leaves the gauge and records no wait.Behaviour is unchanged: requests still wait and are never refused, and the permit still moves into the blocking task.
Tests
test_every_upload_refusal_is_counted_by_its_code: builds everyFileUploadErrorvariant, refuses it under a local recorder, and checks it's counted once under its own code, and that the code is in the zero-registered list.test_upload_rejection_codes_start_at_zero: every code renders at 0 after initialisation.test_upload_capacity_refusals_are_counted: holds the only upload slot and checks the refused upload's 429 is counted asupload_capacity.records_how_long_a_decode_waited_for_its_slot: uses a paused tokio clock, so it's deterministic with no sleeps. It holds the only slot, checks the gauge reads 1, advances the clock 300 ms, releases the slot, and checks the wait lands between the 0.25 and 0.5 buckets. A decode that found a free slot lands in the first bucket.a_request_dropped_while_waiting_leaves_the_queue: dropping a waiting decode takes the gauge back to 0 and records no duration.just lint rustandjust test rustpass.