Skip to content

Partition export per part local backoff policy#1984

Open
arthurpassos wants to merge 21 commits into
antalya-26.3from
export-partition-retry-backoff
Open

Partition export per part local backoff policy#1984
arthurpassos wants to merge 21 commits into
antalya-26.3from
export-partition-retry-backoff

Conversation

@arthurpassos

@arthurpassos arthurpassos commented Jun 29, 2026

Copy link
Copy Markdown
Collaborator

Changelog category (leave one):

  • Improvement

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

Implements a local backoff policy for export partition - helps with transient and local failures.,

It is controlled by export_merge_tree_partition_retry_initial_backoff_seconds and export_merge_tree_partition_retry_max_backoff_seconds.

Removes export_merge_tree_partition_max_retries as it would conflict with a local backoff policy implementation. We already have a task time out anyways. The only limitation of relying on the task time out is that non retryable errors would be retried until it times out. Not ideal, so I implemented an err classification method that will classify between retryable and non retryable. Non retryable will fail the task immediately.

Documentation entry for user-facing changes

...

CI/CD Options

Exclude tests:

  • Fast test
  • Integration Tests
  • Stateless tests
  • Stateful tests
  • Performance tests
  • Aarch64 tests
  • All with ASAN
  • All with TSAN
  • All with MSAN
  • All with UBSAN
  • All with Coverage
  • All Regression
  • Disable CI Cache

Regression jobs to run:

  • Fast suites (mostly <1h)
  • Aggregate Functions (2h)
  • Alter (1.5h)
  • Benchmark (30m)
  • ClickHouse Keeper (1h)
  • Iceberg (2h)
  • LDAP (1h)
  • OAuth (5m)
  • Parquet (1.5h)
  • RBAC (1.5h)
  • SSL Server (1h)
  • S3 (2h)
  • S3 Export (2h)
  • Swarms (30m)
  • Tiered Storage (2h)

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4d576fefe4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +234 to +235
manifest.retry_initial_backoff_ms = json->getValue<size_t>("retry_initial_backoff_ms");
manifest.retry_max_backoff_ms = json->getValue<size_t>("retry_max_backoff_ms");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Default backoff fields for old export manifests

When a node upgrades with an export created by the previous code still under /exports, that metadata.json does not contain these two new fields because older manifests only wrote max_retries. ExportReplicatedMergeTreePartitionManifest::fromJsonString is used by the manifest updater before it can clean up or kill tasks, and getValue throws on missing keys, so one old in-flight export makes poll fail repeatedly and prevents this table's export state from progressing until the ZooKeeper entry is removed manually. Please read these fields with defaults when absent, similar to the existing allow_lossy_cast compatibility handling.

Useful? React with 👍 / 👎.

@arthurpassos arthurpassos changed the base branch from rw_lock_for_partition_export to antalya-26.3 June 30, 2026 13:53
@github-actions

github-actions Bot commented Jun 30, 2026

Copy link
Copy Markdown

Workflow [PR], commit [8e549de]

@arthurpassos arthurpassos changed the title first vibe coded version of local backoff policy Partition export per part local backoff policy Jun 30, 2026
@arthurpassos

Copy link
Copy Markdown
Collaborator Author

@codex review, do not worry about backwards compatibility

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Bravo.

Reviewed commit: 141d1141fb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@arthurpassos arthurpassos added antalya port-antalya PRs to be ported to all new Antalya releases antalya-26.3 labels Jul 2, 2026
Comment thread src/Core/Settings.cpp
DECLARE(Bool, export_merge_tree_partition_force_export, false, R"(
Ignore existing partition export and overwrite the zookeeper entry
)", 0) \
DECLARE(UInt64, export_merge_tree_partition_max_retries, 3, R"(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

In common case removed settings are not removed completely, added in OBSOLETE_SETTINGS list for backward compatibility. Why not now?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Huh.. mostly because nobody is using this feature, but I suppose you are right. let me fix that

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Should be fixed

if (initial > headroom)
return max_backoff_seconds;

return std::min(initial << shift, max_backoff_seconds);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Up to 2^64-1 seconds (~585 billion years)? :)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Why rush? :)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Up to max_backoff_seconds, which defaults to 300s (export_merge_tree_partition_retry_max_backoff_seconds)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

