Skip to content

Implement stateful per-chunk streaming compression - #2703

Closed
SavinduDimal wants to merge 5 commits into
wso2:mainfrom
SavinduDimal:stream-compressor
Closed

Implement stateful per-chunk streaming compression#2703
SavinduDimal wants to merge 5 commits into
wso2:mainfrom
SavinduDimal:stream-compressor

Conversation

@SavinduDimal

@SavinduDimal SavinduDimal commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Purpose

Issue 1: Analytics + Streaming

  • Commit 70326d6 fixes the initial streaming not working while analytics is enabled by introducing changes to the chunk recompressor. Previously we re-compressed each streaming chunk by calling recompressBody, which spins up a fresh writer and Close()s it every chunk. so each chunk became its own independent, self-contained gzip/brotli member. streamCompressor keeps one writer alive for the life of the stream, flushing (Z_SYNC_FLUSH) after each chunk and closing only at EndOfStream, producing a single continuous member the client decodes in full.

Issue 2: Timeout error

  • Then found a new issue when we work with large responses, response flow got stalled and faced timeout errors.

  • The old streamDecompressor used an io.Pipe + decoder goroutine + a bounded output channel (cap 64). On long/large responses the channel filled, the decoder blocked on the send, stopped reading the pipe, and the next FeedChunk blocked forever on pw.Write — a back-pressure deadlock. This is the mid-stream stall (stream would forward ~part of the body, then go silent until the client timed out). It only showed up on large tasks, which is why short prompts worked and the original 2-chunk tests missed it.

  • A minimal patch could stop the deadlock, but the io.Pipe design has a deeper flaw: pw.Write returning tells you bytes were copied to the reader, not decoded, so FeedChunk had no reliable signal for "all output for this chunk is ready" — it relied on a racy runtime.Gosched() + best-effort drain. Getting a correct signal on top of the pipe means reimplementing exactly what the refactor introduces, so the pipe becomes dead weight.

  • The refactor replaces io.Pipe + channel + errChan + Gosched with a single sync.Cond + a feederReader that records when the decoder has consumed all fed input and parked. FeedChunk now returns exactly when every decodable byte is available — deadlock-free and deterministic — and ends up with fewer moving parts than before.

@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

The PR redesigns streaming decompression with coordinated shared state, adds persistent gzip/Brotli compression across chunks, integrates compressors into streaming request and response translation, and adds regression coverage.

Streaming codec processing

Layer / File(s) Summary
Streaming decompressor state machine
gateway/gateway-runtime/policy-engine/internal/kernel/decompression.go, gateway/gateway-runtime/policy-engine/internal/kernel/decompression_test.go
Coordinates decoder input, output buffering, lifecycle state, closure, terminal errors, and regression coverage for stalls and high-ratio streams.
Stateful compressor lifecycle
gateway/gateway-runtime/policy-engine/internal/kernel/decompression.go, gateway/gateway-runtime/policy-engine/internal/kernel/decompression_test.go
Maintains continuous gzip and Brotli streams, flushes intermediate chunks, closes at end of stream, supports passthrough encoding, and validates closure behavior.
Request and response translator integration
gateway/gateway-runtime/policy-engine/internal/kernel/execution_context.go, gateway/gateway-runtime/policy-engine/internal/kernel/translator.go
Stores per-stream compressor state, closes response decompression on terminal paths, and routes chunks through persistent compressors with error handling.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant StreamChunk
  participant StreamingTranslator
  participant PolicyExecutionContext
  participant streamCompressor
  participant CompressionWriter
  StreamChunk->>StreamingTranslator: Translate streaming chunk
  StreamingTranslator->>PolicyExecutionContext: Get or create compressor
  StreamingTranslator->>streamCompressor: FeedChunk(outputBody, endOfStream)
  streamCompressor->>CompressionWriter: Write and flush chunk
  streamCompressor->>CompressionWriter: Close at end of stream
  streamCompressor-->>StreamingTranslator: Return compressed output
  StreamingTranslator-->>StreamChunk: Forward translated chunk
Loading

