Skip to content

Commit d8a1b9b

Browse files
authored
feat: add a heartbeat worker to check Coco server availability (#988)
* feat: add a heartbeat worker to check Coco server availability * relase notes
1 parent f83b1ba commit d8a1b9b

5 files changed

Lines changed: 127 additions & 5 deletions

File tree

docs/content.en/docs/release-notes/_index.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ Information about release notes of Coco App is provided here.
1313

1414
### 🚀 Features
1515

16+
- feat: add a heartbeat worker to check Coco server availability #988
17+
1618
### 🐛 Bug fix
1719

1820
- fix: search_extension should not panic when ext is not found #983

src-tauri/src/common/register.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,11 @@ impl SearchSourceRegistry {
2222
sources.clear();
2323
}
2424

25-
pub async fn remove_source(&self, id: &str) {
25+
/// Remove the SearchSource specified by `id`, return a boolean indicating
26+
/// if it get removed or not.
27+
pub async fn remove_source(&self, id: &str) -> bool {
2628
let mut sources = self.sources.write().await;
27-
sources.remove(id);
29+
sources.remove(id).is_some()
2830
}
2931

3032
#[allow(dead_code)]

src-tauri/src/lib.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ pub mod util;
1212

1313
use crate::common::register::SearchSourceRegistry;
1414
use crate::common::{CHECK_WINDOW_LABEL, MAIN_WINDOW_LABEL, SETTINGS_WINDOW_LABEL};
15-
use crate::server::servers::{load_or_insert_default_server, load_servers_token};
15+
use crate::server::servers::{
16+
load_or_insert_default_server, load_servers_token, start_bg_heartbeat_worker,
17+
};
1618
use crate::util::logging::set_up_tauri_logger;
1719
use crate::util::prevent_default;
1820
use autostart::change_autostart;
@@ -285,6 +287,12 @@ pub async fn init(app_handle: &AppHandle) {
285287
.await;
286288
}
287289

290+
/*
291+
* Start the background heartbeat worker here after setting up Coco server
292+
* storage and SearchSourceRegistry.
293+
*/
294+
start_bg_heartbeat_worker(app_handle.clone());
295+
288296
extension::built_in::pizza_engine_runtime::start_pizza_engine_runtime().await;
289297
}
290298

src-tauri/src/server/servers.rs

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,11 @@ use serde_json::Value as JsonValue;
1313
use serde_json::from_value;
1414
use std::collections::HashMap;
1515
use std::sync::LazyLock;
16+
use std::thread;
17+
use std::time::Duration;
1618
use tauri::{AppHandle, Manager};
1719
use tauri_plugin_store::StoreExt;
20+
use tokio::runtime;
1821
use tokio::sync::RwLock;
1922

2023
/// Coco sever list
@@ -312,6 +315,109 @@ pub async fn refresh_all_coco_server_info(app_handle: AppHandle) {
312315
}
313316
}
314317