registerLocalBackoff might be a problem, tho - I'll check after the antalya meeting

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

registerLocalBackoff seems to be fine as well

backoff.attempts, manifest.retry_initial_backoff_seconds, manifest.retry_max_backoff_seconds);
const auto now = time(nullptr);
/// Clamp so a huge configured back-off cannot overflow time_t (now is a normal wall-clock value).
const size_t headroom = static_cast<size_t>(std::numeric_limits<time_t>::max() - now);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

time_t is signed, size_t is unsigned. If we allow all possible ranges, may get something unexpected. Mya be set hardcoded limit in billion years?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

std::numeric_limits<time_t>::max() - now can never be negative tho

@mkmkme mkmkme left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I really like the fact that you also changed some exception codes to be more specific :)

using LocalBackoffMap = std::unordered_map<TransactionID, PartNameToBackOffMap>;

mutable std::mutex local_backoff_mutex;
LocalBackoffMap local_backoff;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I'd add TSA_GUARDED_BY(local_backoff_mutex) here

Suggested change
LocalBackoffMap local_backoff;
LocalBackoffMap local_backoff TSA_GUARDED_BY(local_backoff_mutex);

@Selfeer

Selfeer commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

@arthurpassos can you check if this is reasonable? #2005

I was thinking about the statuses that should/should not be retryable and came up with this.

@Selfeer

Selfeer commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

Before the testing is finished, here is the latest audit review results for now:

AI audit note: This review comment was generated by AI (gpt-5.3-codex).

Audit update for PR #1984 (Partition export per part local backoff policy):

Confirmed defects:

  • Medium: Backward-incompatible manifest parsing can stall export-task reconciliation after upgrade.
    • Impact: A replica running this PR can throw while reading pre-upgrade export metadata, which aborts poll early and can leave export discovery/cleanup/status convergence stale; killExportPartition can also fail when it scans ZooKeeper entries.
    • Anchor: src/Storages/ExportReplicatedMergeTreePartitionManifest.h / ExportReplicatedMergeTreePartitionManifest::fromJsonString; src/Storages/MergeTree/ExportPartitionManifestUpdatingTask.cpp / ExportPartitionManifestUpdatingTask::poll; src/Storages/StorageReplicatedMergeTree.cpp / killExportPartition.
    • Trigger: Rolling upgrade with at least one in-flight export entry created by the old format (max_retries present, retry_initial_backoff_seconds and retry_max_backoff_seconds absent).
    • Why defect: fromJsonString unconditionally reads retry_initial_backoff_seconds and retry_max_backoff_seconds via json->getValue, and callers in poll/killExportPartition do not handle this as a per-entry compatibility case.
    • Fix direction (short): Add backward-compatible fallback in fromJsonString (or guarded read at call sites) so old metadata remains parseable and does not abort whole-task scanning.
    • Regression test direction (short): Add an upgrade/compatibility integration test that seeds a legacy metadata.json entry and verifies poll and killExportPartition keep working on the new binary.

Coverage summary:

  • Scope reviewed: Retry/back-off scheduler flow, commit/part failure classification, ZooKeeper state transitions, manifest/schema evolution, system table exposure, and new integration/stateless tests.
  • Categories failed: Serialization/upgrade compatibility (metadata.json field evolution).
  • Categories passed: Local back-off thread-safety, lock release and status transition ordering, fail-open/fail-closed classification intent, rollback on Keeper multi failure, exception accounting race hardening, and non-retryable fast-fail paths.
  • Assumptions/limits: Static audit only (no runtime execution in this review); severity reflects observed code paths and realistic rolling-upgrade behavior.

@arthurpassos arthurpassos requested review from ianton-ru and mkmkme July 7, 2026 13:04
@arthurpassos

