Skip to content

[Improvement-18459][Common] Cap whole-file log download at 47MB to prevent OOM - #18463

Open
xmg333 wants to merge 2 commits into
apache:devfrom
xmg333:fix/log-download-size-check
Open

[Improvement-18459][Common] Cap whole-file log download at 47MB to prevent OOM#18463
xmg333 wants to merge 2 commits into
apache:devfrom
xmg333:fix/log-download-size-check

Conversation

@xmg333

@xmg333 xmg333 commented Aug 4, 2026

Copy link
Copy Markdown

Was this PR generated or assisted by AI?

YES. Implementation and tests drafted with assistance from Claude (Anthropic); reviewed by human.

Purpose of the pull request

getFileContentBytesFromLocal read entire files into memory with no size limit. Downloading a large task log caused OOM on the worker.
This PR caps the read at 47 MB and returns a clear error for oversized logs.

Why 47 MB, not 64 MB? The byte[] is JSON-serialized as base64 (~1.33× expansion) before RPC transmission. 47 MB raw → ~63 MB JSON body, staying under the 64 MB maxFrameSize in TransporterDecoder. 64 MB raw would produce ~86 MB body and be rejected by TooLongFrameException.

close #18459

Brief change log

  • LogUtils: add MAX_LOG_DOWNLOAD_SIZE = 47 MB; getFileContentBytesFromLocal stops reading once the limit is reached.
  • LogServiceImpl: checks file size before reading; returns ERROR with a clear message for oversized logs instead of silently truncating.

Verify this pull request

This change added tests and can be verified as follows:

  • LogServiceImplTest: a 48 MB file returns ERROR with message containing "exceeds maximum download size".
  • ./mvnw spotless:check passes.

Pull Request Notice

Pull Request Notice

@SbloodyS SbloodyS changed the title [Fix-18459][Common] Cap whole-file log download at 47MB to prevent OOM [Improvement-18459][Common] Cap whole-file log download at 47MB to prevent OOM Aug 5, 2026
@SbloodyS SbloodyS added the improvement make more easy to user or prompt friendly label Aug 5, 2026
@SbloodyS SbloodyS added this to the 3.5.0 milestone Aug 5, 2026
@SbloodyS SbloodyS added the first time contributor First-time contributor label Aug 5, 2026

@SbloodyS SbloodyS left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

For a log larger than 47 MB:

  1. LogServiceImpl#getTaskInstanceWholeLogFileBytes returns ERROR.
  2. LogClientDelegate#getWholeLogBytes treats every local error as a reason to call remoteLogClient.getWholeLog(...).
  3. RemoteLogClient calls getFileContentBytesFromRemote, which now uses the same capped reader and silently returns only the first 47 MB.

With remote logging enabled, the API can therefore return a successfully downloaded but truncated log. With remote logging disabled or unavailable, it may return an empty log or a generic download error instead of the clear size-limit message.

Please distinguish “local log unavailable” from “log exceeds the supported size,” propagate the latter to the API, and make the reader fail explicitly rather than silently truncating. The regression test should cover the complete LogClientDelegate/API path, not only LogServiceImpl.

Additionally, the linked issue expects large logs to remain downloadable through chunked streaming. This PR rejects them entirely.

getFileContentBytesFromLocal reads entire files into memory with no limit,
causing OOM when downloading large task logs. This PR adds a chunked RPC
(getTaskInstanceLogFileChunk) that reads 8MB at a time via RandomAccessFile,
and streams the result to the HTTP response via StreamingResponseBody.

Architecture: try chunk -> catch -> fallback whole
- New worker: chunked RPC succeeds, any size log is downloadable
- Old worker: chunk RPC fails, falls back to legacy getWholeLogBytes
- Mid-stream failure (bytes already written): throws IOException (no corruption)
- First-chunk failure (nothing written): safe fallback to remote legacy

The legacy getTaskInstanceWholeLogFileBytes is unchanged (backward compatible).
readFileRange fails explicitly (IOException) on missing files rather than
silently returning empty bytes.

Co-Authored-By: Claude <noreply@anthropic.com>
@xmg333
xmg333 force-pushed the fix/log-download-size-check branch from 2aa5e28 to f7168cf Compare August 5, 2026 05:44
@xmg333
xmg333 requested a review from caishunfeng as a code owner August 5, 2026 05:44
@xmg333

xmg333 commented Aug 5, 2026

Copy link
Copy Markdown
Author

Thanks for the review @SbloodyS . I've reworked the approach based on your feedback. New ** chunked streaming log ** is now completed. Could you confirm if this scope is what you had in mind?

What's new

New RPC: getTaskInstanceLogFileChunk(path, offset, length) reads an 8 MB range via RandomAccessFile. The API loops this RPC and streams
each chunk to the HTTP response via StreamingResponseBody.

