AkurAI Build
Menu

Public repository / 811ef9b7c8ba017fa242f008efa90faab07a0454

AkurAI-Monitor / src/version_poller.rs

Latest change 811ef9b7c8ba017fa242f008efa90faab07a0454 - Adopt AkurAI-Framework UI layer and add framework-version tracking by Ólafur Búi Ólafsson

// Background thread that polls application health endpoints for version info.
// Every 5 minutes, iterates enabled applications with a non-null health_url,
// fetches each, and updates the version columns.

use std::collections::HashSet;
use std::sync::{LazyLock, Mutex};
use std::time::Duration;

use crate::db;

/// Track which apps have failed so we only log once per failure streak.
static FAILED: LazyLock<Mutex<HashSet<i64>>> = LazyLock::new(|| Mutex::new(HashSet::new()));

pub fn spawn() {
    std::thread::spawn(move || {
        loop {
            std::thread::sleep(Duration::from_secs(300));

            let apps: Vec<(i64, String)> = db::with_db(|conn| {
                let mut stmt = conn
                    .prepare(
                        "SELECT id, health_url FROM applications
                     WHERE enabled = 1 AND health_url IS NOT NULL",
                    )
                    .unwrap();
                stmt.query_map([], |row| Ok((row.get(0)?, row.get(1)?)))
                    .unwrap()
                    .filter_map(Result::ok)
                    .collect()
            });

            if apps.is_empty() {
                continue;
            }

            let client = reqwest::blocking::Client::builder()
                .timeout(Duration::from_secs(10))
                .build()
                .unwrap();

            for (app_id, health_url) in &apps {
                match client.get(health_url).send() {
                    Ok(resp) => {
                        let status = resp.status();
                        let body: Result<serde_json::Value, _> = resp.json();
                        let now = chrono::Utc::now().timestamp();

                        match body {
                            Ok(ref json) if status.is_success() => {
                                let app_version = json
                                    .get("version")
                                    .and_then(|v| v.as_str())
                                    .map(String::from);
                                let framework = json
                                    .get("framework")
                                    .and_then(|v| v.as_str())
                                    .map(String::from);
                                let framework_version_val = json
                                    .get("framework_version")
                                    .and_then(|v| v.as_str())
                                    .map(String::from);

                                db::with_db(|conn| {
                                    conn.execute(
                                        "UPDATE applications SET
                                     app_version = ?1,
                                     framework = ?2,
                                     framework_version = ?3,
                                     versions_reported_at = ?4,
                                     updated_at = ?4
                                     WHERE id = ?5",
                                        rusqlite::params![
                                            app_version,
                                            framework,
                                            framework_version_val,
                                            now,
                                            app_id
                                        ],
                                    )
                                    .ok();
                                });
                                // Clear failure tracking on success
                                FAILED.lock().unwrap().remove(app_id);
                            }
                            _ => {
                                log_failure_once(
                                    *app_id,
                                    &format!("health poll {health_url}: HTTP {status}"),
                                );
                            }
                        }
                    }
                    Err(e) => {
                        log_failure_once(*app_id, &format!("health poll {health_url}: {e}"));
                    }
                }
            }
        }
    });
}

fn log_failure_once(app_id: i64, msg: &str) {
    let mut failed = FAILED.lock().unwrap();
    if failed.insert(app_id) {
        tracing::warn!(app_id, "{msg}");
    }
}