Copy link
Copy Markdown
Collaborator Author

Medium: Backward-incompatible manifest parsing can stall export-task reconciliation after upgrade.

hmm.. so far I was opting for not introducing "backwards compatible code". But since we already released 26.3, it might be a good idea to make this safer

@Selfeer

Selfeer commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

No issue from my side do block this PR. The failes described are not considered bugs or issues here. @CarlosFelipeOR could you please fix these tests afterwards for the sake of the clean CI in the future.

PR #1984 CI Failure Triage

PR: Altinity/ClickHouse#1984Partition export per part local backoff policy
Base: antalya-26.3export-partition-retry-backoff
Commit: 8e549de4f747a121b90a36b0c0143f00b8854e22
Workflow run: 28871008590
Report: ci_run_report.html


Verdict

All 11 CI failures are directly caused by this PR's changes. They are not flaky,
not infrastructure, and not cascade failures. Every failure is a regression test whose
expectations conflict with the PR's intended new behavior (error re-classification and
the removal of the fixed retry budget). The tests need to be updated to match the new
behavior (or the two behavior decisions below need to be confirmed as intended).

Everything else in the run is green: s3_export_part, s3_export_partition, all
Integration tests (including the export-partition integration tests modified by the PR),
all Stateless tests (including the modified 03572_* export tests), Fast test, fuzzers,
stress, and builds.


Summary

Category Count Tests
PR-caused regression 11 iceberg export partition casting (8) + transactions (3)
Cascade failures 0
Pre-existing flaky 0
Infrastructure (known/BROKEN, unrelated) 7 see "Known/unrelated" below

The CI report header agrees: New Fails in PR (0), Checks New Fails (0),
Regression New Fails (11), Checks Known Fails (7).

All 11 new failures are in the single job Regression release iceberg_2, all under
/iceberg/export partition/.


Root cause analysis

The PR makes two intended behavioral changes, and both are exactly what the failing
tests assert against.

Change A — lossy-cast rejection error code BAD_ARGUMENTS (36) → INCOMPATIBLE_COLUMNS (122)

PR diff (src/Storages/.../ExportPartitionUtils.cpp write/validation path):