318+
/// Start a background worker that periodically sends heartbeats (`GET /provider/_info`)
319+
/// to the connected Coco servers, checks if they are available and updates the
320+
/// `SearchSourceRegistry` accordingly.
321+
pub(crate) fn start_bg_heartbeat_worker(tauri_app_handle: AppHandle) {
322+
const THREAD_NAME: &str = "Coco background heartbeat worker";
323+
const SLEEP_DURATION: Duration = Duration::from_secs(15);
324+
325+
let main_closure = || {
326+
let single_thread_rt = runtime::Builder::new_current_thread()
327+
.enable_all()
328+
.build()
329+
.unwrap_or_else(|e| {
330+
panic!(
331+
"failed to create a single-threaded Tokio runtime within thread [{}] because [{}]",
332+
THREAD_NAME, e
333+
);
334+
});
335+
336+
single_thread_rt.block_on(async move {
337+
let mut server_removed = Vec::new();
338+
let mut server_added = Vec::new();
339+
340+
let search_sources = tauri_app_handle.state::<SearchSourceRegistry>();
341+
loop {
342+
log::info!("Coco Server Heartbeat worker is working...");
343+
344+
refresh_all_coco_server_info(tauri_app_handle.clone()).await;
345+
346+
/*
347+
* For the Coco servers that are included in the SearchSourceRegistry
348+
* but unavailable, they should be removed from the registry.
349+
*
350+
* We do this step first so that there are less search source to
351+
* scan.
352+
*/
353+
for search_source in search_sources.get_sources().await {
354+
let query_source = search_source.get_type();
355+
let search_source_id = query_source.id;
356+
let search_source_name = query_source.name;
357+
358+
let Some(coco_server) = get_server_by_id(&search_source_id).await else {
359+
// This search source may not be a Coco server, try the next one.
360+
continue;
361+
};
362+
363+
assert!(
364+
coco_server.enabled,
365+
"Coco servers stored in search source list should all be enabled"
366+
);
367+
368+
if !coco_server.available {
369+
let removed = search_sources.remove_source(&search_source_id).await;
370+
if removed {
371+
server_removed.push((search_source_id, search_source_name));
372+
}
373+
}
374+
}
375+
376+
/*
377+
* Coco servers that are available and enabled should be added to
378+
* the SearchSourceRegistry if they are not already included.
379+
*/
380+
for coco_server in get_all_servers().await {
381+
if coco_server.enabled
382+
&& coco_server.available
383+
&& search_sources.get_source(&coco_server.id).await.is_none()
384+
{
385+
server_added.push((coco_server.id.clone(), coco_server.name.clone()));
386+
387+
let source = CocoSearchSource::new(coco_server);
388+
search_sources.register_source(source).await;
389+
}
390+
}
391+
392+
/*
393+
* Log the updates to SearchSourceRegistry
394+
*/
395+
log::info!(
396+
"Coco Server Heartbeat worker: removed {:?} from the SearchSourceRegistry",
397+
server_removed
398+
);
399+
log::info!(
400+
"Coco Server Heartbeat worker: added {:?} to the SearchSourceRegistry",
401+
server_added
402+
);
403+
404+
// Sleep for a period of time
405+
tokio::time::sleep(SLEEP_DURATION).await;
406+
}
407+
});
408+
};
409+
410+
thread::Builder::new()
411+
.name(THREAD_NAME.into())
412+
.spawn(main_closure)
413+
.unwrap_or_else(|e| {
414+
panic!(
415+
"failed to start thread [{}] for reason [{}]",
416+
THREAD_NAME, e
417+
)
418+
});
419+
}
420+
315421
#[tauri::command]
316422
pub async fn refresh_coco_server_info(app_handle: AppHandle, id: String) -> Result<Server, String> {
317423
// Retrieve the server from the cache

src-tauri/src/setup/mod.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,12 @@ pub(crate) async fn backend_setup(tauri_app_handle: AppHandle, app_lang: String)
8282
.expect("global tauri AppHandle already initialized");
8383
log::trace!("global Tauri AppHandle set");
8484

85+
/*
86+
* This should be set before Rust code makes any HTTP requests as it is
87+
* needed to provider HTTP header: X-APP-LANG
88+
*/
89+
update_app_lang(app_lang).await;
90+
8591
let registry = SearchSourceRegistry::default();
8692
tauri_app_handle.manage(registry); // Store registry in Tauri's app state
8793

@@ -110,8 +116,6 @@ pub(crate) async fn backend_setup(tauri_app_handle: AppHandle, app_lang: String)
110116

111117
autostart::ensure_autostart_state_consistent(&tauri_app_handle).unwrap();
112118

113-
update_app_lang(app_lang).await;
114-
115119
// Invoked, now update the state
116120
BACKEND_SETUP_COMPLETED.store(true, Ordering::Relaxed);
117121
}

0 commit comments

Comments
 (0)