Handle Qdrant startup failures gracefully (#683)
Build and Release / Read metadata (push) Waiting to run
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage deb updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage deb updater) (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg updater) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions

Co-authored-by: Thorsten Sommer <SommerEngineering@users.noreply.github.com>
This commit is contained in:
Paul KoudelkaandThorsten Sommer authored and GitHub committed 2026-03-14 12:25:58 +01:00
1 parent 078d48ca6f
commit 2f5a300e74
13 files changed
+240 -66

No files matched your search

+5 -5
View File
@@ -107,16 +107,16 @@ pub fn start_tauri() {
DATA_DIRECTORY.set(data_path.to_str().unwrap().to_string()).map_err(|_| error!("Was not able to set the data directory.")).unwrap();
CONFIG_DIRECTORY.set(app.path_resolver().app_config_dir().unwrap().to_str().unwrap().to_string()).map_err(|_| error!("Was not able to set the config directory.")).unwrap();
cleanup_qdrant();
cleanup_dotnet_server();
if is_dev() {
#[cfg(debug_assertions)]
create_startup_env_file();
} else {
cleanup_dotnet_server();
start_dotnet_server();
}
start_qdrant_server();
cleanup_qdrant();
start_qdrant_server(app.path_resolver());
info!(Source = "Bootloader Tauri"; "Reconfigure the file logger to use the app data directory {data_path:?}");
switch_to_file_logging(data_path).map_err(|e| error!("Failed to switch logging to file: {e}")).unwrap();
@@ -1067,4 +1067,4 @@ fn set_pdfium_path(path_resolver: PathResolver) {
let pdfium_source_path = pdfium_source_path.unwrap();
let pdfium_source_path = pdfium_source_path.to_str().unwrap().to_string();
*PDFIUM_LIB_PATH.lock().unwrap() = Some(pdfium_source_path.clone());
}
}
+119 -32
View File
@@ -12,9 +12,10 @@ use rocket::serde::json::Json;
use rocket::serde::Serialize;
use tauri::api::process::{Command, CommandChild, CommandEvent};
use crate::api_token::{APIToken};
use crate::environment::DATA_DIRECTORY;
use crate::environment::{is_dev, DATA_DIRECTORY};
use crate::certificate_factory::generate_certificate;
use std::path::PathBuf;
use tauri::PathResolver;
use tempfile::{TempDir, Builder};
use crate::stale_process_cleanup::{kill_stale_process, log_potential_stale_process};
use crate::sidecar_types::SidecarType;
@@ -38,10 +39,24 @@ static API_TOKEN: Lazy<APIToken> = Lazy::new(|| {
});
static TMPDIR: Lazy<Mutex<Option<TempDir>>> = Lazy::new(|| Mutex::new(None));
static QDRANT_STATUS: Lazy<Mutex<QdrantStatus>> = Lazy::new(|| Mutex::new(QdrantStatus::default()));
const PID_FILE_NAME: &str = "qdrant.pid";
const SIDECAR_TYPE:SidecarType = SidecarType::Qdrant;
#[derive(Default)]
struct QdrantStatus {
is_available: bool,
unavailable_reason: Option<String>,
}
fn qdrant_base_path() -> PathBuf {
let qdrant_directory = if is_dev() { "qdrant_test" } else { "qdrant" };
Path::new(DATA_DIRECTORY.get().unwrap())
.join("databases")
.join(qdrant_directory)
}
#[derive(Serialize)]
pub struct ProvideQdrantInfo {
path: String,
@@ -49,34 +64,62 @@ pub struct ProvideQdrantInfo {
port_grpc: u16,
fingerprint: String,
api_token: String,
is_available: bool,
unavailable_reason: Option<String>,
}
#[get("/system/qdrant/info")]
pub fn qdrant_port(_token: APIToken) -> Json<ProvideQdrantInfo> {
let status = QDRANT_STATUS.lock().unwrap();
let is_available = status.is_available;
let unavailable_reason = status.unavailable_reason.clone();
Json(ProvideQdrantInfo {
path: Path::new(DATA_DIRECTORY.get().unwrap()).join("databases").join("qdrant").to_str().unwrap().to_string(),
port_http: *QDRANT_SERVER_PORT_HTTP,
port_grpc: *QDRANT_SERVER_PORT_GRPC,
fingerprint: CERTIFICATE_FINGERPRINT.get().expect("Certificate fingerprint not available").to_string(),
api_token: API_TOKEN.to_hex_text().to_string(),
path: if is_available {
qdrant_base_path().to_string_lossy().to_string()
} else {
String::new()
},
port_http: if is_available { *QDRANT_SERVER_PORT_HTTP } else { 0 },
port_grpc: if is_available { *QDRANT_SERVER_PORT_GRPC } else { 0 },
fingerprint: if is_available {
CERTIFICATE_FINGERPRINT.get().cloned().unwrap_or_default()
} else {
String::new()
},
api_token: if is_available {
API_TOKEN.to_hex_text().to_string()
} else {
String::new()
},
is_available,
unavailable_reason,
})
}
/// Starts the Qdrant server in a separate process.
pub fn start_qdrant_server(){
let base_path = DATA_DIRECTORY.get().unwrap();
let path = Path::new(base_path).join("databases").join("qdrant");
pub fn start_qdrant_server(path_resolver: PathResolver){
let path = qdrant_base_path();
if !path.exists() {
if let Err(e) = fs::create_dir_all(&path){
error!(Source="Qdrant"; "The required directory to host the Qdrant database could not be created: {}", e.to_string());
error!(Source="Qdrant"; "The required directory to host the Qdrant database could not be created: {}", e);
set_qdrant_unavailable(format!("The Qdrant data directory could not be created: {e}"));
return;
};
}
let (cert_path, key_path) =create_temp_tls_files(&path).unwrap();
let storage_path = path.join("storage").to_str().unwrap().to_string();
let snapshot_path = path.join("snapshots").to_str().unwrap().to_string();
let init_path = path.join(".qdrant-initalized").to_str().unwrap().to_string();
let (cert_path, key_path) = match create_temp_tls_files(&path) {
Ok(paths) => paths,
Err(e) => {
error!(Source="Qdrant"; "TLS files for Qdrant could not be created: {e}");
set_qdrant_unavailable(format!("TLS files for Qdrant could not be created: {e}"));
return;
}
};
let storage_path = path.join("storage").to_string_lossy().to_string();
let snapshot_path = path.join("snapshots").to_string_lossy().to_string();
let init_path = path.join(".qdrant-initialized").to_string_lossy().to_string();
let qdrant_server_environment = HashMap::from_iter([
(String::from("QDRANT__SERVICE__HTTP_PORT"), QDRANT_SERVER_PORT_HTTP.to_string()),
@@ -84,22 +127,52 @@ pub fn start_qdrant_server(){
(String::from("QDRANT_INIT_FILE_PATH"), init_path),
(String::from("QDRANT__STORAGE__STORAGE_PATH"), storage_path),
(String::from("QDRANT__STORAGE__SNAPSHOTS_PATH"), snapshot_path),
(String::from("QDRANT__TLS__CERT"), cert_path.to_str().unwrap().to_string()),
(String::from("QDRANT__TLS__KEY"), key_path.to_str().unwrap().to_string()),
(String::from("QDRANT__TLS__CERT"), cert_path.to_string_lossy().to_string()),
(String::from("QDRANT__TLS__KEY"), key_path.to_string_lossy().to_string()),
(String::from("QDRANT__SERVICE__ENABLE_TLS"), "true".to_string()),
(String::from("QDRANT__SERVICE__API_KEY"), API_TOKEN.to_hex_text().to_string()),
]);
let server_spawn_clone = QDRANT_SERVER.clone();
let qdrant_relative_source_path = "resources/databases/qdrant/config.yaml";
let qdrant_source_path = match path_resolver.resolve_resource(qdrant_relative_source_path) {
Some(path) => path,
None => {
let reason = format!("The Qdrant config resource '{qdrant_relative_source_path}' could not be resolved.");
error!(Source = "Qdrant"; "{reason} Starting the app without Qdrant.");
set_qdrant_unavailable(reason);
return;
}
};
let qdrant_source_path_display = qdrant_source_path.to_string_lossy().to_string();
tauri::async_runtime::spawn(async move {
let (mut rx, child) = Command::new_sidecar("qdrant")
.expect("Failed to create sidecar for Qdrant")
.args(["--config-path", "resources/databases/qdrant/config.yaml"])
let sidecar = match Command::new_sidecar("qdrant") {
Ok(sidecar) => sidecar,
Err(e) => {
let reason = format!("Failed to create sidecar for Qdrant: {e}");
error!(Source = "Qdrant"; "{reason}");
set_qdrant_unavailable(reason);
return;
}
};
let (mut rx, child) = match sidecar
.args(["--config-path", qdrant_source_path_display.as_str()])
.envs(qdrant_server_environment)
.spawn()
.expect("Failed to spawn Qdrant server process.");
{
Ok(process) => process,
Err(e) => {
let reason = format!("Failed to spawn Qdrant server process with config path '{}': {e}", qdrant_source_path_display);
error!(Source = "Qdrant"; "{reason}");
set_qdrant_unavailable(reason);
return;
}
};
let server_pid = child.pid();
set_qdrant_available();
info!(Source = "Bootloader Qdrant"; "Qdrant server process started with PID={server_pid}.");
log_potential_stale_process(path.join(PID_FILE_NAME), server_pid, SIDECAR_TYPE);
@@ -137,7 +210,10 @@ pub fn stop_qdrant_server() {
if let Some(server_process) = QDRANT_SERVER.lock().unwrap().take() {
let server_kill_result = server_process.kill();
match server_kill_result {
Ok(_) => warn!(Source = "Qdrant"; "Qdrant server process was stopped."),
Ok(_) => {
set_qdrant_unavailable("Qdrant server was stopped.".to_string());
warn!(Source = "Qdrant"; "Qdrant server process was stopped.")
},
Err(e) => error!(Source = "Qdrant"; "Failed to stop Qdrant server process: {e}."),
}
} else {
@@ -148,7 +224,7 @@ pub fn stop_qdrant_server() {
cleanup_qdrant();
}
/// Create temporary directory with TLS relevant files
/// Create a temporary directory with TLS relevant files
pub fn create_temp_tls_files(path: &PathBuf) -> Result<(PathBuf, PathBuf), Box<dyn Error>> {
let cert = generate_certificate();
@@ -157,10 +233,10 @@ pub fn create_temp_tls_files(path: &PathBuf) -> Result<(PathBuf, PathBuf), Box<d
let key_path = temp_dir.join("key.pem");
let mut cert_file = File::create(&cert_path)?;
cert_file.write_all(&*cert.certificate)?;
cert_file.write_all(&cert.certificate)?;
let mut key_file = File::create(&key_path)?;
key_file.write_all(&*cert.private_key)?;
key_file.write_all(&cert.private_key)?;
CERTIFICATE_FINGERPRINT.set(cert.fingerprint).expect("Could not set the certificate fingerprint.");
@@ -187,24 +263,35 @@ pub fn drop_tmpdir() {
/// Remove old Pid files and kill the corresponding processes
pub fn cleanup_qdrant() {
let pid_path = Path::new(DATA_DIRECTORY.get().unwrap()).join("databases").join("qdrant").join(PID_FILE_NAME);
let path = qdrant_base_path();
let pid_path = path.join(PID_FILE_NAME);
if let Err(e) = kill_stale_process(pid_path, SIDECAR_TYPE) {
warn!(Source = "Qdrant"; "Error during the cleanup of Qdrant: {}", e);
}
if let Err(e) = delete_old_certificates() {
if let Err(e) = delete_old_certificates(path) {
warn!(Source = "Qdrant"; "Error during the cleanup of Qdrant: {}", e);
}
}
pub fn delete_old_certificates() -> Result<(), Box<dyn Error>> {
let dir_path = Path::new(DATA_DIRECTORY.get().unwrap()).join("databases").join("qdrant");
fn set_qdrant_available() {
let mut status = QDRANT_STATUS.lock().unwrap();
status.is_available = true;
status.unavailable_reason = None;
}
if !dir_path.exists() {
fn set_qdrant_unavailable(reason: String) {
let mut status = QDRANT_STATUS.lock().unwrap();
status.is_available = false;
status.unavailable_reason = Some(reason);
}
pub fn delete_old_certificates(path: PathBuf) -> Result<(), Box<dyn Error>> {
if !path.exists() {
return Ok(());
}
for entry in fs::read_dir(dir_path)? {
for entry in fs::read_dir(path)? {
let entry = entry?;
let path = entry.path();
+1 -1
View File
@@ -68,7 +68,7 @@
"target/databases/qdrant/qdrant"
],
"resources": [
"resources/*"
"resources/**"
],
"macOS": {
"exceptionDomain": "localhost"