systemprompt_api/services/server/
runner.rs1use anyhow::{Context, Result};
12use std::sync::Arc;
13use systemprompt_identifiers::Actor;
14use systemprompt_runtime::AppContext;
15use systemprompt_scheduler::services::SchedulerHandle;
16use systemprompt_traits::{
17 Dependencies, JobContext, OwnedTask, Phase, StartupEvent, StartupEventExt, StartupEventSender,
18};
19
20use super::lifecycle::{
21 initialize_scheduler, reconcile_agents, reconcile_system_services, start_event_bridge,
22 start_registry_heartbeat, start_schema_watch, start_secrets_refresh,
23};
24use super::routes::role::LifecyclePlan;
25
26pub async fn run_server(
27 ctx: AppContext,
28 events: Option<StartupEventSender>,
29 early: super::startup::EarlyServer,
30) -> Result<()> {
31 let start_time = std::time::Instant::now();
32
33 let instance_claim = super::replica_identity::claim(&ctx)
34 .await
35 .context("replica identity")?;
36 tracing::info!(instance_id = %instance_claim.instance_id(), "replica identity claimed");
37
38 let plan = super::routes::role::lifecycle_plan(ctx.config().role);
39 tracing::info!(role = %ctx.config().role, "node role");
40
41 let scheduler_handle = start_role_phases(&ctx, plan, events.as_ref()).await?;
42
43 if let Some(ref tx) = events {
44 tx.phase_started(Phase::ApiServer);
45 }
46 let router = crate::services::server::setup_api_server(&ctx, events.as_ref())?;
47 start_accounting_recovery(&ctx).await?;
48 let addr = ctx.server_address();
49
50 early.activate(router);
51 let metrics_listener = start_metrics_listener(&ctx, plan.scheduler).await?;
52 super::readiness::signal_ready();
53
54 if let Some(ref tx) = events {
55 tx.phase_completed(Phase::ApiServer);
56 }
57
58 if let Some(ref tx) = events {
59 tx.startup_complete(start_time.elapsed(), format!("http://{}", addr), vec![]);
60 }
61
62 systemprompt_logging::set_startup_mode(false);
63
64 serve_until_shutdown(
65 &ctx,
66 early,
67 metrics_listener,
68 scheduler_handle,
69 instance_claim,
70 )
71 .await
72}
73
74async fn start_role_phases(
75 ctx: &AppContext,
76 plan: LifecyclePlan,
77 events: Option<&StartupEventSender>,
78) -> Result<Option<SchedulerHandle>> {
79 start_event_bridge(ctx);
80 start_registry_heartbeat(ctx);
81 start_secrets_refresh(ctx);
82 start_schema_watch(ctx);
83 if plan.reconcile_mcp {
84 let mcp_orchestrator = create_mcp_orchestrator(ctx)?;
85 reconcile_system_services(ctx, &mcp_orchestrator, events).await?;
86 }
87 if plan.reconcile_agents {
88 run_agents_phase(ctx, events).await?;
89 }
90 let scheduler_handle = if plan.scheduler {
91 run_scheduler_phase(ctx, events).await?
92 } else {
93 None
94 };
95 if plan.runner_services {
96 start_runner_services(ctx);
97 }
98 Ok(scheduler_handle)
99}
100
101async fn serve_until_shutdown(
102 ctx: &AppContext,
103 early: super::startup::EarlyServer,
104 metrics_listener: Option<OwnedTask<()>>,
105 scheduler_handle: Option<SchedulerHandle>,
106 instance_claim: systemprompt_database::InstanceClaim,
107) -> Result<()> {
108 let timings = ctx.config().shutdown;
109 tracing::info!(
110 pre_stop_delay_secs = timings.pre_stop_delay_secs,
111 drain_grace_secs = timings.drain_grace_secs,
112 forced_exit_grace_secs = timings.forced_exit_grace_secs,
113 total_budget_secs = timings.total_budget().as_secs(),
114 "Shutdown budget; the orchestrator's termination grace must cover the total"
115 );
116 let restart = ctx.shutdown_request().clone();
117 let serve_result = super::shutdown::join_within_drain_grace(
118 early.join(),
119 &restart,
120 &timings,
121 ctx.background_tasks().stream_drain(),
122 )
123 .await;
124
125 let forced_exit = super::shutdown::arm_forced_exit(restart, timings);
126 if let Some(listener) = metrics_listener
127 && listener.abort_and_join().await.is_some()
128 {
129 tracing::debug!("Metrics listener had already stopped on the shutdown signal");
130 }
131 super::shutdown::drain(ctx, scheduler_handle).await;
132 instance_claim.release().await;
133 forced_exit.abort();
134
135 serve_result
136}
137
138fn start_runner_services(ctx: &AppContext) {
139 let shared = Arc::new(ctx.clone());
140 let mut started = 0usize;
141 for extension in ctx.extension_registry().extensions() {
142 for service in extension.runner_services() {
143 let name = service.name();
144 let actor = Actor::job(ctx.system_admin().id().clone(), name);
145 let dependencies = Dependencies::new()
146 .with(Arc::clone(ctx.app_paths_arc()))
147 .with(Arc::clone(ctx.db_pool()))
148 .with(Arc::clone(&shared));
149 let job_ctx = JobContext::new(actor, dependencies);
150 ctx.background_tasks()
151 .spawn_cancellable(name, move |shutdown| async move {
152 service.run(job_ctx, shutdown).await;
153 });
154 tracing::info!(
155 extension = extension.id(),
156 service = name,
157 "runner service started"
158 );
159 started += 1;
160 }
161 }
162 tracing::info!(started, "runner services started");
163}
164
165async fn run_agents_phase(ctx: &AppContext, events: Option<&StartupEventSender>) -> Result<()> {
166 if let Some(tx) = events {
167 tx.phase_started(Phase::Agents);
168 }
169 match reconcile_agents(ctx, events).await {
170 Ok(started_count) => {
171 if let Some(tx) = events {
172 send_startup_event(
173 tx,
174 StartupEvent::AgentReconciliationComplete {
175 running: started_count,
176 total: started_count,
177 },
178 );
179 tx.phase_completed(Phase::Agents);
180 }
181 Ok(())
182 },
183 Err(e) => Err(fail_phase(
184 events,
185 Phase::Agents,
186 format!("Agent reconciliation failed: {e}"),
187 e,
188 )),
189 }
190}
191
192async fn run_scheduler_phase(
193 ctx: &AppContext,
194 events: Option<&StartupEventSender>,
195) -> Result<Option<SchedulerHandle>> {
196 if let Some(tx) = events {
197 tx.phase_started(Phase::Scheduler);
198 }
199 match initialize_scheduler(ctx, events).await {
200 Ok(handle) => {
201 if let Some(tx) = events {
202 tx.phase_completed(Phase::Scheduler);
203 }
204 Ok(handle)
205 },
206 Err(e) => Err(fail_phase(
207 events,
208 Phase::Scheduler,
209 format!("Scheduler initialization failed: {e}"),
210 e,
211 )),
212 }
213}
214
215fn fail_phase(
216 events: Option<&StartupEventSender>,
217 phase: Phase,
218 message: String,
219 error: anyhow::Error,
220) -> anyhow::Error {
221 if let Some(tx) = events {
222 tx.phase_failed(phase, error.to_string());
223 send_startup_event(
224 tx,
225 StartupEvent::Error {
226 message,
227 fatal: true,
228 },
229 );
230 }
231 error
232}
233
234fn send_startup_event(tx: &StartupEventSender, event: StartupEvent) {
235 if tx.unbounded_send(event).is_err() {
236 tracing::debug!("Startup event receiver dropped");
237 }
238}
239
240fn create_mcp_orchestrator(
241 ctx: &AppContext,
242) -> Result<Arc<systemprompt_mcp::services::McpOrchestrator>> {
243 use systemprompt_mcp::services::McpOrchestrator;
244 let manager = McpOrchestrator::new(
245 (**ctx.service_repository()).clone(),
246 Arc::clone(ctx.app_paths_arc()),
247 ctx.mcp_registry().clone(),
248 )?;
249 Ok(Arc::new(manager))
250}
251
252async fn start_metrics_listener(
253 ctx: &AppContext,
254 runs_scheduler: bool,
255) -> Result<Option<OwnedTask<()>>> {
256 let Some(port) = ctx.config().metrics_port else {
257 return Ok(None);
258 };
259 let handle = super::metrics::install_recorder(
260 &ctx.config().instance_id,
261 &ctx.extension_registry().all_metrics(),
262 )?;
263 super::pool_metrics::spawn_sampler(ctx);
264 super::ops_metrics::spawn_samplers(ctx, runs_scheduler);
265 let addr = std::net::SocketAddr::new(ctx.config().host.parse()?, port);
266 Ok(Some(
267 super::metrics::serve_metrics_listener(addr, handle).await?,
268 ))
269}
270
271async fn start_accounting_recovery(ctx: &AppContext) -> Result<()> {
272 let settlement = crate::routes::gateway::gateway_repositories(ctx)?.settlement();
273 let settled = systemprompt_gateway::audit::journal::recover(&settlement).await?;
274 if settled > 0 {
275 tracing::info!(settled, "Gateway accounting receipts recovered at startup");
276 }
277 systemprompt_gateway::audit::journal::spawn_recovery(settlement, ctx.background_tasks());
278 Ok(())
279}