Skip to main content

systemprompt_api/routes/admin/services/
refresh.rs

1//! `POST /admin/services/refresh`.
2//!
3//! Copyright (c) systemprompt.io — Business Source License 1.1.
4//! See <https://systemprompt.io> for licensing details.
5
6use std::time::Duration;
7
8use axum::Json;
9use axum::extract::{Extension, Query, State};
10use serde::Deserialize;
11use systemprompt_config::{ProfileBootstrap, SecretsBootstrap};
12use systemprompt_loader::ServicesSourceBootstrap;
13use systemprompt_loader::bundle::{BundleCache, cache_root};
14use systemprompt_loader::services_root::ServicesRootBootstrap;
15use systemprompt_models::RequestContext;
16use systemprompt_models::api::ApiError;
17use systemprompt_runtime::AppContext;
18
19use super::{
20    RefreshLock, ServicesRefreshResponse, composed_hash_of, provenance_view, source_views,
21};
22use crate::error::ApiHttpError;
23
24const RESTART_DELAY: Duration = Duration::from_millis(250);
25const RESTART_REASON: &str = "admin services refresh";
26
27#[derive(Debug, Clone, Copy, Default, Deserialize)]
28pub struct RefreshQuery {
29    #[serde(default)]
30    pub restart: bool,
31}
32
33pub async fn refresh(
34    State(ctx): State<AppContext>,
35    Extension(lock): Extension<RefreshLock>,
36    Extension(req_ctx): Extension<RequestContext>,
37    Query(query): Query<RefreshQuery>,
38) -> Result<Json<ServicesRefreshResponse>, ApiHttpError> {
39    let Some(_guard) = lock.try_acquire() else {
40        return Err(ApiError::conflict("a services refresh is already running").into());
41    };
42
43    let profile = ProfileBootstrap::get()
44        .map_err(|e| ApiHttpError::internal_error(format!("profile not ready: {e}")))?;
45    let secrets = SecretsBootstrap::get()
46        .map_err(|e| ApiHttpError::internal_error(format!("secrets not ready: {e}")))?;
47
48    let resolved = ServicesSourceBootstrap::resolve(
49        profile,
50        |name| secrets.get(name).cloned(),
51        env!("CARGO_PKG_VERSION"),
52    )
53    .await?;
54
55    let active_hash = ServicesRootBootstrap::get().and_then(composed_hash_of);
56    let new_hash = composed_hash_of(&resolved);
57    let changed = new_hash != active_hash;
58
59    let state = BundleCache::new(cache_root(profile)).read_state();
60    let restarting = changed && query.restart;
61
62    tracing::info!(
63        user_id = %req_ctx.user_id(),
64        changed,
65        composed_hash = new_hash.unwrap_or("none"),
66        provenance = %provenance_view(&resolved.provenance).kind,
67        restarting,
68        "Admin services refresh"
69    );
70
71    if restarting {
72        let ctx = ctx.clone();
73        tokio::spawn(async move {
74            tokio::time::sleep(RESTART_DELAY).await;
75            ctx.request_restart(RESTART_REASON);
76        });
77    }
78
79    Ok(Json(ServicesRefreshResponse {
80        changed,
81        composed_hash: new_hash.map(str::to_owned),
82        sources: source_views(&state),
83        restarting,
84    }))
85}