Skip to content

Deploys no longer stall after a partition heals: Corrosion syncs once per commit instead of every page - #3674

Merged
nick-potts merged 4 commits into
mainfrom
agent/fix-2925-partition-corrosion-timeout
Oct 31, 2026
Merged

nick-potts merged 4 commits into
mainfrom
agent/fix-2925-partition-corrosion-timeout

Conversation

@nick-potts

@nick-potts nick-potts commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

Closes #2925.

Why

Deploys must keep working while part of a Cluster is partitioned and right after it heals, which is exactly when an operator deploys to recover. On main they stall for minutes or fail with Corrosion I/O timed out. The cause is the inode sync flag (chattr +S) ployzd put on the Corrosion store so that a commit survives a power cut. It makes every SQLite page write synchronous. While a healing Machine ingests the other side's changes, one sync commit holds Corrosion's only write connection for up to a minute on a slow disk. ployzd's own writes on that Machine then pass its 10 s limit.

SQLite's own synchronous = FULL gives the same guarantee with one sync per commit. Corrosion hardcodes NORMAL, so our fork gains an optional [db] synchronous (getployz/corrosion#1). Nick approved that fork patch as Q161, superseding Q76's "no fork".

Summary

 ployzd start
   install()                      # writes config.toml
-    chattr +S on the store        # every page write synchronous
+    [db] synchronous = "full"     # one WAL sync per commit, before broadcast
   DockerService::start            # replace Corrosion when image/config changed
+  clear +S from the store         # only now: the running Corrosion syncs each commit
   wait ready

Evidence

All runs are Boat --cloud with 4 Machines. Manifest revision = PR head bd2dd303b6. dashboard/.verify/run/sdk.log reads sdk: hit local eb07fd70 (5a3013cf42ed), an exact hit keyed by the SDK's source files: bd2dd303 changes only ployzd, which the SDK doesn't include, so it isn't stale main@. Doctor passed. Every saved or posted output went through redact with 0 leaks.

Partition A/B, same Machines. Each side ran one writer of 150 transactions × 200 rows during a 2/2 underlay split. Then the split healed and 8 deploys followed. machine-3 sits on a slow node: a synchronous 4 KiB write takes about 118 ms there, against 0.2 to 0.3 ms on the others. The "main" column is this build with the store forced back to main's mode (+S on, no synchronous line).

Main (+S, NORMAL) This PR (FULL)
Longest sync commit (machine-3) 60.8 s 2.2 s
machine-3 catch-up after the heal 200 of 30,000 rows in 3 min 30,000 rows in ≤30 s
machine-3's 30,000 local writes ~8 min, worst tx 44.6 s ~14 s, worst tx 1.06 s
ployzd Corrosion I/O timed out 16 0
8 deploys after the heal none exit 0: 5 × exit 3, 3 × exit 1 7 × exit 0 in 13 to 19 s, 1 × exit 3 (deployed while peers still read Down, as documented)

On an earlier main run (0.3.11 binaries), the first deploy after the heal took 301 s. With a config-only batch cap it failed with exit 1 after 397 s, so that approach is dropped. Switching the cluster back to this PR's mode let machine-3 finish its stuck 120,000-row catch-up within about 2 minutes.

Plain SQLite on the same disks, 50 tx × 200 rows:

Disk +S, NORMAL FULL, no +S
slow 209.8 s, worst commit 24.2 s 23.6 s, worst 1.77 s
normal 17.0 s, worst 0.99 s 0.54 s, worst 0.034 s

