Skip to content

fix(local): stop read-only commands creating project state (#1077) - #1166

Open
sdairs wants to merge 2 commits into
claude/1157-split-cloud-organizationsfrom
claude/1077-readonly-no-project-state
Open

sdairs wants to merge 2 commits into
claude/1157-split-cloud-organizationsfrom
claude/1077-readonly-no-project-state

Conversation

@sdairs

@sdairs sdairs commented Oct 30, 2026 •

Copy link
Copy Markdown
Member

Closes #1077.

Summary

Read-only and lookup local commands no longer create .clickhouse/servers/.metadata.lock in the current directory. Only local server start, local postgres start and local init create project state now.

  • New lock entry point. MetadataLock::acquire_existing_at and server::lock_existing_metadata() return None when .clickhouse/servers doesn't exist. The lock file is opened without create_dir_all, and ENOENT maps to None. There's no separate existence check, so there's no race: a concurrent start either created the directory first, in which case the reader waits on the lock, or it comes after the reader, which correctly sees no servers.
  • Callers now on the read path:
    • local server list, stop, stop-all, remove and dotenv
    • managed local client
    • local udf list, reload, remove and deploy
    • local postgres stop, stop-all, remove, client and dotenv, through a shared lock_and_resolve_pg_target
    • the in-use guards in local remove <version> and in install
    • server start's pre-checks, so a start that fails before installing leaves nothing behind
  • Output is unchanged. I compared 32 commands in an empty directory against origin/main: exit codes, stdout and stderr were identical. The only difference is that main creates the lock directory.

Visible changes

  • stop or remove with no name after a list. These now report "no project state", because list no longer leaves .clickhouse/ behind. The state-machine tests are updated to match.
  • .clickhouse/servers is a file. The error now says "open the server metadata lock file" instead of "create the server metadata lock directory".
  • Orphan recovery still works without project state (second commit, from review). When .clickhouse/servers is missing, read commands first run a discovery-only pass that writes nothing. Only if it finds a running ClickHouse process or a labelled Postgres container for this project do they create the directory, lock, and recover as before. Example: after rm -rf .clickhouse with a Postgres container still running, stop-all still finds and stops it. Recovery runs again under the lock, so a concurrent start isn't duplicated. An empty non-project directory still gets nothing.

Notes

Tests

  • New subprocess tests: tests/local_readonly_no_project_state_test.rs, 9 tests. Each command runs in an empty directory, in an init-only directory, and as local remove <version>. The file is classified in both CI scripts and in the mirrored test set.
  • New unit tests: 5 for the lock helper.
  • Checks: fmt, both clippy configurations, --no-default-features check, cargo test -p clickhousectl and the Python classifier tests. I also checked by hand that the issue's repro leaves an empty directory.

🤖 Generated with Claude Code


Note

Medium Risk
Release workflow and self-update version resolution now depend on latest.txt; a failed or stale publish affects all updaters, while the org module split is large but should be behavior-preserving if tests pass.

Overview
This PR bundles release discovery, Cloud API/client updates, and a cloud CLI layout refactor (the attached PR title/description about local read-only commands is not reflected in this diff).

Release: Adds a Publish latest.txt job after uploads that writes clickhousectl/latest.txt to builds.clickhouse.com with forward-only semver (re-tagging an older release does not downgrade). The job verifies the CDN-served file and fails the release if it does not match. Docs and the release skill now say clickhousectl update and install.sh read that manifest (GitHub releases/latest only as fallback).

clickhouse-cloud-api: Refreshes the OpenAPI spec and models for ClickStack log sources (eventNameExpression, severityNumberExpression) and MongoDB ClickPipe table mappings (mongoConfig.persistDeletedDocuments). Conversion, README, and unit/smoke tests cover round-trips and optional fields.

clickhousectl: Deletes the monolithic organizations.rs in favor of the organizations/ directory pattern (with blame-ignore entries for similar splits of services/ and postgres/). Drops the service_query module re-export from cloud/mod.rs. ClickPipes --table-mapping-json accepts mongoConfig; ClickStack source file tests include the new log expressions. Agent docs (AGENTS.md, add-cli-command, cli-help-text) describe the directory layout and stricter convert_error_for_lookup guidance.

Reviewed by Cursor Bugbot for commit aff3978. Bugbot is set up for automated code reviews on this repo. Configure here.

@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 2 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 533c6d2. Configure here.

Ok(current_project_processes(processes, &current_dir)
.next()
.is_some()
|| !docker::discover_project_postgres_blocking(&current_dir).is_empty())

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stopped Postgres recreates project state

Medium Severity

The new “create state only if something is still running” gate uses discover_project_postgres_blocking, which lists every labelled container (all(true)), not just running ones. postgres stop leaves the container in place, so after .clickhouse/ is deleted any read path that calls lock_recovered_metadata (server list, udf list, local remove, client, and start pre-checks) recreates .clickhouse/servers/ for a stopped instance. That reintroduces #1077 for the common leftover-container case, and it contradicts the comments and docs that recovery should run only when something is still running.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 533c6d2. Configure here.

sdairs and others added 2 commits October 30, 2026 16:39
`server::lock_metadata()` created `.clickhouse/servers/` to open its lock
file, so listing, stopping, removing, connecting to or deploying UDFs to
servers left `.clickhouse/servers/.metadata.lock` in any directory.

Add `MetadataLock::acquire_existing_at` / `lock_existing_metadata()`, which
returns `None` when the servers directory does not exist. Opening the lock
file is the existence check, so there is no check-then-use window: a
concurrent start either created the directory first (and is waited for
through the lock) or is ordered after the reader, which sees no servers.

Server list/stop/stop-all/remove/dotenv, managed `local client`, the
`local udf` server lookup, postgres stop/stop-all/remove/client/dotenv,
`local remove <version>`'s in-use guard and the start pre-checks use it and
return their existing empty or not-found results. Only server start,
postgres start and `local init` create the directory. Postgres lookups share
one lock-and-resolve helper so the not-found error stays identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ng (#1077)

Read paths returned early when `.clickhouse/servers/` was absent, so a
still-running project-labelled Postgres container or a ClickHouse process
started from the project was no longer recovered after `rm -rf .clickhouse`:
`postgres stop-all` reported nothing running and `postgres stop default`
reported server_not_found while the container kept its port.

Add `server::lock_recovered_metadata()`. With server state it locks and
recovers as before. Without it, it runs the same best-effort discovery
(ClickHouse process scan plus the Docker label lookup, which tolerates Docker
being unreachable) writing nothing; only when that finds an instance of this
project does it create the directory, take the lock and recover under it, so
the command behaves as it did before #1077. Recovery re-checks under the lock
and skips anything a concurrent command recorded since. An empty directory
still gains no `.clickhouse/`.

Every lookup that recovered orphans now goes through the helper, and the
Docker recovery is split into a discovery step it shares.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sdairs
sdairs force-pushed the claude/1077-readonly-no-project-state branch from 533c6d2 to aff3978 Compare October 30, 2026 17:27
@sdairs
sdairs changed the base branch from main to claude/1157-split-cloud-organizations October 30, 2026 17:27
@sdairs
sdairs added this pull request to stack #1153 October 30, 2026 17:28

This branch has not been deployed

No deployments
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.

Read-only local commands create .clickhouse/servers/.metadata.lock outside a project

1 participant