Suggested reviewers: pubudu538, malinthaprasan, tgtshanika, chamilaadhi, lasanthas

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description covers purpose and some context, but most required template sections are missing or empty. Add the missing Goals, Approach, User stories, Documentation, Automation tests, Security checks, Samples, Related PRs, and Test environment sections.
✅ Passed checks (4 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly matches the main change: introducing stateful per-chunk streaming compression.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@gateway/gateway-runtime/policy-engine/internal/kernel/translator.go`:
- Around line 1527-1533: The request recompression failure path in translator.go
around lines 1527-1533 must fail or terminate the stream instead of sending
uncompressed data; apply the same change to the response recompression failure
path around lines 1615-1621. Preserve the forwarded Content-Encoding state and
propagate the recompression error or terminate the corresponding
request/response stream rather than falling back to raw chunks.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 26680381-4b10-4551-99e1-c5776bd558a9

📥 Commits

Reviewing files that changed from the base of the PR and between f59bd67 and f10ff74.

📒 Files selected for processing (4)
  • gateway/gateway-runtime/policy-engine/internal/kernel/decompression.go
  • gateway/gateway-runtime/policy-engine/internal/kernel/decompression_test.go
  • gateway/gateway-runtime/policy-engine/internal/kernel/execution_context.go
  • gateway/gateway-runtime/policy-engine/internal/kernel/translator.go

Comment thread gateway/gateway-runtime/policy-engine/internal/kernel/translator.go Outdated
coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 15, 2026
@SavinduDimal
SavinduDimal requested a review from Piumal1999 as a code owner July 22, 2026 13:53

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@gateway/gateway-runtime/policy-engine/internal/kernel/decompression.go`:
- Around line 67-78: Bound decompressed output in streamDecompressor instead of
retaining an unbounded bytes.Buffer. Update the decoder/output flow used by
FeedChunk to enforce maxStreamAccumulatorSize while preserving chunked
consumption and terminal error behavior, and avoid copying decoded payloads
beyond the configured cap.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6dbe6d62-8a49-4c01-b1b3-eb6cd0e247cc

📥 Commits

Reviewing files that changed from the base of the PR and between f10ff74 and 711023c.

📒 Files selected for processing (4)
  • gateway/gateway-runtime/policy-engine/internal/kernel/decompression.go
  • gateway/gateway-runtime/policy-engine/internal/kernel/decompression_test.go
  • gateway/gateway-runtime/policy-engine/internal/kernel/execution_context.go
  • gateway/gateway-runtime/policy-engine/internal/kernel/translator.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • gateway/gateway-runtime/policy-engine/internal/kernel/execution_context.go

coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 22, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
gateway/gateway-runtime/policy-engine/internal/kernel/execution_context.go (1)

858-875: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Close the response decompressor on error paths.

The new cleanup runs only after ExecuteStreamingResponsePolicies succeeds. If it returns an error at Line 859, the function exits before responseStreamDecomp.Close(), leaving the decoder goroutine blocked. A translation error from Line 875 can cause the same leak. Centralize cleanup and invoke it before every terminal/error return.

Proposed cleanup flow
+		closeResponseDecomp := func() {
+			if ec.responseStreamDecomp != nil {
+				ec.responseStreamDecomp.Close()
+				ec.responseStreamDecomp = nil
+			}
+		}
+
 		execResult, err := ec.server.executor.ExecuteStreamingResponsePolicies(
 			ctx,
 			ec.policyChain.Policies,
@@
 		)
 		if err != nil {
+			closeResponseDecomp()
 			return ec.handlePolicyError(ctx, err, "response_body_streaming"), nil
 		}
@@
-		return TranslateStreamingResponseChunkAction(execResult, chunk, ec)
+		resp, err := TranslateStreamingResponseChunkAction(execResult, chunk, ec)
+		if err != nil {
+			closeResponseDecomp()
+		}
+		return resp, err
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@gateway/gateway-runtime/policy-engine/internal/kernel/execution_context.go`
around lines 858 - 875, Centralize response decompressor cleanup in the
streaming response execution flow, using the existing responseStreamDecomp
lifecycle and terminal conditions. Ensure cleanup runs before the
handlePolicyError return when ExecuteStreamingResponsePolicies fails, before
returning any TranslateStreamingResponseChunkAction error, and on normal
EndOfStream or StreamTerminated completion; keep it idempotent and clear the
field after closing.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@gateway/gateway-runtime/policy-engine/internal/kernel/execution_context.go`:
- Around line 858-875: Centralize response decompressor cleanup in the streaming
response execution flow, using the existing responseStreamDecomp lifecycle and
terminal conditions. Ensure cleanup runs before the handlePolicyError return
when ExecuteStreamingResponsePolicies fails, before returning any
TranslateStreamingResponseChunkAction error, and on normal EndOfStream or
StreamTerminated completion; keep it idempotent and clear the field after
closing.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 5c5b67d6-e25d-488e-bb25-1d9d833b6f9d

📥 Commits

Reviewing files that changed from the base of the PR and between 711023c and 47a7d90.

📒 Files selected for processing (4)
  • gateway/gateway-runtime/policy-engine/internal/kernel/decompression.go
  • gateway/gateway-runtime/policy-engine/internal/kernel/decompression_test.go
  • gateway/gateway-runtime/policy-engine/internal/kernel/execution_context.go
  • gateway/gateway-runtime/policy-engine/internal/kernel/translator.go
🚧 Files skipped from review as they are similar to previous changes (3)
  • gateway/gateway-runtime/policy-engine/internal/kernel/translator.go
  • gateway/gateway-runtime/policy-engine/internal/kernel/decompression_test.go
  • gateway/gateway-runtime/policy-engine/internal/kernel/decompression.go

@renuka-fernando

Copy link
Copy Markdown
Contributor

Related PR: #2878

@SavinduDimal

Copy link
Copy Markdown
Contributor Author

Closing this PR since the issues are not reproducible after PR: #2878

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.

4 participants