Durability (scripts/verify/power-cut-replication.sh):

  • machine-3, two cuts, the second 8 s after boot: machine-3 kept its Corrosion actor, rows agree on every Machine: 120024 rows, PASS.
  • machine-4, hot (HOT_SERVICE=web, the first cut lands the moment service restart returns, which Draft for 0.3.5: rotate the Corrosion actor after a power cut instead of synchronous store writes #3214 measured losing 9 versions without +S): machine-4 kept its Corrosion actor, rows agree on every Machine: 120024 rows, PASS.
  • Review (Opus) straced the real pinned image: normal gave 10 API commits → 0 fsync/fdatasync, and full gave 10 commits → 10.

Swarm (landing.md), each agent on its own Boat at bd2dd303:

  • Upgrade and mixed versions: 0.3.11 → roll one Server → update all → downgrade one. Every deploy exited 0 (6 to 11 s, through Cloud and each Entry). Postgres held 1000|5aa148… throughout, with 0 timeouts. Polling at 0.1 s, +S cleared only once the new FULL Corrosion ran. On downgrade it came back before the old image started, so at no moment were both protections off. A 0.3.11 Entry drops store_syncs_commits when it relays a newer Machine's record, so in a mixed cluster server ls through it can still warn about an upgraded ZFS or Btrfs Server. That over-warns and never under-warns, and absence claims nothing by design.
  • Power cut, partition, restart (3 Machines): every Machine ran synchronous = "full" with no +S and reported store_syncs_commits: true, and no power-cut warning showed. A double cut on a non-db Machine left rows-agree.sh passing, and server ls and ps agreed through every Entry. A cut on the db Machine during the ack probe lost 0 ids (probe: acked 214995, lost []). Deploys during and after a partition exited 3, 3 and 0, as scaling.md says. After systemctl restart ployz, a deploy exited 0 in 9 s with 0 timeouts and no +S. Unrelated to this PR, Cloud picked the partitioned Machine as the Entry (filed Cloud picks a partitioned Machine as the Entry, so a deploy lands every new replica on the cut-off side #3675), and the progress count is deploy live view counts Servers, not replicas #2082.

Unit tests: a_store_an_older_daemon_flagged_stops_writing_every_page_synchronously, one_unreadable_file_leaves_the_rest_of_the_store_cleared (run on ext4, where +S takes), a_server_whose_store_syncs_each_commit_is_not_warned_about_on_any_filesystem. In the fork: write_connections_use_the_configured_synchronous and synchronous_defaults_to_normal_and_reads_full.

Review: round 1 had Opus (no defects) and Codex. Codex found that clearing +S at install left the old NORMAL Corrosion unprotected during the image pull. ployzd has no writers in that window today, but the clear now waits for the new Corrosion anyway. Round 2 (Codex, full diff) found no BUG-class issues.

Design bets

  • Stable promise. No wire, RPC, row, schema or store-format change. store_syncs_commits is an optional field: older readers ignore it, and absence claims nothing. The Corrosion config is local to the Machine and moves with the pinned image, and the old image tolerates the new line. Downgrading to 0.3.11 sets +S again, which is today's state. Compat impact: a mixed cluster can over-warn as described above, nothing more.
  • 9, lean on proven primitives. +S was OS tuning standing in for SQLite's own durability knob. FULL is the primitive's intended mechanism. The cost is one small fork patch, the red flag this crosses, approved as Q161.
  • 2, AP over C. One slow-disk Machine on the healing side no longer stalls or fails deploys for the whole Cluster.

Merge Danger

Door: two-way. Reverting restores +S at the next daemon start, and the store format is unchanged.

Blast Radius: every Machine's store.

Every Machine's Corrosion is recreated on the new image at upgrade. On a disk whose fsync is slow, commits now cost one sync each instead of none (with NORMAL) or one per page (with +S). The swarm measured deploys at 6 to 11 s.

…ery store write synchronous

The store's inode sync flag synced every page, so a healing Machine held
Corrosion's one write connection for up to 51 s per commit and its own
writes passed ployzd's 10 s limit. FULL syncs the WAL once per commit,
before Corrosion broadcasts it, which keeps the power-cut guarantee.
Stores an older daemon flagged are cleared at start. Machines now report
store_syncs_commits, so the CLI warns about ZFS and Btrfs stores only on
daemons that predate this.
Brings rows-agree.sh and rows_diff.py from #3214. Equal clocks can hide
different values, so the check now waits for every Machine to hold the
same rows in every replicated table.
getployz/corrosion#1, built by the fork's image workflow at 28f00b417271.
Clearing it at install left an older Corrosion, still serving while the new
image pulled, writing with neither the flag nor synchronous=full. A failed
pull kept it that way. Found in review.
@nick-potts nick-potts added this to the 0.3.12 milestone Oct 31, 2026
@coderabbitai

coderabbitai Bot commented Oct 31, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: f097d4cd-0f1d-4314-baaf-463d838c4a02

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 31, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-31T01:38:51.537503Z bd2dd30 Draft marked ready
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@nick-potts
nick-potts added this pull request to the merge queue Oct 31, 2026
Merged via the queue into main with commit 17c069e Oct 31, 2026
31 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Deploys fail intermittently with 'start Container failed: Corrosion I/O timed out' while part of the cluster is partitioned, and right after it heals

1 participant