Skip to content

Add opt-in append_blob write mode for WASB task logs - #71064

Open
bujjibabukatta wants to merge 8 commits into
apache:mainfrom
bujjibabukatta:fix/#70867
Open

Add opt-in append_blob write mode for WASB task logs#71064
bujjibabukatta wants to merge 8 commits into
apache:mainfrom
bujjibabukatta:fix/#70867

Conversation

@bujjibabukatta

Copy link
Copy Markdown
Contributor

Summary
WASB task logs are rewritten from scratch on every upload -- the whole
existing blob is downloaded, the new segment appended in memory, and the
full result re-uploaded. For a long-running task this means O(N^2) traffic
across N log writes instead of O(N).

Root cause
WasbRemoteLogIO.write() always calls hook.load_string(..., overwrite=True),
which replaces the entire blob object regardless of how much of it is
actually new.

Fix
Added an opt-in write_mode="append_blob" that uses Azure's native AppendBlob
API (append_block() with a position guard) to append only the new segment,
instead of rewriting the whole object. Falls back to the existing
block-blob rewrite path for any blob already written as a block blob, since
Azure doesn't support converting a blob's type in place. Segments over the
4 MiB per-block limit are chunked automatically, and a concurrent-writer
position mismatch fails safely rather than writing at the wrong offset.

closes: #70867

Was generative AI tooling used ?

  • Yes - Claude

Generated-by: Claude following the guidelines

@bmanan7

bmanan7 commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Thanks for picking this up. Nice to see it go straight to append_block() with appendpos_condition rather than upload_blob(blob_type="AppendBlob"), since that distinction is easy to miss, and the block-blob fallback for existing objects is exactly right.

One thing I think is worth handling before this lands: the chunk loop can leave a lifecycle partially applied, and nothing records how much of it landed.

Take a segment larger than the 4 MiB block limit, say 10 MiB split into A1, A2, A3:

  1. append_block(A1, offset=0) commits. The blob is now 4 MiB.
  2. append_block(A2, offset=4MiB) hits a transient error, so write() returns False.
  3. upload() sees has_uploaded=False and correctly leaves the local file intact so the segment can be retried (the behaviour added in Fix duplicated task logs in the WASB log handler #70860).

The blob now holds A1, while the local file still holds A1+A2+A3. On the next upload to the same key, offset is read as properties.size, which is 4 MiB, and the whole local file is appended from there. A1 ends up stored twice.

appendpos_condition does not catch this: the blob really is 4 MiB at that point, so the guard passes. It protects against writing at the wrong offset, not against re-sending content that already landed. The block-blob path never had this problem because its single overwrite=True PUT is atomic, so a lifecycle could not end half-applied.

It needs a second upload against the same key to surface, so in practice a deferrable resume or a reschedule poke whose individual lifecycle writes more than 4 MiB. Rare, but it is the same class of duplication that #70860 just fixed, which is why I thought it worth raising.

The awkward part is that write() receives the log string and returns a bool, so it has no way to tell upload() "4 of 10 MiB committed" or to trim the local file itself. A few directions that might work:

  • Retry the remaining chunks in place before giving up, which narrows the window without closing it.
  • Return bytes written, or move the chunking up into upload(), so the committed prefix can be trimmed from the local file and the "local file is exactly what has not been uploaded" invariant holds again.
  • Simplest option: do not chunk, and fall back to the block-blob path for segments over the limit, keeping the append path atomic.

Drafted-by: Claude Code (Opus 5); reviewed by @bmanan7 before posting

@bujjibabukatta

Copy link
Copy Markdown
Contributor Author

Hi @bmanan7 — good catch. Went with your second option: _write_append_blob
now returns bytes committed, and upload() trims the local file to just
the uncommitted part on a partial failure, so a retry doesn't resend what
already landed. Also caught that chunk boundaries could split a multi-byte
character, so fixed that too. Added tests for both.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

WASB task log uploads rewrite the whole log each time instead of appending

2 participants