From cdc1760025a290c4025f6f72bc5d4d8410945df7 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sat, 14 Mar 2026 12:17:29 +0100 Subject: [PATCH] Made Qdrant startup more resilient --- .../Assistants/I18N/allTexts.lua | 15 +++ .../Pages/Information.razor.cs | 4 +- .../plugin.lua | 15 +++ .../plugin.lua | 15 +++ app/MindWork AI Studio/Program.cs | 59 +++++---- .../Tools/Databases/DatabaseClient.cs | 2 + .../Tools/Databases/NoDatabaseClient.cs | 24 ++++ .../Tools/Rust/QdrantInfo.cs | 4 + runtime/src/qdrant.rs | 116 +++++++++++++++--- 9 files changed, 210 insertions(+), 44 deletions(-) create mode 100644 app/MindWork AI Studio/Tools/Databases/NoDatabaseClient.cs diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index eeb90c5b..c108582a 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -5290,6 +5290,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3494984593"] = "Tauri is used to -- Motivation UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3563271893"] = "Motivation" +-- not available +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3574465749"] = "not available" + -- This library is used to read Excel and OpenDocument spreadsheet files. This is necessary, e.g., for using spreadsheets as a data source for a chat. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3722989559"] = "This library is used to read Excel and OpenDocument spreadsheet files. This is necessary, e.g., for using spreadsheets as a data source for a chat." @@ -5311,6 +5314,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3986423270"] = "Check Pandoc Ins -- Versions UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4010195468"] = "Versions" +-- Database +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4036243672"] = "Database" + -- This library is used to create asynchronous streams in Rust. It allows us to work with streams of data that can be produced asynchronously, making it easier to handle events or data that arrive over time. We use this, e.g., to stream arbitrary data from the file system to the embedding system. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4079152443"] = "This library is used to create asynchronous streams in Rust. It allows us to work with streams of data that can be produced asynchronously, making it easier to handle events or data that arrive over time. We use this, e.g., to stream arbitrary data from the file system to the embedding system." @@ -5896,6 +5902,15 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::CONFIDENCESCHEMESEXTENSIONS::T3893997203"] = " -- Trust all LLM providers UI_TEXT_CONTENT["AISTUDIO::TOOLS::CONFIDENCESCHEMESEXTENSIONS::T4107860491"] = "Trust all LLM providers" +-- Reason +UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T1093747001"] = "Reason" + +-- Unavailable +UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T3662391977"] = "Unavailable" + +-- Status +UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T6222351"] = "Status" + -- Storage size UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::QDRANT::QDRANTCLIENTIMPLEMENTATION::T1230141403"] = "Storage size" diff --git a/app/MindWork AI Studio/Pages/Information.razor.cs b/app/MindWork AI Studio/Pages/Information.razor.cs index 2027285f..1f3d946e 100644 --- a/app/MindWork AI Studio/Pages/Information.razor.cs +++ b/app/MindWork AI Studio/Pages/Information.razor.cs @@ -58,7 +58,9 @@ public partial class Information : MSGComponentBase private string VersionPdfium => $"{T("Used PDFium version")}: v{META_DATA_LIBRARIES.PdfiumVersion}"; - private string VersionDatabase => $"{T("Database version")}: {this.DatabaseClient.Name} v{META_DATA_DATABASES.DatabaseVersion}"; + private string VersionDatabase => this.DatabaseClient.IsAvailable + ? $"{T("Database version")}: {this.DatabaseClient.Name} v{META_DATA_DATABASES.DatabaseVersion}" + : $"{T("Database")}: {this.DatabaseClient.Name} - {T("not available")}"; private string versionPandoc = TB("Determine Pandoc version, please wait..."); private PandocInstallation pandocInstallation; diff --git a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua index 08d78e5a..9cb9d0f8 100644 --- a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua @@ -5292,6 +5292,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3494984593"] = "Tauri wird verwe -- Motivation UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3563271893"] = "Motivation" +-- not available +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3574465749"] = "nicht verfügbar" + -- This library is used to read Excel and OpenDocument spreadsheet files. This is necessary, e.g., for using spreadsheets as a data source for a chat. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3722989559"] = "Diese Bibliothek wird verwendet, um Excel- und OpenDocument-Tabellendateien zu lesen. Dies ist zum Beispiel notwendig, wenn Tabellen als Datenquelle für einen Chat verwendet werden sollen." @@ -5313,6 +5316,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3986423270"] = "Pandoc-Installat -- Versions UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4010195468"] = "Versionen" +-- Database +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4036243672"] = "Datenbank" + -- This library is used to create asynchronous streams in Rust. It allows us to work with streams of data that can be produced asynchronously, making it easier to handle events or data that arrive over time. We use this, e.g., to stream arbitrary data from the file system to the embedding system. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4079152443"] = "Diese Bibliothek wird verwendet, um asynchrone Datenströme in Rust zu erstellen. Sie ermöglicht es uns, mit Datenströmen zu arbeiten, die asynchron bereitgestellt werden, wodurch sich Ereignisse oder Daten, die nach und nach eintreffen, leichter verarbeiten lassen. Wir nutzen dies zum Beispiel, um beliebige Daten aus dem Dateisystem an das Einbettungssystem zu übertragen." @@ -5898,6 +5904,15 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::CONFIDENCESCHEMESEXTENSIONS::T3893997203"] = " -- Trust all LLM providers UI_TEXT_CONTENT["AISTUDIO::TOOLS::CONFIDENCESCHEMESEXTENSIONS::T4107860491"] = "Allen LLM-Anbietern vertrauen" +-- Reason +UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T1093747001"] = "Grund" + +-- Unavailable +UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T3662391977"] = "Nicht verfügbar" + +-- Status +UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T6222351"] = "Status" + -- Storage size UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::QDRANT::QDRANTCLIENTIMPLEMENTATION::T1230141403"] = "Speichergröße" diff --git a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua index 5ab2e446..556a2d08 100644 --- a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua @@ -5292,6 +5292,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3494984593"] = "Tauri is used to -- Motivation UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3563271893"] = "Motivation" +-- not available +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3574465749"] = "not available" + -- This library is used to read Excel and OpenDocument spreadsheet files. This is necessary, e.g., for using spreadsheets as a data source for a chat. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3722989559"] = "This library is used to read Excel and OpenDocument spreadsheet files. This is necessary, e.g., for using spreadsheets as a data source for a chat." @@ -5313,6 +5316,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3986423270"] = "Check Pandoc Ins -- Versions UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4010195468"] = "Versions" +-- Database +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4036243672"] = "Database" + -- This library is used to create asynchronous streams in Rust. It allows us to work with streams of data that can be produced asynchronously, making it easier to handle events or data that arrive over time. We use this, e.g., to stream arbitrary data from the file system to the embedding system. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4079152443"] = "This library is used to create asynchronous streams in Rust. It allows us to work with streams of data that can be produced asynchronously, making it easier to handle events or data that arrive over time. We use this, e.g., to stream arbitrary data from the file system to the embedding system." @@ -5898,6 +5904,15 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::CONFIDENCESCHEMESEXTENSIONS::T3893997203"] = " -- Trust all LLM providers UI_TEXT_CONTENT["AISTUDIO::TOOLS::CONFIDENCESCHEMESEXTENSIONS::T4107860491"] = "Trust all LLM providers" +-- Reason +UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T1093747001"] = "Reason" + +-- Unavailable +UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T3662391977"] = "Unavailable" + +-- Status +UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T6222351"] = "Status" + -- Storage size UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::QDRANT::QDRANTCLIENTIMPLEMENTATION::T1230141403"] = "Storage size" diff --git a/app/MindWork AI Studio/Program.cs b/app/MindWork AI Studio/Program.cs index 85e97b07..70cc6831 100644 --- a/app/MindWork AI Studio/Program.cs +++ b/app/MindWork AI Studio/Program.cs @@ -86,37 +86,46 @@ internal sealed class Program } var qdrantInfo = await rust.GetQdrantInfo(); - if (qdrantInfo.Path == string.Empty) + DatabaseClient databaseClient; + if (!qdrantInfo.IsAvailable) { - Console.WriteLine("Error: Failed to get the Qdrant path from Rust."); - return; + Console.WriteLine($"Warning: Qdrant is not available. Starting without vector database. Reason: '{qdrantInfo.UnavailableReason ?? "unknown"}'."); + databaseClient = new NoDatabaseClient("Qdrant", qdrantInfo.UnavailableReason); } - - if (qdrantInfo.PortHttp == 0) + else { - Console.WriteLine("Error: Failed to get the Qdrant HTTP port from Rust."); - return; - } + if (qdrantInfo.Path == string.Empty) + { + Console.WriteLine("Error: Failed to get the Qdrant path from Rust."); + return; + } + + if (qdrantInfo.PortHttp == 0) + { + Console.WriteLine("Error: Failed to get the Qdrant HTTP port from Rust."); + return; + } - if (qdrantInfo.PortGrpc == 0) - { - Console.WriteLine("Error: Failed to get the Qdrant gRPC port from Rust."); - return; - } + if (qdrantInfo.PortGrpc == 0) + { + Console.WriteLine("Error: Failed to get the Qdrant gRPC port from Rust."); + return; + } - if (qdrantInfo.Fingerprint == string.Empty) - { - Console.WriteLine("Error: Failed to get the Qdrant fingerprint from Rust."); - return; - } - - if (qdrantInfo.ApiToken == string.Empty) - { - Console.WriteLine("Error: Failed to get the Qdrant API token from Rust."); - return; - } + if (qdrantInfo.Fingerprint == string.Empty) + { + Console.WriteLine("Error: Failed to get the Qdrant fingerprint from Rust."); + return; + } + + if (qdrantInfo.ApiToken == string.Empty) + { + Console.WriteLine("Error: Failed to get the Qdrant API token from Rust."); + return; + } - var databaseClient = new QdrantClientImplementation("Qdrant", qdrantInfo.Path, qdrantInfo.PortHttp, qdrantInfo.PortGrpc, qdrantInfo.Fingerprint, qdrantInfo.ApiToken); + databaseClient = new QdrantClientImplementation("Qdrant", qdrantInfo.Path, qdrantInfo.PortHttp, qdrantInfo.PortGrpc, qdrantInfo.Fingerprint, qdrantInfo.ApiToken); + } var builder = WebApplication.CreateBuilder(); builder.WebHost.ConfigureKestrel(kestrelServerOptions => diff --git a/app/MindWork AI Studio/Tools/Databases/DatabaseClient.cs b/app/MindWork AI Studio/Tools/Databases/DatabaseClient.cs index b50aafe1..b80cba94 100644 --- a/app/MindWork AI Studio/Tools/Databases/DatabaseClient.cs +++ b/app/MindWork AI Studio/Tools/Databases/DatabaseClient.cs @@ -3,6 +3,8 @@ public abstract class DatabaseClient(string name, string path) { public string Name => name; + + public virtual bool IsAvailable => true; private string Path => path; diff --git a/app/MindWork AI Studio/Tools/Databases/NoDatabaseClient.cs b/app/MindWork AI Studio/Tools/Databases/NoDatabaseClient.cs new file mode 100644 index 00000000..7b3b0cd4 --- /dev/null +++ b/app/MindWork AI Studio/Tools/Databases/NoDatabaseClient.cs @@ -0,0 +1,24 @@ +using AIStudio.Tools.PluginSystem; + +namespace AIStudio.Tools.Databases; + +public sealed class NoDatabaseClient(string name, string? unavailableReason) : DatabaseClient(name, string.Empty) +{ + private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(NoDatabaseClient).Namespace, nameof(NoDatabaseClient)); + + public override bool IsAvailable => false; + + public override async IAsyncEnumerable<(string Label, string Value)> GetDisplayInfo() + { + yield return (TB("Status"), TB("Unavailable")); + + if (!string.IsNullOrWhiteSpace(unavailableReason)) + yield return (TB("Reason"), unavailableReason); + + await Task.CompletedTask; + } + + public override void Dispose() + { + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/Rust/QdrantInfo.cs b/app/MindWork AI Studio/Tools/Rust/QdrantInfo.cs index c847235f..5315eca7 100644 --- a/app/MindWork AI Studio/Tools/Rust/QdrantInfo.cs +++ b/app/MindWork AI Studio/Tools/Rust/QdrantInfo.cs @@ -5,6 +5,10 @@ /// public readonly record struct QdrantInfo { + public bool IsAvailable { get; init; } + + public string? UnavailableReason { get; init; } + public string Path { get; init; } public int PortHttp { get; init; } diff --git a/runtime/src/qdrant.rs b/runtime/src/qdrant.rs index ffb241d5..dff8814f 100644 --- a/runtime/src/qdrant.rs +++ b/runtime/src/qdrant.rs @@ -39,10 +39,17 @@ static API_TOKEN: Lazy = Lazy::new(|| { }); static TMPDIR: Lazy>> = Lazy::new(|| Mutex::new(None)); +static QDRANT_STATUS: Lazy> = 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, +} + fn qdrant_base_path() -> PathBuf { let qdrant_directory = if is_dev() { "qdrant_test" } else { "qdrant" }; Path::new(DATA_DIRECTORY.get().unwrap()) @@ -57,16 +64,36 @@ pub struct ProvideQdrantInfo { port_grpc: u16, fingerprint: String, api_token: String, + is_available: bool, + unavailable_reason: Option, } #[get("/system/qdrant/info")] pub fn qdrant_port(_token: APIToken) -> Json { + let status = QDRANT_STATUS.lock().unwrap(); + let is_available = status.is_available; + let unavailable_reason = status.unavailable_reason.clone(); + Json(ProvideQdrantInfo { - path: qdrant_base_path().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, }) } @@ -76,13 +103,23 @@ pub fn start_qdrant_server(path_resolver: PathResolver){ 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); + 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-initialized").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()), @@ -90,24 +127,52 @@ pub fn start_qdrant_server(path_resolver: PathResolver){ (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 = String::from("resources/databases/qdrant/config.yaml"); - let qdrant_source_path = path_resolver.resolve_resource(qdrant_relative_source_path); + 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", qdrant_source_path.unwrap().to_str().unwrap()]) + 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); @@ -145,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 { @@ -206,6 +274,18 @@ pub fn cleanup_qdrant() { } +fn set_qdrant_available() { + let mut status = QDRANT_STATUS.lock().unwrap(); + status.is_available = true; + status.unavailable_reason = None; +} + +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> { if !path.exists() { return Ok(());