ericdelorefice opened a new pull request, #74996:
URL: https://github.com/apache/airflow/pull/74996

   This brings the WASB remote log handler to the contract the GCS handler 
adopted in #67511: when an append-mode upload cannot read the log already 
stored in the blob, it keeps the local log and returns `False` instead of 
uploading over the blob with only the new content.
   
   ### Problem
   
   `WasbRemoteLogIO.write(log, remote_log_location, append=True)` checks 
whether the blob exists, reads it, and uploads old plus new content with 
`overwrite=True`. Both helpers it calls swallow errors: `wasb_log_exists` 
returns `False` on any exception, and `wasb_read` returns `""` on any 
exception. So when either request fails for a reason other than "blob not 
found" (a throttled request, a timeout, a network error), `write` treats the 
stored log as empty, replaces the blob with only the new segment and returns 
`True`. `upload()` then truncates the local file, because the write reported 
success. The earlier part of the attempt log is gone from both places.
   
   One attempt log is uploaded many times in some lifecycles. #70867 describes 
the clearest case: a reschedule-mode sensor writes every poke to the same 
`attempt=N.log`. One failed read during any poke drops every earlier poke from 
the stored log.
   
   ### Reproduction
   
   The real `WasbRemoteLogIO` from `main` (79ce98f110), the real `WasbHook` and 
`azure-storage-blob` 12.31.0, against Azurite (the Azure Storage emulator) 
holding the blob. The failure is injected: a small local proxy in front of 
Azurite answers requests for the log blob with HTTP 503 `ServerBusy`, which is 
what Azure Storage returns when it throttles, and azure-core retries three 
times before raising. I have not seen this in a user report. Nothing in Airflow 
or the Azure SDK is patched.
   
   Four pokes of a reschedule-mode sensor, each appending a line locally and 
calling `upload("attempt=1.log")`; poke 3 is throttled.
   
   On `main`, with the GET (read) throttled:
   
   ```
   --- after poke 2
   remote blob: 'poke 1: ...\npoke 2: ...\n'
   [error] Could not read logs from 
dag_id=d/run_id=r/task_id=sensor/attempt=1.log
   azure.core.exceptions.HttpResponseError: ... ErrorCode:ServerBusy
   --- after poke 3 (GET throttled)
   remote blob: 'poke 3: ...\n'
   local file : ''
   --- after poke 4
   remote blob: 'poke 3: ...\npoke 4: ...\n'
   ```
   
   With the HEAD (existence check) throttled instead, the result is the same, 
and nothing is logged above debug level, because `wasb_log_exists` logs the 
error at debug and returns `False`.
   
   With this change, both runs keep the stored log and the local copy, and the 
next poke uploads everything:
   
   ```
   [warning] Refusing to overwrite remote log 
dag_id=d/run_id=r/task_id=sensor/attempt=1.log: could not read existing content 
(... ErrorCode:ServerBusy). Keeping local logs for later retry.
   --- after poke 3 (GET throttled)
   remote blob: 'poke 1: ...\npoke 2: ...\n'
   local file : 'poke 3: ...\n'
   --- after poke 4
   remote blob: 'poke 1: ...\npoke 2: ...\npoke 3: ...\npoke 4: ...\n'
   local file : ''
   ```
   
   ### Change
   
   `write` now calls `hook.check_for_blob` and `hook.read_file` directly, 
inside one `try`. `check_for_blob` already returns `False` only for 
`ResourceNotFoundError` and raises for anything else, so a missing blob is 
still written as a new blob. Any other failure logs a warning naming the blob 
and the error and returns `False` without uploading, the same message and 
contract as the GCS handler. `append=False` is unchanged. `wasb_log_exists` and 
`wasb_read` are left as they are; nothing else in the provider calls them.
   
   Intended behaviour change: an append-mode upload that cannot read the 
existing log no longer uploads. The local log stays on the worker, and a later 
upload of the same attempt sends it. If there is no later upload, it stays only 
on the worker, the same trade-off #67511 made for GCS. A worker whose 
credentials can write blobs but cannot read them now keeps its logs locally 
instead of overwriting the blob on every upload.
   
   The S3 handler has the same read-side gap (`s3_read` returns `""` on error, 
and `write` appends to that). I will send that separately, since #73517 is 
changing the S3 read path.
   
   ### Testing
   
   macOS, Python 3.12.14, plain `pytest` (not Breeze) with `apache-airflow` 
3.3.2 from PyPI and this provider installed editable from the branch.
   
   
`providers/microsoft/azure/tests/unit/microsoft/azure/log/test_wasb_task_handler.py`:
   
   - 3 new tests fail on `main` and pass with this change: `write` returns 
`False` and does not call `load_string` when the existence check fails and when 
the read fails, and the warning names the blob and carries the storage error; 
and an `upload()` sequence where the third read fails leaves the stored blob 
and the local file untouched, then the fourth upload stores all four segments 
and truncates the local file.
   - 1 new control passes both ways: a blob that does not exist is written with 
the new content, and `read_file` is not called.
   - 3 existing tests (`test_write_log`, `test_write_on_existing_log`, 
`test_write_on_existing_log_already_newline_terminated`) patched 
`wasb_log_exists` and `wasb_read`; they now set the hook's `check_for_blob` and 
`read_file` instead, with the same expectations, and pass both ways.
   - With the source reverted to `main` and the new tests kept, the 3 fail and 
the controls pass, five runs each way.
   - Whole file: 31 passed on `main`, 35 passed on this branch.
   - `prek run --files` on both files: all 46 hooks that apply passed (ruff, 
ruff format, codespell, the provider import checks, lazy logging and the rest). 
The `mypy-providers` pre-push hook needs breeze, which needs Docker, and I 
don't have Docker on this machine, so I ran `mypy` with the repo's 
`pyproject.toml` configuration in the Azure provider's uv environment instead: 
no issues in either file.
   
   ---
   
   ##### Was generative AI tooling used to co-author this PR?
   
   - [X] Yes (please specify the tool below)
   
   Generated-by: Claude Code (Claude Opus 5.5) following [the 
guidelines](https://github.com/apache/airflow/blob/main/contributing-docs/05_pull_requests.rst#gen-ai-assisted-contributions)
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to