Skip to content

Remove orphan cache file on non-ErrnoException download failure#110549

Merged
kssenii merged 4 commits into
ClickHouse:masterfrom
groeneai:groeneai/fix-filesystem-cache-orphan-non-errno-110532
Jul 17, 2026
Merged

Remove orphan cache file on non-ErrnoException download failure#110549
kssenii merged 4 commits into
ClickHouse:masterfrom
groeneai:groeneai/fix-filesystem-cache-orphan-non-errno-110532

Conversation

@groeneai

Copy link
Copy Markdown
Contributor

Closes: #110532

Changelog category (leave one):

  • Bug Fix (user-visible misbehavior in an official stable release)

Changelog entry (a user-readable short description of the changes that goes into CHANGELOG.md):

Fix a logical error in the filesystem cache (Expected file ... not to exist) that could occur when a background cache download failed with a non-ErrnoException (for example while OPTIMIZE-ing an Iceberg table), leaving an empty orphan cache file behind.

Description

FileSegment::write() creates the cache file before any bytes are accounted for. On a write failure, only the ErrnoException catch block removed the just-created empty file when downloaded_size == 0. The generic Exception and fs::filesystem_error catch blocks never got the same cleanup, so a non-ErrnoException failure after the file was created orphaned an empty file. The segment is later reset to EMPTY while the file persists, and assertCacheCorrectness (debug/sanitizer builds) then aborts with LOGICAL_ERROR.

Same assertion as the closed #99195, but reproduced on current master with a concrete trigger and background download: 1.

The fix mirrors the existing ErrnoException orphan-removal into the two non-Errno catch blocks, using the noexcept fs::remove overload so the original download error is not masked. A cache_filesystem_failure_non_errno failpoint and a stateless regression test reproduce the exact assertion.

cc @kssenii

FileSegment::write() creates the cache file before any bytes are accounted
for. When the write fails, only the ErrnoException catch block removes the
just-created empty file (when downloaded_size == 0). The generic Exception and
fs::filesystem_error catch blocks (added in 2024) never got the same removal,
so a non-Errno failure after the file is created leaves an empty orphan on
disk. The failing segment is later reset to EMPTY while the orphan persists,
and the debug/sanitizer cache self-check (assertCacheCorrectness) then aborts
with LOGICAL_ERROR "Expected file ... not to exist".

Concretely reproduced by OPTIMIZE TABLE on an Iceberg table: compaction reads
Avro manifests through the filesystem cache with background download enabled;
a manifest being background-downloaded is concurrently rewritten by the
compaction, so the background read fails with a non-ErrnoException.

Mirror the ErrnoException orphan-removal into the two non-Errno catch blocks.
The noexcept fs::remove overload is used so the original download error is not
masked. Adds a cache_filesystem_failure_non_errno failpoint and a regression
test that reproduces the exact assertion (background download: 1).

Closes: ClickHouse#110532

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@groeneai

Copy link
Copy Markdown
Contributor Author
Pre-PR validation gate (click to expand)
# Question Answer
a Deterministic repro? Yes. New cache_filesystem_failure_non_errno failpoint throws a generic (non-Errno) Exception after the cache file is created; a read then fails on demand.
b Root cause explained? FileSegment::write() creates the file before accounting bytes; only the ErrnoException catch removed the empty file when downloaded_size==0. The generic Exception / fs::filesystem_error catches did not, so a non-Errno failure orphans an empty file; the segment resets to EMPTY while the file persists, tripping assertCacheCorrectness.
c Fix matches root cause? Yes. Mirrors the exact ErrnoException orphan-removal into the two non-Errno catch blocks.
d Test intent preserved / new tests added? New stateless test 04545_filesystem_cache_orphan_file_non_errno_failure reproduces the exact assertion (background download: 1, downloaded size: 0). No existing test weakened.
e Demonstrated both directions? Yes. Without fix: server aborts with Logical error: 'Expected file .../0 not to exist ... state: PARTIALLY_DOWNLOADED_NO_CONTINUATION, downloaded size: 0 ... background download: 1'. With fix: self-check passes, correct result, server alive.
f Fix general, not a narrow patch? Yes. Both non-Errno catch families (Exception and fs::filesystem_error) get identical cleanup; the ErrnoException path already had it. All three write-failure paths now symmetric.
g Generalizes across inputs? The orphan condition is downloaded_size==0 regardless of failure type or segment kind; the guard covers every non-Errno write failure at file-creation time.
h Backward compatible? Yes. Pure cleanup on an error path; no format/setting/default change.
i Invariants preserved? Yes. Uses the noexcept fs::remove overload so the original download error is not masked; setDownloadFailedUnlocked still runs first; the rethrow behavior is unchanged.

Session id: cron:clickhouse-worker-slot-4:20260715-112800

@groeneai

Copy link
Copy Markdown
Contributor Author

