1use super::active_registry::{ActiveTransactionRegistry, ControlMessage};
4use super::admission::{admit, AdmissionContext};
5use super::bootstrap::RuntimeBootstrap;
6use super::callback_service::CallbackService;
7use super::capacity::CapacityManagers;
8use super::channel_registry::{ChannelBinding, LiveChannel};
9use super::error::StartupError;
10use super::finalization::build_transaction_end;
11use super::host_tools::HostToolRegistry;
12use super::mcp::McpGateway;
13use super::state::RuntimeState;
14use monoloop_contracts::{
15 AdmissionError, AdmissionErrorKind, AdmissionReceipt, ChannelId, ChannelKind,
16 EventDeliveryOutcome, Shutdown, ShutdownDisposition, TerminationDisposition, TerminationMode,
17 TransactionEndKind, TransactionRequest, TransactionRuntime, TransactionSelector,
18};
19use std::collections::HashMap;
20use std::future::Future;
21use std::pin::Pin;
22use std::sync::atomic::{AtomicU8, Ordering};
23use std::sync::{Arc, Mutex};
24use std::time::Duration;
25use tokio::sync::mpsc;
26use tokio::sync::Mutex as AsyncMutex;
27
28pub type Startup = Pin<
30 Box<dyn Future<Output = Result<Arc<DefaultTransactionRuntime>, StartupError>> + Send + 'static>,
31>;
32
33const STATE_ACCEPTING: u8 = 1;
34const STATE_DRAINING: u8 = 2;
35const STATE_STOPPED: u8 = 3;
36
37fn decode_state(v: u8) -> RuntimeState {
38 match v {
39 STATE_ACCEPTING => RuntimeState::Accepting,
40 STATE_DRAINING => RuntimeState::Draining,
41 STATE_STOPPED => RuntimeState::Stopped,
42 _ => RuntimeState::Starting,
43 }
44}
45
46struct RuntimeInner {
47 state: Arc<AtomicU8>,
48 config: super::bootstrap::RuntimeConfig,
49 channels: Arc<HashMap<ChannelId, LiveChannel>>,
50 tools: HostToolRegistry,
51 capacity: Arc<CapacityManagers>,
52 registry: Arc<Mutex<ActiveTransactionRegistry>>,
53 mcp: AsyncMutex<Option<McpGateway>>,
54 mcp_handle: Option<super::mcp::McpGatewayHandle>,
56 callbacks: CallbackService,
58 executor: tokio::runtime::Handle,
60 shutdown_disposition: AsyncMutex<Option<ShutdownDisposition>>,
62}
63
64pub struct DefaultTransactionRuntime {
66 inner: Arc<RuntimeInner>,
67}
68
69impl DefaultTransactionRuntime {
70 pub fn start(bootstrap: RuntimeBootstrap) -> Startup {
72 Box::pin(async move { Self::start_inner(bootstrap).await })
73 }
74
75 async fn start_inner(bootstrap: RuntimeBootstrap) -> Result<Arc<Self>, StartupError> {
76 bootstrap.config.validate()?;
77 let executor = bootstrap.executor.clone();
78 let _ = executor.id();
79
80 let mut realized: Vec<(ChannelId, LiveChannel)> = Vec::new();
81 let mut capacity_pairs: Vec<(ChannelId, usize)> = Vec::new();
82
83 for (id, binding) in bootstrap.channels.iter() {
84 binding.descriptor().validate()?;
85
86 let instance = match binding.connector_factory.create() {
87 Ok(i) => i,
88 Err(e) => {
89 cleanup_partial(realized, None).await;
90 return Err(StartupError::from(e));
91 }
92 };
93
94 match binding.kind {
95 ChannelKind::DirectLlm => {
96 if instance.sessions.is_some() {
97 cleanup_partial(realized, None).await;
98 return Err(StartupError::SessionAdapterMismatch(
99 "DirectLlm must not have SessionAdapter",
100 ));
101 }
102 }
103 ChannelKind::ExternalAgent => {
104 if instance.sessions.is_none() {
105 cleanup_partial(realized, None).await;
106 return Err(StartupError::SessionAdapterMismatch(
107 "ExternalAgent requires SessionAdapter",
108 ));
109 }
110 }
111 }
112
113 capacity_pairs.push((
114 id.clone(),
115 binding
116 .limits
117 .max_active_transactions
118 .min(bootstrap.config.transaction_limits.max_active_per_channel),
119 ));
120
121 realized.push((
122 id.clone(),
123 LiveChannel {
124 binding: clone_binding(binding),
125 instance,
126 },
127 ));
128 }
129
130 let (mcp, mcp_handle) = if bootstrap.config.enable_mcp_listener {
131 match McpGateway::bind_loopback(256).await {
132 Ok(gw) => {
133 let handle = gw.handle();
134 (Some(gw), Some(handle))
135 }
136 Err(_) => {
137 cleanup_partial(realized, None).await;
138 return Err(StartupError::McpBindFailed);
139 }
140 }
141 } else {
142 (None, None)
143 };
144
145 let capacity = Arc::new(CapacityManagers::new(
146 bootstrap.config.transaction_limits.max_active_transactions,
147 capacity_pairs,
148 ));
149
150 let callbacks = CallbackService::new(
152 bootstrap
153 .config
154 .transaction_limits
155 .max_active_transactions
156 .max(1),
157 bootstrap.config.transaction_limits.callback_deadline,
158 executor.clone(),
159 );
160
161 let mut channels = HashMap::with_capacity(realized.len());
162 for (id, live) in realized {
163 channels.insert(id, live);
164 }
165
166 Ok(Arc::new(Self {
167 inner: Arc::new(RuntimeInner {
168 state: Arc::new(AtomicU8::new(STATE_ACCEPTING)),
169 config: bootstrap.config,
170 channels: Arc::new(channels),
171 tools: bootstrap.tools,
172 capacity,
173 registry: Arc::new(Mutex::new(ActiveTransactionRegistry::new())),
174 mcp: AsyncMutex::new(mcp),
175 mcp_handle,
176 callbacks,
177 executor,
178 shutdown_disposition: AsyncMutex::new(None),
179 }),
180 }))
181 }
182
183 pub fn state(&self) -> RuntimeState {
185 decode_state(self.inner.state.load(Ordering::SeqCst))
186 }
187
188 pub fn tools(&self) -> &HostToolRegistry {
190 &self.inner.tools
191 }
192
193 pub fn capacity(&self) -> &Arc<CapacityManagers> {
195 &self.inner.capacity
196 }
197
198 pub fn active_count(&self) -> usize {
200 self.inner.registry.lock().map(|r| r.len()).unwrap_or(0)
201 }
202
203 pub fn channel_count(&self) -> usize {
205 self.inner.channels.len()
206 }
207
208 pub async fn mcp_local_addr(&self) -> Option<std::net::SocketAddr> {
210 self.inner.mcp.lock().await.as_ref().map(|m| m.local_addr())
211 }
212
213 pub fn live_channel(&self, id: &ChannelId) -> Option<&LiveChannel> {
215 self.inner.channels.get(id)
216 }
217
218 async fn shutdown_inner(&self, deadline: Duration) -> ShutdownDisposition {
219 let global = if deadline.is_zero() {
221 self.inner.config.default_shutdown_deadline
222 } else {
223 deadline
224 };
225 let deadline_at = tokio::time::Instant::now() + global;
226
227 let prev = self.inner.state.swap(STATE_DRAINING, Ordering::SeqCst);
228 if prev == STATE_STOPPED || prev == STATE_DRAINING {
229 while self.inner.state.load(Ordering::SeqCst) != STATE_STOPPED {
231 if tokio::time::Instant::now() >= deadline_at {
232 break;
233 }
234 if let Some(d) = self.inner.shutdown_disposition.lock().await.clone() {
235 return d;
236 }
237 tokio::time::sleep(Duration::from_millis(10)).await;
238 }
239 return self
240 .inner
241 .shutdown_disposition
242 .lock()
243 .await
244 .clone()
245 .unwrap_or_default();
246 }
247
248 let active = {
249 let mut reg = self
250 .inner
251 .registry
252 .lock()
253 .unwrap_or_else(|e| e.into_inner());
254 reg.drain_all()
255 };
256
257 for entry in &active {
259 let _ = entry.control_tx.try_send(ControlMessage::ForceTerminate);
260 }
261
262 let mut normally_finalized = 0u64;
263 let mut supervisor_finalized = 0u64;
264 let mut callback_failed = 0u64;
265 let mut callback_aborted = 0u64;
266 let mut invariant_failed = 0u64;
267 let cb_cfg = self.inner.config.transaction_limits.callback_deadline;
268
269 let n = active.len().max(1);
270 let mut handles = Vec::with_capacity(active.len());
271 for entry in active {
272 let abort = entry.actor_join.abort_handle();
273 handles.push((entry, abort));
274 }
275
276 for (entry, abort) in handles {
277 let remaining = deadline_at.saturating_duration_since(tokio::time::Instant::now());
278 if remaining.is_zero() {
279 abort.abort();
280 let _ = entry.actor_join.await;
281 if let Some(payload) = entry.guard.try_claim() {
282 entry.guard.mark_callback_scheduled();
283 let end = build_transaction_end(
284 &payload,
285 TransactionEndKind::RuntimeShutdown,
286 None,
287 EventDeliveryOutcome::Failed,
288 entry.guard.sequencer().last_allocated(),
289 );
290 let cb_budget = Duration::ZERO;
292 match run_callback_isolated(payload.callback, end, cb_budget).await {
293 CallbackRun::Ok => supervisor_finalized += 1,
294 CallbackRun::Failed => {
295 supervisor_finalized += 1;
296 callback_failed += 1;
297 }
298 CallbackRun::Aborted => {
299 supervisor_finalized += 1;
300 callback_aborted += 1;
301 }
302 }
303 } else {
304 supervisor_finalized += 1;
305 }
306 (entry.release_capacity)();
307 continue;
308 }
309
310 let per = (remaining / n as u32).max(Duration::from_millis(20));
311 let mut join = entry.actor_join;
312 match tokio::time::timeout(per, &mut join).await {
313 Ok(Ok(())) => {
314 if entry.guard.callback_was_scheduled() {
315 normally_finalized += 1;
316 } else if let Some(payload) = entry.guard.try_claim() {
317 entry.guard.mark_callback_scheduled();
318 let end = build_transaction_end(
319 &payload,
320 TransactionEndKind::RuntimeShutdown,
321 None,
322 EventDeliveryOutcome::Failed,
323 entry.guard.sequencer().last_allocated(),
324 );
325 let cb_budget = cb_cfg.min(
326 deadline_at.saturating_duration_since(tokio::time::Instant::now()),
327 );
328 match run_callback_isolated(payload.callback, end, cb_budget).await {
329 CallbackRun::Ok => supervisor_finalized += 1,
330 CallbackRun::Failed => {
331 supervisor_finalized += 1;
332 callback_failed += 1;
333 }
334 CallbackRun::Aborted => {
335 supervisor_finalized += 1;
336 callback_aborted += 1;
337 }
338 }
339 } else {
340 normally_finalized += 1;
341 }
342 }
343 Ok(Err(_)) => {
344 invariant_failed += 1;
345 if let Some(payload) = entry.guard.try_claim() {
346 entry.guard.mark_callback_scheduled();
347 let end = build_transaction_end(
348 &payload,
349 TransactionEndKind::RuntimeShutdown,
350 None,
351 EventDeliveryOutcome::Failed,
352 0,
353 );
354 let cb_budget = deadline_at
355 .saturating_duration_since(tokio::time::Instant::now())
356 .min(cb_cfg);
357 let _ = run_callback_isolated(payload.callback, end, cb_budget).await;
358 supervisor_finalized += 1;
359 }
360 }
361 Err(_) => {
362 abort.abort();
364 let join_budget =
365 deadline_at.saturating_duration_since(tokio::time::Instant::now());
366 if join_budget.is_zero() {
367 let _ = join.await;
368 } else {
369 let _ = tokio::time::timeout(join_budget, join).await;
370 }
371 if let Some(payload) = entry.guard.try_claim() {
372 entry.guard.mark_callback_scheduled();
373 let end = build_transaction_end(
374 &payload,
375 TransactionEndKind::RuntimeShutdown,
376 None,
377 EventDeliveryOutcome::Failed,
378 entry.guard.sequencer().last_allocated(),
379 );
380 let cb_budget = cb_cfg.min(
381 deadline_at.saturating_duration_since(tokio::time::Instant::now()),
382 );
383 match run_callback_isolated(payload.callback, end, cb_budget).await {
384 CallbackRun::Ok => supervisor_finalized += 1,
385 CallbackRun::Failed => {
386 supervisor_finalized += 1;
387 callback_failed += 1;
388 }
389 CallbackRun::Aborted => {
390 supervisor_finalized += 1;
391 callback_aborted += 1;
392 }
393 }
394 } else {
395 supervisor_finalized += 1;
396 }
397 }
398 }
399 (entry.release_capacity)();
400 }
401
402 let mcp_budget = deadline_at.saturating_duration_since(tokio::time::Instant::now());
404 if let Some(mcp) = self.inner.mcp.lock().await.take() {
405 if !mcp_budget.is_zero() {
406 let _ = tokio::time::timeout(mcp_budget, mcp.shutdown()).await;
407 }
408 }
409
410 let cb_drain = deadline_at.saturating_duration_since(tokio::time::Instant::now());
412 if !cb_drain.is_zero() {
413 self.inner.callbacks.drain(cb_drain).await;
414 }
415
416 let disposition = ShutdownDisposition {
417 normally_finalized,
418 supervisor_finalized,
419 callback_failed,
420 callback_aborted,
421 invariant_failed,
422 };
423 *self.inner.shutdown_disposition.lock().await = Some(disposition.clone());
424 self.inner.state.store(STATE_STOPPED, Ordering::SeqCst);
425 disposition
426 }
427}
428
429enum CallbackRun {
431 Ok,
432 Failed,
433 Aborted,
434}
435
436async fn run_callback_isolated(
438 callback: Box<dyn monoloop_contracts::CompletionCallback>,
439 end: monoloop_contracts::TransactionEnd,
440 deadline: Duration,
441) -> CallbackRun {
442 let call = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| callback.call(end)));
443 match call {
444 Ok(fut) => {
445 let handle = tokio::spawn(fut);
446 let abort = handle.abort_handle();
447 match tokio::time::timeout(deadline, handle).await {
448 Ok(Ok(Ok(()))) => CallbackRun::Ok,
449 Ok(Ok(Err(_))) => CallbackRun::Failed,
450 Ok(Err(_)) => CallbackRun::Failed, Err(_) => {
452 abort.abort();
453 CallbackRun::Aborted
454 }
455 }
456 }
457 Err(_) => CallbackRun::Failed, }
459}
460
461fn clone_binding(binding: &ChannelBinding) -> ChannelBinding {
462 ChannelBinding {
463 id: binding.id.clone(),
464 kind: binding.kind,
465 tool_mode: binding.tool_mode,
466 connector_factory: Arc::clone(&binding.connector_factory),
467 encoder: Arc::clone(&binding.encoder),
468 interpreter: Arc::clone(&binding.interpreter),
469 endpoint_ref: binding.endpoint_ref.clone(),
470 credential_ref: binding.credential_ref.clone(),
471 defaults: binding.defaults.clone(),
472 capabilities: binding.capabilities.clone(),
473 limits: binding.limits.clone(),
474 }
475}
476
477async fn cleanup_partial(realized: Vec<(ChannelId, LiveChannel)>, mcp: Option<McpGateway>) {
478 drop(realized);
479 if let Some(m) = mcp {
480 m.shutdown().await;
481 }
482}
483
484impl TransactionRuntime for DefaultTransactionRuntime {
485 fn submit(&self, request: TransactionRequest) -> Result<AdmissionReceipt, AdmissionError> {
486 match self.state() {
487 RuntimeState::Accepting => {}
488 RuntimeState::Starting | RuntimeState::Draining | RuntimeState::Stopped => {
489 return Err(AdmissionError::new(
490 AdmissionErrorKind::RuntimeShuttingDown,
491 "runtime is not accepting submissions",
492 ));
493 }
494 }
495
496 let ctx = AdmissionContext {
497 channels: Arc::clone(&self.inner.channels),
498 tools: self.inner.tools.clone(),
499 capacity: Arc::clone(&self.inner.capacity),
500 registry: Arc::clone(&self.inner.registry),
501 limits: self.inner.config.transaction_limits.clone(),
502 mcp: self.inner.mcp_handle.clone(),
503 runtime_state: Arc::clone(&self.inner.state),
504 callbacks: self.inner.callbacks.clone(),
505 executor: self.inner.executor.clone(),
506 };
507 admit(&ctx, request)
508 }
509
510 fn terminate(
511 &self,
512 selector: TransactionSelector,
513 mode: TerminationMode,
514 ) -> TerminationDisposition {
515 if !matches!(
516 self.state(),
517 RuntimeState::Accepting | RuntimeState::Draining
518 ) {
519 return TerminationDisposition::NotFound;
520 }
521 let reg = self
522 .inner
523 .registry
524 .lock()
525 .unwrap_or_else(|e| e.into_inner());
526 let tx = match selector {
527 TransactionSelector::Transaction(id) => reg.control_tx(&id),
528 TransactionSelector::Session(key) => reg.control_tx_by_session(&key),
529 };
530 drop(reg);
531 let Some(tx) = tx else {
532 return TerminationDisposition::NotFound;
533 };
534 let msg = match mode {
535 TerminationMode::Cancel { .. } => ControlMessage::Cancel,
536 TerminationMode::ForceTerminate { .. } => ControlMessage::ForceTerminate,
537 };
538 match tx.try_send(msg) {
539 Ok(()) => TerminationDisposition::Accepted,
540 Err(mpsc::error::TrySendError::Full(_)) => TerminationDisposition::AlreadyRequested,
541 Err(mpsc::error::TrySendError::Closed(_)) => TerminationDisposition::AlreadyTerminal,
542 }
543 }
544
545 fn shutdown(&self, deadline: Duration) -> Shutdown {
546 let inner = Arc::clone(&self.inner);
547 Box::pin(async move {
548 let view = DefaultTransactionRuntime { inner };
549 view.shutdown_inner(deadline).await
550 })
551 }
552}