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