1use crate::audit::{AuditCategory, AuditEntry, AuditOutcome, AuditRecord};
14use crate::context::RuntimeContext;
15use crate::envelope::CommandEnvelope;
16use crate::error::{RuntimeError, RuntimeResult};
17use crate::handler::CommandResult;
18use crate::idempotency::{
19 IdempotencyRecord, IdempotencyStatus, IdempotencyStore, InMemoryIdempotencyStore,
20};
21use crate::lifecycle::{RuntimeLifecycle, RuntimeLifecycleEvent, RuntimeLifecycleState};
22use crate::runtime::RuntimeInstance;
23use parking_lot::Mutex;
24use std::sync::Arc;
25
26pub struct RuntimeController {
28 instance: Arc<RuntimeInstance>,
29 idempotency: Mutex<Box<dyn IdempotencyStore + Send + Sync>>,
30}
31
32impl RuntimeController {
33 pub fn new(instance: RuntimeInstance) -> Self {
35 Self {
36 instance: Arc::new(instance),
37 idempotency: Mutex::new(Box::new(InMemoryIdempotencyStore::new())),
38 }
39 }
40
41 pub fn with_idempotency_store(
43 instance: RuntimeInstance,
44 idempotency: Box<dyn IdempotencyStore + Send + Sync>,
45 ) -> Self {
46 Self {
47 instance: Arc::new(instance),
48 idempotency: Mutex::new(idempotency),
49 }
50 }
51
52 pub fn instance(&self) -> &RuntimeInstance {
54 &self.instance
55 }
56
57 pub fn instance_arc(&self) -> Arc<RuntimeInstance> {
59 self.instance.clone()
60 }
61
62 pub fn lifecycle(&self) -> &RuntimeLifecycle {
64 self.instance.lifecycle()
65 }
66
67 pub fn idempotency_len(&self) -> usize {
69 self.idempotency.lock().len()
70 }
71
72 pub fn idempotency_contains(&self, key: &str) -> RuntimeResult<bool> {
74 Ok(self.idempotency.lock().get(key)?.is_some())
75 }
76
77 pub fn apply_lifecycle_event(
79 &mut self,
80 event: RuntimeLifecycleEvent,
81 ) -> RuntimeResult<RuntimeLifecycleState> {
82 self.instance.lifecycle().apply(event)
83 }
84
85 pub fn dispatch_command(
87 &mut self,
88 command: &CommandEnvelope,
89 context: &dyn RuntimeContext,
90 ) -> RuntimeResult<CommandResult> {
91 match self.pre_dispatch(command)? {
92 Some(Ok(replay)) => Ok(replay),
93 Some(Err(err)) => Err(err),
94 None => {
95 let result = self.instance.dispatch_command(command, context);
96 self.post_dispatch(command, &result)?;
97 result
98 }
99 }
100 }
101
102 pub fn pre_dispatch(
104 &self,
105 command: &CommandEnvelope,
106 ) -> RuntimeResult<Option<RuntimeResult<CommandResult>>> {
107 if let Some(rejection) = self.check_lifecycle_readiness() {
108 let res = Ok(rejection);
109 self.record_audit(command, &res);
110 return Ok(Some(res));
111 }
112
113 if let Some(key) = command.idempotency_key.as_deref() {
114 let mut store = self.idempotency.lock();
115 if let Some(record) = store.get(key)? {
116 let payload_hash = hash_payload(&command.payload);
117 if record.request_hash != payload_hash {
118 let err = RuntimeError::IdempotencyConflict {
119 key: key.to_string(),
120 };
121 self.record_audit(command, &Err(err.clone()));
122 return Err(err);
123 }
124 match record.status {
125 IdempotencyStatus::Pending => {
126 let err = RuntimeError::IdempotencyPending {
127 key: key.to_string(),
128 };
129 self.record_audit(command, &Err(err.clone()));
130 return Err(err);
131 }
132 IdempotencyStatus::Resolved {
133 response_status,
134 ref response_body,
135 } => {
136 if response_status >= 400 {
137 store.remove(key)?;
138 return Ok(None);
139 }
140 let result = if response_body.is_empty() {
141 CommandResult::accepted(Vec::new())
142 } else {
143 serde_json::from_str::<CommandResult>(response_body).map_err(|e| {
144 RuntimeError::IdempotencyStoreIo {
145 operation: "deserialize_replay",
146 message: e.to_string(),
147 }
148 })?
149 };
150 let res = Ok(result);
151 self.record_audit(command, &res);
152 return Ok(Some(res));
153 }
154 }
155 }
156
157 let payload_hash = hash_payload(&command.payload);
159 let record = IdempotencyRecord {
160 key: key.to_string(),
161 request_hash: payload_hash,
162 status: IdempotencyStatus::Pending,
163 created_at_ms: now_ms(),
164 };
165 store.insert(record)?;
166 }
167
168 Ok(None)
169 }
170
171 pub fn post_dispatch(
173 &self,
174 command: &CommandEnvelope,
175 dispatch_result: &RuntimeResult<CommandResult>,
176 ) -> RuntimeResult<()> {
177 if let Some(key) = command.idempotency_key.as_ref() {
178 let mut store = self.idempotency.lock();
179 match dispatch_result {
180 Ok(result) => {
181 let response_body = serde_json::to_string(result).map_err(|e| {
182 RuntimeError::IdempotencyStoreIo {
183 operation: "serialize_response",
184 message: e.to_string(),
185 }
186 })?;
187 let record = IdempotencyRecord {
188 key: key.to_string(),
189 request_hash: hash_payload(&command.payload),
190 status: IdempotencyStatus::Resolved {
191 response_status: 200,
192 response_body,
193 },
194 created_at_ms: now_ms(),
195 };
196 store.insert(record)?;
197 }
198 Err(_) => {
199 store.remove(key)?;
200 }
201 }
202 }
203
204 self.emit_events_and_audit(command, dispatch_result);
205 Ok(())
206 }
207
208 fn check_lifecycle_readiness(&self) -> Option<CommandResult> {
209 match self.lifecycle().current() {
210 RuntimeLifecycleState::Running | RuntimeLifecycleState::Degraded => None,
211 RuntimeLifecycleState::Restricted => {
212 Some(CommandResult::rejected("runtime is restricted"))
213 }
214 _ => Some(CommandResult::rejected("runtime is not ready")),
215 }
216 }
217
218 fn emit_events_and_audit(
219 &self,
220 command: &CommandEnvelope,
221 dispatch_result: &RuntimeResult<CommandResult>,
222 ) {
223 if let Ok(result) = dispatch_result {
224 if result.is_accepted() {
225 let events = result
226 .events()
227 .iter()
228 .cloned()
229 .map(|event| {
230 if event.trace.is_none() {
231 if let Some(trace) = &command.trace {
232 return event.with_trace(trace.clone());
233 }
234 }
235 event
236 })
237 .collect::<Vec<_>>();
238 for event in &events {
239 let completed_at_ms = now_ms();
240 self.instance.audit_log().push_entry(
241 AuditEntry::new(
242 AuditCategory::Event,
243 event.event_id.clone(),
244 event.event_name.as_str(),
245 event.occurred_at_ms,
246 completed_at_ms,
247 AuditOutcome::Accepted,
248 )
249 .with_runtime_scope(&event.app_id, &event.node_id)
250 .with_trace(event.trace.clone()),
251 );
252 }
253 self.instance.event_bus().emit_many(events);
254 }
255 }
256 self.record_audit(command, dispatch_result);
257 }
258
259 fn record_audit(
260 &self,
261 command: &CommandEnvelope,
262 dispatch_result: &RuntimeResult<CommandResult>,
263 ) {
264 let record = match dispatch_result {
265 Ok(result) => AuditRecord {
266 command_id: command.command_id.clone(),
267 command_name: command.command_name.clone(),
268 app_id: command.app_id.clone(),
269 node_id: command.node_id.clone(),
270 timestamp_ms: command.issued_at_ms,
271 outcome: if result.is_accepted() {
272 AuditOutcome::Accepted
273 } else {
274 AuditOutcome::Rejected
275 },
276 message: result.message().map(|msg| msg.to_string()),
277 trace: command.trace.clone(),
278 },
279 Err(error) => AuditRecord {
280 command_id: command.command_id.clone(),
281 command_name: command.command_name.clone(),
282 app_id: command.app_id.clone(),
283 node_id: command.node_id.clone(),
284 timestamp_ms: command.issued_at_ms,
285 outcome: AuditOutcome::Error,
286 message: Some(format!("{error:?}")),
287 trace: command.trace.clone(),
288 },
289 };
290
291 self.instance.audit_log().push(record);
292 }
293}
294
295fn hash_payload(payload: &[u8]) -> String {
296 use sha2::{Digest, Sha256};
297 let mut hasher = Sha256::new();
298 hasher.update(payload);
299 let result = hasher.finalize();
300 let mut hex = String::with_capacity(result.len() * 2);
301 for byte in result {
302 hex.push_str(&format!("{:02x}", byte));
303 }
304 hex
305}
306
307fn now_ms() -> u64 {
308 std::time::SystemTime::now()
309 .duration_since(std::time::UNIX_EPOCH)
310 .map(|d| d.as_millis() as u64)
311 .unwrap_or(0)
312}
313
314#[cfg(test)]
315mod controller_tests;