use systemprompt_identifiers::ServiceName;
use systemprompt_manifest::services::{ServiceModule, ServiceStatus};
use systemprompt_traits::ProcessLiveness;
use super::claim::INSTANCE_CLAIM_CLASS;
use super::repo::ServiceRepository;
use crate::error::{DatabaseResult, RepositoryError};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReapedService {
pub name: ServiceName,
pub module_name: ServiceModule,
pub pid: u32,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Reaped {
pub stale_rows: u64,
pub dead: Vec<ReapedService>,
}
impl Reaped {
#[must_use]
pub const fn total(&self) -> u64 {
self.stale_rows + self.dead.len() as u64
}
}
impl ServiceRepository {
pub async fn reap_stale(&self, liveness: &dyn ProcessLiveness) -> DatabaseResult<Reaped> {
let stale_rows = sqlx::query!(
r#"
DELETE FROM services
WHERE instance_id = $1
AND (status = $2 OR (status = $3 AND pid IS NULL))
"#,
self.instance_id.as_str(),
ServiceStatus::Error.as_str(),
ServiceStatus::Running.as_str()
)
.execute(&*self.write_pool)
.await?
.rows_affected();
let candidates = sqlx::query!(
r#"
SELECT name, module_name, pid AS "pid!"
FROM services
WHERE instance_id = $1 AND status = $2 AND pid IS NOT NULL
ORDER BY name
"#,
self.instance_id.as_str(),
ServiceStatus::Running.as_str()
)
.fetch_all(&*self.write_pool)
.await?;
let mut dead = Vec::new();
for row in candidates {
let name = ServiceName::new(row.name);
let module_name = row.module_name.parse::<ServiceModule>().map_err(|source| {
RepositoryError::Decode {
context: "services.module_name".to_owned(),
source: Box::new(source),
}
})?;
let Ok(pid) = u32::try_from(row.pid) else {
self.delete_service(&name).await?;
dead.push(ReapedService {
name,
module_name,
pid: 0,
});
continue;
};
if liveness.owns(pid, &name).await {
continue;
}
self.delete_service(&name).await?;
dead.push(ReapedService {
name,
module_name,
pid,
});
}
Ok(Reaped { stale_rows, dead })
}
pub async fn touch_heartbeat(&self) -> DatabaseResult<u64> {
let result = sqlx::query!(
r#"UPDATE services SET heartbeat_at = CURRENT_TIMESTAMP WHERE instance_id = $1"#,
self.instance_id.as_str()
)
.execute(&*self.write_pool)
.await?;
Ok(result.rows_affected())
}
pub async fn delete_dead_instances(&self, older_than_secs: i64) -> DatabaseResult<u64> {
let result = sqlx::query!(
r#"
DELETE FROM services
WHERE heartbeat_at < CURRENT_TIMESTAMP - make_interval(secs => $1::double precision)
AND NOT EXISTS (
SELECT 1 FROM pg_locks l
WHERE l.locktype = 'advisory'
AND l.granted
AND l.classid = ($2::int)::oid
AND l.objid = (hashtext(services.instance_id))::oid
AND l.objsubid = 2
)
"#,
older_than_secs as f64,
INSTANCE_CLAIM_CLASS
)
.execute(&*self.write_pool)
.await?;
Ok(result.rows_affected())
}
}