fix(transform): do not emit empty payloads from a stale aggregate - #4
Merged
Steven Patterson (stevepatterson5) merged 3 commits intoJul 27, 2026
Conversation
aggregate.Add refreshes a.now on every successful add as well as on reset, so the configured duration behaves as an idle timeout rather than a batch age limit. Once an aggregate has sat idle for longer than that duration, the next Add returns false. aggregate_to_array treats a false Add as "flush the current batch", but in this case the batch is still empty, so aggToArray returns nil and an empty message is emitted. send_http_post then POSTs an empty HTTP body, which a receiver rejects: LogScale answers 400 "The request is not valid JSON". In the seceng LogScale pipelines this accounted for roughly 40 rejected batches per hour on the busiest source. It presents as intermittent because it depends on instance idle time; with cpu_idle enabled, Cloud Run instances routinely sit idle for over the configured minute, and the first batch after an idle gap emits one empty POST. Skips the emit when the array is empty, in both the control-flush and the mid-stream paths, and guards send_http_post against posting an empty body so that any auxiliary transform combination is covered. The underlying Add semantics are left alone deliberately: changing when a.now is refreshed would alter batching for every transform and every caller. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Splitting on a separator yields blank elements for trailing or repeated
separators, and newline-delimited files normally end with a trailing newline.
aggregate_from_string turned each one into a message and aggToArray joined them
verbatim, producing a malformed array such as `[{"a":1},]`.
In practice a following transform that sets a key, such as time_now, rescued
the blank into a valid object, so the malformed array was rarely observed.
Instead the blank arrived at the sink as a content-free event, e.g.
{"seceng_pipeline_time":...}, injecting one junk record per blank line.
Blank and whitespace-only elements are now skipped in both places. The
whitespace check also covers the carriage return left behind when CRLF data is
split on a newline.
This is a separate defect from the empty-payload fix in the preceding commit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Steven Patterson (stevepatterson5)
requested a review
from a team
as a code owner
July 27, 2026 16:36
Steven Patterson (stevepatterson5)
requested review from
Preetika Kuntala (preetikakuntala) and
Rutvi Patel (rutvi126)
July 27, 2026 16:36
Contributor
✅ Snyk checks have passed. No issues have been found so far.
💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse. |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
🔗 Linked repositories identifiedCodeRabbit considers these linked repositories for cross-repo context during reviews:
🚧 Files skipped from review as they are similar to previous changes (1)
WalkthroughChangesAggregation now removes blank or whitespace-only elements, suppresses empty aggregate emissions including stale-batch cases, and prevents HTTP POST requests for empty transformed payloads. Aggregation empty-output handling
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
Comment |
Guarding against the empty aggregate added a nested branch inside the control flush, which pushed the block past the nestif threshold. Extracting the message construction removes the nesting and drops the duplication between the control and mid-stream paths, which built the same message two different ways. No behaviour change; TestAggregateToArrayStaleBatch still fails against the pre-fix aggregate_to_array.go and passes here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
sherinaren-liveramp
approved these changes
Jul 27, 2026
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.
Description
Two related but distinct defects in aggregation. The first is the cause of the
400 The request is not valid JSONresponses that LiveRamp/substation#3 made visible.1. Empty payloads from a stale aggregate (the 400 cause).
internal/aggregate.Addrefreshesa.nowon every successful add as well as on reset:So
durationbehaves as an idle timeout, not a batch-age limit. Once an aggregate has sat idle longer than that duration, the nextAddreturnsfalse.aggregate_to_arraytreats a falseAddas "flush the current batch" — but here the batch is still empty, soaggToArrayreturns nil and an empty message is emitted.send_http_postthen POSTs an empty HTTP body, which LogScale rejects as not valid JSON.Fixed by skipping the emit when the array is empty, in both the control-flush and mid-stream paths, plus a guard in
send_http_postso any auxiliary-transform combination is covered.2. Blank elements when splitting (separate, smaller).
Splitting on a separator yields blank elements for trailing or repeated separators, and NDJSON files normally end with a trailing newline. These became messages and were joined verbatim. In practice a following transform that sets a key (
time_now) rescued the blank into a valid object, so the malformed array was rarely seen — instead the blank reached the sink as a content-free event ({"seceng_pipeline_time":...}), injecting one junk record per blank line. Blank and whitespace-only elements are now skipped in both places; the whitespace check also covers the carriage return left when CRLF data is split on a newline.Motivation and Context
Measured in the seceng LogScale pipelines: roughly 40 rejected batches per hour on
gcp_cloudauditalone (~10.6% of objects). It presents as intermittent because it depends on instance idle time — withcpu_idleenabled, Cloud Run instances routinely sit idle for over the configured minute, and the first batch after an idle gap emits one empty POST.This explanation is consistent with everything observed: source objects contain no invalid JSON (5,340/5,340 lines parse), failures are independent of trailing newlines (a confirmed-failing object had none), they are size-independent, and rejections return in ~80 ms versus ~17 s for a successful ingest — an empty body is rejected immediately.
For transparency, two earlier hypotheses were investigated and falsified: that trailing-newline blanks corrupted the array (disproved —
time_nowrescues them), and thatbufio.Scannertruncated long lines (disproved — the cap is 128 MB on GCP, and the largest line was 76,612 bytes).How Has This Been Tested?
gofmtclean;go build ./...clean; full suite green across 12 packages underTZ=UTC.TestAggregateToArrayStaleBatch(new) — drives an aggregate past its duration and asserts no empty payload is emitted.TestAggToArray(new) — trailing blank, interior blank, whitespace-only, all-blank, and normal cases; asserts output is always valid JSON.TestAggregateFromString— new cases for trailing and repeated separators.aggregate_to_array.goyieldsemitted an empty payload; sinks cannot distinguish this from real data.FuzzTestAggToArrayandFuzzTestAggregateFromStringseeds still pass, including the existing empty-input seed.Without
TZ=UTC,TestTimeToStringandTestTransform/time_to_stringfail on any non-UTC host. Pre-existing and unrelated; CI and the container build both run UTC.Types of changes
Checklist:
Deliberately out of scope
The underlying
aggregate.Addsemantics are left alone. Refreshinga.nowon every add — makingdurationan idle timeout rather than a batch-age limit — is arguably the deeper design bug, but changing it would alter batching behaviour for every transform and every caller. Worth a maintainer opinion on whether that should be addressed separately.Verification after deploy
Unit tests prove the mechanism, not that it was the production cause; that is not provable in a test. The confirmation is the 400 rate dropping to zero once deployed, which should be obvious within an hour given ~40/hour on
gcp_cloudaudit.Tracked in SEC-1695.
🤖 Generated with Claude Code
Summary by CodeRabbit