Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 35 additions & 31 deletions crates/clickhousectl/src/local/docker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1179,9 +1179,8 @@ pub async fn start_existing(docker: &Docker, id: &str) -> Result<()> {
/// a metadata file under `.clickhouse/servers/`, and write a `ServerInfo` for
/// each so they show up in `local server list` and can be managed.
///
/// Safe to call multiple times in one CLI invocation. When Docker isn't
/// reachable, `connect()` fails fast (no socket → immediate error, no I/O
/// timeout) and we return silently.
/// Safe to call multiple times in one CLI invocation. Docker being unreachable
/// is not an error: see [`discover_project_postgres_blocking`].
pub fn recover_project_postgres_blocking(
project_cwd: &str,
lock: &crate::local::server::MetadataLock,
Expand All @@ -1190,36 +1189,41 @@ pub fn recover_project_postgres_blocking(
Engine, ServerInfo, ensure_pg_data_dir, load_info_locked, pg_instance_key,
save_server_info_locked,
};
let cwd_owned = project_cwd.to_string();
block_on(async move {
let docker = match connect().await {
Ok(d) => d,
Err(_) => return Ok::<(), Error>(()),
};
let containers = match list_project_postgres(&docker, &cwd_owned).await {
Ok(c) => c,
Err(_) => return Ok(()),
for c in discover_project_postgres_blocking(project_cwd) {
let key = pg_instance_key(&c.user_name, &c.major);
if load_info_locked(&key, lock)?.is_some() {
continue;
}
ensure_pg_data_dir(&c.user_name, &c.major)?;
let info = ServerInfo {
name: key,
pid: 0,
version: c.image,
http_port: 0,
tcp_port: c.host_port.unwrap_or(0),
started_at: "recovered".to_string(),
cwd: project_cwd.to_string(),
engine: Engine::Postgres,
container_id: Some(c.container_id),
};
for c in containers {
let key = pg_instance_key(&c.user_name, &c.major);
if load_info_locked(&key, lock)?.is_some() {
continue;
}
ensure_pg_data_dir(&c.user_name, &c.major)?;
let info = ServerInfo {
name: key,
pid: 0,
version: c.image.clone(),
http_port: 0,
tcp_port: c.host_port.unwrap_or(0),
started_at: "recovered".to_string(),
cwd: cwd_owned.clone(),
engine: Engine::Postgres,
container_id: Some(c.container_id.clone()),
};
save_server_info_locked(&info, lock)?;
save_server_info_locked(&info, lock)?;
}
Ok(())
}

/// Postgres containers labelled with `project_cwd`, writing nothing.
///
/// Discovery is best effort: when Docker isn't reachable, `connect()` fails
/// fast (no socket → immediate error, no I/O timeout) and this finds nothing,
/// as does a failed container listing.
pub fn discover_project_postgres_blocking(project_cwd: &str) -> Vec<DiscoveredContainer> {
block_on(async {
match connect().await {
Ok(docker) => list_project_postgres(&docker, project_cwd)
.await
.unwrap_or_default(),
Err(_) => Vec::new(),
}
Ok(())
})
}

Expand Down
81 changes: 53 additions & 28 deletions crates/clickhousectl/src/local/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -391,12 +391,13 @@ fn run_client(
None,
)
};
let metadata_lock = server::lock_metadata().map_err(project_state_error)?;
server::recover_current_project_servers_locked(&metadata_lock)
.map_err(project_state_error)?;
let server_not_found = || managed_error(ManagedClientErrorKind::ServerNotFound, None);
let metadata_lock = server::lock_recovered_metadata()
.map_err(project_state_error)?
.ok_or_else(server_not_found)?;
let entry = server::server_entry_locked(server_name, &metadata_lock)
.map_err(project_state_error)?
.ok_or_else(|| managed_error(ManagedClientErrorKind::ServerNotFound, None))?;
.ok_or_else(server_not_found)?;
if !entry.running {
return Err(managed_error(
ManagedClientErrorKind::ServerNotRunning,
Expand Down Expand Up @@ -793,10 +794,10 @@ fn dotenv_server(
json: bool,
) -> Result<()> {
let server_name = name.unwrap_or("default");
let metadata_lock = server::lock_metadata()?;
server::recover_current_project_servers_locked(&metadata_lock)?;
let entry = server::server_entry_locked(server_name, &metadata_lock)?
.ok_or_else(|| Error::ServerNotFound(server_name.to_string()))?;
let server_not_found = || Error::ServerNotFound(server_name.to_string());
let metadata_lock = server::lock_recovered_metadata()?.ok_or_else(server_not_found)?;
let entry =
server::server_entry_locked(server_name, &metadata_lock)?.ok_or_else(server_not_found)?;
if !entry.running {
return Err(Error::ServerNotRunning(server_name.to_string()));
}
Expand Down Expand Up @@ -1038,16 +1039,19 @@ fn stop_server(
let project_dir = init::canonical_project_dir()?;
let project_state_missing = !init::local_dir().is_dir();

// Recover orphaned servers so we can stop processes
// that lost their metadata files.
let metadata_lock = server::lock_metadata()?;
server::recover_current_project_servers_locked(&metadata_lock)?;
// Recover orphaned servers so we can stop processes that lost their
// metadata files. Without server state or an orphan there is nothing to
// stop.
let metadata_lock = server::lock_recovered_metadata()?;
let (name, selection, known) = match name_input {
ServerNameInput::Positional(name) | ServerNameInput::NameFlag(name) => {
(name, output::ServerSelection::Explicit, false)
}
ServerNameInput::Omitted => {
let names = server::list_clickhouse_server_names_locked(&metadata_lock)?;
let names = match &metadata_lock {
Some(lock) => server::list_clickhouse_server_names_locked(lock)?,
None => Vec::new(),
};
if names.iter().any(|name| name == "default") {
(
"default".to_string(),
Expand Down Expand Up @@ -1083,6 +1087,14 @@ fn stop_server(
}
};

let Some(metadata_lock) = metadata_lock else {
// Only reachable for an explicit name: an omitted one returned above.
return Err(Error::ProjectServerNotFound(ProjectServerNotFound {
command: ProjectServerCommand::Stop,
project_dir,
server_name: name,
}));
};
match classify_stop(
server::is_server_running_locked(&name, &metadata_lock)?,
known || server::server_data_dir(&name).exists(),
Expand Down Expand Up @@ -1132,16 +1144,19 @@ fn remove_server(name_input: ServerNameInput, json: bool) -> Result<()> {
let project_dir = init::canonical_project_dir()?;
let project_state_missing = !init::local_dir().is_dir();

// Recover orphaned servers so we correctly detect a running
// process even when its metadata file is missing.
let metadata_lock = server::lock_metadata()?;
server::recover_current_project_servers_locked(&metadata_lock)?;
// Recover orphaned servers so we correctly detect a running process even
// when its metadata file is missing. Without server state or an orphan
// there is nothing to remove.
let metadata_lock = server::lock_recovered_metadata()?;
let (name, selection) = match name_input {
ServerNameInput::Positional(name) | ServerNameInput::NameFlag(name) => {
(name, output::ServerSelection::Explicit)
}
ServerNameInput::Omitted => {
let names = server::list_clickhouse_server_names_locked(&metadata_lock)?;
let names = match &metadata_lock {
Some(lock) => server::list_clickhouse_server_names_locked(lock)?,
None => Vec::new(),
};
if names.iter().any(|name| name == "default") {
("default".to_string(), output::ServerSelection::Implicit)
} else {
Expand All @@ -1160,6 +1175,17 @@ fn remove_server(name_input: ServerNameInput, json: bool) -> Result<()> {
}
};

let server_not_found = |server_name| {
Error::ProjectServerNotFound(ProjectServerNotFound {
command: ProjectServerCommand::Remove,
project_dir,
server_name,
})
};
// Only reachable for an explicit name: an omitted one returned above.
let Some(metadata_lock) = metadata_lock else {
return Err(server_not_found(name));
};
if server::is_server_running_locked(&name, &metadata_lock)? {
return Err(Error::ServerRunningCannotRemove {
command: format!("clickhousectl local server stop {name}"),
Expand All @@ -1168,11 +1194,7 @@ fn remove_server(name_input: ServerNameInput, json: bool) -> Result<()> {
}
let data_dir = server::server_data_dir(&name);
if !data_dir.exists() {
return Err(Error::ProjectServerNotFound(ProjectServerNotFound {
command: ProjectServerCommand::Remove,
project_dir,
server_name: name,
}));
return Err(server_not_found(name));
}
// Remove the whole server directory (parent of data/)
let server_dir = data_dir.parent().unwrap();
Expand Down Expand Up @@ -1374,11 +1396,14 @@ fn global_stop_target(name_input: ServerNameInput) -> Result<(String, output::Se
}

fn stop_all_servers_local(json: bool) -> Result<()> {
let metadata_lock = server::lock_metadata()?;
server::recover_current_project_servers_locked(&metadata_lock)?;
let servers = server::list_running_servers_locked(&metadata_lock)?;
let out = stop_servers(&servers, json, |name| {
server::kill_server_locked(name, &metadata_lock)
let metadata_lock = server::lock_recovered_metadata()?;
let servers = match &metadata_lock {
Some(lock) => server::list_running_servers_locked(lock)?,
None => Vec::new(),
};
let out = stop_servers(&servers, json, |name| match &metadata_lock {
Some(lock) => server::kill_server_locked(name, lock),
None => Err(Error::ServerNotRunning(name.to_string())),
});
if json {
output::print_output(&out, json);
Expand Down
67 changes: 44 additions & 23 deletions crates/clickhousectl/src/local/postgres.rs
Original file line number Diff line number Diff line change
Expand Up @@ -617,6 +617,33 @@ fn resolve_pg_start_version_locked(
}
}

/// Lock project metadata, recover orphans, and resolve the target under that
/// lock. A project with neither server state nor an orphan has no target, so
/// this then creates no `.clickhouse/servers/`.
fn lock_and_resolve_pg_target(
user_name: &str,
version: Option<&str>,
) -> Result<(server::ServerInfo, server::MetadataLock)> {
let Some(metadata_lock) = server::lock_recovered_metadata()? else {
return Err(missing_pg_target(user_name, version));
};
let target = resolve_pg_target_locked(user_name, version, &metadata_lock)?;
Ok((target, metadata_lock))
}

/// The error for a `[NAME] [--version <V>]` selection that matches nothing.
fn missing_pg_target(user_name: &str, version: Option<&str>) -> Error {
match version {
Some(v) => match validate_pg_tag(v) {
Ok(()) => {
Error::ServerNotFound(format!("{user_name} (postgres:{})", pg_major_from_tag(v)))
}
Err(error) => error,
},
None => Error::ServerNotFound(user_name.to_string()),
}
}

/// Resolve `[NAME] [--version <V>]` to a single Postgres instance on disk.
/// If `version` is given, target the (X, major(V)) pair directly. Otherwise:
/// 0 instances → ServerNotFound; 1 → use it; >1 → ask for `--version`.
Expand All @@ -631,11 +658,11 @@ fn resolve_pg_target_locked(
let key = server::pg_instance_key(user_name, &major);
return server::load_info_locked(&key, metadata_lock)?
.filter(|i| i.engine == Engine::Postgres)
.ok_or_else(|| Error::ServerNotFound(format!("{user_name} (postgres:{major})")));
.ok_or_else(|| missing_pg_target(user_name, version));
}
let instances = server::find_pg_instances_locked(user_name, metadata_lock)?;
match instances.len() {
0 => Err(Error::ServerNotFound(user_name.to_string())),
0 => Err(missing_pg_target(user_name, None)),
1 => Ok(instances.into_iter().next().unwrap()),
_ => {
let versions: Vec<String> = instances.iter().map(|i| i.version.clone()).collect();
Expand Down Expand Up @@ -998,9 +1025,7 @@ fn generate_password() -> String {

async fn stop(name: &str, version: Option<&str>, json: bool) -> Result<()> {
server::validate_server_name(name)?;
let metadata_lock = server::lock_metadata()?;
server::recover_current_project_servers_locked(&metadata_lock)?;
let target = resolve_pg_target_locked(name, version, &metadata_lock)?;
let (target, metadata_lock) = lock_and_resolve_pg_target(name, version)?;
if !json {
let display = format!("{} ({})", user_name_from_key(&target.name), target.version);
println!("Stopping Postgres {}...", display);
Expand All @@ -1016,19 +1041,22 @@ async fn stop(name: &str, version: Option<&str>, json: bool) -> Result<()> {
}

async fn stop_all(json: bool) -> Result<()> {
let metadata_lock = server::lock_metadata()?;
server::recover_current_project_servers_locked(&metadata_lock)?;
let servers: Vec<_> = server::list_running_servers_locked(&metadata_lock)?
.into_iter()
.filter(|s| s.engine == Engine::Postgres)
.collect();
let metadata_lock = server::lock_recovered_metadata()?;
let servers: Vec<_> = match &metadata_lock {
Some(lock) => server::list_running_servers_locked(lock)?,
None => Vec::new(),
}
.into_iter()
.filter(|s| s.engine == Engine::Postgres)
.collect();
if !json && servers.is_empty() {
println!("No running Postgres servers");
return Ok(());
}

let out = super::stop_servers(&servers, json, |name| {
server::kill_server_locked(name, &metadata_lock)
let out = super::stop_servers(&servers, json, |name| match &metadata_lock {
Some(lock) => server::kill_server_locked(name, lock),
None => Err(Error::ServerNotRunning(name.to_string())),
});
if json {
output::print_output(&out, json);
Expand All @@ -1040,10 +1068,7 @@ async fn stop_all(json: bool) -> Result<()> {

fn remove(name: &str, version: Option<&str>, json: bool) -> Result<()> {
server::validate_server_name(name)?;
let metadata_lock = server::lock_metadata()?;
server::recover_current_project_servers_locked(&metadata_lock)?;

let target = resolve_pg_target_locked(name, version, &metadata_lock)?;
let (target, metadata_lock) = lock_and_resolve_pg_target(name, version)?;
let key = target.name.clone();
if server::is_server_running_locked(&key, &metadata_lock)? {
let tag = target
Expand Down Expand Up @@ -1114,10 +1139,8 @@ async fn client(
);
}

let metadata_lock = server::lock_metadata()?;
server::recover_current_project_servers_locked(&metadata_lock)?;
let server_name = name.as_deref().unwrap_or("default");
let info = resolve_pg_target_locked(server_name, version.as_deref(), &metadata_lock)?;
let (info, metadata_lock) = lock_and_resolve_pg_target(server_name, version.as_deref())?;
if !server::is_server_running_locked(&info.name, &metadata_lock)? {
return Err(Error::ServerNotRunning(server_name.to_string()));
}
Expand Down Expand Up @@ -1259,10 +1282,8 @@ fn exec_host_psql(
}

fn dotenv(name: Option<&str>, version: Option<&str>, use_local: bool, json: bool) -> Result<()> {
let metadata_lock = server::lock_metadata()?;
server::recover_current_project_servers_locked(&metadata_lock)?;
let server_name = name.unwrap_or("default");
let info = resolve_pg_target_locked(server_name, version, &metadata_lock)?;
let (info, metadata_lock) = lock_and_resolve_pg_target(server_name, version)?;
if !server::is_server_running_locked(&info.name, &metadata_lock)? {
return Err(Error::ServerNotRunning(server_name.to_string()));
}
Expand Down
Loading
Loading