Fallback logic (the important part)

The API server runs a chunked loop with an offset counter tracking bytes already written to the response:

streamWholeLog(taskInstance, outputStream):
    if worker not in registry:
        → remote getWholeLogBytes (legacy, unchanged)

    offset = 0
    loop:
        try:
            chunk = localLogClient.getLogChunk(offset, 8MB)
            if chunk.code != SUCCESS:
                if offset == 0:  ← nothing written yet, safe to fallback
                    → remote getWholeLogBytes; return
                else:            ← bytes already streamed, can't restart
                    → throw IOException
            write chunk.bytes; offset += chunk.bytes.length
            if chunk.eof: return
        catch Exception:          ← old worker (method not found), timeout, etc.
            if offset == 0:      ← still safe
                → remote getWholeLogBytes; return
            else:
                → throw IOException

The core invariant: fallback only happens when offset == 0 (nothing written yet). Once bytes have been streamed (offset > 0), there's no
safe way to restart — falling back to getWholeLogBytes would write the whole file from the beginning, duplicating the prefix that's already in
the response. So mid-stream failures throw instead.

Three concrete scenarios:

Scenario offset Behavior
Old worker, first chunk RPC fails (method not found) 0 → fallback to legacy getWholeLogBytes (rolling upgrade safe)
Worker dies mid-stream after writing 24 MB 24 MB → throw IOException (client sees truncated download, not corrupted)
Worker offline from the start 0 → go directly to remote getWholeLogBytes

Legacy path unchanged: getTaskInstanceWholeLogFileBytes and getFileContentBytesFromLocal are untouched from upstream/dev — no silent
truncation, no size cap added.

readFileRange (the new reader) fails explicitly: missing file → IOException, not empty bytes.

Tests cover all three scenarios in LogClientDelegateTest.

@xmg333
xmg333 force-pushed the fix/log-download-size-check branch 7 times, most recently from 93169b9 to b2731e8 Compare August 6, 2026 17:15
Comment on lines +196 to +200
final byte[] bytes = remoteLogClient.getWholeLog(taskInstance);
if (bytes != null && bytes.length > 0) {
outputStream.write(bytes);
}
outputStream.flush();

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.

If remote returns null/empty (archive missing), this still flush()es and the download ends as HTTP 200 with only the log header. Please throw when bytes are absent so a missing remote log is not reported as a successful download.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Thanks for catching this. Fixed in LogClientDelegate.writeRemoteLegacy: when remoteLogClient.getWholeLog(...) returns null/empty (remote archive missing), it now throws IOException instead of flushing an empty body. The exception propagates through streamWholeLog → StreamingResponseBody and aborts the response, so a missing log is no longer reported as a successful HTTP 200 download.
While reviewing the fix I also found and fixed a related gap: LoggerServiceImpl.checkDownloadLogAuth validated host but not logPath. It's now fixed and would return a clear error before streaming starts.

Replace whole-file log download with chunked streaming:
- Add ILogService#getTaskInstanceLogFileChunk RPC to read [offset, offset+length)
  ranges from the worker, clamped to 8 MB per chunk.
- Worker computes EOF from data.length < length, avoiding a second file stat.
- API streams chunks via StreamingResponseBody; auth is checked synchronously
  before the HTTP response is committed so @ApiException still returns JSON errors.
- On chunked RPC failure at offset==0, fall back to the legacy whole-file worker
  RPC (getTaskInstanceWholeLogFileBytes) — which old workers without the chunk
  method still serve — then to remote log storage. Mid-stream failure throws
  IOException to avoid a corrupted download.
- When the final remote-storage fallback yields no data (missing archive), throw
  IOException so a missing log is not reported as a successful header-only HTTP 200.
- checkDownloadLogAuth validates logPath (not just host) to avoid NPE on a task
  whose host is set but whose log path is not yet persisted.
- Remove catch-and-write-error-text inside StreamingResponseBody.
- Add explicit Content-Type application/octet-stream.
- Tests cover the full path end-to-end: LogServiceImpl (worker chunk RPC),
  LogClientDelegate (chunk loop + fallbacks + mid-stream failure + missing-remote
  throw), LoggerServiceImpl (auth + logPath guard + streaming), a real-RPC
  LogStreamingIntegrationTest (multi-chunk download + old-worker legacy fallback),
  and LoggerController (MockMvc: auth failure returns JSON, success streams
  octet-stream).

Co-Authored-By: Claude <noreply@anthropic.com>
@xmg333
xmg333 force-pushed the fix/log-download-size-check branch from b2731e8 to 2191ccc Compare August 7, 2026 15:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend first time contributor First-time contributor improvement make more easy to user or prompt friendly test

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Improvement] [API] Apiserver OOM when downloading large task log

3 participants