cc @kssenii for review (per @ PedroTadim on #110532). Small filesystem-cache orphan-file fix.

@clickhouse-gh

clickhouse-gh Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Workflow [PR], commit [e3dd72c]

Summary:

job_name test_name status info comment
Integration tests (amd_msan, 7/8) FAIL
test_replicated_database/test.py::test_replicated_table_structure_alter FAIL cidb, issue

AI Review

Summary

This PR mirrors the empty-orphan cleanup into the non-ErrnoException cache-write failure paths and adds a failpoint-based regression script. The C++ fix itself looks consistent with the existing ErrnoException handling, but the new test still does not prove that the failing branch actually ran, so the evidence is not strong enough to approve.

Tests
  • ⚠️ [tests/queries/0_stateless/04545_filesystem_cache_orphan_file_non_errno.sh:27] The first SELECT is expected to fail, but the script never checks that it did. .sh tests are not run with set -e through shell_config.sh, so this command can succeed or fail and the script still continues to the final SELECT. If the failpoint stops hitting the intended non-ErrnoException path, the test still goes green and no longer proves that the orphan-cleanup branch ran. Capture the status or output, disable the failpoint, then assert that the query failed (optionally matching the injected error text).
Final Verdict
  • Status: ⚠️ Request changes

@clickhouse-gh clickhouse-gh Bot added the pr-bugfix Pull request with bugfix, not backported by default label Jul 15, 2026
The CI style checker (ci/jobs/check_style.py) rejects stateless test
filenames containing 'fail'. Rename 04545_filesystem_cache_orphan_file_non_errno_failure
to 04545_filesystem_cache_orphan_file_non_errno.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
# file was created. The read is expected to fail; we only care that no orphan file
# survives.
${CLICKHOUSE_CLIENT} -q "SYSTEM ENABLE FAILPOINT cache_filesystem_failure_non_errno"
${CLICKHOUSE_CLIENT} --enable_filesystem_cache=1 --read_from_filesystem_cache_if_exists_otherwise_bypass_cache=0 \

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.

Because .sh tests are executed without set -e unless the script opts in, this SELECT can fail or succeed and the script will still continue. That makes the regression weak: if the failpoint stops hitting this path, the test goes green without ever proving the non-ErrnoException branch ran.

Please make the expected failure explicit here, for example by inverting the command with if ...; then echo "expected failure"; exit 1; fi (and optionally checking that the error text is the injected one).

@kssenii kssenii self-assigned this Jul 15, 2026
{
/// A non-ErrnoException failure (e.g. a generic Exception thrown by the
/// underlying object-storage read) after the cache file was created.
throw Exception(ErrorCodes::CANNOT_READ_ALL_DATA, "Failpoint: simulated cache non-errno failure");

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.

Let's make it MEMORY_LIMIT_EXCEEDED, it will be more plausible than CANNOT_READ_ALL_DATA

Per review: MEMORY_LIMIT_EXCEEDED is a more plausible real-world cause of a
non-ErrnoException background-download failure than CANNOT_READ_ALL_DATA.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@groeneai

Copy link
Copy Markdown
Contributor Author

Done in 9ca22ed: the non-errno failpoint now throws MEMORY_LIMIT_EXCEEDED instead of CANNOT_READ_ALL_DATA. Re-verified both directions locally (debug, cache over local_blob_storage, background_download_threads=2): failpoint fires with background download: 1 / downloaded size: 0, orphan removed, self-check passes (correct result 4999950000), server alive.

@groeneai

Copy link
Copy Markdown
Contributor Author

CI finish ledger — 9ca22ed

CI fully finished (Finish Workflow pass). The only red job is the fleet-wide host-OOM window; not PR-caused.

Check / test Reason Owner / fixing PR
Stateless tests (amd_asan_ubsan, distributed plan, parallel) / unrelated tests host-OOM (Code 241 (total) memory limit exceeded, RSS ~41.84 GiB host cap) #110503 (ours, open)
Mergeable Check aggregate of the above #110503 (ours, open)

@clickhouse-gh

clickhouse-gh Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

LLVM Coverage Report

Metric Baseline Current Δ
Lines 85.90% 85.90% +0.00%
Functions 92.70% 92.70% +0.00%
Branches 78.00% 78.00% +0.00%

Changed lines: Changed C/C++ lines covered: 21/29 (72.41%) · Uncovered code

Full report · Diff report

@kssenii
kssenii added this pull request to the merge queue Jul 17, 2026
Merged via the queue into ClickHouse:master with commit c3c2bb3 Jul 17, 2026
176 of 178 checks passed
@robot-ch-test-poll4 robot-ch-test-poll4 added the pr-synced-to-cloud The PR is synced to the cloud repo label Jul 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

can be tested Allows running workflows for external contributors pr-bugfix Pull request with bugfix, not backported by default pr-synced-to-cloud The PR is synced to the cloud repo

Projects

None yet

4 participants