-                throw Exception(ErrorCodes::BAD_ARGUMENTS,
+                throw Exception(ErrorCodes::INCOMPATIBLE_COLUMNS,
                     "Cannot export to {}: column '{}' requires a lossy cast from {} to {}, "

The regression tests assert a synchronous rejection with client exit code 36
(iceberg/tests/export_partition/steps/casting.py, BAD_ARGUMENTS = 36). The server now
returns code 122 (INCOMPATIBLE_COLUMNS). The rejection message still contains
"lossy cast", so only the exit-code assertion (exitcode should be 36) fails.

Observed in the log:

Code: 122. DB::Exception: ... column 'id' requires a lossy cast from UInt64 to Int32,
which may change values. Set `export_merge_tree_part_allow_lossy_cast = 1` ...
(INCOMPATIBLE_COLUMNS)
...
✘ Fail exitcode should be 36

Affected tests (8):

  • /iceberg/export partition/ice catalog/casting/out_of_bounds/UInt64 above INT64_MAX rejected without allow_lossy_cast
  • /iceberg/export partition/no catalog/casting/out_of_bounds/UInt64 above INT64_MAX rejected without allow_lossy_cast
  • /iceberg/export partition/ice catalog/casting/out_of_bounds/Int64 above INT32_MAX rejected without allow_lossy_cast
  • /iceberg/export partition/no catalog/casting/out_of_bounds/Int64 above INT32_MAX rejected without allow_lossy_cast
  • /iceberg/export partition/ice catalog/casting/out_of_bounds/Int64 below INT32_MIN rejected without allow_lossy_cast
  • /iceberg/export partition/no catalog/casting/out_of_bounds/Int64 below INT32_MIN rejected without allow_lossy_cast
  • /iceberg/export partition/ice catalog/casting/lossy/UInt64 narrows to Int32 without allow_lossy_cast
  • /iceberg/export partition/no catalog/casting/lossy/UInt64 narrows to Int32 without allow_lossy_cast

Change B — new retryable / non-retryable error classification

The PR removes export_merge_tree_partition_max_retries (now obsolete) and adds
ExportPartitionUtils::isNonRetryableExportError(code). The non-retryable set includes
BAD_ARGUMENTS (36) and INCOMPATIBLE_COLUMNS (122), among others. Non-retryable
commit failures transition the export task straight to FAILED instead of retrying.

The transactions/commit survives pre-publish failure (non-retry cleanup) scenario arms
the iceberg_writes_non_retry_cleanup failpoint and expects the export to retry and
reach COMPLETED
. The failpoint throws code 36 (BAD_ARGUMENTS), which the PR now
treats as non-retryable, so the task ends FAILED.

Test assertion:

AssertionError: Expected status 'COMPLETED' ... but got 'FAILED'

Server log (all 3 catalog variants show the same):

ExportPartition: Commit failure recorded for .../exports/2020_... (code 36),
task transitioned to FAILED (non-retryable)
ExportPartition scheduler task: Commit for ... transitioned to FAILED due to
non-retryable error (code 36)

Affected tests (3):

  • /iceberg/export partition/glue catalog/transactions/commit survives pre-publish failure (non-retry cleanup)
  • /iceberg/export partition/ice catalog/transactions/commit survives pre-publish failure (non-retry cleanup)
  • /iceberg/export partition/no catalog/transactions/commit survives pre-publish failure (non-retry cleanup)

Points to confirm with the PR author

  1. Lossy-cast error code change (36 → 122). Confirm INCOMPATIBLE_COLUMNS is the
    intended user-facing code for lossy-cast rejection, then update the casting regression
    tests to assert exit code 122 (message assertion already passes).

  2. BAD_ARGUMENTS (36) is now non-retryable. The commit survives pre-publish failure (non-retry cleanup) scenario was written on the assumption that this
    pre-publish failure is transient and gets retried to success. With code 36 now
    non-retryable, the task fails immediately. Decide whether:

    • the test should be updated to expect FAILED (i.e. the failpoint models a
      non-retryable error), or
    • the failpoint/classification should use a retryable code so the "survives / retries
      to completion" contract still holds.

Both are behavior/expectation questions introduced by this PR — neither is a flaky or
infra failure.


Known / unrelated failures (not caused by the PR)

These are pre-existing BROKEN/known issues listed under Checks Known Fails (7) and are
infrastructure or already-tracked problems, unrelated to this PR:

  • test_dirty_pages_force_purge — KNOWN, Altinity#1369
  • test_backup_restore_on_cluster/test_different_versions — INVESTIGATE (NETLINK_ERROR)
  • 03441_deltalake_clickhouse_public_datasets — INVESTIGATE (S3 sometimes unreachable) ×2
  • 03441_deltalake_clickhouse_virtual_columns — INVESTIGATE (S3 sometimes unreachable) ×2
  • 00157_cache_dictionary — KNOWN (timeout on slow Altinity runners, passes upstream)

Stateless tests (amd_debug, parallel) shows Failed: 1, but the run reports
Checks New Fails (0) — it is not a new failure introduced by this PR.


Recommendation

  • Do not treat these as flaky and re-run — they will fail deterministically because
    they reflect the PR's intended behavior change.
  • Update the affected iceberg export-partition regression tests (casting exit code, and
    the non-retry-cleanup transaction scenario) to match the new error code and
    retry semantics, after confirming the two decisions above with the PR author.

@Selfeer Selfeer added the verified Approved for release label Jul 7, 2026
@arthurpassos

Copy link
Copy Markdown
Collaborator Author

@ianton-ru @mkmkme can I get a 2nd pass?

@mkmkme mkmkme left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

New changes LGTM!

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

Labels

antalya antalya-26.3 port-antalya PRs to be ported to all new Antalya releases verified Approved for release

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants