Skip to content

Make local server metadata atomic and concurrency-safe - #516

Open
sdairs wants to merge 2 commits into
issue-473-omitted-server-selectionfrom
issue-472-atomic-server-metadata
Open

Make local server metadata atomic and concurrency-safe#516
sdairs wants to merge 2 commits into
issue-473-omitted-server-selectionfrom
issue-472-atomic-server-metadata

Conversation

@sdairs

@sdairs sdairs commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • write local server metadata through unique sibling temporary files with full writes, flush/sync, and atomic rename
  • serialize ClickHouse and Postgres lifecycle, recovery, removal, and stale-PID normalization with per-server cross-process locks so stale state cannot overwrite a restart
  • preserve selected metadata read, permission, parse, and write failures as distinct actionable human and structured errors
  • add deterministic corruption, invalid UTF-8, permission, interrupted-write, concurrent lifecycle/client/stop, and stale-normalization/restart coverage

Closes #472

Tests

  • cargo fmt --all --check
  • cargo build -p clickhousectl
  • cargo check -p clickhousectl --no-default-features
  • cargo test -p clickhousectl
  • cargo clippy -p clickhousectl --all-targets -- -D warnings

Stack

@sdairs sdairs changed the title issue 472 atomic server metadata Make local server metadata atomic and concurrency-safe Aug 24, 2026
pub fn running_server_count() -> usize {
list_running_servers().len()
pub fn running_server_count() -> Result<usize> {
Ok(list_running_servers()?.len())

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟠 High local/server.rs:564

running_server_count fails with an I/O error when .clickhouse/servers contains a directory such as foo.json/, causing commands that count running servers—and therefore starting another server—to fail. list_all_servers now treats every .json directory entry as metadata and calls normalize_server_info without verifying it is a regular file; restore the file-type check before normalizing entries.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @crates/clickhousectl/src/local/server.rs around line 564:

`running_server_count` fails with an I/O error when `.clickhouse/servers` contains a directory such as `foo.json/`, causing commands that count running servers—and therefore starting another server—to fail. `list_all_servers` now treats every `.json` directory entry as metadata and calls `normalize_server_info` without verifying it is a regular file; restore the file-type check before normalizing entries.

&& is_master
&& replaced_existing
&& version_in_use_by_running_server(&exact_version)
&& version_in_use_by_running_server(&exact_version)?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟠 High version_manager/install.rs:177

A metadata/recovery error at version_in_use_by_running_server(&exact_version)? reports the master install as failed after the existing version directory and binary have already been replaced. Because the error returns before cleanup and master::record, the installed state is changed but the master record remains stale, forcing later downloads; perform this fallible check before mutating version_dir and retain its result for the notification.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @crates/clickhousectl/src/version_manager/install.rs around line 177:

A metadata/recovery error at `version_in_use_by_running_server(&exact_version)?` reports the master install as failed after the existing version directory and binary have already been replaced. Because the error returns before cleanup and `master::record`, the installed state is changed but the master record remains stale, forcing later downloads; perform this fallible check before mutating `version_dir` and retain its result for the notification.

@cursor cursor 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.

Cursor Bugbot has reviewed your changes using high effort and found 3 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit b40f62e. Configure here.


entries.sort_by(|a, b| b.running.cmp(&a.running).then(a.name.cmp(&b.name)));
entries
Ok(entries)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Corrupt metadata blocks unrelated servers

High Severity

list_all_servers now returns on the first metadata read/parse failure instead of skipping that entry. Callers such as running_server_count during start, stop-all, version removal, and find_pg_instances therefore fail for every server when a single unrelated .json file is corrupt or unreadable. Previously those files were treated as absent and other servers remained usable.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b40f62e. Configure here.


/// Default user-facing name when `--name` is omitted: `"default"` if no
/// postgres "default" is running, otherwise a random adjective-noun.
fn default_pg_name() -> String {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Lock held across Postgres pull

Medium Severity

ServerLock for a Postgres instance is acquired before Docker connect/image pull and kept through wait_running (and through resume_existing). Cross-process flock therefore blocks list, stop, client, and other commands on that key for the full pull/start duration. ClickHouse start drops the lock right after saving metadata, so the engines behave inconsistently and Postgres start can stall the CLI for minutes.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b40f62e. Configure here.

container_id: Some(c.container_id.clone()),
};
let _ = save_server_info(&info);
save_recovered_server_info(&info, false)?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Recovery fails before metadata check

Medium Severity

Postgres orphan recovery always calls ensure_pg_data_dir before save_recovered_server_info, and no longer skips containers that already have metadata. A permission or I/O failure creating a data dir aborts recovery even when metadata already exists and no write is needed. Because recover_current_project_servers runs on most local commands, that failure surfaces as a hard error across start/stop/list/client.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b40f62e. Configure here.

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.

Make local server metadata writes atomic and concurrency-safe

1 participant