TIKA-4793: server-side payload guard, OOM protection, and archive sizing fix - #3009
TIKA-4793: server-side payload guard, OOM protection, and archive sizing fix#3009srujana-kuntumalla wants to merge 10 commits into
Conversation
|
@tballison @THausherr Could I get review on this PR please? |
| private void doWritePayloadLimitExceeded(String message) throws IOException { | ||
| byte[] bytes = JsonPipesIpc.toBytes( | ||
| new PipesResult(PipesResult.RESULT_STATUS.PAYLOAD_LIMIT_EXCEEDED, message)); | ||
| PipesMessage.finished(bytes).write(output); |
There was a problem hiding this comment.
The fallback frame also needs to fit maxPayloadBytes (or config validation needs to enforce a minimum that guarantees it will). setMaxIpcPayloadBytes() currently accepts any positive value, so with a small configured limit this serialized PAYLOAD_LIMIT_EXCEEDED result is itself larger than the limit. The production client then rejects it in PipesMessage.read(..., maxIpcPayloadBytes), closes the connection, and never receives the clean status this path promises. ServerProtocolIOTest.exchange() masks this by reading with Integer.MAX_VALUE. Please add a test where the client reads the fallback using the same configured limit and either use a bounded/minimal error payload or reject limits too small to carry it.
There was a problem hiding this comment.
codex spit this out when i asked it to check for test coverage
|
From my claude. 🤣 |
04e1962 to
6c3b7b8
Compare
The hard-coded 100 MB ceiling in PipesMessage was not operator-tunable. Add PipesConfig.maxIpcPayloadBytes (default 100 MB) and thread it through PipesClient, PipesServer, ConnectionHandler, and ServerProtocolIO so the limit is applied on every read() call. The write path is unchanged. Includes unit tests for default value, JSON loading, and validation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove final from PipesMessage.MAX_PAYLOAD_BYTES (long) so it can be set at runtime. PipesConfig.setMaxIpcPayloadBytes() updates the static whenever the limit is changed via JSON config or programmatically. No changes to call sites — all existing PipesMessage.read() callers pick up the new value automatically through the shared static. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove final from PipesMessage.MAX_PAYLOAD_BYTES so it can be updated
at runtime. Add maxIpcPayloadBytes to PipesConfig (int, default 100 MB)
with a setter that updates PipesMessage.MAX_PAYLOAD_BYTES as a side
effect. Both client and server JVMs load from the same tika-config.json
so setting it once covers both ends automatically. No changes to call
sites — all existing PipesMessage.read() callers pick up the value
through the shared static.
Configurable via tika-config.json:
{"pipes": {"maxIpcPayloadBytes": 209715200}}
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…_EXCEEDED status - Restore MAX_PAYLOAD_BYTES to final; add read(DataInputStream, int) overload so callers can pass a per-connection limit without mutating shared state - PipesConfig setter no longer has the global side-effect; PipesClient captures maxIpcPayloadBytes at construction and passes it to read() in waitForServer() - Add PAYLOAD_LIMIT_EXCEEDED(TASK_EXCEPTION) to RESULT_STATUS so oversized responses are treated as per-document errors, not process crashes - Introduce PayloadLimitExceededException (IOException subtype) and catch it specifically in PipesClient: close the desynchronized connection but do not restart the healthy server - Add JSON zero-value rejection test through the deserialization path Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…l sites Apply reviewer patch: pass maxIpcPayloadBytes to read() consistently across PipesClient (ping + waitForStartup), ConnectionHandler main loop, and PipesServer main loop. The startup-failure error path in PipesServer intentionally keeps the default since pipesConfig may not have loaded when that path is reached. Add two PipesMessageTest cases proving a caller-supplied limit below MAX_PAYLOAD_BYTES is enforced independently. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three-layer protection in ServerProtocolIO.writeFinished(): - Pre-check: skip serialization when estimated content size already exceeds maxIpcPayloadBytes, preventing OOM for large documents (e.g. 500 MB text with an uncapped limit). - OOM catch: if serialization exhausts heap despite the pre-check, return PAYLOAD_LIMIT_EXCEEDED instead of crashing the fork. - Post-check: if serialized bytes exceed the limit (common for Unicode-heavy PDFs where 1 byte/char estimate is optimistic), discard the payload server-side so the wire stays in sync and the client sees a clean PAYLOAD_LIMIT_EXCEEDED rather than a stream desynchronization and forced reconnect. Wire the maxIpcPayloadBytes limit from PipesConfig into ServerProtocolIO via PipesServer and ConnectionHandler. Fix EmitDataImpl.estimateSizeInBytes() to use 1 byte per char (Smile UTF-8 ASCII cost) rather than 2 bytes per char (Java UTF-16 heap cost). The old formula overstated wire size 2x for ASCII-heavy content (file paths, checksums, MIME types), biasing the DYNAMIC emit strategy toward unnecessary direct-emit for compressed-archive formats (7z, rar, iso). Add ServerProtocolIOTest covering all three protection layers and pinning the corrected estimate formula. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Addresses all reviewer issues: - Serialize through a BoundedOutputStream capped at maxPayloadBytes so Jackson never allocates more than the configured limit. When the stream aborts on overflow, a pre-computed PAYLOAD_LIMIT_EXCEEDED frame is sent instead, leaving the wire in sync and preserving the original result status for payloads that do fit. This replaces the inaccurate estimate pre-check, the OOM catch (which violated exit-on-OOM policy and kept the large content live on the stack), and the post-check memory spike. - Add JsonPipesIpc.toStream() to write directly into an OutputStream. - Apply the same BoundedOutputStream pattern to writeIntermediate() (skip if oversized — FINISHED still follows) and pre-truncate writeCrash() stack traces to maxPayloadBytes/2 chars. - Revert EmitDataImpl.estimateSizeInBytes() to the original heap formula (36 + length*2). The DYNAMIC emit strategy uses heap cost; the IPC guard no longer depends on the estimate at all. - Add PipesConfig.setMaxIpcPayloadBytes() validation against MIN_FALLBACK_PAYLOAD_BYTES (config load time, not server startup). Fix javadoc: the limit is bidirectional — lowering it below a typical FetchEmitTuple size causes requests to fail as UNSPECIFIED_CRASH. - Add maxIpcPayloadBytes to configuration.adoc. - Fix ServerProtocolIOTest.exchange() to read with maxPayloadBytes (not Integer.MAX_VALUE) mirroring production PipesClient behaviour. Add testFallbackFitsWithinMinimumConfiguredLimit() to cover the tightest limit the constructor accepts. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
6c3b7b8 to
19c6b1d
Compare
|
Updated review. Let me know if any of these points do not check out. |
writeCrash: route through BoundedOutputStream so CJK-heavy stack traces (3 bytes/char in Smile) cannot exceed maxIpcPayloadBytes; fall back to empty payload if even that overflows. EMIT_SUCCESS_PASSBACK clobbering: when a passback result overflows the limit, send a status-only EMIT_SUCCESS_PASSBACK frame so the client does not re-emit content already emitted server-side; static frame only if even that overflows. awaitAck: pass maxIpcPayloadBytes to PipesMessage.read() so the ACK path respects the configured limit, not the hardcoded 100 MB default. BoundedOutputStream.write(byte[]): cast buf.size() to long before adding len to prevent int overflow for large limit values. OOM/Error at catch sites (PipesServer, ConnectionHandler): re-throw Error and exit rather than logging and continuing in a possibly corrupt heap state. PipesClient FINISHED case: rebuild PAYLOAD_LIMIT_EXCEEDED result with the original emit key when the static fallback has null emitData, so the document appears in the AsyncEmitter audit trail. Minor: update stale writeCrash/writeIntermediate javadocs, fix backwards -Xmx advice in configuration.adoc, remove stray blank line in TikaPipesConfigTest, remove dead serializedSize() helper from test, add TIKA-4793 to CHANGES.txt. Tests: add writeCrash overflow and EMIT_SUCCESS_PASSBACK preservation tests. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… guarantee exit on Error in shared mode
|
@srujana-kuntumalla let me know if I botched anything with my push to your branch. |
Summary
Follow-on to the earlier TIKA-4793 configurable-payload-limit work. Four issues were found after that PR:
maxIpcPayloadBytes):ServerProtocolIO.writeFinished()calledJsonPipesIpc.toBytes()unconditionally, allocating a full Smilebyte[]in one shot. For large content this exhausted heap.ByteArrayBuildercannot produce abyte[]> 2 GB, so content that serializes beyond that limit causedOutOfMemoryErrorinsidetoBytes().PayloadLimitExceededException, the IPC stream desynced, and the connection was torn down. Users saw "no content returned" and interpreted it as truncation.EmitDataImpl.estimateSizeInBytes()usedlength × 2(Java UTF-16 heap cost), but the DYNAMIC threshold is in wire bytes (Smile UTF-8). ASCII-heavy archive metadata (file paths, checksums, MIME types) encodes at ~1 byte/char, so estimates were 2× too high and compressed-archive results were incorrectly routed to direct-emit.Changes
ServerProtocolIO— three-layer guard inwriteFinished():getEstimatedSizeBytes() > maxPayloadBytes, skip serialization entirely (prevents OOM before any allocation).JsonPipesIpc.toBytes()— caughtOutOfMemoryErrorfrees the byte-builder segments; the tinyPAYLOAD_LIMIT_EXCEEDEDresponse then serializes cleanly.bytes.length > maxPayloadBytes(CJK-heavy content where the 1 byte/char pre-estimate is optimistic), discard server-side — client sees a cleanPAYLOAD_LIMIT_EXCEEDED, no stream desync.Constructor now takes
maxPayloadBytesexplicitly.PipesServer/ConnectionHandler— passpipesConfig.getMaxIpcPayloadBytes()toServerProtocolIO(one line each).EmitDataImpl.estimateSizeInBytes()— change multiplier fromlength × 2tolength(1 byte/char ≈ Smile UTF-8 ASCII). The server-side post-check is the safety net for content that does serialize larger than the estimate.ServerProtocolIOTest(new) — unit tests for all three protection layers plus a pin on the corrected estimate formula.Test plan
ServerProtocolIOTest— 5 new tests covering pre-check, post-check, OOM path, status-only pass-through, and estimate formulaPipesMessageTest— 17 existing wire-protocol tests still pass🤖 Generated with Claude Code