1use crate::FileLoggingConfig;
4use saddle_core::{CallContext, Diagnostic};
5use serde::Serialize;
6use std::{
7 fmt,
8 fs::{self, File, OpenOptions},
9 io::{self, Write},
10 path::PathBuf,
11 sync::{
12 Arc, Condvar, Mutex, OnceLock,
13 atomic::{AtomicBool, AtomicU64, Ordering},
14 mpsc::{self, SyncSender, TrySendError},
15 },
16 thread::{self, JoinHandle},
17 time::Duration,
18};
19
20pub const EMERGENCY_FILE_NAME: &str = "saddle.emergency.log";
21pub const BOOTSTRAP_FILE_NAME: &str = "saddle.bootstrap.log";
22const QUEUE_CAPACITY: usize = 64;
23const RECORD_BYTES: usize = 32 * 1024;
24const TARGET_PATH_BYTES: usize = 4160;
25static ACTIVE: AtomicBool = AtomicBool::new(false);
26
27#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
28#[serde(rename_all = "snake_case")]
29pub enum DiagnosticSubmission {
30 OutputUnavailable,
33 Enqueued,
34 Written,
36 Full,
37 Closed,
38 EncodingFailed,
39}
40#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
41#[serde(rename_all = "snake_case")]
42pub enum DiagnosticShutdown {
43 Pending,
44 Finished,
45 WorkerPanicked,
46}
47#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize)]
48pub struct DiagnosticOutputSnapshot {
49 pub initialized: bool,
50 pub first_failure: Option<DiagnosticIoFailure>,
51 pub enqueued: u64,
52 pub written: u64,
53 pub dropped: u64,
54 pub output_failed: u64,
55 pub stderr_failed: u64,
56 pub stderr_suppressed: u64,
57 pub truncated: u64,
58 pub closed: bool,
59}
60#[derive(Default)]
61struct Status {
62 owns_active: AtomicBool,
63 initialized: AtomicBool,
64 failure: AtomicU64,
65 enqueued: AtomicU64,
66 written: AtomicU64,
67 dropped: AtomicU64,
68 output_failed: AtomicU64,
69 stderr_failed: AtomicU64,
70 stderr_suppressed: AtomicU64,
71 truncated: AtomicU64,
72 closed: AtomicBool,
73 submitting: AtomicU64,
74 worker_stopped: AtomicBool,
75 fallback_path: OnceLock<BoundedTargetPath>,
76 fallback_file: Mutex<Option<File>>,
77 confirmation: Mutex<Confirmation>,
78 confirmation_ready: Condvar,
79}
80impl Drop for Status {
81 fn drop(&mut self) {
82 if self.owns_active.load(Ordering::Acquire) {
83 drop(self.fallback_file.get_mut().unwrap_or_else(|e| e.into_inner()).take());
84 ACTIVE.store(false, Ordering::Release);
85 }
86 }
87}
88#[derive(Default)]
89struct Confirmation {
90 next: u64,
91 active: Option<u64>,
92 completed: Option<(u64, io::Result<()>)>,
93}
94struct BoundedTargetPath {
95 bytes: [u8; TARGET_PATH_BYTES],
96 len: usize,
97}
98impl BoundedTargetPath {
99 fn new(path: &std::path::Path) -> io::Result<Self> {
100 use std::os::unix::ffi::OsStrExt;
101 let source = path.as_os_str().as_bytes();
102 if source.len() > TARGET_PATH_BYTES {
103 return Err(io::Error::new(io::ErrorKind::InvalidInput, "emergency path exceeds bound"));
104 }
105 let mut bytes = [0; TARGET_PATH_BYTES];
106 bytes[..source.len()].copy_from_slice(source);
107 Ok(Self { bytes, len: source.len() })
108 }
109 fn as_path(&self) -> &std::path::Path {
110 use std::os::unix::ffi::OsStrExt;
111 std::path::Path::new(std::ffi::OsStr::from_bytes(&self.bytes[..self.len]))
112 }
113}
114struct ConfirmationLease<'a> {
115 status: &'a Status,
116 sequence: u64,
117}
118impl Status {
119 fn acquire_confirmation(&self) -> ConfirmationLease<'_> {
120 let mut state = self.confirmation.lock().unwrap_or_else(|e| e.into_inner());
121 while state.active.is_some() {
122 state = self.confirmation_ready.wait(state).unwrap_or_else(|e| e.into_inner());
123 }
124 state.next = state.next.wrapping_add(1);
125 let sequence = state.next;
126 state.active = Some(sequence);
127 ConfirmationLease { status: self, sequence }
128 }
129 fn acknowledge(&self, sequence: u64, delivery: io::Result<()>) {
130 let mut state = self.confirmation.lock().unwrap_or_else(|e| e.into_inner());
131 if state.active == Some(sequence) {
132 state.completed = Some((sequence, delivery));
133 self.confirmation_ready.notify_all();
134 }
135 }
136}
137impl ConfirmationLease<'_> {
138 fn wait(&self) -> io::Result<()> {
139 let mut state = self.status.confirmation.lock().unwrap_or_else(|e| e.into_inner());
140 loop {
141 if let Some((sequence, delivery)) = state.completed.take() {
142 debug_assert_eq!(sequence, self.sequence);
143 return delivery;
144 }
145 if self.status.worker_stopped.load(Ordering::Acquire) {
146 return Err(io::Error::new(io::ErrorKind::BrokenPipe, "diagnostic writer stopped"));
147 }
148 state = self.status.confirmation_ready.wait(state).unwrap_or_else(|e| e.into_inner());
149 }
150 }
151}
152impl Drop for ConfirmationLease<'_> {
153 fn drop(&mut self) {
154 let mut state = self.status.confirmation.lock().unwrap_or_else(|e| e.into_inner());
155 if state.active == Some(self.sequence) {
156 state.active = None;
157 state.completed = None;
158 self.status.confirmation_ready.notify_all();
159 }
160 }
161}
162impl Status {
163 fn fail(&self, stage: u64, error: &io::Error) {
164 let kind = match error.kind() {
165 io::ErrorKind::PermissionDenied => 1,
166 io::ErrorKind::NotFound => 2,
167 io::ErrorKind::NotADirectory => 3,
168 io::ErrorKind::InvalidInput => 4,
169 io::ErrorKind::StorageFull => 5,
170 io::ErrorKind::BrokenPipe => 6,
171 io::ErrorKind::WouldBlock => 7,
172 _ => 8,
173 };
174 let os = error.raw_os_error();
175 let packed = (stage << 56)
176 | (kind << 48)
177 | (u64::from(os.is_some()) << 40)
178 | u64::from(os.unwrap_or(0) as u32);
179 let _ = self
180 .failure
181 .compare_exchange(0, packed, Ordering::AcqRel, Ordering::Acquire);
182 }
183 fn snapshot(&self) -> DiagnosticOutputSnapshot {
184 let failure = self.failure.load(Ordering::Acquire);
185 DiagnosticOutputSnapshot {
186 initialized: self.initialized.load(Ordering::Acquire),
187 first_failure: (failure != 0).then(|| DiagnosticIoFailure {
188 stage: if failure >> 56 == 1 {
189 "initialize"
190 } else {
191 "write"
192 },
193 kind: [
194 "none",
195 "permission_denied",
196 "not_found",
197 "not_a_directory",
198 "invalid_input",
199 "storage_full",
200 "broken_pipe",
201 "would_block",
202 "other",
203 ][((failure >> 48) & 255) as usize],
204 os_code: (failure & (1 << 40) != 0).then_some(failure as u32 as i32),
205 }),
206 enqueued: self.enqueued.load(Ordering::Acquire),
207 written: self.written.load(Ordering::Acquire),
208 dropped: self.dropped.load(Ordering::Acquire),
209 output_failed: self.output_failed.load(Ordering::Acquire),
210 stderr_failed: self.stderr_failed.load(Ordering::Acquire),
211 stderr_suppressed: self.stderr_suppressed.load(Ordering::Acquire),
212 truncated: self.truncated.load(Ordering::Acquire),
213 closed: self.closed.load(Ordering::Acquire),
214 }
215 }
216}
217
218#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
219pub struct DiagnosticIoFailure {
220 pub stage: &'static str,
221 pub kind: &'static str,
222 pub os_code: Option<i32>,
223}
224
225pub struct EmergencyDiagnostics {
227 handle: EmergencyDiagnosticHandle,
228 worker: Option<JoinHandle<()>>,
229 target: PathBuf,
230 source_target_preopened: bool,
231}
232#[derive(Clone)]
241pub struct SourceOutput(EmergencyDiagnosticHandle);
242impl SourceOutput {
243 pub fn handle(&self) -> &EmergencyDiagnosticHandle { &self.0 }
244}
245#[derive(Clone)]
249pub struct BootstrapDiagnostics {
250 inner: Arc<BootstrapInner>,
251}
252struct BootstrapInner {
253 file: Mutex<File>,
254 target: PathBuf,
255}
256static BOOTSTRAP_ACTIVE: OnceLock<Mutex<std::sync::Weak<BootstrapInner>>> = OnceLock::new();
257
258impl BootstrapDiagnostics {
259 pub fn start(directory: &std::path::Path) -> Result<Self, EmergencyInitError> {
260 validate_emergency_directory(directory)?;
261 let target = directory.join(BOOTSTRAP_FILE_NAME);
262 let registry = BOOTSTRAP_ACTIVE.get_or_init(|| Mutex::new(std::sync::Weak::new()));
263 let mut active = registry.lock().unwrap_or_else(|error| error.into_inner());
264 if let Some(inner) = active.upgrade() {
265 return if inner.target == target {
266 Ok(Self { inner })
267 } else {
268 Err(EmergencyInitError::AlreadyActive)
269 };
270 }
271 let file = open_emergency_file(&target, true)
272 .map_err(|error| EmergencyInitError::Spawn(error.kind()))?;
273 let inner = Arc::new(BootstrapInner { file: Mutex::new(file), target });
274 *active = Arc::downgrade(&inner);
275 Ok(Self { inner })
276 }
277
278 pub fn target(&self) -> &std::path::Path { &self.inner.target }
279
280 pub(crate) fn write_original_record<T: Serialize>(&self, record: &T) -> io::Result<()> {
281 let mut bytes = FixedDiagnosticBytes { bytes: [0; 8192], len: 0 };
282 serde_json::to_writer(&mut bytes, record).map_err(io::Error::other)?;
283 bytes.bytes[bytes.len] = b'\n';
284 bytes.len += 1;
285 self.inner.file.lock().unwrap_or_else(|error| error.into_inner())
286 .write_all(&bytes.bytes[..bytes.len])
287 }
288}
289#[derive(Clone)]
290pub struct EmergencyDiagnosticHandle {
291 sender: SyncSender<DiagnosticPacket>,
292 status: Arc<Status>,
293}
294
295pub(crate) fn finalization_process_instance() -> u128 {
298 static INSTANCE: std::sync::OnceLock<u128> = std::sync::OnceLock::new();
299 *INSTANCE.get_or_init(|| std::time::SystemTime::now()
300 .duration_since(std::time::UNIX_EPOCH).map_or(0, |v| v.as_nanos()))
301}
302struct DetachedObjectLifetime {
303 context: saddle_core::request_context::RequestContextSnapshot,
304 output: EmergencyDiagnosticHandle,
305 process_instance: u128,
306 kind: &'static str,
307}
308impl DetachedObjectLifetime {
309 fn emit(&self, fact: saddle_admission::ObjectLifetimeFact, created: bool) {
310 static SEQUENCE: AtomicU64 = AtomicU64::new(1);
311 static CLOCK: OnceLock<std::time::Instant> = OnceLock::new();
312 #[derive(Serialize)]
313 struct Record<'a> {
314 schema: &'static str, pid: u32, process_instance: u128, local_request: u64,
315 sequence: u64, monotonic_ns: u128, kind: &'static str, phase: &'static str,
316 object_id: u64, ledger_instance: u64, account: saddle_admission::AccountIdentity,
317 storage_bytes: usize, storage_allocated: bool, ledger_charge_applied: bool,
318 storage_destroyed: bool, ledger_charge_released: bool,
319 context: &'a saddle_core::request_context::RequestContextSnapshot,
320 }
321 let _ = self.output.submit_fixed_record(&Record {
322 schema: "request-object-lifetime/1", pid: std::process::id(),
323 process_instance: self.process_instance, local_request: self.context.local_request_id(),
324 sequence: SEQUENCE.fetch_add(1, Ordering::Relaxed),
325 monotonic_ns: CLOCK.get_or_init(std::time::Instant::now).elapsed().as_nanos().saturating_add(1),
326 kind: self.kind, phase: if created { "created" } else { "released" },
327 object_id: fact.object_id, ledger_instance: fact.ledger_instance, account: fact.account,
328 storage_bytes: fact.storage_bytes, storage_allocated: created, ledger_charge_applied: created,
329 storage_destroyed: !created, ledger_charge_released: !created, context: &self.context,
330 });
331 }
332}
333impl saddle_admission::ObjectLifetimeObserver for DetachedObjectLifetime {
334 fn created(&self, fact: saddle_admission::ObjectLifetimeFact) { self.emit(fact, true); }
335 fn released(&self, fact: saddle_admission::ObjectLifetimeFact) { self.emit(fact, false); }
336}
337fn rpc_lifecycle_stamp() -> (u64, u128) {
338 static SEQUENCE: AtomicU64 = AtomicU64::new(1);
339 static CLOCK: OnceLock<std::time::Instant> = OnceLock::new();
340 (SEQUENCE.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |n| n.checked_add(1)).unwrap_or(0),
341 CLOCK.get_or_init(std::time::Instant::now).elapsed().as_nanos().saturating_add(1))
342}
343#[doc(hidden)]
345#[derive(Clone)]
346pub struct RpcPhysicalOwnerReceipt {
347 context: saddle_core::request_context::RequestContextSnapshot,
348 output: EmergencyDiagnosticHandle,
349 process_instance: u128,
350 call_id: [u8; 256],
351 call_id_len: usize,
352}
353impl RpcPhysicalOwnerReceipt {
354 #[doc(hidden)]
355 pub fn driver_phase(&self, stage: saddle_admission::RpcStageReleaseFact,
356 driver_id: u64, phase: &'static str) {
357 #[derive(Serialize)]
358 struct Record<'a> {
359 schema: &'static str, event: &'static str, pid: u32, process_instance: u128,
360 local_request: u64, call_id: &'a str, driver_id: u64, phase: &'static str,
361 sequence: u64, monotonic_ns: u128, stage: saddle_admission::RpcStageReleaseFact,
362 context: &'a saddle_core::request_context::RequestContextSnapshot,
363 }
364 let (sequence, monotonic_ns) = rpc_lifecycle_stamp();
365 let _ = self.output.submit_fixed_record(&Record {
366 schema: "request-rpc-driver/1", event: "rpc_driver_lifetime", pid: std::process::id(),
367 process_instance: self.process_instance, local_request: self.context.local_request_id(),
368 call_id: std::str::from_utf8(&self.call_id[..self.call_id_len]).unwrap(), driver_id, phase,
369 sequence, monotonic_ns, stage, context: &self.context,
370 });
371 }
372 #[doc(hidden)]
373 pub fn released(self, stage: saddle_admission::RpcStageReleaseFact) {
374 #[derive(Serialize)]
375 struct Record<'a> {
376 schema: &'static str, event: &'static str, pid: u32,
377 process_instance: u128, local_request: u64, call_id: &'a str,
378 sequence: u64, monotonic_ns: u128,
379 stage: saddle_admission::RpcStageReleaseFact,
380 context: &'a saddle_core::request_context::RequestContextSnapshot,
381 }
382 let (sequence, monotonic_ns) = rpc_lifecycle_stamp();
383 let _ = self.output.submit_fixed_record(&Record {
384 schema: "request-rpc-physical-owner/1", event: "rpc_shared_allocation_released",
385 pid: std::process::id(), process_instance: self.process_instance,
386 local_request: self.context.local_request_id(),
387 call_id: std::str::from_utf8(&self.call_id[..self.call_id_len]).unwrap(),
388 sequence, monotonic_ns, stage, context: &self.context,
389 });
390 }
391}
392struct DetachedRpcStageRelease {
393 context: saddle_core::request_context::RequestContextSnapshot,
394 output: EmergencyDiagnosticHandle,
395 process_instance: u128,
396 call_id: [u8; 256],
397 call_id_len: usize,
398}
399impl saddle_admission::RpcStageReleaseObserver for DetachedRpcStageRelease {
400 fn released(&self, fact: saddle_admission::RpcStageReleaseFact) {
401 #[derive(Serialize)]
402 struct Record<'a> {
403 schema: &'static str, event: &'static str, pid: u32,
404 process_instance: u128, local_request: u64, call_id: &'a str,
405 sequence: u64, monotonic_ns: u128,
406 stage: saddle_admission::RpcStageReleaseFact,
407 context: &'a saddle_core::request_context::RequestContextSnapshot,
408 }
409 let (sequence, monotonic_ns) = rpc_lifecycle_stamp();
410 let call_id = std::str::from_utf8(&self.call_id[..self.call_id_len]).unwrap();
411 let _ = self.output.submit_fixed_record(&Record {
412 schema: "request-rpc-stage/1", event: "rpc_stage_credit_released",
413 pid: std::process::id(), process_instance: self.process_instance,
414 local_request: self.context.local_request_id(), call_id,
415 sequence, monotonic_ns,
416 stage: fact, context: &self.context,
417 });
418 }
419}
420struct DetachedAccountRelease {
421 context: saddle_core::request_context::RequestContextSnapshot,
422 output: EmergencyDiagnosticHandle,
423 process_instance: u128,
424}
425impl saddle_admission::AccountReleaseObserver for DetachedAccountRelease {
426 fn released(&self, fact: saddle_admission::AccountReleaseFact) {
427 #[derive(Serialize)]
428 struct Record<'a> {
429 schema: &'static str, event: &'static str, process_id: u32,
430 process_instance: u128, completion: &'static str,
431 account: saddle_admission::AccountReleaseFact,
432 context: &'a saddle_core::request_context::RequestContextSnapshot,
433 }
434 let _ = self.output.submit_fixed_record(&Record {
435 schema: "request-finalization/1", event: "request_account_released",
436 process_id: std::process::id(), process_instance: self.process_instance,
437 completion: "physically_released", account: fact, context: &self.context,
438 });
439 }
440}
441impl EmergencyDiagnosticHandle {
442 #[doc(hidden)]
443 pub fn record_request_stop_decision(&self,
444 context: saddle_core::request_context::RequestContextSnapshot, cause: &'static str) {
445 if !matches!(cause, "original_deadline" | "peer_disconnected") { return; }
446 #[derive(Serialize)]
447 struct Record {
448 schema: &'static str, event: &'static str, decision_point: &'static str,
449 pid: u32, process_instance: u128, local_request: u64,
450 sequence: u64, monotonic_ns: u128, cause: &'static str,
451 context: saddle_core::request_context::RequestContextSnapshot,
452 }
453 let (sequence, monotonic_ns) = rpc_lifecycle_stamp();
454 let _ = self.submit_fixed_record(&Record {
455 schema: "request-stop/1", event: "request_stop_decided",
456 decision_point: "handler_select_before_dispatch_drop", pid: std::process::id(),
457 process_instance: finalization_process_instance(), local_request: context.local_request_id(),
458 sequence, monotonic_ns, cause, context,
459 });
460 }
461 #[doc(hidden)]
464 pub fn record_encoding_selection_association(&self,
465 context: saddle_core::request_context::RequestContextSnapshot,
466 original_deadline_unix_ms: i64,
467 source: Option<(saddle_core::DiagnosticOccurrence, saddle_core::RequestExecutionView)>) -> DiagnosticSubmission {
468 #[derive(Serialize)]
469 struct Record<'a> {
470 schema: &'static str, event: &'static str, decision_point: &'static str,
471 pid: u32, process_instance: u128, local_request: u64,
472 sequence: u64, monotonic_ns: u128, original_deadline_unix_ms: i64,
473 source_association: &'static str,
474 source_occurrence: Option<saddle_core::DiagnosticOccurrence>,
475 source_context: Option<saddle_core::request_context::UnifiedContext<'a>>,
476 context: saddle_core::request_context::RequestContextSnapshot,
477 }
478 let (sequence, monotonic_ns) = rpc_lifecycle_stamp();
479 crate::root_diagnostic::original::write_encoding_selection(self, &Record {
480 schema: "encoding-terminal-source/1", event: "encoding_terminal_selected",
481 decision_point: "encoding_response_selection", pid: std::process::id(),
482 process_instance: finalization_process_instance(), local_request: context.local_request_id(),
483 sequence, monotonic_ns, original_deadline_unix_ms,
484 source_association: if source.is_some() { "original_timeout" } else { "unavailable" },
485 source_occurrence: source.as_ref().map(|value| value.0),
486 source_context: source.as_ref().map(|value| value.1.unified_context()), context,
487 })
488 }
489 #[doc(hidden)]
490 pub fn rpc_physical_owner_receipt(&self,
491 context: saddle_core::request_context::RequestContextSnapshot, call_id: &str)
492 -> Option<RpcPhysicalOwnerReceipt> {
493 if call_id.is_empty() || call_id.len() > 256 { return None; }
494 let mut copied = [0; 256];
495 copied[..call_id.len()].copy_from_slice(call_id.as_bytes());
496 Some(RpcPhysicalOwnerReceipt { context, output: self.clone(),
497 process_instance: finalization_process_instance(), call_id: copied, call_id_len: call_id.len() })
498 }
499 #[doc(hidden)]
500 pub fn prepare_rpc_stage_release(&self,
501 context: saddle_core::request_context::RequestContextSnapshot,
502 storage: &saddle_admission::ProcessLogStorage, call_id: &str)
503 -> Option<saddle_admission::RpcStageReleaseObservation> {
504 if call_id.is_empty() || call_id.len() > 256 { return None; }
506 let mut copied = [0u8; 256];
507 copied[..call_id.len()].copy_from_slice(call_id.as_bytes());
508 saddle_admission::RpcStageReleaseObservation::prepare(storage, DetachedRpcStageRelease {
509 context, output: self.clone(), process_instance: finalization_process_instance(),
510 call_id: copied, call_id_len: call_id.len(),
511 }).ok()
512 }
513 #[doc(hidden)]
514 pub fn record_shutdown_signal_received(&self, application: saddle_core::ContextLabel, kind: &'static str, received: bool) {
515 #[derive(Serialize)]
516 struct Record {
517 schema: &'static str, pid: u32, process_instance: u128, application: saddle_core::ContextLabel,
518 signal: &'static str, received: bool, sequence: u64, monotonic_ns: u128,
519 }
520 let (sequence, monotonic_ns) = rpc_lifecycle_stamp();
521 let _ = self.submit_fixed_record(&Record {
522 schema: "process-signal-receipt/1", pid: std::process::id(),
523 process_instance: finalization_process_instance(), application,
524 signal: kind, received, sequence, monotonic_ns,
525 });
526 }
527 #[doc(hidden)]
528 pub fn record_request_task_first_poll(&self,
529 context: saddle_core::request_context::RequestContextSnapshot) {
530 #[derive(Serialize)]
531 struct Record {
532 schema: &'static str, event: &'static str, pid: u32, process_instance: u128,
533 local_request: u64, sequence: u64, monotonic_ns: u128,
534 context: saddle_core::request_context::RequestContextSnapshot,
535 }
536 let (sequence, monotonic_ns) = rpc_lifecycle_stamp();
537 let _ = self.submit_fixed_record(&Record {
538 schema: "request-task-execution/1", event: "before_first_body_poll",
539 pid: std::process::id(), process_instance: finalization_process_instance(),
540 local_request: context.local_request_id(), sequence, monotonic_ns, context,
541 });
542 }
543 #[doc(hidden)]
544 pub fn record_request_lifecycle(&self, application: &str, event: &'static str,
545 observed_phase: u8, active_claims: usize) {
546 #[derive(Serialize)]
547 struct Record<'a> {
548 schema: &'static str, pid: u32, process_instance: u128,
549 application: &'a str, event: &'static str, observed_phase: u8,
550 active_claims: usize, sequence: u64, monotonic_ns: u128,
551 }
552 let (sequence, monotonic_ns) = rpc_lifecycle_stamp();
553 let _ = self.submit_fixed_record(&Record {
554 schema: "request-lifecycle-boundary/1", pid: std::process::id(),
555 process_instance: finalization_process_instance(), application, event,
556 observed_phase, active_claims, sequence, monotonic_ns,
557 });
558 }
559 #[doc(hidden)]
560 pub fn record_delivery_attempt(&self, context: saddle_core::request_context::RequestContextSnapshot,
561 attempt_id: u64, phase: &'static str, write_starts: u64, write_completions: u64,
562 bytes_written: u64, delivery: saddle_core::ResponseDelivery) {
563 let (sequence, monotonic_ns) = rpc_lifecycle_stamp();
564 if sequence == 0 { return; }
565 #[derive(Serialize)]
566 struct Record {
567 schema: &'static str, pid: u32, process_instance: u128, local_request: u64,
568 sequence: u64, monotonic_ns: u128,
569 attempt_id: u64, phase: &'static str, write_starts: u64, write_completions: u64,
570 bytes_written: u64, delivery: saddle_core::ResponseDelivery,
571 context: saddle_core::request_context::RequestContextSnapshot,
572 }
573 let _ = self.submit_fixed_record(&Record {
574 schema: "request-delivery-attempt/1", pid: std::process::id(),
575 process_instance: finalization_process_instance(), local_request: context.local_request_id(),
576 sequence, monotonic_ns, attempt_id, phase, write_starts, write_completions, bytes_written, delivery, context,
577 });
578 }
579 #[doc(hidden)]
580 pub fn record_request_database_admission(&self,
581 context: saddle_core::request_context::RequestContextSnapshot,
582 ledger_instance: u64, account: saddle_admission::AccountIdentity,
583 accepted: bool, reason: &'static str) {
584 #[derive(Serialize)]
585 struct Record {
586 schema: &'static str, event: &'static str, pid: u32, process_instance: u128,
587 local_request: u64, ledger_instance: u64, account: saddle_admission::AccountIdentity,
588 accepted: bool, reason: &'static str, sequence: u64, monotonic_ns: u128,
589 context: saddle_core::request_context::RequestContextSnapshot,
590 }
591 let (sequence, monotonic_ns) = rpc_lifecycle_stamp();
592 let _ = self.submit_fixed_record(&Record {
593 schema: "request-database-admission/1", event: "database_stage_attempt",
594 pid: std::process::id(), process_instance: finalization_process_instance(),
595 local_request: context.local_request_id(), ledger_instance, account, accepted, reason,
596 sequence, monotonic_ns, context,
597 });
598 }
599 #[doc(hidden)]
600 pub fn record_request_task_owner(&self,
601 context: saddle_core::request_context::RequestContextSnapshot,
602 ledger_instance: u64, account: saddle_admission::AccountIdentity, task_role: &'static str) {
603 #[derive(Serialize)]
604 struct Record {
605 schema: &'static str, event: &'static str, pid: u32, process_instance: u128,
606 local_request: u64, ledger_instance: u64, account: saddle_admission::AccountIdentity,
607 task_role: &'static str, sequence: u64, monotonic_ns: u128,
608 context: saddle_core::request_context::RequestContextSnapshot,
609 }
610 let (sequence, monotonic_ns) = rpc_lifecycle_stamp();
611 let _ = self.submit_fixed_record(&Record {
612 schema: "request-task-roster/2", event: "task_owner_bound", pid: std::process::id(),
613 process_instance: finalization_process_instance(), local_request: context.local_request_id(),
614 ledger_instance, account, task_role, sequence, monotonic_ns, context,
615 });
616 }
617 #[doc(hidden)]
618 pub fn record_request_read_owner(&self,
619 context: saddle_core::request_context::RequestContextSnapshot,
620 ledger_instance: u64, account: saddle_admission::AccountIdentity,
621 peer: std::net::SocketAddrV4) {
622 #[derive(Serialize)]
623 struct Record {
624 schema: &'static str, event: &'static str, pid: u32, process_instance: u128,
625 local_request: u64, ledger_instance: u64, account: saddle_admission::AccountIdentity,
626 peer_address: [u8; 4], peer_port: u16, sequence: u64, monotonic_ns: u128,
627 context: saddle_core::request_context::RequestContextSnapshot,
628 }
629 let (sequence, monotonic_ns) = rpc_lifecycle_stamp();
630 let _ = self.submit_fixed_record(&Record {
631 schema: "request-read-owner/1", event: "recognition_read_head_entered",
632 pid: std::process::id(), process_instance: finalization_process_instance(),
633 local_request: context.local_request_id(), ledger_instance, account,
634 peer_address: peer.ip().octets(), peer_port: peer.port(), sequence, monotonic_ns, context,
635 });
636 }
637 #[doc(hidden)]
638 pub fn record_request_account_identity(&self, view: &saddle_core::RequestExecutionView,
639 permit: &saddle_admission::StoragePermit) {
640 let Some(account) = permit.release_account_identity() else { return; };
641 let context = view.diagnostic_snapshot();
642 #[derive(Serialize)]
643 struct Record<'a> {
644 schema: &'static str, event: &'static str, pid: u32, process_instance: u128,
645 local_request: u64, ledger_instance: u64, account: saddle_admission::AccountIdentity,
646 context: &'a saddle_core::request_context::RequestContextSnapshot,
647 }
648 let _ = self.submit_fixed_record(&Record {
649 schema: "request-account-identity/1", event: "request_account_bound",
650 pid: std::process::id(), process_instance: finalization_process_instance(),
651 local_request: context.local_request_id(), ledger_instance: permit.diagnostic_ledger_instance(),
652 account, context: &context,
653 });
654 }
655 #[doc(hidden)]
656 pub fn prepare_object_lifetime(&self, view: &saddle_core::RequestExecutionView,
657 storage: &saddle_admission::ProcessLogStorage, kind: &'static str)
658 -> Option<saddle_admission::ObjectLifetimeObservation> {
659 if !matches!(kind, "raw_body" | "typed_input" | "db_parameter_text") { return None; }
660 saddle_admission::ObjectLifetimeObservation::prepare(storage, DetachedObjectLifetime {
661 context: view.diagnostic_snapshot(), output: self.clone(),
662 process_instance: finalization_process_instance(), kind,
663 }).ok()
664 }
665 #[doc(hidden)]
668 pub fn prepare_account_release(&self, view: &saddle_core::RequestExecutionView,
669 storage: &saddle_admission::ProcessLogStorage)
670 -> Option<saddle_admission::AccountReleaseObservation> {
671 let context = view.diagnostic_snapshot();
674 saddle_admission::AccountReleaseObservation::prepare(storage, DetachedAccountRelease {
675 context, output: self.clone(), process_instance: finalization_process_instance(),
676 }).ok()
677 }
678}
679
680#[allow(clippy::large_enum_variant)] enum DiagnosticPacket {
683 Startup(Vec<u8>, Option<saddle_core::DeferredDiagnosticStack>),
684 Admitted(FixedDiagnosticBytes),
685 Original(FixedDiagnosticBytes),
686 ConfirmedOriginal(FixedDiagnosticBytes, u64),
687}
688impl DiagnosticPacket {
689 fn bytes(&self) -> &[u8] {
690 match self {
691 Self::Startup(v, _) => v,
692 Self::Admitted(v) | Self::Original(v) | Self::ConfirmedOriginal(v, _) => &v.bytes[..v.len],
693 }
694 }
695 fn resolve_on_worker(&mut self, status: &Status) {
696 let Self::Startup(bytes, stack) = self else {
697 return;
698 };
699 let Some(stack) = stack.take() else {
700 return;
701 };
702 let resolved = stack.resolve_on_output_worker();
703 let Ok(mut record) = serde_json::from_slice::<serde_json::Value>(bytes) else {
704 status.dropped.fetch_add(1, Ordering::Relaxed);
705 return;
706 };
707 record["diagnostic"]["stack_status"] = resolved.stack_status.into();
708 record["diagnostic"]["stack"] = resolved.stack.into();
709 record["diagnostic"]["stack_truncated"] = resolved.stack_truncated.into();
710 if resolved.stack_truncated {
711 status.truncated.fetch_add(1, Ordering::Relaxed);
712 }
713 let mut encoded = LimitedBytes(Vec::with_capacity(RECORD_BYTES));
714 if serde_json::to_writer(&mut encoded, &record).is_err() {
715 encoded.0.clear();
716 record["diagnostic"]["stack"] = "".into();
717 record["diagnostic"]["stack_status"] = "omitted_output_limit".into();
718 record["diagnostic"]["stack_truncated"] = true.into();
719 record["encoding_truncated"] = true.into();
720 status.truncated.fetch_add(1, Ordering::Relaxed);
721 if serde_json::to_writer(&mut encoded, &record).is_err() {
722 return;
724 }
725 }
726 encoded.0.push(b'\n');
727 *bytes = encoded.0;
728 }
729}
730#[derive(Clone)]
731pub(crate) struct FixedDiagnosticBytes {
732 pub(crate) bytes: [u8; 8192],
733 pub(crate) len: usize,
734}
735pub(crate) fn root_frame_layouts() -> (std::alloc::Layout, std::alloc::Layout, usize) {
736 (
737 std::alloc::Layout::new::<FixedDiagnosticBytes>(),
738 std::alloc::Layout::new::<DiagnosticPacket>(),
739 QUEUE_CAPACITY,
740 )
741}
742pub(crate) fn root_status_owner_layout() -> std::alloc::Layout {
745 #[repr(C, align(2))]
747 struct ArcOwner<T> { strong: std::sync::atomic::AtomicUsize, weak: std::sync::atomic::AtomicUsize, value: T }
748 std::alloc::Layout::new::<ArcOwner<Status>>()
749}
750impl Write for FixedDiagnosticBytes {
751 fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
752 if bytes.len() > self.bytes.len().saturating_sub(self.len + 1) {
753 return Err(io::ErrorKind::WriteZero.into());
754 }
755 self.bytes[self.len..self.len + bytes.len()].copy_from_slice(bytes);
756 self.len += bytes.len();
757 Ok(bytes.len())
758 }
759 fn flush(&mut self) -> io::Result<()> {
760 Ok(())
761 }
762}
763
764#[derive(Debug)]
765pub enum EmergencyInitError {
766 InvalidDirectory,
767 AlreadyActive,
768 Open(io::Error),
769 Spawn(io::ErrorKind),
770}
771impl fmt::Display for EmergencyInitError {
772 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
773 write!(f, "emergency diagnostic initialization failed: {self:?}")
774 }
775}
776impl std::error::Error for EmergencyInitError {
777 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
778 match self { Self::Open(error) => Some(error), _ => None }
779 }
780}
781
782struct LimitedBytes(Vec<u8>);
783impl Write for LimitedBytes {
784 fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
785 if self.0.len().saturating_add(bytes.len()) > RECORD_BYTES - 1 {
786 return Err(io::Error::new(
787 io::ErrorKind::InvalidData,
788 "diagnostic encoding bound",
789 ));
790 }
791 self.0.extend_from_slice(bytes);
792 Ok(bytes.len())
793 }
794 fn flush(&mut self) -> io::Result<()> {
795 Ok(())
796 }
797}
798#[derive(Serialize)]
799struct Envelope<'a> {
800 event: &'static str,
801 timestamp_unix_ms: u128,
802 diagnostic: &'a Diagnostic,
803 trace_id: Option<&'a str>,
804 rpc_id: Option<String>,
805 span_id: Option<String>,
806 operation: Option<String>,
807 request: Option<&'a str>,
808 route: Option<&'a str>,
809}
810
811impl<'a> Envelope<'a> {
812 fn new(
813 diagnostic: &'a Diagnostic,
814 context: Option<(&'a CallContext, &'a crate::EventContext)>,
815 ) -> Self {
816 Self {
817 event: "framework.diagnostic",
818 timestamp_unix_ms: std::time::SystemTime::now()
819 .duration_since(std::time::UNIX_EPOCH)
820 .unwrap_or_default()
821 .as_millis(),
822 diagnostic,
823 trace_id: context.map(|(call, _)| call.trace_correlation_id().as_str()),
824 rpc_id: context
825 .and_then(|(call, _)| call.rpc_correlation_id().map(|rpc| rpc.as_str().to_owned())),
826 span_id: context.map(|(call, _)| call.span_id().to_string()),
827 operation: context.map(|(call, _)| call.operation().to_string()),
828 request: context.map(|(_, event)| event.diagnostic_request()),
829 route: context.map(|(_, event)| event.diagnostic_route()),
830 }
831 }
832}
833
834impl EmergencyDiagnosticHandle {
835 pub fn submit_bounded(
839 &self,
840 diagnostic: Option<&saddle_core::BoundedDiagnostic>,
841 axes: &saddle_core::DiagnosticOutcomeAxes,
842 context: Option<(&CallContext, &crate::EventContext)>,
843 ) -> DiagnosticSubmission {
844 self.submit_bounded_parts(
845 diagnostic,
846 axes,
847 context.map(|(c, e)| (c, e.diagnostic_request(), e.diagnostic_route())),
848 None,
849 )
850 }
851 pub(crate) fn submit_bounded_parts(
852 &self,
853 diagnostic: Option<&saddle_core::BoundedDiagnostic>,
854 axes: &saddle_core::DiagnosticOutcomeAxes,
855 context: Option<(&CallContext, &str, &str)>,
856 stage: Option<(&'static str, u64)>,
857 ) -> DiagnosticSubmission {
858 self.encode_bounded(
859 if stage.is_some() { None } else { diagnostic },
860 diagnostic.map(|d| d.occurrence()),
861 axes,
862 context,
863 stage,
864 stage.is_some(),
865 )
866 }
867 pub fn submit_boundary(
870 &self,
871 occurrence: Option<saddle_core::DiagnosticOccurrence>,
872 axes: &saddle_core::DiagnosticOutcomeAxes,
873 context: Option<(&CallContext, &crate::EventContext)>,
874 ) -> DiagnosticSubmission {
875 self.encode_bounded(
876 None,
877 occurrence,
878 axes,
879 context.map(|(c, e)| (c, e.diagnostic_request(), e.diagnostic_route())),
880 None,
881 true,
882 )
883 }
884 pub(crate) fn submit_boundary_parts(
885 &self,
886 occurrence: Option<saddle_core::DiagnosticOccurrence>,
887 axes: &saddle_core::DiagnosticOutcomeAxes,
888 context: Option<(&CallContext, &str, &str)>,
889 stage: Option<(&'static str, u64)>,
890 ) -> DiagnosticSubmission {
891 self.encode_bounded(None, occurrence, axes, context, stage, true)
892 }
893 #[allow(clippy::too_many_arguments)]
894 fn encode_bounded(
895 &self,
896 diagnostic: Option<&saddle_core::BoundedDiagnostic>,
897 occurrence: Option<saddle_core::DiagnosticOccurrence>,
898 axes: &saddle_core::DiagnosticOutcomeAxes,
899 context: Option<(&CallContext, &str, &str)>,
900 stage: Option<(&'static str, u64)>,
901 boundary: bool,
902 ) -> DiagnosticSubmission {
903 self.status.submitting.fetch_add(1, Ordering::AcqRel);
904 struct Exit<'a>(&'a AtomicU64);
905 impl Drop for Exit<'_> {
906 fn drop(&mut self) {
907 self.0.fetch_sub(1, Ordering::Release);
908 }
909 }
910 let _exit = Exit(&self.status.submitting);
911 if context.is_some_and(|(c, request, route)| {
913 c.operation().as_str().len() > 256 || request.len() > 128 || route.len() > 192
914 }) {
915 self.status.dropped.fetch_add(1, Ordering::Relaxed);
916 return DiagnosticSubmission::EncodingFailed;
917 }
918 if self.status.closed.load(Ordering::Acquire) {
919 self.status.dropped.fetch_add(1, Ordering::Relaxed);
920 return DiagnosticSubmission::Closed;
921 }
922 #[derive(Serialize)]
923 struct Record<'a> {
924 event: &'static str,
925 stage: Option<&'static str>,
926 elapsed_ms: Option<u64>,
927 timestamp_unix_ms: u128,
928 diagnostic: Option<&'a saddle_core::BoundedDiagnostic>,
929 diagnostic_reference: Option<saddle_core::DiagnosticOccurrence>,
930 axes: &'a saddle_core::DiagnosticOutcomeAxes,
931 trace_id: Option<&'a str>,
932 rpc_id: Option<&'a str>,
933 span_id: Option<&'a str>,
934 operation: Option<&'a str>,
935 request: Option<&'a str>,
936 route: Option<&'a str>,
937 }
938 let mut span = [b'0'; 16];
939 if let Some((call, _, _)) = context {
940 let value = call.span_id().as_u64();
941 for (i, byte) in span.iter_mut().enumerate() {
942 *byte = b"0123456789abcdef"[((value >> ((15 - i) * 4)) & 15) as usize];
943 }
944 }
945 let record = Record {
946 event: if stage.is_some() {
947 "framework.stage.finished"
948 } else if boundary {
949 "framework.boundary.outcome"
950 } else {
951 "framework.diagnostic"
952 },
953 stage: stage.map(|s| s.0),
954 elapsed_ms: stage.map(|s| s.1),
955 timestamp_unix_ms: std::time::SystemTime::now()
956 .duration_since(std::time::UNIX_EPOCH)
957 .unwrap_or_default()
958 .as_millis(),
959 diagnostic,
960 diagnostic_reference: occurrence,
961 axes,
962 trace_id: context.map(|(c, _, _)| c.trace_correlation_id().as_str()),
963 rpc_id: context.and_then(|(c, _, _)| c.rpc_correlation_id().map(|r| r.as_str())),
964 span_id: context.and_then(|_| std::str::from_utf8(&span).ok()),
965 operation: context.map(|(c, _, _)| c.operation().as_str()),
966 request: context.map(|(_, request, _)| request),
967 route: context.map(|(_, _, route)| route),
968 };
969 self.encode_and_queue(&record)
970 }
971 pub(crate) fn submit_fixed_record<T: Serialize>(&self, record: &T) -> DiagnosticSubmission {
973 self.status.submitting.fetch_add(1, Ordering::AcqRel);
974 let result = if self.status.closed.load(Ordering::Acquire) {
975 self.status.dropped.fetch_add(1, Ordering::Relaxed);
976 DiagnosticSubmission::Closed
977 } else {
978 self.encode_and_queue(record)
979 };
980 self.status.submitting.fetch_sub(1, Ordering::Release);
981 result
982 }
983 fn encode_and_queue<T: Serialize>(&self, record: &T) -> DiagnosticSubmission {
984 self.encode_and_queue_fallback(record, None, false)
985 }
986 pub(crate) fn submit_original_record<T: Serialize>(&self, record: &T) -> DiagnosticSubmission {
987 self.status.submitting.fetch_add(1, Ordering::AcqRel);
988 let result = if self.status.closed.load(Ordering::Acquire) {
989 self.status.dropped.fetch_add(1, Ordering::Relaxed);
990 DiagnosticSubmission::Closed
991 } else {
992 self.encode_and_queue_fallback(record, None, true)
993 };
994 self.status.submitting.fetch_sub(1, Ordering::Release);
995 result
996 }
997 pub(crate) fn write_original_record<T: Serialize>(&self, record: &T) -> io::Result<()> {
1000 self.status.submitting.fetch_add(1, Ordering::AcqRel);
1001 struct Exit<'a>(&'a AtomicU64);
1002 impl Drop for Exit<'_> {
1003 fn drop(&mut self) { self.0.fetch_sub(1, Ordering::Release); }
1004 }
1005 let _exit = Exit(&self.status.submitting);
1006 let mut bytes = FixedDiagnosticBytes { bytes: [0; 8192], len: 0 };
1007 serde_json::to_writer(&mut bytes, record).map_err(io::Error::other)?;
1008 bytes.bytes[bytes.len] = b'\n';
1009 bytes.len += 1;
1010 let lease = self.status.acquire_confirmation();
1011 let retry = bytes.clone();
1012 match self.sender.send(DiagnosticPacket::ConfirmedOriginal(bytes, lease.sequence)) {
1013 Ok(()) => {
1014 self.status.enqueued.fetch_add(1, Ordering::Relaxed);
1015 match lease.wait() {
1016 Err(error) if error.kind() == io::ErrorKind::BrokenPipe
1017 && self.status.worker_stopped.load(Ordering::Acquire) => {
1018 self.write_original_after_worker(&retry, false)
1019 }
1020 result => result,
1021 }
1022 }
1023 Err(_) => self.write_original_after_worker(&retry, true),
1024 }
1025 }
1026 fn write_original_after_worker(&self, bytes: &FixedDiagnosticBytes, count_admission: bool) -> io::Result<()> {
1029 if !self.status.worker_stopped.load(Ordering::Acquire) {
1030 return Err(io::Error::new(io::ErrorKind::BrokenPipe, "diagnostic writer unavailable"));
1031 }
1032 let path = self.status.fallback_path.get().ok_or_else(||
1033 io::Error::new(io::ErrorKind::NotFound, "diagnostic output target unavailable"))?;
1034 let mut fallback = self.status.fallback_file.lock().unwrap_or_else(|e| e.into_inner());
1035 if fallback.is_none() {
1036 *fallback = Some(open_emergency_file(path.as_path(), false)?);
1037 }
1038 fallback.as_mut().expect("fallback file opened").write_all(&bytes.bytes[..bytes.len])?;
1039 if count_admission { self.status.enqueued.fetch_add(1, Ordering::Relaxed); }
1040 self.status.written.fetch_add(1, Ordering::Release);
1041 Ok(())
1042 }
1043 pub(crate) fn submit_fixed_with_fallback<T: Serialize>(
1044 &self,
1045 record: &T,
1046 fallback: &T,
1047 ) -> DiagnosticSubmission {
1048 self.status.submitting.fetch_add(1, Ordering::AcqRel);
1049 let result = if self.status.closed.load(Ordering::Acquire) {
1050 self.status.dropped.fetch_add(1, Ordering::Relaxed);
1051 DiagnosticSubmission::Closed
1052 } else {
1053 self.encode_and_queue_fallback(record, Some(fallback), false)
1054 };
1055 self.status.submitting.fetch_sub(1, Ordering::Release);
1056 result
1057 }
1058 fn encode_and_queue_fallback<T: Serialize>(
1059 &self,
1060 record: &T,
1061 fallback: Option<&T>,
1062 original: bool,
1063 ) -> DiagnosticSubmission {
1064 let mut bytes = FixedDiagnosticBytes {
1065 bytes: [0; 8192],
1066 len: 0,
1067 };
1068 if serde_json::to_writer(&mut bytes, record).is_err() {
1069 bytes.len = 0;
1070 let recovered = fallback
1071 .is_some_and(|fallback| serde_json::to_writer(&mut bytes, fallback).is_ok());
1072 if !recovered {
1073 self.status.dropped.fetch_add(1, Ordering::Relaxed);
1074 return DiagnosticSubmission::EncodingFailed;
1075 }
1076 self.status.truncated.fetch_add(1, Ordering::Relaxed);
1077 }
1078 bytes.bytes[bytes.len] = b'\n';
1079 bytes.len += 1;
1080 let packet = if original {
1081 DiagnosticPacket::Original(bytes)
1082 } else {
1083 DiagnosticPacket::Admitted(bytes)
1084 };
1085 match self.sender.try_send(packet) {
1086 Ok(()) => {
1087 self.status.enqueued.fetch_add(1, Ordering::Relaxed);
1088 DiagnosticSubmission::Enqueued
1089 }
1090 Err(TrySendError::Full(_)) => {
1091 self.status.dropped.fetch_add(1, Ordering::Relaxed);
1092 DiagnosticSubmission::Full
1093 }
1094 Err(TrySendError::Disconnected(_)) => {
1095 self.status.dropped.fetch_add(1, Ordering::Relaxed);
1096 DiagnosticSubmission::Closed
1097 }
1098 }
1099 }
1100 pub fn snapshot(&self) -> DiagnosticOutputSnapshot {
1101 self.status.snapshot()
1102 }
1103 pub fn submit(&self, diagnostic: &Diagnostic) -> DiagnosticSubmission {
1105 self.submit_context(diagnostic, None)
1106 }
1107 pub fn submit_context(
1108 &self,
1109 diagnostic: &Diagnostic,
1110 context: Option<(&CallContext, &crate::EventContext)>,
1111 ) -> DiagnosticSubmission {
1112 self.submit_existing_record(
1113 &Envelope::new(diagnostic, context),
1114 diagnostic.deferred_stack(),
1115 )
1116 }
1117 pub(crate) fn submit_existing_record<T: Serialize>(
1120 &self,
1121 record: &T,
1122 stack: Option<saddle_core::DeferredDiagnosticStack>,
1123 ) -> DiagnosticSubmission {
1124 let active = self.status.submitting.fetch_add(1, Ordering::AcqRel);
1125 struct Exit<'a>(&'a AtomicU64);
1126 impl Drop for Exit<'_> {
1127 fn drop(&mut self) {
1128 self.0.fetch_sub(1, Ordering::AcqRel);
1129 }
1130 }
1131 let _exit = Exit(&self.status.submitting);
1132 if active >= QUEUE_CAPACITY as u64 {
1133 self.status.dropped.fetch_add(1, Ordering::Relaxed);
1134 return DiagnosticSubmission::Full;
1135 }
1136 if self.status.closed.load(Ordering::Acquire) {
1137 self.status.dropped.fetch_add(1, Ordering::Relaxed);
1138 return DiagnosticSubmission::Closed;
1139 }
1140 let mut bytes = LimitedBytes(Vec::with_capacity(RECORD_BYTES));
1141 if serde_json::to_writer(&mut bytes, record).is_err() {
1142 bytes.0.clear();
1143 let mut reduced = match serde_json::to_value(record) {
1144 Ok(value) => value,
1145 Err(_) => {
1146 self.status.dropped.fetch_add(1, Ordering::Relaxed);
1147 return DiagnosticSubmission::EncodingFailed;
1148 }
1149 };
1150 reduced["diagnostic"]["stack"] = serde_json::Value::String(String::new());
1151 reduced["diagnostic"]["stack_truncated"] = true.into();
1152 reduced["encoding_truncated"] = true.into();
1153 self.status.truncated.fetch_add(1, Ordering::Relaxed);
1154 if serde_json::to_writer(&mut bytes, &reduced).is_err() {
1155 self.status.dropped.fetch_add(1, Ordering::Relaxed);
1156 return DiagnosticSubmission::EncodingFailed;
1157 }
1158 }
1159 bytes.0.push(b'\n');
1160 match self
1161 .sender
1162 .try_send(DiagnosticPacket::Startup(bytes.0, stack))
1163 {
1164 Ok(()) => {
1165 self.status.enqueued.fetch_add(1, Ordering::Relaxed);
1166 DiagnosticSubmission::Enqueued
1167 }
1168 Err(TrySendError::Full(_)) => {
1169 self.status.dropped.fetch_add(1, Ordering::Relaxed);
1170 DiagnosticSubmission::Full
1171 }
1172 Err(TrySendError::Disconnected(_)) => {
1173 self.status.dropped.fetch_add(1, Ordering::Relaxed);
1174 DiagnosticSubmission::Closed
1175 }
1176 }
1177 }
1178}
1179
1180impl crate::Observer {
1181 pub fn record_diagnostic(
1184 &self,
1185 diagnostic: &Diagnostic,
1186 emergency: &EmergencyDiagnosticHandle,
1187 context: Option<(&CallContext, &crate::EventContext)>,
1188 ) -> DiagnosticSubmission {
1189 let result = emergency.submit_context(diagnostic, context);
1190 self.mirror_existing_record(&Envelope::new(diagnostic, context), diagnostic.category());
1191 result
1192 }
1193 pub(crate) fn mirror_existing_record<T: Serialize>(
1194 &self,
1195 record: &T,
1196 category: saddle_core::DiagnosticCategory,
1197 ) {
1198 if let Ok(serde_json::Value::Object(fields)) = serde_json::to_value(record) {
1199 let level = if matches!(category, saddle_core::DiagnosticCategory::ExpectedRejection) {
1200 crate::EventLevel::Info
1201 } else {
1202 crate::EventLevel::Error
1203 };
1204 let mut record = crate::logger::LogRecord::new(level, "framework.diagnostic");
1205 record.data = fields;
1206 record.data.remove("event");
1207 record.data.remove("timestamp_unix_ms");
1208 self.emit(record);
1209 }
1210 }
1211}
1212fn open_emergency_file(path: &std::path::Path, create_parent: bool) -> io::Result<File> {
1213 if create_parent {
1214 if let Some(parent) = path.parent() { fs::create_dir_all(parent)?; }
1215 }
1216 use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
1218 let file = OpenOptions::new()
1219 .create(true)
1220 .append(true)
1221 .mode(0o644)
1222 .custom_flags(
1223 rustix::fs::OFlags::NOFOLLOW.bits() as i32
1224 | rustix::fs::OFlags::NONBLOCK.bits() as i32,
1225 )
1226 .open(path)?;
1227 if !file.metadata()?.is_file() {
1228 return Err(io::Error::new(io::ErrorKind::InvalidInput, "non-regular emergency file"));
1229 }
1230 rustix::fs::flock(&file, rustix::fs::FlockOperation::NonBlockingLockExclusive)
1231 .map_err(io::Error::from)?;
1232 file.set_permissions(fs::Permissions::from_mode(0o644))?;
1233 Ok(file)
1234}
1235
1236fn validate_emergency_directory(directory: &std::path::Path) -> Result<(), EmergencyInitError> {
1237 let directory = directory.to_str().ok_or(EmergencyInitError::InvalidDirectory)?;
1238 if directory.is_empty()
1239 || directory.len() > 4096
1240 || directory.chars().any(char::is_control)
1241 || directory.contains("://")
1242 || directory.contains(['@', '?', '#'])
1243 {
1244 return Err(EmergencyInitError::InvalidDirectory);
1245 }
1246 Ok(())
1247}
1248
1249impl EmergencyDiagnostics {
1250 pub fn start(config: &FileLoggingConfig) -> Result<Self, EmergencyInitError> {
1253 Self::start_inner(config, false)
1254 }
1255
1256 pub fn start_checked(config: &FileLoggingConfig) -> Result<Self, EmergencyInitError> {
1259 Self::start_inner(config, true)
1260 }
1261
1262 fn start_inner(config: &FileLoggingConfig, preopen: bool) -> Result<Self, EmergencyInitError> {
1263 validate_emergency_directory(config.directory())?;
1264 let target = config.directory().join(EMERGENCY_FILE_NAME);
1265 let fallback_path = BoundedTargetPath::new(&target)
1266 .map_err(|_| EmergencyInitError::InvalidDirectory)?;
1267 if ACTIVE
1268 .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
1269 .is_err()
1270 {
1271 return Err(EmergencyInitError::AlreadyActive);
1272 }
1273 let initial_file = if preopen {
1274 match open_emergency_file(&target, true) {
1275 Ok(file) => Some(file),
1276 Err(error) => {
1277 ACTIVE.store(false, Ordering::Release);
1278 return Err(EmergencyInitError::Open(error));
1279 }
1280 }
1281 } else { None };
1282 let path = target.clone();
1283 let (sender, receiver) = mpsc::sync_channel::<DiagnosticPacket>(QUEUE_CAPACITY);
1284 let status = Arc::new(Status::default());
1285 status.owns_active.store(true, Ordering::Release);
1286 assert!(status.fallback_path.set(fallback_path).is_ok(), "new diagnostic status");
1287 let worker_status = status.clone();
1288 let worker = thread::Builder::new()
1289 .name("saddle-emergency-writer".into())
1290 .spawn(move || {
1291 struct ActiveGuard(Arc<Status>);
1292 impl Drop for ActiveGuard {
1293 fn drop(&mut self) {
1294 let _state = self.0.confirmation.lock().unwrap_or_else(|e| e.into_inner());
1295 self.0.worker_stopped.store(true, Ordering::Release);
1296 self.0.confirmation_ready.notify_all();
1297 }
1298 }
1299 let _active = ActiveGuard(worker_status.clone());
1300 let file = initial_file.map(Ok).unwrap_or_else(|| open_emergency_file(&path, true)).and_then(|file| {
1301 let takeover = file.try_clone()?;
1305 *worker_status.fallback_file.lock().unwrap_or_else(|e| e.into_inner()) = Some(takeover);
1306 Ok(file)
1307 });
1308 if let Err(error) = &file {
1309 worker_status.fail(1, error);
1310 }
1311 worker_status.initialized.store(true, Ordering::Release);
1312 let mut output = file;
1313 let mut stderr_attempted = false;
1314 loop {
1315 match receiver.recv_timeout(Duration::from_millis(10)) {
1316 Ok(mut bytes) => {
1317 bytes.resolve_on_worker(&worker_status);
1318 let delivery = deliver_with_disclosure(
1319 &mut output,
1320 &mut io::stderr(),
1321 &path,
1322 bytes.bytes(),
1323 &worker_status,
1324 &mut stderr_attempted,
1325 !matches!(bytes, DiagnosticPacket::Original(_) | DiagnosticPacket::ConfirmedOriginal(_, _)),
1326 );
1327 if let DiagnosticPacket::ConfirmedOriginal(_, sequence) = bytes {
1328 worker_status.acknowledge(sequence, delivery);
1329 }
1330 }
1331 Err(mpsc::RecvTimeoutError::Timeout) => {
1332 if worker_status.closed.load(Ordering::Acquire)
1333 && worker_status.submitting.load(Ordering::Acquire) == 0
1334 {
1335 break;
1336 }
1337 }
1338 Err(mpsc::RecvTimeoutError::Disconnected) => break,
1339 }
1340 }
1341 })
1342 .map_err(|error| {
1343 status.owns_active.store(false, Ordering::Release);
1344 ACTIVE.store(false, Ordering::Release);
1345 EmergencyInitError::Spawn(error.kind())
1346 })?;
1347 Ok(Self {
1348 handle: EmergencyDiagnosticHandle { sender, status },
1349 worker: Some(worker),
1350 target,
1351 source_target_preopened: preopen,
1352 })
1353 }
1354 pub fn source_output(&self) -> Option<SourceOutput> {
1358 self.source_target_preopened.then(|| SourceOutput(self.handle()))
1359 }
1360 pub fn handle(&self) -> EmergencyDiagnosticHandle {
1361 self.handle.clone()
1362 }
1363 pub fn target(&self) -> &std::path::Path {
1364 &self.target
1365 }
1366 pub fn snapshot(&self) -> DiagnosticOutputSnapshot {
1367 self.handle.snapshot()
1368 }
1369 pub fn shutdown(&mut self) -> DiagnosticShutdown {
1371 self.handle.status.closed.store(true, Ordering::Release);
1372 if self
1373 .worker
1374 .as_ref()
1375 .is_some_and(|worker| !worker.is_finished())
1376 {
1377 return DiagnosticShutdown::Pending;
1378 }
1379 match self.worker.take().map(JoinHandle::join) {
1380 Some(Err(_)) => DiagnosticShutdown::WorkerPanicked,
1381 _ => DiagnosticShutdown::Finished,
1382 }
1383 }
1384}
1385impl Drop for EmergencyDiagnostics {
1386 fn drop(&mut self) {
1387 self.handle.status.closed.store(true, Ordering::Release);
1388 }
1389}
1390
1391#[cfg(test)]
1392fn deliver<W: Write, E: Write>(
1393 output: &mut io::Result<W>,
1394 stderr: &mut E,
1395 target: &std::path::Path,
1396 bytes: &[u8],
1397 status: &Status,
1398 stderr_attempted: &mut bool,
1399) {
1400 let _observed_result = deliver_with_disclosure(
1401 output,
1402 stderr,
1403 target,
1404 bytes,
1405 status,
1406 stderr_attempted,
1407 true,
1408 );
1409}
1410
1411#[allow(clippy::too_many_arguments)] fn deliver_with_disclosure<W: Write, E: Write>(
1413 output: &mut io::Result<W>,
1414 stderr: &mut E,
1415 target: &std::path::Path,
1416 bytes: &[u8],
1417 status: &Status,
1418 stderr_attempted: &mut bool,
1419 allow_stderr_content: bool,
1420) -> io::Result<()> {
1421 let result = match output {
1422 Ok(writer) => writer.write_all(bytes),
1423 Err(error) => Err(error
1424 .raw_os_error()
1425 .map(io::Error::from_raw_os_error)
1426 .unwrap_or_else(|| io::Error::from(error.kind()))),
1427 };
1428 if let Err(ref error) = result {
1429 status.fail(2, &error);
1430 status.output_failed.fetch_add(1, Ordering::Relaxed);
1431 if !*stderr_attempted {
1433 *stderr_attempted = true;
1434 let failure = serde_json::json!({"event":"framework.diagnostic.output_failed", "target":target.to_string_lossy(), "io_kind":format!("{:?}", error.kind()), "os_code":error.raw_os_error()});
1435 if serde_json::to_writer(&mut *stderr, &failure)
1436 .and_then(|_| stderr.write_all(b"\n").map_err(serde_json::Error::io))
1437 .is_err()
1438 || (allow_stderr_content && stderr.write_all(bytes).is_err())
1439 {
1440 status.stderr_failed.fetch_add(1, Ordering::Relaxed);
1441 }
1442 } else {
1443 status.stderr_suppressed.fetch_add(1, Ordering::Relaxed);
1444 }
1445 } else {
1446 status.written.fetch_add(1, Ordering::Release);
1447 }
1448 result
1449}
1450
1451#[cfg(test)]
1452mod tests {
1453 use super::*;
1454
1455 #[test]
1456 fn finalization_maximum_identity_frame_and_queue_failures_are_bounded() {
1457 use saddle_core::request_context::{ContextFact, ContextLabel, RequestIdentityGroup,
1458 RequestLocalFacts, RequestRootPublisher, RequestViewPhase};
1459 let label = "a".repeat(256);
1460 let request = "\\".repeat(256);
1461 let call = saddle_core::CallContext::new(label.clone().into(), label.clone().into(),
1462 label.clone().into(), label.clone().into(), saddle_core::TraceId::from_u128(1),
1463 saddle_core::SpanId::from_u64(1));
1464 let mut publisher = RequestRootPublisher::create(ContextLabel::checked(&label).unwrap(), ContextFact::NotEstablished).unwrap();
1465 publisher.publish(RequestIdentityGroup::from_validated(&call, &request, &label, u32::MAX,
1466 ContextFact::Present(ContextLabel::checked(&label).unwrap())).unwrap()).ok().unwrap();
1467 let view = publisher.reference().view(RequestLocalFacts::new(RequestViewPhase::Reading));
1468 let mut context = FixedDiagnosticBytes { bytes: [0; 8192], len: 0 };
1469 serde_json::to_writer(&mut context, &view).unwrap();
1470 let (sender, receiver) = mpsc::sync_channel(1);
1471 let status = Arc::new(Status::default());
1472 let observer = DetachedAccountRelease { context: view.diagnostic_snapshot(), output: EmergencyDiagnosticHandle {
1473 sender, status: status.clone(),
1474 }, process_instance: u128::MAX };
1475 let fact = saddle_admission::AccountReleaseFact { account_slot: usize::MAX,
1476 account_generation: u64::MAX, audit_generation: u64::MAX, healthy: true };
1477 saddle_admission::AccountReleaseObserver::released(&observer, fact);
1478 assert_eq!(status.enqueued.load(Ordering::Relaxed), 1);
1479 saddle_admission::AccountReleaseObserver::released(&observer, fact);
1481 assert!(status.dropped.load(Ordering::Relaxed) > 0);
1482 let DiagnosticPacket::Admitted(bytes) = receiver.recv().unwrap() else { panic!("fixed admitted frame"); };
1483 assert!(bytes.len <= 8192);
1484 let row: serde_json::Value = serde_json::from_slice(&bytes.bytes[..bytes.len]).unwrap();
1485 assert_eq!(row["context"]["request"]["value"], request);
1486 assert_eq!(row["account"]["account_generation"], u64::MAX);
1487 status.closed.store(true, Ordering::Release);
1488 saddle_admission::AccountReleaseObserver::released(&observer, fact);
1489 assert!(receiver.try_recv().is_err());
1490 assert_eq!(status.written.load(Ordering::Relaxed), 0);
1491 }
1492
1493 #[test]
1494 fn object_lifetime_native_frame_preserves_identity_and_queue_loss() {
1495 use saddle_core::request_context::{ContextFact, ContextLabel, RequestLocalFacts,
1496 RequestRootPublisher, RequestViewPhase};
1497 let publisher = RequestRootPublisher::create(ContextLabel::checked("objects").unwrap(),
1498 ContextFact::NotEstablished).unwrap();
1499 let view = publisher.reference().view(RequestLocalFacts::new(RequestViewPhase::Reading));
1500 let (sender, receiver) = mpsc::sync_channel(1);
1501 let status = Arc::new(Status::default());
1502 let observer = DetachedObjectLifetime {
1503 context: view.diagnostic_snapshot(), output: EmergencyDiagnosticHandle {sender, status: status.clone()},
1504 process_instance: 77, kind: "raw_body",
1505 };
1506 let fact = saddle_admission::ObjectLifetimeFact {
1507 object_id: 19, ledger_instance: 23, storage_bytes: 4096,
1508 account: saddle_admission::AccountIdentity {account_slot: 0, account_generation: 7, audit_generation: 11},
1509 };
1510 observer.emit(fact, true);
1511 observer.emit(fact, false);
1512 assert_eq!(status.dropped.load(Ordering::Relaxed), 1);
1513 let DiagnosticPacket::Admitted(bytes) = receiver.recv().unwrap() else { panic!("fixed native frame") };
1514 let row: serde_json::Value = serde_json::from_slice(&bytes.bytes[..bytes.len]).unwrap();
1515 assert_eq!(row["schema"], "request-object-lifetime/1");
1516 assert_eq!(row["local_request"], row["context"]["local_request"]);
1517 assert_eq!(row["ledger_instance"], 23);
1518 assert_eq!(row["account"]["audit_generation"], 11);
1519 assert_eq!(row["storage_allocated"], true);
1520 assert_eq!(row["storage_destroyed"], false);
1521 observer.emit(fact, false);
1522 let DiagnosticPacket::Admitted(bytes) = receiver.recv().unwrap() else { panic!("fixed native frame") };
1523 let released: serde_json::Value = serde_json::from_slice(&bytes.bytes[..bytes.len]).unwrap();
1524 assert_eq!(released["phase"], "released");
1525 assert_eq!(released["storage_destroyed"], true);
1526 assert!(released["sequence"].as_u64().unwrap() > row["sequence"].as_u64().unwrap());
1527 assert!(released["monotonic_ns"].as_u64().unwrap() >= row["monotonic_ns"].as_u64().unwrap());
1528 assert_eq!(status.written.load(Ordering::Relaxed), 0);
1529 }
1530
1531 #[test]
1532 fn source_output_requires_checked_preopen_and_keeps_selected_target() {
1533 let directory = std::env::temp_dir().join(format!(
1534 "saddle-source-output-{}-{}", std::process::id(),
1535 std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH)
1536 .unwrap().as_nanos()));
1537 std::fs::create_dir(&directory).unwrap();
1538 let config = FileLoggingConfig::new(directory.clone(), crate::Rotation::Daily);
1539 let mut unchecked = EmergencyDiagnostics::start(&config).unwrap();
1540 assert!(unchecked.source_output().is_none());
1541 let deadline = std::time::Instant::now() + Duration::from_secs(5);
1542 while unchecked.shutdown() == DiagnosticShutdown::Pending
1543 && std::time::Instant::now() < deadline { std::thread::yield_now(); }
1544 assert_eq!(unchecked.shutdown(), DiagnosticShutdown::Finished);
1545 drop(unchecked);
1546
1547 let mut checked = EmergencyDiagnostics::start_checked(&config).unwrap();
1548 let selected = checked.source_output().expect("checked output");
1549 selected.handle().write_original_record(&"selected source 3701").unwrap();
1550 assert!(std::fs::read_to_string(checked.target()).unwrap()
1551 .contains("selected source 3701"));
1552 drop(selected);
1553 let deadline = std::time::Instant::now() + Duration::from_secs(5);
1554 while checked.shutdown() == DiagnosticShutdown::Pending
1555 && std::time::Instant::now() < deadline { std::thread::yield_now(); }
1556 assert_eq!(checked.shutdown(), DiagnosticShutdown::Finished);
1557 drop(checked);
1558 std::fs::remove_dir_all(directory).unwrap();
1559 }
1560
1561 #[test]
1562 fn confirmed_original_waits_for_writer_acknowledgement() {
1563 let (sender, receiver) = mpsc::sync_channel(1);
1564 let status = Arc::new(Status::default());
1565 let handle = EmergencyDiagnosticHandle { sender, status: status.clone() };
1566 status.closed.store(true, Ordering::Release);
1569 let (done, completed) = mpsc::sync_channel(1);
1570 let worker = thread::spawn(move || {
1571 done.send(handle.write_original_record(&"confirmed source")).unwrap();
1572 });
1573 let packet = receiver.recv().unwrap();
1574 assert!(matches!(completed.try_recv(), Err(mpsc::TryRecvError::Empty)));
1575 let DiagnosticPacket::ConfirmedOriginal(bytes, sequence) = packet else { panic!("confirmed packet"); };
1576 assert!(std::str::from_utf8(&bytes.bytes[..bytes.len]).unwrap().contains("confirmed source"));
1577 status.acknowledge(sequence, Ok(()));
1578 completed.recv().unwrap().unwrap();
1579 worker.join().unwrap();
1580
1581 let (sender, receiver) = mpsc::sync_channel(1);
1582 let status = Arc::new(Status::default());
1583 let handle = EmergencyDiagnosticHandle { sender, status: status.clone() };
1584 let worker = thread::spawn(move || handle.write_original_record(&"failed source"));
1585 let DiagnosticPacket::ConfirmedOriginal(_, sequence) = receiver.recv().unwrap() else { panic!("confirmed packet"); };
1586 status.acknowledge(sequence, Err(io::ErrorKind::PermissionDenied.into()));
1587 assert_eq!(worker.join().unwrap().unwrap_err().kind(), io::ErrorKind::PermissionDenied);
1588 }
1589
1590 #[test]
1591 fn confirmed_original_serializes_concurrent_sources_and_shutdown() {
1592 let (sender, receiver) = mpsc::sync_channel(2);
1593 let status = Arc::new(Status::default());
1594 let handle = EmergencyDiagnosticHandle { sender, status: status.clone() };
1595 let first = handle.clone();
1596 let second = handle.clone();
1597 let a = thread::spawn(move || first.write_original_record(&"source A"));
1598 let DiagnosticPacket::ConfirmedOriginal(_, first_sequence) = receiver.recv().unwrap() else { panic!("first packet"); };
1599 let b = thread::spawn(move || second.write_original_record(&"source B"));
1600 assert!(matches!(receiver.try_recv(), Err(mpsc::TryRecvError::Empty)));
1603 status.acknowledge(first_sequence, Ok(()));
1604 assert!(a.join().unwrap().is_ok());
1605 let DiagnosticPacket::ConfirmedOriginal(_, second_sequence) = receiver.recv_timeout(Duration::from_secs(1)).unwrap() else { panic!("second packet"); };
1606 status.acknowledge(second_sequence, Ok(()));
1607 assert!(b.join().unwrap().is_ok());
1608 status.closed.store(true, Ordering::Release);
1609 drop(receiver);
1610 status.worker_stopped.store(true, Ordering::Release);
1611 assert_eq!(handle.write_original_record(&"without a configured target").unwrap_err().kind(), io::ErrorKind::NotFound);
1612 assert_eq!(status.submitting.load(Ordering::Acquire), 0);
1613 }
1614
1615 #[test]
1616 fn cancelled_confirmation_cannot_acknowledge_next_source() {
1617 let status = Status::default();
1618 let cancelled = status.acquire_confirmation();
1619 let old_sequence = cancelled.sequence;
1620 drop(cancelled);
1621 let current = status.acquire_confirmation();
1622 status.acknowledge(old_sequence, Ok(()));
1623 assert!(status.confirmation.lock().unwrap().completed.is_none());
1624 status.acknowledge(current.sequence, Err(io::ErrorKind::PermissionDenied.into()));
1625 assert_eq!(current.wait().unwrap_err().kind(), io::ErrorKind::PermissionDenied);
1626 }
1627
1628 #[test]
1629 fn stopped_writer_releases_waiter_without_receipt() {
1630 let (sender, receiver) = mpsc::sync_channel(1);
1631 let status = Arc::new(Status::default());
1632 let weak_owner = Arc::downgrade(&status);
1633 let handle = EmergencyDiagnosticHandle { sender, status: status.clone() };
1634 let waiter = thread::spawn(move || handle.write_original_record(&"unwritten source"));
1635 let _packet = receiver.recv_timeout(Duration::from_secs(1)).unwrap();
1636 {
1637 let _state = status.confirmation.lock().unwrap();
1638 status.worker_stopped.store(true, Ordering::Release);
1639 status.confirmation_ready.notify_all();
1640 }
1641 assert_eq!(waiter.join().unwrap().unwrap_err().kind(), io::ErrorKind::NotFound);
1644 assert_eq!(status.snapshot().written, 0);
1645 assert_eq!(status.submitting.load(Ordering::Acquire), 0);
1646 drop(receiver);
1647 drop(status);
1648 assert!(weak_owner.upgrade().is_none(), "last owner releases the physical confirmation allocation");
1649 }
1650
1651 #[test]
1652 fn stopped_writer_with_preopened_target_writes_before_waiter_returns() {
1653 let _serial = FILE_TEST.lock().unwrap();
1654 let directory = std::env::temp_dir().join(format!(
1655 "saddle-stopped-writer-target-{}-{}", std::process::id(), sample().id()
1656 ));
1657 fs::create_dir(&directory).unwrap();
1658 let target = directory.join(EMERGENCY_FILE_NAME);
1659 let file = open_emergency_file(&target, true).unwrap();
1660 let (sender, receiver) = mpsc::sync_channel(1);
1661 let status = Arc::new(Status::default());
1662 status.fallback_path.set(BoundedTargetPath::new(&target).unwrap()).ok().unwrap();
1663 *status.fallback_file.lock().unwrap() = Some(file);
1664 let handle = EmergencyDiagnosticHandle { sender, status: status.clone() };
1665 let (done, completed) = mpsc::sync_channel(1);
1666 let waiter = thread::spawn(move || {
1667 done.send(handle.write_original_record(&"source after stopped writer 7814")).unwrap();
1668 });
1669 let DiagnosticPacket::ConfirmedOriginal(_, _) = receiver.recv_timeout(Duration::from_secs(1)).unwrap()
1670 else { panic!("confirmed packet"); };
1671 assert!(matches!(completed.try_recv(), Err(mpsc::TryRecvError::Empty)));
1672 {
1673 let _state = status.confirmation.lock().unwrap();
1674 status.worker_stopped.store(true, Ordering::Release);
1675 status.confirmation_ready.notify_all();
1676 }
1677 completed.recv_timeout(Duration::from_secs(2)).unwrap().unwrap();
1678 waiter.join().unwrap();
1679 assert_eq!(fs::read_to_string(&target).unwrap(), "\"source after stopped writer 7814\"\n");
1680 assert_eq!(status.snapshot().written, 1);
1681 assert_eq!(status.submitting.load(Ordering::Acquire), 0);
1682 drop(receiver);
1683 drop(status);
1684 fs::remove_file(target).unwrap();
1685 fs::remove_dir(directory).unwrap();
1686 }
1687
1688 #[test]
1689 fn stopped_writer_without_target_keeps_owned_original_unconfirmed() {
1690 use crate::root_diagnostic::{ComponentSourceKind, RecordedSaddleError};
1691 use saddle_core::{ContextLabel, ErrorKind};
1692 #[derive(Debug)]
1693 struct OriginalChain { cause: io::Error }
1694 impl fmt::Display for OriginalChain {
1695 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1696 formatter.write_str("unconfigured top source 7817")
1697 }
1698 }
1699 impl std::error::Error for OriginalChain {
1700 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { Some(&self.cause) }
1701 }
1702 let (sender, receiver) = mpsc::sync_channel(1);
1703 drop(receiver);
1704 let status = Arc::new(Status::default());
1705 status.worker_stopped.store(true, Ordering::Release);
1706 let output = SourceOutput(EmergencyDiagnosticHandle { sender, status: status.clone() });
1707 let application = ContextLabel::checked("missing-target-test").unwrap();
1708 let result = RecordedSaddleError::component_result::<(), _>(
1709 Err(OriginalChain { cause: io::Error::other("unconfigured nested source 7818") }),
1710 &application, &output, ComponentSourceKind::Cleanup, None,
1711 ErrorKind::Infrastructure, "test.missing_target", "safe failure",
1712 );
1713 let failure = result.unwrap_err();
1714 assert_eq!(failure.safe().code(), "test.missing_target");
1715 let original = failure.original_if_unconfirmed().expect("no target cannot sign Written");
1716 assert_eq!(original.to_string(), "unconfigured top source 7817");
1717 assert_eq!(original.source().unwrap().to_string(), "unconfigured nested source 7818");
1718 assert_eq!(status.snapshot().written, 0);
1719 assert_eq!(status.submitting.load(Ordering::Acquire), 0);
1720 }
1721
1722 #[test]
1723 fn failed_writer_confirmation_retains_component_original_and_cause() {
1724 use crate::root_diagnostic::{ComponentSourceKind, RecordedSaddleError};
1725 use saddle_core::{ContextLabel, ErrorKind};
1726 #[derive(Debug)]
1727 struct OriginalChain { cause: io::Error }
1728 impl fmt::Display for OriginalChain {
1729 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1730 formatter.write_str("top source 7815")
1731 }
1732 }
1733 impl std::error::Error for OriginalChain {
1734 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { Some(&self.cause) }
1735 }
1736 let (sender, receiver) = mpsc::sync_channel(1);
1737 let status = Arc::new(Status::default());
1738 let output = SourceOutput(EmergencyDiagnosticHandle { sender, status: status.clone() });
1739 let (done, completed) = mpsc::sync_channel(1);
1740 let waiter = thread::spawn(move || {
1741 let original = OriginalChain { cause: io::Error::other("nested source 7816") };
1742 let application = ContextLabel::checked("confirmation-failure-test").unwrap();
1743 let result = RecordedSaddleError::component_result::<(), _>(
1744 Err(original), &application, &output, ComponentSourceKind::Cleanup, None,
1745 ErrorKind::Infrastructure, "test.confirmation_failed", "safe failure",
1746 );
1747 done.send(result).unwrap();
1748 });
1749 let DiagnosticPacket::ConfirmedOriginal(bytes, sequence) = receiver.recv_timeout(Duration::from_secs(2)).unwrap()
1750 else { panic!("confirmed packet"); };
1751 assert!(matches!(completed.try_recv(), Err(mpsc::TryRecvError::Empty)));
1752 let mut output: io::Result<Failed> = Ok(Failed);
1755 let mut stderr = Vec::new();
1756 let mut stderr_attempted = false;
1757 let delivery = deliver_with_disclosure(&mut output, &mut stderr,
1758 std::path::Path::new("/failed/controlled/source"), &bytes.bytes[..bytes.len],
1759 &status, &mut stderr_attempted, false);
1760 assert_eq!(delivery.as_ref().unwrap_err().raw_os_error(), Some(28));
1761 status.acknowledge(sequence, delivery);
1762 let failure = completed.recv_timeout(Duration::from_secs(2)).unwrap().unwrap_err();
1763 waiter.join().unwrap();
1764 assert_eq!(failure.safe().code(), "test.confirmation_failed");
1765 let original = failure.original_if_unconfirmed().expect("failed confirmation keeps owned source");
1766 assert_eq!(original.to_string(), "top source 7815");
1767 assert_eq!(original.source().unwrap().to_string(), "nested source 7816");
1768 assert_eq!(status.snapshot().written, 0);
1769 assert_eq!(status.snapshot().output_failed, 1);
1770 assert_eq!(status.submitting.load(Ordering::Acquire), 0);
1771 }
1772
1773 #[test]
1774 fn confirmed_writer_releases_process_owner_after_shutdown() {
1775 let _serial = FILE_TEST.lock().unwrap();
1776 let directory = std::env::temp_dir().join(format!(
1777 "saddle-confirm-owner-{}-{}", std::process::id(), sample().id()
1778 ));
1779 let mut owner = EmergencyDiagnostics::start(&FileLoggingConfig::new(&directory, crate::Rotation::Daily)).unwrap();
1780 let handle = owner.handle();
1781 let weak_owner = Arc::downgrade(&handle.status);
1782 handle.write_original_record(&"complete source").unwrap();
1783 let text = fs::read_to_string(owner.target()).unwrap();
1784 assert!(text.contains("complete source"));
1785 let end = std::time::Instant::now() + Duration::from_secs(3);
1786 while owner.shutdown() == DiagnosticShutdown::Pending && std::time::Instant::now() < end {
1787 thread::sleep(Duration::from_millis(1));
1788 }
1789 assert_eq!(owner.shutdown(), DiagnosticShutdown::Finished);
1790 let path = owner.target().to_path_buf();
1791 let archived = directory.join("saddle.emergency.archived.log");
1792 fs::rename(&path, &archived).unwrap();
1793 handle.write_original_record(&"after shutdown A").unwrap();
1794 handle.write_original_record(&"after shutdown B").unwrap();
1795 let archived_text = fs::read_to_string(&archived).unwrap();
1796 assert!(archived_text.contains("after shutdown A") && archived_text.contains("after shutdown B"),
1797 "the takeover pins one inode across external rotation");
1798 assert!(matches!(EmergencyDiagnostics::start(&FileLoggingConfig::new(&directory, crate::Rotation::Daily)),
1799 Err(EmergencyInitError::AlreadyActive)), "a live source handle owns the takeover target");
1800 drop(handle);
1801 drop(owner);
1802 assert!(weak_owner.upgrade().is_none());
1803 let mut restarted = EmergencyDiagnostics::start(&FileLoggingConfig::new(&directory, crate::Rotation::Daily)).unwrap();
1804 restarted.handle().write_original_record(&"new owner").unwrap();
1805 let restarted_end = std::time::Instant::now() + Duration::from_secs(3);
1806 while restarted.shutdown() == DiagnosticShutdown::Pending && std::time::Instant::now() < restarted_end {
1807 thread::sleep(Duration::from_millis(1));
1808 }
1809 assert_eq!(restarted.shutdown(), DiagnosticShutdown::Finished);
1810 drop(restarted);
1811 assert!(fs::read_to_string(&path).unwrap().contains("new owner"));
1812 fs::remove_file(path).unwrap();
1813 fs::remove_file(archived).unwrap();
1814 fs::remove_dir(directory).unwrap();
1815 }
1816
1817 #[test]
1818 fn ae_paused_queue_partial_full_disconnect_and_admitted_encoding() {
1819 use crate::{
1820 AdmissionCapacityFacts, AdmissionConstruction, AdmissionEventContext,
1821 CapacityDimension, Observer, ObserverConfig,
1822 };
1823 use saddle_admission::*;
1824 use saddle_core::*;
1825 use std::{
1826 future::Future,
1827 task::{Context, Poll, Waker},
1828 };
1829 let pending = freeze_deployment_resource_budget(
1831 2, 128, 5000, 1, 3, 1_000_000, 1_000_000, 1_000_000, 1_000_000,
1832 )
1833 .unwrap()
1834 .reserve_known_database_memory(0, 0, 0)
1835 .ok()
1836 .unwrap();
1837 let (application, listener) = BootstrapRendezvousIssuer::issue()
1838 .freeze_application(GeneratedApplicationFreezeSource::new(
1839 "app",
1840 b"descriptor",
1841 &["route"],
1842 ))
1843 .unwrap();
1844 let listener = listener
1845 .freeze_listener(ListenerStartupFreezeSource::new(
1846 "app",
1847 "127.0.0.1:8000".parse().unwrap(),
1848 "127.0.0.1:9000".parse().unwrap(),
1849 Duration::from_millis(5000),
1850 ))
1851 .ok()
1852 .unwrap();
1853 let (whole, receipt) = pair_bootstrap_rendezvous(application, listener)
1854 .ok()
1855 .unwrap();
1856 let budget = bind_deployment_resource_budget_bootstrap(pending, whole, receipt)
1857 .ok()
1858 .unwrap();
1859 let process = prepare_profusegw_lightweight_profile(budget).ok().unwrap();
1860 let (permit, original) = match process.verified_profile().try_admit_observed() {
1861 ProfuseGwLightweightObservedAdmissionOutcome::Ready(permit, receipt) => {
1862 (permit, receipt)
1863 }
1864 _ => panic!("fixture admission failed"),
1865 };
1866 let facts = AdmissionCapacityFacts::from_receipt(&original);
1867 let observer = Observer::with_writer(ObserverConfig::default(), io::sink()).unwrap();
1868 let app = ContextLabel::checked("app").unwrap();
1869 let context = AdmissionEventContext::Unrooted {
1870 application: &app,
1871 lifecycle: RequestViewPhase::Reading,
1872 };
1873 let (sender, receiver) = mpsc::sync_channel(QUEUE_CAPACITY);
1874 let handle = EmergencyDiagnosticHandle {
1875 sender,
1876 status: Arc::new(Status::default()),
1877 };
1878 for _ in 0..63 {
1879 assert_eq!(
1880 handle.submit_fixed_record(&0),
1881 DiagnosticSubmission::Enqueued
1882 );
1883 }
1884 let ledger = ProcessLedger::new(ResourceConfig {
1885 managed_capacity: 1024 * 1024,
1886 entry_reserve: 1024 * 1024,
1887 framework_reserve: 1024 * 1024,
1888 task_reserve: 1024 * 1024,
1889 process_state_reserve: 1024 * 1024,
1890 system_estimate: 1024 * 1024,
1891 safety_margin: 1024 * 1024,
1892 process_limit: 8 * 1024 * 1024,
1893 max_active_requests: 1,
1894 })
1895 .unwrap();
1896 let future = ledger
1897 .try_envelope(65536, 65536, |_| async {
1898 let result = observer.record_admission_capacity(
1899 Some(&handle),
1900 context,
1901 &facts,
1902 AdmissionConstruction::Cancelled,
1903 );
1904 assert_eq!(result.cpu, DiagnosticSubmission::Enqueued);
1905 assert_eq!(result.memory, DiagnosticSubmission::Full);
1906 assert_eq!(result.database, DiagnosticSubmission::Full);
1907 assert_eq!(result.profuse_contract, DiagnosticSubmission::Full);
1908 })
1909 .unwrap();
1910 let mut future = std::pin::pin!(future);
1911 assert!(
1912 matches!(
1913 future
1914 .as_mut()
1915 .poll(&mut Context::from_waker(Waker::noop())),
1916 Poll::Ready(Ok(_))
1917 ),
1918 "actual unrooted encode must not escape the request allocation envelope"
1919 );
1920 for _ in 0..63 {
1921 receiver.try_recv().unwrap();
1922 }
1923 let row: serde_json::Value =
1924 serde_json::from_slice(receiver.try_recv().unwrap().bytes()).unwrap();
1925 assert_eq!(row["capacity_dimension"], "cpu");
1926 assert_eq!(row["decision"], "accepted");
1927 assert_eq!(row["resource_source"], "admission_ledger_same_lock");
1928 assert!(row["resource"]["managed_available_bytes"].as_u64().is_some());
1929 assert!(row["resource"]["framework_demand_bytes"].as_u64().is_some());
1930 assert_eq!(row["construction"]["status"], "cancelled");
1931 assert_eq!(handle.snapshot().enqueued, 64);
1932 assert_eq!(handle.snapshot().dropped, 3);
1933 assert_eq!(
1936 handle.submit_fixed_record(&"x".repeat(8192)),
1937 DiagnosticSubmission::EncodingFailed
1938 );
1939 assert!(receiver.try_recv().is_err());
1940 drop(receiver);
1941 let closed = observer.record_admission_capacity(
1942 Some(&handle),
1943 context,
1944 &facts,
1945 AdmissionConstruction::Ready,
1946 );
1947 assert_eq!(
1948 [
1949 closed.cpu,
1950 closed.memory,
1951 closed.database,
1952 closed.profuse_contract
1953 ],
1954 [DiagnosticSubmission::Closed; 4]
1955 );
1956 for dimension in [
1957 CapacityDimension::Cpu,
1958 CapacityDimension::Memory,
1959 CapacityDimension::Database,
1960 CapacityDimension::ProfuseContract,
1961 ] {
1962 assert_eq!(
1963 observer
1964 .metrics_snapshot()
1965 .capacity_accepted(Some(dimension)),
1966 2
1967 );
1968 }
1969 assert_eq!(original.decision(), ProfuseGwCapacityDecision::Accepted);
1970 permit.into_execution().cancel();
1971 process.finish().unwrap();
1972 }
1973
1974 #[test]
1975 fn root_original_partial_cycle_full_and_disclosure_are_explicit() {
1976 use crate::root_diagnostic::OriginalCaptureState;
1977 use crate::{RootDiagnosticScope, RootOutcomeFacts, RootRequestEvent};
1978 use saddle_core::*;
1979 #[derive(Debug)]
1980 struct BadFormat;
1981 impl std::fmt::Display for BadFormat {
1982 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1983 f.write_str("exact-before-format-error")?;
1984 Err(std::fmt::Error)
1985 }
1986 }
1987 impl std::error::Error for BadFormat {}
1988 #[derive(Debug)]
1989 struct Cycle;
1990 impl std::fmt::Display for Cycle {
1991 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1992 f.write_str("cycle-original")
1993 }
1994 }
1995 impl std::error::Error for Cycle {
1996 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
1997 Some(self)
1998 }
1999 }
2000 let root = RequestRootPublisher::create(
2001 ContextLabel::checked("test").unwrap(),
2002 ContextFact::NotEstablished,
2003 )
2004 .unwrap();
2005 let view = root
2006 .reference()
2007 .view(RequestLocalFacts::new(RequestViewPhase::Handler));
2008 let (sender, receiver) = mpsc::sync_channel(64);
2009 let handle = EmergencyDiagnosticHandle {
2010 sender,
2011 status: Arc::new(Status::default()),
2012 };
2013 let source = |error: &(dyn std::error::Error + 'static), output| {
2014 RootDiagnosticScope::new(&view, output).source_error(
2015 error,
2016 DiagnosticStage::RequestHandler,
2017 RootRequestEvent::Handler,
2018 RootOutcomeFacts::default(),
2019 )
2020 };
2021 let missing = source(&BadFormat, None);
2022 assert_eq!(
2023 missing.original_capture(),
2024 OriginalCaptureState::OutputUnavailable
2025 );
2026 let partial = source(&BadFormat, Some(&handle));
2027 assert_eq!(
2028 partial.original_capture(),
2029 OriginalCaptureState::FormattingFailed
2030 );
2031 assert_eq!(partial.submission(), DiagnosticSubmission::EncodingFailed);
2032 let mut rows: Vec<serde_json::Value> = receiver
2033 .try_iter()
2034 .map(|p| serde_json::from_slice(p.bytes()).unwrap())
2035 .collect();
2036 assert_eq!(rows.last().unwrap()["payload"], "exact-before-format-error");
2037 assert_eq!(rows.last().unwrap()["state"], "formatting_failed");
2038 assert!(!rows.iter().any(|r| r["state"] == "exposed_chain_complete"));
2039 let cycle = source(&Cycle, Some(&handle));
2040 assert_eq!(cycle.original_capture(), OriginalCaptureState::CauseCycle);
2041 rows = receiver
2042 .try_iter()
2043 .map(|p| serde_json::from_slice(p.bytes()).unwrap())
2044 .collect();
2045 assert_eq!(rows.last().unwrap()["state"], "cause_cycle");
2046 for _ in 0..64 {
2048 assert_eq!(
2049 handle.submit_original_record(&0),
2050 DiagnosticSubmission::Enqueued
2051 );
2052 }
2053 let full = source(&Cycle, Some(&handle));
2054 assert_eq!(
2055 full.original_capture(),
2056 OriginalCaptureState::OutputRejected
2057 );
2058 assert_eq!(full.submission(), DiagnosticSubmission::Full);
2059 let _ = receiver.try_iter().count();
2060 handle.status.closed.store(true, Ordering::Release);
2061 assert_eq!(
2062 source(&Cycle, Some(&handle)).submission(),
2063 DiagnosticSubmission::Closed
2064 );
2065 let mut file: io::Result<Vec<u8>> = Err(io::ErrorKind::PermissionDenied.into());
2068 let mut stderr = Vec::new();
2069 let result = deliver_with_disclosure(
2070 &mut file,
2071 &mut stderr,
2072 std::path::Path::new("saddle.emergency.log"),
2073 b"CT_ARTIFICIAL_SECRET_NOT_A_CREDENTIAL",
2074 &Status::default(),
2075 &mut false,
2076 false,
2077 );
2078 assert_eq!(result.unwrap_err().kind(), io::ErrorKind::PermissionDenied);
2079 let stderr = String::from_utf8(stderr).unwrap();
2080 assert!(stderr.contains("output_failed"));
2081 assert!(!stderr.contains("CT_ARTIFICIAL_SECRET"));
2082 }
2083
2084 #[test]
2085 fn root_original_admitted_and_existing_panic_keep_identity() {
2086 use crate::root_diagnostic::OriginalCaptureState;
2087 use crate::{RootDiagnosticScope, RootOutcomeFacts, RootRequestEvent};
2088 use saddle_core::*;
2089 use std::{
2090 future::Future,
2091 task::{Context, Poll, Waker},
2092 };
2093 let root = RequestRootPublisher::create(
2094 ContextLabel::checked("test").unwrap(),
2095 ContextFact::Unavailable,
2096 )
2097 .unwrap();
2098 let view = root
2099 .reference()
2100 .view(RequestLocalFacts::new(RequestViewPhase::Handler));
2101 let (sender, receiver) = mpsc::sync_channel(64);
2102 let handle = EmergencyDiagnosticHandle {
2103 sender,
2104 status: Arc::new(Status::default()),
2105 };
2106 let diagnostic = Diagnostic::capture(
2107 DiagnosticCategory::Panic,
2108 CaptureSite::Origin,
2109 DiagnosticCause::new(
2110 DiagnosticStage::RequestHandler,
2111 DiagnosticCode::new("panic.caught").unwrap(),
2112 ),
2113 );
2114 let id = diagnostic.id();
2115 let error = io::Error::from(io::ErrorKind::TimedOut);
2116 let ledger = saddle_admission::ProcessLedger::new(saddle_admission::ResourceConfig {
2117 managed_capacity: 1024 * 1024,
2118 entry_reserve: 1024 * 1024,
2119 framework_reserve: 1024 * 1024,
2120 task_reserve: 1024 * 1024,
2121 process_state_reserve: 1024 * 1024,
2122 system_estimate: 1024 * 1024,
2123 safety_margin: 1024 * 1024,
2124 process_limit: 8 * 1024 * 1024,
2125 max_active_requests: 1,
2126 })
2127 .unwrap();
2128 let future = ledger
2129 .try_envelope(65536, 65536, |_| async {
2130 let scope = RootDiagnosticScope::new(&view, Some(&handle));
2131 let failure = scope.source_error(
2132 &error,
2133 DiagnosticStage::RequestDb,
2134 RootRequestEvent::Database,
2135 RootOutcomeFacts::default(),
2136 );
2137 assert_eq!(
2138 failure.original_capture(),
2139 OriginalCaptureState::CompleteEnqueued
2140 );
2141 let panic = scope.source_existing_description(
2142 &"raw panic: CT_ARTIFICIAL_SECRET_NOT_A_CREDENTIAL",
2143 diagnostic,
2144 DiagnosticCode::new("task.failed").unwrap(),
2145 RootRequestEvent::Supervision,
2146 RootOutcomeFacts::default(),
2147 );
2148 assert_eq!(
2149 panic.original_capture(),
2150 OriginalCaptureState::CompleteEnqueued
2151 );
2152 panic
2153 .finish(&view, None, RootOutcomeFacts::default())
2154 .ok()
2155 .unwrap()
2156 })
2157 .unwrap();
2158 let mut future = std::pin::pin!(future);
2159 assert!(
2160 matches!(
2161 future
2162 .as_mut()
2163 .poll(&mut Context::from_waker(Waker::noop())),
2164 Poll::Ready(Ok(_))
2165 ),
2166 "source formatter/queue must not escape admission"
2167 );
2168 let rows: Vec<serde_json::Value> = receiver
2169 .try_iter()
2170 .map(|p| serde_json::from_slice(p.bytes()).unwrap())
2171 .collect();
2172 let panic: Vec<_> = rows
2173 .iter()
2174 .filter(|r| r["occurrence"]["diagnostic_id"] == id)
2175 .collect();
2176 assert_eq!(panic.len(), 4);
2177 let header: serde_json::Value =
2178 serde_json::from_str(panic[0]["payload"].as_str().unwrap()).unwrap();
2179 assert_eq!(header["facts"]["diagnostic_id"], id);
2180 assert_eq!(header["facts"]["category"], "panic");
2181 assert_eq!(
2182 header["source_interface"],
2183 "unavailable_description_debug_only"
2184 );
2185 assert_eq!(
2186 panic[1]["payload"],
2187 "raw panic: CT_ARTIFICIAL_SECRET_NOT_A_CREDENTIAL"
2188 );
2189 assert_eq!(panic[2]["channel"], "debug");
2190 assert_eq!(panic[3]["state"], "description_complete_source_unavailable");
2191 }
2192 use saddle_core::{
2193 CaptureSite, DiagnosticCategory, DiagnosticCause, DiagnosticCode, DiagnosticStage,
2194 };
2195 #[test]
2196 fn root_contract_oversize_detail_and_basic_encoding_failure_are_explicit() {
2197 use crate::{RootDiagnosticScope, RootOutcomeFacts, RootRequestEvent};
2198 use saddle_core::*;
2199 let (sender, receiver) = mpsc::sync_channel(4);
2200 let handle = EmergencyDiagnosticHandle {
2201 sender,
2202 status: Arc::new(Status::default()),
2203 };
2204 let text = "m".repeat(256);
2205 let protocol = "\\\"".repeat(128);
2206 let call = CallContext::new(
2207 ApplicationId::new(text.clone()),
2208 ModuleId::new(text.clone()),
2209 ServiceId::new(text.clone()),
2210 OperationId::new(text.clone()),
2211 TraceId::from_u128(1),
2212 SpanId::from_u64(1),
2213 )
2214 .with_trace_correlation_id(TraceCorrelationId::new(protocol.clone()).unwrap())
2215 .with_rpc_correlation_id(RpcCorrelationId::new(&protocol));
2216 let mut root = RequestRootPublisher::create(
2217 ContextLabel::checked(&text).unwrap(),
2218 ContextFact::NotEstablished,
2219 )
2220 .unwrap();
2221 assert!(
2222 root.publish(
2223 RequestIdentityGroup::from_validated(
2224 &call,
2225 &protocol,
2226 &text,
2227 1,
2228 ContextFact::Present(ContextLabel::checked(&text).unwrap())
2229 )
2230 .unwrap()
2231 )
2232 .is_ok()
2233 );
2234 let view = root
2235 .reference()
2236 .view(RequestLocalFacts::new(RequestViewPhase::Database));
2237 let metadata = "T".repeat(192);
2238 let cause = BoundedDiagnosticCause::new(
2239 DiagnosticStage::RequestDb,
2240 DiagnosticCode::new("driver.type").unwrap(),
2241 )
2242 .with_object(InlineDiagnosticText::metadata(&metadata))
2243 .with_types(
2244 Some(InlineDiagnosticText::metadata(&metadata)),
2245 Some(InlineDiagnosticText::metadata(&metadata)),
2246 None,
2247 );
2248 let detail = BoundedDiagnostic::capture(
2249 DiagnosticCategory::UnexpectedError,
2250 CaptureSite::Origin,
2251 cause,
2252 )
2253 .wrap(cause)
2254 .wrap(cause)
2255 .wrap(cause);
2256 let id = detail.id();
2257 let receipt = RootDiagnosticScope::new(&view, Some(&handle)).source(
2258 detail,
2259 DiagnosticCode::new("driver.type").unwrap(),
2260 RootRequestEvent::Database,
2261 RootOutcomeFacts::default(),
2262 );
2263 assert_eq!(receipt.submission(), DiagnosticSubmission::Enqueued);
2264 let row: serde_json::Value =
2265 serde_json::from_slice(receiver.try_recv().unwrap().bytes()).unwrap();
2266 assert_eq!(row["detail_status"], "omitted_encoding_capacity");
2267 assert_eq!(row["occurrence"]["diagnostic_id"], id);
2268 assert!(row["diagnostic"].is_null());
2269 assert_eq!(handle.snapshot().truncated, 1);
2270 let (receipt, result) = receipt
2271 .boundary(
2272 &view,
2273 Some(&handle),
2274 RootRequestEvent::Database,
2275 RootOutcomeFacts::default(),
2276 )
2277 .ok()
2278 .unwrap();
2279 assert_eq!(result, DiagnosticSubmission::EncodingFailed);
2280 assert!(receiver.try_recv().is_err());
2281 assert_eq!(receipt.submission(), DiagnosticSubmission::Enqueued);
2282 assert_eq!(handle.snapshot().dropped, 1);
2283 }
2284
2285 #[test]
2286 fn root_contract_stage_terminal_is_single_and_admitted() {
2287 use crate::{
2288 Observer, ObserverConfig, RootDiagnosticScope, RootOutcomeFacts, RootRequestEvent,
2289 Stage, StageOutcome,
2290 };
2291 use saddle_core::*;
2292 use std::{
2293 future::Future,
2294 task::{Context, Poll, Waker},
2295 };
2296 let observer = Observer::with_writer(ObserverConfig::default(), std::io::sink()).unwrap();
2297 let root = RequestRootPublisher::create(
2298 ContextLabel::checked("app").unwrap(),
2299 ContextFact::NotEstablished,
2300 )
2301 .unwrap();
2302 let view = root
2303 .reference()
2304 .view(RequestLocalFacts::new(RequestViewPhase::Handler));
2305 let foreign = RequestRootPublisher::create(
2306 ContextLabel::checked("app").unwrap(),
2307 ContextFact::NotEstablished,
2308 )
2309 .unwrap();
2310 let foreign_view = foreign
2311 .reference()
2312 .view(RequestLocalFacts::new(RequestViewPhase::Handler));
2313 let ledger = saddle_admission::ProcessLedger::new(saddle_admission::ResourceConfig {
2314 managed_capacity: 1024 * 1024,
2315 entry_reserve: 1024 * 1024,
2316 framework_reserve: 1024 * 1024,
2317 task_reserve: 1024 * 1024,
2318 process_state_reserve: 1024 * 1024,
2319 system_estimate: 1024 * 1024,
2320 safety_margin: 1024 * 1024,
2321 process_limit: 8 * 1024 * 1024,
2322 max_active_requests: 1,
2323 })
2324 .unwrap();
2325 let future = ledger
2326 .try_envelope(65536, 65536, |_| async {
2327 let facts = |operation| RootOutcomeFacts {
2328 axes: DiagnosticOutcomeAxes {
2329 operation,
2330 ..Default::default()
2331 },
2332 ..Default::default()
2333 };
2334 let stage = RootDiagnosticScope::new(&view, None)
2335 .start_stage(&observer, RootRequestEvent::Response);
2336 let stage = stage
2338 .finish_nonfailure(facts(OperationOutcome::Failed))
2339 .err()
2340 .unwrap();
2341 assert_eq!(
2342 stage
2343 .finish_nonfailure(facts(OperationOutcome::Succeeded))
2344 .ok()
2345 .unwrap(),
2346 DiagnosticSubmission::OutputUnavailable
2347 );
2348 let source = RootDiagnosticScope::new(&view, None).source(
2349 BoundedDiagnostic::capture(
2350 DiagnosticCategory::UnexpectedError,
2351 CaptureSite::Origin,
2352 BoundedDiagnosticCause::new(
2353 DiagnosticStage::RequestHandler,
2354 DiagnosticCode::new("handler.failed").unwrap(),
2355 ),
2356 ),
2357 DiagnosticCode::new("handler.failed").unwrap(),
2358 RootRequestEvent::Handler,
2359 facts(OperationOutcome::Failed),
2360 );
2361 let foreign_stage = RootDiagnosticScope::new(&foreign_view, None)
2362 .start_stage(&observer, RootRequestEvent::Response);
2363 let (foreign_stage, source) = foreign_stage
2364 .finish_failure(source, facts(OperationOutcome::Failed))
2365 .err()
2366 .unwrap();
2367 assert_eq!(
2368 foreign_stage
2369 .finish_nonfailure(facts(OperationOutcome::Rejected))
2370 .ok()
2371 .unwrap(),
2372 DiagnosticSubmission::OutputUnavailable
2373 );
2374 let stage = RootDiagnosticScope::new(&view, None)
2375 .start_stage(&observer, RootRequestEvent::Response);
2376 let (source, status) = stage
2377 .finish_failure(source, facts(OperationOutcome::Failed))
2378 .ok()
2379 .unwrap();
2380 assert_eq!(status, DiagnosticSubmission::OutputUnavailable);
2381 source
2382 .finish(&view, None, facts(OperationOutcome::Failed))
2383 .ok()
2384 .unwrap()
2385 })
2386 .unwrap();
2387 let mut future = std::pin::pin!(future);
2388 assert!(matches!(
2389 future
2390 .as_mut()
2391 .poll(&mut Context::from_waker(Waker::noop())),
2392 Poll::Ready(Ok(_))
2393 ));
2394 let metrics = observer.metrics_snapshot();
2395 assert_eq!(metrics.requests(StageOutcome::Success), 1);
2396 assert_eq!(metrics.requests(StageOutcome::Rejected), 1);
2397 assert_eq!(metrics.requests(StageOutcome::Failure), 1);
2398 assert_eq!(metrics.requests(StageOutcome::Cancelled), 0);
2399 assert_eq!(
2400 metrics.stage_latency(Stage::Response).iter().sum::<u64>(),
2401 3
2402 );
2403 }
2404
2405 #[test]
2406 fn root_contract_existing_source_cleanup_and_admitted_submission() {
2407 use crate::{RootDiagnosticScope, RootOutcomeFacts, RootRequestEvent};
2408 use saddle_core::*;
2409 use std::{
2410 future::Future,
2411 task::{Context, Poll, Waker},
2412 };
2413 let (sender, receiver) = mpsc::sync_channel(4);
2414 let handle = EmergencyDiagnosticHandle {
2415 sender,
2416 status: Arc::new(Status::default()),
2417 };
2418 let root = RequestRootPublisher::create(
2419 ContextLabel::checked("app").unwrap(),
2420 ContextFact::Unavailable,
2421 )
2422 .unwrap();
2423 let view = root.reference().view(
2424 RequestLocalFacts::new(RequestViewPhase::Handler).with_task(ContextFact::Present(17)),
2425 );
2426 let diagnostic = Diagnostic::capture(
2427 DiagnosticCategory::Panic,
2428 CaptureSite::Origin,
2429 DiagnosticCause::new(
2430 DiagnosticStage::RequestHandler,
2431 DiagnosticCode::new("panic.caught").unwrap(),
2432 ),
2433 );
2434 let id = diagnostic.id();
2435 let first = RootDiagnosticScope::new(&view, Some(&handle)).source_existing(
2436 diagnostic,
2437 DiagnosticCode::new("task.failed").unwrap(),
2438 RootRequestEvent::Supervision,
2439 RootOutcomeFacts::default(),
2440 );
2441 let occurrence = first.occurrence();
2442 let ledger = saddle_admission::ProcessLedger::new(saddle_admission::ResourceConfig {
2443 managed_capacity: 1024 * 1024,
2444 entry_reserve: 1024 * 1024,
2445 framework_reserve: 1024 * 1024,
2446 task_reserve: 1024 * 1024,
2447 process_state_reserve: 1024 * 1024,
2448 system_estimate: 1024 * 1024,
2449 safety_margin: 1024 * 1024,
2450 process_limit: 8 * 1024 * 1024,
2451 max_active_requests: 1,
2452 })
2453 .unwrap();
2454 let future = ledger
2455 .try_envelope(65536, 65536, |_| async {
2456 let cleanup = BoundedDiagnostic::capture(
2457 DiagnosticCategory::UnexpectedError,
2458 CaptureSite::Origin,
2459 BoundedDiagnosticCause::new(
2460 DiagnosticStage::FinalizerResource,
2461 DiagnosticCode::new("cleanup.failed").unwrap(),
2462 ),
2463 )
2464 .during_cleanup_occurrence(occurrence);
2465 let second = RootDiagnosticScope::new(&view, Some(&handle)).source(
2466 cleanup,
2467 DiagnosticCode::new("cleanup.failed").unwrap(),
2468 RootRequestEvent::Finalization,
2469 RootOutcomeFacts::default(),
2470 );
2471 assert_eq!(second.submission(), DiagnosticSubmission::Enqueued);
2472 first
2473 .finish(&view, None, RootOutcomeFacts::default())
2474 .ok()
2475 .unwrap()
2476 })
2477 .unwrap();
2478 let mut future = std::pin::pin!(future);
2479 let result = future
2480 .as_mut()
2481 .poll(&mut Context::from_waker(Waker::noop()));
2482 assert!(
2483 matches!(result, Poll::Ready(Ok(_))),
2484 "bounded receipt path must not allocate outside admitted contract: {result:?}"
2485 );
2486 let primary: serde_json::Value =
2487 serde_json::from_slice(receiver.try_recv().unwrap().bytes()).unwrap();
2488 let cleanup: serde_json::Value =
2489 serde_json::from_slice(receiver.try_recv().unwrap().bytes()).unwrap();
2490 assert_eq!(primary["diagnostic"]["diagnostic_id"], id);
2491 assert_eq!(primary["category"], "panic");
2492 assert_eq!(
2493 primary["diagnostic"]["stack_status"],
2494 "unavailable_deferred"
2495 );
2496 assert_eq!(cleanup["occurrence"]["primary_diagnostic_id"], id);
2497 assert_ne!(cleanup["occurrence"]["diagnostic_id"], id);
2498 assert_eq!(cleanup["context"], primary["context"]);
2499 }
2500
2501 #[test]
2502 fn root_contract_real_file_readback_preserves_safe_facts_without_secret() {
2503 use crate::{RootDiagnosticScope, RootOutcomeFacts, RootRequestEvent};
2504 use saddle_core::*;
2505 let (sender, receiver) = mpsc::sync_channel(1);
2506 let handle = EmergencyDiagnosticHandle {
2507 sender,
2508 status: Arc::new(Status::default()),
2509 };
2510 let root = RequestRootPublisher::create(
2511 ContextLabel::checked("app").unwrap(),
2512 ContextFact::NotEstablished,
2513 )
2514 .unwrap();
2515 let view = root
2516 .reference()
2517 .view(RequestLocalFacts::new(RequestViewPhase::Reading));
2518 let cause = BoundedDiagnosticCause::new(
2519 DiagnosticStage::RequestDb,
2520 DiagnosticCode::new("db.type").unwrap(),
2521 )
2522 .with_object(InlineDiagnosticText::metadata(
2523 "mysql://SECRET_SENTINEL@host",
2524 ))
2525 .with_sqlstate("22003")
2526 .with_database_code(1264)
2527 .with_column(Some(3), Some(4))
2528 .with_types(
2529 Some(InlineDiagnosticText::metadata("i32")),
2530 Some(InlineDiagnosticText::metadata("BIGINT")),
2531 None,
2532 );
2533 let failure = RootDiagnosticScope::new(&view, Some(&handle)).source(
2534 BoundedDiagnostic::capture(
2535 DiagnosticCategory::UnexpectedError,
2536 CaptureSite::Origin,
2537 cause,
2538 ),
2539 DiagnosticCode::new("db.type").unwrap(),
2540 RootRequestEvent::Database,
2541 RootOutcomeFacts::default(),
2542 );
2543 let public = failure
2544 .finish(&view, None, RootOutcomeFacts::default())
2545 .ok()
2546 .unwrap();
2547 drop((view, root));
2548 let path = std::env::temp_dir().join(format!(
2549 "saddle-c-readback-{}-{}.jsonl",
2550 std::process::id(),
2551 std::time::SystemTime::now()
2552 .duration_since(std::time::UNIX_EPOCH)
2553 .unwrap()
2554 .as_nanos()
2555 ));
2556 let file = std::fs::OpenOptions::new()
2557 .create_new(true)
2558 .write(true)
2559 .open(&path)
2560 .unwrap();
2561 let packet = receiver.try_recv().unwrap();
2562 let mut output = Ok(file);
2563 deliver(
2564 &mut output,
2565 &mut io::sink(),
2566 &path,
2567 packet.bytes(),
2568 &handle.status,
2569 &mut false,
2570 );
2571 output.as_mut().unwrap().flush().unwrap();
2572 let raw = std::fs::read(&path).unwrap();
2573 std::fs::remove_file(&path).unwrap();
2574 assert!(!String::from_utf8_lossy(&raw).contains("SECRET_SENTINEL"));
2575 let row: serde_json::Value = serde_json::from_slice(&raw).unwrap();
2576 assert_eq!(row["diagnostic"]["causes"][0]["object"]["redacted"], true);
2577 assert_eq!(row["diagnostic"]["causes"][0]["sqlstate"]["value"], "22003");
2578 assert_eq!(row["diagnostic"]["causes"][0]["column_index"], 3);
2579 assert_eq!(handle.snapshot().written, 1);
2580 assert_eq!(public.source_submission(), DiagnosticSubmission::Enqueued);
2581 assert_eq!(
2582 public.terminal_submission(),
2583 DiagnosticSubmission::OutputUnavailable
2584 );
2585 }
2586 #[test]
2587 fn root_contract_source_boundary_supervision_and_queue_degradation() {
2588 use crate::{
2589 RootDiagnosticScope, RootOutcomeFacts, RootRequestEvent,
2590 };
2591 use saddle_core::{
2592 BoundedDiagnostic, BoundedDiagnosticCause, ContextFact, ContextLabel,
2593 RequestLocalFacts, RequestRootPublisher, RequestViewPhase,
2594 };
2595 let (sender, receiver) = mpsc::sync_channel(2);
2596 let handle = EmergencyDiagnosticHandle {
2597 sender,
2598 status: Arc::new(Status::default()),
2599 };
2600 let root = RequestRootPublisher::create(
2601 ContextLabel::checked("app").unwrap(),
2602 ContextFact::NotEstablished,
2603 )
2604 .unwrap();
2605 let view = root
2606 .reference()
2607 .view(RequestLocalFacts::new(RequestViewPhase::Reading));
2608 let make = |output| {
2609 RootDiagnosticScope::new(&view, output).source(
2610 BoundedDiagnostic::capture(
2611 DiagnosticCategory::UnexpectedError,
2612 CaptureSite::Origin,
2613 BoundedDiagnosticCause::new(
2614 DiagnosticStage::RequestDecode,
2615 DiagnosticCode::new("read.failed").unwrap(),
2616 ),
2617 ),
2618 DiagnosticCode::new("decode.failed").unwrap(),
2619 RootRequestEvent::Ingress,
2620 RootOutcomeFacts::default(),
2621 )
2622 };
2623 let missing = make(None);
2624 assert_eq!(
2625 missing.submission(),
2626 DiagnosticSubmission::OutputUnavailable
2627 );
2628 let receipt = make(Some(&handle));
2629 assert_eq!(receipt.submission(), DiagnosticSubmission::Enqueued);
2630 let foreign = RequestRootPublisher::create(
2631 ContextLabel::checked("app").unwrap(),
2632 ContextFact::NotEstablished,
2633 )
2634 .unwrap()
2635 .reference()
2636 .view(RequestLocalFacts::new(RequestViewPhase::Reading));
2637 let receipt = receipt
2638 .boundary(
2639 &foreign,
2640 Some(&handle),
2641 RootRequestEvent::Supervision,
2642 RootOutcomeFacts::default(),
2643 )
2644 .err()
2645 .unwrap();
2646 let public = receipt
2647 .finish(&view, Some(&handle), RootOutcomeFacts::default())
2648 .ok()
2649 .unwrap();
2650 assert_eq!(make(Some(&handle)).submission(), DiagnosticSubmission::Full);
2651 handle.status.closed.store(true, Ordering::Release);
2652 assert_eq!(
2653 make(Some(&handle)).submission(),
2654 DiagnosticSubmission::Closed
2655 );
2656 drop((missing, foreign, view, root));
2657 let source: serde_json::Value =
2660 serde_json::from_slice(receiver.try_recv().unwrap().bytes()).unwrap();
2661 let boundary: serde_json::Value =
2662 serde_json::from_slice(receiver.try_recv().unwrap().bytes()).unwrap();
2663 let public = serde_json::to_value(public).unwrap();
2664 assert_eq!(source["occurrence"], boundary["occurrence"]);
2665 assert_eq!(source["occurrence"], public["occurrence"]);
2666 assert_eq!(source["context"], boundary["source_context"]);
2667 assert_eq!(source["context"]["trace_id"]["state"], "not_established");
2668 assert!(source["diagnostic"].is_object());
2669 assert!(boundary["diagnostic"].is_null());
2670 assert!(public.get("context").is_none());
2671 assert_eq!(handle.snapshot().written, 0);
2672 assert_eq!(handle.snapshot().enqueued, 2);
2673 assert_eq!(handle.snapshot().dropped, 2);
2674 }
2675 #[test]
2676 fn outbound_child_queue_source_boundary_full_closed() {
2677 use crate::{EventContext, RequestDiagnosticScope, RequestIdentity, RouteIdentity};
2678 use saddle_core::{
2679 BoundedDiagnosticCause, CallContext, DiagnosticOutcomeAxes, RpcCorrelationId, SpanId,
2680 TraceId,
2681 };
2682 let (sender, receiver) = mpsc::sync_channel(2);
2683 let handle = EmergencyDiagnosticHandle {
2684 sender,
2685 status: Arc::new(Status::default()),
2686 };
2687 let call = |rpc: &str, span| {
2688 CallContext::new(
2689 "app".into(),
2690 "module".into(),
2691 "service".into(),
2692 "operation".into(),
2693 TraceId::from_u128(1),
2694 SpanId::from_u64(span),
2695 )
2696 .with_rpc_correlation_id(RpcCorrelationId::new(rpc))
2697 };
2698 let parent_call = call("0", 1);
2699 let child_call = call("0.1", 2);
2700 let event = EventContext::new(
2701 RequestIdentity::new("request-1").unwrap(),
2702 RouteIdentity::new("route").unwrap(),
2703 1,
2704 )
2705 .unwrap();
2706 let parent = RequestDiagnosticScope::established(&handle, &parent_call, &event);
2707 let child = parent.derive_outbound_child(&child_call, &event).unwrap();
2708 let fail = || {
2709 child.fail(
2710 7,
2711 DiagnosticCategory::UnexpectedError,
2712 BoundedDiagnosticCause::new(
2713 DiagnosticStage::RequestDecode,
2714 DiagnosticCode::new("outbound.failed").unwrap(),
2715 ),
2716 )
2717 };
2718 let source = fail();
2719 assert_eq!(source.submission(), DiagnosticSubmission::Enqueued);
2720 let (retained, delivery) =
2721 source.finish_boundary_retained(Some(&handle), &DiagnosticOutcomeAxes::default());
2722 assert_eq!(*retained.error(), 7);
2723 assert_eq!(
2724 delivery.boundary_submission(),
2725 DiagnosticSubmission::Enqueued
2726 );
2727 assert_eq!(fail().submission(), DiagnosticSubmission::Full);
2728 handle.status.closed.store(true, Ordering::Release);
2729 assert_eq!(fail().submission(), DiagnosticSubmission::Closed);
2730 let source: serde_json::Value =
2731 serde_json::from_slice(receiver.try_recv().unwrap().bytes()).unwrap();
2732 let boundary: serde_json::Value =
2733 serde_json::from_slice(receiver.try_recv().unwrap().bytes()).unwrap();
2734 assert_eq!(source["context"], boundary["context"]);
2735 assert_eq!(
2736 source["diagnostic_reference"],
2737 boundary["diagnostic_reference"]
2738 );
2739 assert_eq!(source["context"]["rpc_id"]["value"], "0.1");
2740 assert_eq!(source["context"]["span_id"]["value"], "0000000000000002");
2741 assert!(source["diagnostic"].is_object());
2742 assert!(boundary["diagnostic"].is_null());
2743 assert_eq!(handle.snapshot().written, 0);
2744 }
2745 #[test]
2746 fn early_request_source_boundary_admitted_full_closed_and_missing_output() {
2747 use crate::{DiagnosticRequestPhase, EarlyRequestContext, RequestDiagnosticScope};
2748 use saddle_core::{BoundedDiagnosticCause, DiagnosticOutcomeAxes, OperationOutcome};
2749 use std::{
2750 future::Future,
2751 task::{Context, Poll, Waker},
2752 };
2753 let (sender, receiver) = mpsc::sync_channel(2);
2754 let handle = EmergencyDiagnosticHandle {
2755 sender,
2756 status: Arc::new(Status::default()),
2757 };
2758 let known_call = saddle_core::CallContext::new(
2759 "app".into(),
2760 "module".into(),
2761 "service".into(),
2762 "operation".into(),
2763 saddle_core::TraceId::from_u128(1),
2764 saddle_core::SpanId::from_u64(2),
2765 );
2766 let known_event = crate::EventContext::new(
2767 crate::RequestIdentity::new("real-request").unwrap(),
2768 crate::RouteIdentity::new("/route").unwrap(),
2769 1,
2770 )
2771 .unwrap();
2772 let ledger = saddle_admission::ProcessLedger::new(saddle_admission::ResourceConfig {
2773 managed_capacity: 1024 * 1024,
2774 entry_reserve: 1024 * 1024,
2775 framework_reserve: 1024 * 1024,
2776 task_reserve: 1024 * 1024,
2777 process_state_reserve: 1024 * 1024,
2778 system_estimate: 1024 * 1024,
2779 safety_margin: 1024 * 1024,
2780 process_limit: 8 * 1024 * 1024,
2781 max_active_requests: 1,
2782 })
2783 .unwrap();
2784 let future = ledger
2785 .try_envelope(65536, 65536, |_| async {
2786 let axes = DiagnosticOutcomeAxes {
2787 operation: OperationOutcome::Failed,
2788 ..Default::default()
2789 };
2790 let cause = || {
2791 BoundedDiagnosticCause::new(
2792 DiagnosticStage::RequestDecode,
2793 DiagnosticCode::new("request.read_failed").unwrap(),
2794 )
2795 };
2796 let enriched = RequestDiagnosticScope::early(
2797 None,
2798 EarlyRequestContext::socket_accepted("app"),
2799 )
2800 .with_task(crate::DiagnosticTaskId::from_runtime_id("42").unwrap())
2801 .unwrap_or_else(|_| panic!("task conflict"))
2802 .bind_established(&known_call, &known_event)
2803 .unwrap_or_else(|_| panic!("context conflict"));
2804 let unchanged =
2805 enriched
2806 .reborrow()
2807 .fail(22u32, DiagnosticCategory::UnexpectedError, cause());
2808 assert_eq!(
2809 unchanged.submission(),
2810 DiagnosticSubmission::OutputUnavailable
2811 );
2812 let scope = RequestDiagnosticScope::early(
2813 Some(&handle),
2814 EarlyRequestContext::socket_accepted("app"),
2815 )
2816 .with_phase(DiagnosticRequestPhase::ReadingHead);
2817 let failure = scope.fail(23u32, DiagnosticCategory::UnexpectedError, cause());
2818 let id = failure.source_diagnostic().id();
2819 assert_eq!(failure.submission(), DiagnosticSubmission::Enqueued);
2820 let (failure, delivered) = failure.finish_boundary_retained(Some(&handle), &axes);
2821 assert_eq!(
2822 delivered.boundary_submission(),
2823 DiagnosticSubmission::Enqueued
2824 );
2825 assert_eq!(*failure.error(), 23);
2826 let full = scope.fail(24u32, DiagnosticCategory::UnexpectedError, cause());
2827 assert_eq!(full.submission(), DiagnosticSubmission::Full);
2828 let (full, delivered) = full.finish_boundary_retained(None, &axes);
2829 assert_eq!(*full.error(), 24);
2830 assert_eq!(delivered.source_submission(), DiagnosticSubmission::Full);
2831 assert_eq!(
2832 delivered.boundary_submission(),
2833 DiagnosticSubmission::OutputUnavailable
2834 );
2835 handle.status.closed.store(true, Ordering::Release);
2836 let closed = scope.fail(25u32, DiagnosticCategory::UnexpectedError, cause());
2837 assert_eq!(closed.submission(), DiagnosticSubmission::Closed);
2838 let no_output = scope.with_output(None).fail(
2839 26u32,
2840 DiagnosticCategory::UnexpectedError,
2841 cause(),
2842 );
2843 assert_eq!(
2844 no_output.submission(),
2845 DiagnosticSubmission::OutputUnavailable
2846 );
2847 assert_eq!(*no_output.error(), 26);
2848 id
2849 })
2850 .unwrap();
2851 let mut future = std::pin::pin!(future);
2852 let result = future
2853 .as_mut()
2854 .poll(&mut Context::from_waker(Waker::noop()));
2855 assert!(
2856 matches!(result, Poll::Ready(Ok(_))),
2857 "early path escaped: {result:?}"
2858 );
2859 let source: serde_json::Value =
2860 serde_json::from_slice(receiver.try_recv().unwrap().bytes()).unwrap();
2861 let boundary: serde_json::Value =
2862 serde_json::from_slice(receiver.try_recv().unwrap().bytes()).unwrap();
2863 assert_eq!(source["context"], boundary["context"]);
2864 assert_eq!(
2865 source["diagnostic_reference"],
2866 boundary["diagnostic_reference"]
2867 );
2868 assert_eq!(source["context"]["trace_id"]["state"], "not_established");
2869 assert_eq!(
2870 source["context"]["lifecycle"]["value"]["value"],
2871 "reading_head"
2872 );
2873 assert!(source["diagnostic"].is_object());
2874 assert!(boundary["diagnostic"].is_null());
2875 assert_eq!(boundary["axes"]["operation"], "failed");
2876 assert_eq!(handle.snapshot().written, 0); }
2878 use std::time::Instant;
2879 #[test]
2880 fn early_retained_reference_late_output_keeps_original_submission_and_context() {
2881 use crate::{EarlyRequestContext, RequestDiagnosticScope};
2882 use saddle_core::{BoundedDiagnostic, BoundedDiagnosticCause, DiagnosticOutcomeAxes};
2883 let (sender, receiver) = mpsc::sync_channel(2);
2884 let handle = EmergencyDiagnosticHandle {
2885 sender,
2886 status: Arc::new(Status::default()),
2887 };
2888 let scope =
2889 RequestDiagnosticScope::early(None, EarlyRequestContext::socket_accepted("app"));
2890 let diagnostic = BoundedDiagnostic::capture(
2891 DiagnosticCategory::UnexpectedError,
2892 CaptureSite::FirstObserved,
2893 BoundedDiagnosticCause::new(
2894 DiagnosticStage::RequestDecode,
2895 DiagnosticCode::new("request.read_failed").unwrap(),
2896 ),
2897 );
2898 let source = serde_json::to_value(&diagnostic).unwrap();
2899 let reference = scope.capture_required(diagnostic).into_reference();
2900 let (retained, delivery) =
2901 reference.finish_retained(Some(&handle), &DiagnosticOutcomeAxes::default());
2902 assert_eq!(
2903 delivery.source_submission(),
2904 DiagnosticSubmission::OutputUnavailable
2905 );
2906 assert_eq!(
2907 delivery.boundary_submission(),
2908 DiagnosticSubmission::Enqueued
2909 );
2910 assert_eq!(
2911 serde_json::to_value(retained.source_diagnostic()).unwrap(),
2912 source
2913 );
2914 let packet = receiver.try_recv().unwrap();
2915 let row: serde_json::Value = serde_json::from_slice(packet.bytes()).unwrap();
2916 assert_eq!(row["source_submission"], "output_unavailable");
2917 assert_eq!(row["context"]["trace_id"]["state"], "not_established");
2918 assert!(row["diagnostic"].is_null()); assert!(receiver.try_recv().is_err());
2920 assert_eq!(handle.snapshot().written, 0);
2921 }
2922 static FILE_TEST: std::sync::Mutex<()> = std::sync::Mutex::new(());
2923 #[test]
2924 fn zone_projection_exact_missing_and_unsafe_are_explicit() {
2925 use crate::{
2926 DiagnosticContextMissing as Missing, DiagnosticZone, EventContext,
2927 RequestDiagnosticScope, RequestIdentity, RouteIdentity,
2928 };
2929 let (sender, receiver) = mpsc::sync_channel(2);
2930 let handle = EmergencyDiagnosticHandle {
2931 sender,
2932 status: Arc::new(Status::default()),
2933 };
2934 let call = CallContext::new(
2935 "app".into(),
2936 "module".into(),
2937 "service".into(),
2938 "query".into(),
2939 saddle_core::TraceId::from_u128(1),
2940 saddle_core::SpanId::from_u64(2),
2941 );
2942 let event = EventContext::new(
2943 RequestIdentity::new("r1").unwrap(),
2944 RouteIdentity::new("/query").unwrap(),
2945 1,
2946 )
2947 .unwrap();
2948 let cause = saddle_core::BoundedDiagnosticCause::new(
2949 DiagnosticStage::RequestDb,
2950 DiagnosticCode::new("db.decode.type").unwrap(),
2951 );
2952 for zone in ["zone-a", "杭州-zone/蓝"] {
2953 let scope = RequestDiagnosticScope::established(&handle, &call, &event)
2954 .with_zone(DiagnosticZone::from_validated_ingress(zone).unwrap())
2955 .with_zone_missing(Missing::Unavailable);
2956 let source = scope
2957 .fail((), DiagnosticCategory::UnexpectedError, cause)
2958 .into_reference();
2959 source.record(&handle, &saddle_core::DiagnosticOutcomeAxes::default());
2960 for _ in 0..2 {
2961 let row: serde_json::Value =
2962 serde_json::from_slice(receiver.recv().unwrap().bytes()).unwrap();
2963 assert_eq!(
2964 row["context"]["zone"],
2965 serde_json::json!({"state":"present","value":zone})
2966 );
2967 }
2968 }
2969 for (reason, expected) in [
2970 (Missing::NotApplicable, "not_applicable"),
2971 (Missing::NotEstablished, "not_established"),
2972 (Missing::Unavailable, "unavailable"),
2973 ] {
2974 RequestDiagnosticScope::established(&handle, &call, &event)
2975 .with_zone_missing(reason)
2976 .fail((), DiagnosticCategory::UnexpectedError, cause);
2977 let row: serde_json::Value =
2978 serde_json::from_slice(receiver.recv().unwrap().bytes()).unwrap();
2979 assert_eq!(
2980 row["context"]["zone"],
2981 serde_json::json!({"state":expected})
2982 );
2983 }
2984 for invalid in [
2985 "",
2986 " ",
2987 "https://user:SECRET@host",
2988 "zone\nSECRET",
2989 "zone?token=SECRET",
2990 ] {
2991 assert!(DiagnosticZone::from_validated_ingress(invalid).is_err());
2992 }
2993 assert!(DiagnosticZone::from_validated_ingress(&"a".repeat(257)).is_err());
2994 assert!(DiagnosticZone::from_validated_ingress(&"a".repeat(256)).is_ok());
2995 }
2996 #[test]
2997 fn existing_source_receipt_keeps_stack_and_submits_once() {
2998 use crate::{EventContext, RequestDiagnosticScope, RequestIdentity, RouteIdentity};
2999 let (sender, receiver) = mpsc::sync_channel(2);
3000 let handle = EmergencyDiagnosticHandle {
3001 sender,
3002 status: Arc::new(Status::default()),
3003 };
3004 let call = CallContext::new(
3005 "app".into(),
3006 "module".into(),
3007 "service".into(),
3008 "query".into(),
3009 saddle_core::TraceId::from_u128(1),
3010 saddle_core::SpanId::from_u64(2),
3011 );
3012 let event = EventContext::new(
3013 RequestIdentity::new("request-1").unwrap(),
3014 RouteIdentity::new("/query").unwrap(),
3015 1,
3016 )
3017 .unwrap();
3018 let diagnostic = Diagnostic::capture(
3019 DiagnosticCategory::UnexpectedError,
3020 CaptureSite::FirstObserved,
3021 DiagnosticCause::new(
3022 DiagnosticStage::RequestDb,
3023 DiagnosticCode::new("db.decode.type").unwrap(),
3024 ),
3025 );
3026 let before = serde_json::to_value(&diagnostic).unwrap();
3027 let receipt = RequestDiagnosticScope::established(&handle, &call, &event)
3028 .capture_existing(diagnostic, None);
3029 assert_eq!(receipt.submission(), DiagnosticSubmission::Enqueued);
3030 let reference = receipt.into_reference();
3031 assert_eq!(
3032 serde_json::to_value(reference.source_diagnostic()).unwrap(),
3033 before
3034 );
3035 let source: serde_json::Value =
3036 serde_json::from_slice(receiver.recv().unwrap().bytes()).unwrap();
3037 assert_eq!(source["diagnostic"], before);
3038 assert!(receiver.try_recv().is_err());
3039 assert_eq!(handle.snapshot().enqueued, 1);
3040 assert_eq!(
3041 reference.record(&handle, &saddle_core::DiagnosticOutcomeAxes::default()),
3042 DiagnosticSubmission::Enqueued
3043 );
3044 let boundary: serde_json::Value =
3045 serde_json::from_slice(receiver.recv().unwrap().bytes()).unwrap();
3046 assert!(boundary["diagnostic"].is_null());
3047 assert_eq!(
3048 source["diagnostic_reference"],
3049 boundary["diagnostic_reference"]
3050 );
3051 assert_eq!(source["context"], boundary["context"]);
3052 assert_eq!(handle.snapshot().enqueued, 2);
3053 assert_eq!(handle.snapshot().written, 0);
3054 }
3055 #[test]
3056 fn controlled_request_source_preserves_error_projection_and_degradation() {
3057 use crate::{EventContext, RequestDiagnosticScope, RequestIdentity, RouteIdentity};
3058 use saddle_core::*;
3059 let (sender, receiver) = mpsc::sync_channel(2);
3060 let handle = EmergencyDiagnosticHandle {
3061 sender,
3062 status: Arc::new(Status::default()),
3063 };
3064 let call = CallContext::new(
3065 "app".into(),
3066 "module".into(),
3067 "service".into(),
3068 "query".into(),
3069 TraceId::from_u128(1),
3070 SpanId::from_u64(2),
3071 );
3072 let event = EventContext::new(
3073 RequestIdentity::new("req-1").unwrap(),
3074 RouteIdentity::new("/agent/{code}").unwrap(),
3075 2,
3076 )
3077 .unwrap();
3078 let scope = RequestDiagnosticScope::established(&handle, &call, &event);
3079 let cause = BoundedDiagnosticCause::new(
3080 DiagnosticStage::RequestDb,
3081 DiagnosticCode::new("db.decode.type").unwrap(),
3082 );
3083 let failure = scope.fail(17u32, DiagnosticCategory::UnexpectedError, cause);
3084 assert_eq!(failure.submission(), DiagnosticSubmission::Enqueued);
3085 let failure = failure.map_error(|error| (error, false));
3086 assert_eq!(failure.error(), &(17, false));
3087 assert_eq!(
3088 failure.record_boundary(&handle, &DiagnosticOutcomeAxes::default()),
3089 DiagnosticSubmission::Enqueued
3090 );
3091 let full = scope.fail(19, DiagnosticCategory::UnexpectedError, cause);
3092 assert_eq!(full.submission(), DiagnosticSubmission::Full);
3093 assert_eq!(full.error(), &19);
3094 let source: serde_json::Value =
3095 serde_json::from_slice(receiver.recv().unwrap().bytes()).unwrap();
3096 let boundary: serde_json::Value =
3097 serde_json::from_slice(receiver.recv().unwrap().bytes()).unwrap();
3098 assert_eq!(source["context"], boundary["context"]);
3099 assert_eq!(
3100 source["diagnostic_reference"],
3101 boundary["diagnostic_reference"]
3102 );
3103 assert!(boundary["diagnostic"].is_null());
3104 assert!(source["diagnostic"].is_object());
3105 let c = &source["context"];
3106 for (key, expected) in [
3107 ("application", "app"),
3108 ("module", "module"),
3109 ("service", "service"),
3110 ("operation", "query"),
3111 ("request", "req-1"),
3112 ("route", "/agent/{code}"),
3113 ] {
3114 assert_eq!(c[key]["value"]["value"], expected);
3115 }
3116 assert_eq!(c["attempt"]["value"], 2);
3117 assert_eq!(c["span_id"]["value"], "0000000000000002");
3118 assert_eq!(c["rpc_id"]["state"], "unavailable");
3119 for field in ["scope", "task", "lifecycle", "zone", "target"] {
3120 assert_eq!(c[field]["state"], "unavailable");
3121 }
3122 handle.status.closed.store(true, Ordering::Release);
3123 let closed = scope.fail(21, DiagnosticCategory::UnexpectedError, cause);
3124 assert_eq!(closed.submission(), DiagnosticSubmission::Closed);
3125 assert_eq!(closed.error(), &21);
3126 assert_eq!(handle.snapshot().written, 0);
3127 let unavailable = RequestDiagnosticScope::output_unavailable(&call, &event);
3128 let original = BoundedDiagnostic::capture(
3129 DiagnosticCategory::UnexpectedError,
3130 CaptureSite::FirstObserved,
3131 cause,
3132 );
3133 let original_id = original.id();
3134 let reference = unavailable.capture_required(original).into_reference();
3135 assert_eq!(
3136 reference.source_submission(),
3137 DiagnosticSubmission::OutputUnavailable
3138 );
3139 assert_eq!(reference.source_diagnostic().id(), original_id);
3140 assert_eq!(
3141 reference.record(&handle, &DiagnosticOutcomeAxes::default()),
3142 DiagnosticSubmission::Closed
3143 );
3144 }
3145 #[test]
3146 fn source_detail_and_boundary_reference_are_separate() {
3147 use saddle_core::*;
3148 let (sender, receiver) = mpsc::sync_channel(4);
3149 let handle = EmergencyDiagnosticHandle {
3150 sender,
3151 status: Arc::new(Status::default()),
3152 };
3153 let cause = BoundedDiagnosticCause::new(
3154 DiagnosticStage::RequestDb,
3155 DiagnosticCode::new("db.decode.type").unwrap(),
3156 );
3157 let primary = BoundedDiagnostic::capture(
3158 DiagnosticCategory::UnexpectedError,
3159 CaptureSite::FirstObserved,
3160 cause,
3161 );
3162 let cleanup = BoundedDiagnostic::capture(
3163 DiagnosticCategory::UnexpectedError,
3164 CaptureSite::FirstObserved,
3165 cause,
3166 )
3167 .during_cleanup_of(&primary);
3168 let reference = cleanup.occurrence();
3169 let axes = DiagnosticOutcomeAxes::default();
3170 assert_eq!(
3171 handle.submit_bounded(Some(&cleanup), &axes, None),
3172 DiagnosticSubmission::Enqueued
3173 );
3174 drop(cleanup);
3175 assert_eq!(
3176 handle.submit_boundary(Some(reference), &axes, None),
3177 DiagnosticSubmission::Enqueued
3178 );
3179 assert_eq!(
3180 handle.submit_boundary_parts(Some(reference), &axes, None, Some(("database", 1))),
3181 DiagnosticSubmission::Enqueued
3182 );
3183 let source: serde_json::Value =
3184 serde_json::from_slice(receiver.recv().unwrap().bytes()).unwrap();
3185 assert!(source["diagnostic"].is_object());
3186 for event in ["framework.boundary.outcome", "framework.stage.finished"] {
3187 let boundary: serde_json::Value =
3188 serde_json::from_slice(receiver.recv().unwrap().bytes()).unwrap();
3189 assert_eq!(boundary["event"], event);
3190 assert!(boundary["diagnostic"].is_null());
3191 assert_eq!(
3192 boundary["diagnostic_reference"],
3193 source["diagnostic_reference"]
3194 );
3195 assert_eq!(
3196 boundary["diagnostic_reference"]["primary_diagnostic_id"],
3197 primary.id()
3198 );
3199 assert!(!boundary.to_string().contains("db.decode.type"));
3200 }
3201 assert_eq!(handle.snapshot().written, 0);
3202 }
3203 #[test]
3204 fn deferred_unavailable_preserves_source_through_output_worker() {
3205 use saddle_core::*;
3206 let (sender, receiver) = mpsc::sync_channel(1);
3207 let handle = EmergencyDiagnosticHandle {
3208 sender,
3209 status: Arc::new(Status::default()),
3210 };
3211 let diagnostic = Diagnostic::capture(
3212 DiagnosticCategory::UnexpectedError,
3213 CaptureSite::FirstObserved,
3214 DiagnosticCause::new(
3215 DiagnosticStage::RequestDb,
3216 DiagnosticCode::new("db.decode.type").unwrap(),
3217 ),
3218 );
3219 let original = serde_json::to_value(&diagnostic).unwrap();
3220 assert_eq!(original["stack_status"], "unavailable_deferred");
3221 assert!(diagnostic.deferred_stack().is_none());
3222 assert!(original["stack"].as_str().unwrap().is_empty());
3223 assert_eq!(handle.submit(&diagnostic), DiagnosticSubmission::Enqueued);
3224 let packet = receiver.try_recv().unwrap();
3225 let queued: serde_json::Value = serde_json::from_slice(packet.bytes()).unwrap();
3226 assert_eq!(queued["diagnostic"], original);
3227 let status = handle.status.clone();
3228 let row = std::thread::spawn(move || {
3229 let mut packet = packet;
3230 packet.resolve_on_worker(&status);
3231 serde_json::from_slice::<serde_json::Value>(packet.bytes()).unwrap()
3232 })
3233 .join()
3234 .unwrap();
3235 for key in ["diagnostic_id", "primary_diagnostic_id", "origin", "causes"] {
3236 assert_eq!(row["diagnostic"][key], original[key]);
3237 }
3238 assert_eq!(row["diagnostic"]["stack_status"], "unavailable_deferred");
3239 assert!(row["diagnostic"]["stack"].as_str().unwrap().is_empty());
3240 assert_eq!(serde_json::to_value(&diagnostic).unwrap(), original);
3241 }
3242 #[test]
3243 fn request_stage_finishes_once_with_original_receipt_context() {
3244 use saddle_core::*;
3245 let (sender, receiver) = mpsc::sync_channel(4);
3246 let handle = EmergencyDiagnosticHandle {
3247 sender,
3248 status: Arc::new(Status::default()),
3249 };
3250 let observer =
3251 crate::Observer::with_writer(crate::ObserverConfig::default(), io::sink()).unwrap();
3252 let (call, _) = observer
3253 .start_external_call_checked("app", "module", "service", "query", None)
3254 .unwrap();
3255 let event = crate::EventContext::new(
3256 crate::RequestIdentity::new("original-request").unwrap(),
3257 crate::RouteIdentity::new("/query").unwrap(),
3258 1,
3259 )
3260 .unwrap();
3261 let mut scope = crate::RequestDiagnosticScope::established(&handle, call.context(), &event)
3262 .with_zone(crate::DiagnosticZone::from_validated_ingress("zone-a").unwrap())
3263 .with_db_operation(
3264 crate::DiagnosticDbOperation::from_registered("AgentByCode").unwrap(),
3265 )
3266 .with_db_operation_missing(crate::DiagnosticContextMissing::Unavailable);
3267 let reference = scope
3268 .capture_required(BoundedDiagnostic::capture(
3269 DiagnosticCategory::UnexpectedError,
3270 CaptureSite::FirstObserved,
3271 BoundedDiagnosticCause::new(
3272 DiagnosticStage::RequestDb,
3273 DiagnosticCode::new("db.decode.type").unwrap(),
3274 ),
3275 ))
3276 .into_reference();
3277 scope.set_db_operation(
3278 crate::DiagnosticDbOperation::from_registered("NextOperation").unwrap(),
3279 );
3280 let stage = observer.start_stage(call.context(), crate::Stage::Database, event.clone());
3281 let axes = DiagnosticOutcomeAxes {
3282 operation: OperationOutcome::Failed,
3283 physical: PhysicalDispositionFact::Returned,
3284 ..Default::default()
3285 };
3286 let delivery = stage.finish_request_failure(Some(&handle), &axes, &reference);
3287 assert_eq!(
3288 delivery.boundary_submission(),
3289 DiagnosticSubmission::Enqueued
3290 );
3291 let source: serde_json::Value =
3292 serde_json::from_slice(receiver.try_recv().unwrap().bytes()).unwrap();
3293 let boundary: serde_json::Value =
3294 serde_json::from_slice(receiver.try_recv().unwrap().bytes()).unwrap();
3295 assert_eq!(source["context"], boundary["context"]);
3296 assert_eq!(source["context"]["db_operation"]["value"], "AgentByCode");
3297 assert_eq!(source["context"]["operation"]["value"]["value"], "query");
3298 let next = scope.fail(
3299 (),
3300 DiagnosticCategory::ExpectedRejection,
3301 BoundedDiagnosticCause::new(
3302 DiagnosticStage::RequestDb,
3303 DiagnosticCode::new("db.next").unwrap(),
3304 ),
3305 );
3306 assert_eq!(next.submission(), DiagnosticSubmission::Enqueued);
3307 let next: serde_json::Value =
3308 serde_json::from_slice(receiver.try_recv().unwrap().bytes()).unwrap();
3309 assert_eq!(next["context"]["db_operation"]["value"], "NextOperation");
3310 for field in ["operation", "request", "scope", "zone"] {
3311 assert_eq!(next["context"][field], source["context"][field]);
3312 }
3313 assert!(crate::DiagnosticDbOperation::from_registered("SELECT * FROM private").is_none());
3314 assert!(crate::DiagnosticDbOperation::from_registered("https://secret@host").is_none());
3315 assert_eq!(
3316 source["diagnostic_reference"],
3317 boundary["diagnostic_reference"]
3318 );
3319 assert_eq!(boundary["source_submission"], "enqueued");
3320 assert_eq!(boundary["axes"]["operation"], "failed");
3321 assert!(boundary["diagnostic"].is_null());
3322 assert!(boundary["elapsed_ms"].is_u64());
3323 assert!(receiver.try_recv().is_err());
3324 let stage = observer.start_stage(call.context(), crate::Stage::Database, event);
3325 assert_eq!(
3326 stage
3327 .finish_request_failure(None, &axes, &reference)
3328 .boundary_submission(),
3329 DiagnosticSubmission::OutputUnavailable
3330 );
3331 assert!(receiver.try_recv().is_err());
3332 assert_eq!(
3333 observer
3334 .metrics_snapshot()
3335 .stage_latency(crate::Stage::Database)
3336 .iter()
3337 .sum::<u64>(),
3338 2
3339 );
3340 call.succeed();
3341 let _ = observer.shutdown();
3342 }
3343 #[test]
3344 fn nonfailure_finish_optional_output_and_no_failed_bypass() {
3345 use saddle_core::*;
3346 let (sender, receiver) = mpsc::sync_channel(2);
3347 let handle = EmergencyDiagnosticHandle {
3348 sender,
3349 status: Arc::new(Status::default()),
3350 };
3351 let observer =
3352 crate::Observer::with_writer(crate::ObserverConfig::default(), io::sink()).unwrap();
3353 let (call, _) = observer
3354 .start_external_call_checked("app", "module", "service", "query", None)
3355 .unwrap();
3356 let event = crate::EventContext::new(
3357 crate::RequestIdentity::new("r").unwrap(),
3358 crate::RouteIdentity::new("/query").unwrap(),
3359 1,
3360 )
3361 .unwrap();
3362 for operation in [OperationOutcome::Succeeded, OperationOutcome::Rejected] {
3363 let stage = observer.start_stage(call.context(), crate::Stage::Database, event.clone());
3364 let axes = DiagnosticOutcomeAxes {
3365 operation,
3366 ..Default::default()
3367 };
3368 assert!(matches!(
3369 stage.finish_nonfailure_bounded(Some(&handle), &axes),
3370 Ok(DiagnosticSubmission::Enqueued)
3371 ));
3372 let row: serde_json::Value =
3373 serde_json::from_slice(receiver.try_recv().unwrap().bytes()).unwrap();
3374 assert!(row["diagnostic_reference"].is_null());
3375 assert!(row["diagnostic"].is_null());
3376 assert!(receiver.try_recv().is_err());
3377 let stage = observer.start_stage(call.context(), crate::Stage::Database, event.clone());
3378 assert!(matches!(
3379 stage.finish_nonfailure_bounded(None, &axes),
3380 Ok(DiagnosticSubmission::OutputUnavailable)
3381 ));
3382 }
3383 for operation in [
3384 OperationOutcome::Failed,
3385 OperationOutcome::Panicked,
3386 OperationOutcome::Unknown,
3387 OperationOutcome::TimedOut,
3388 OperationOutcome::Cancelled,
3389 ] {
3390 let stage = observer.start_stage(call.context(), crate::Stage::Database, event.clone());
3391 let axes = DiagnosticOutcomeAxes {
3392 operation,
3393 ..Default::default()
3394 };
3395 let stage = match stage.finish_nonfailure_bounded(None, &axes) {
3396 Err(stage) => stage,
3397 Ok(_) => panic!("technical axis bypass"),
3398 };
3399 assert!(receiver.try_recv().is_err());
3400 let reference =
3401 crate::RequestDiagnosticScope::output_unavailable(call.context(), &event)
3402 .capture_required(BoundedDiagnostic::capture(
3403 DiagnosticCategory::UnexpectedError,
3404 CaptureSite::FirstObserved,
3405 BoundedDiagnosticCause::new(
3406 DiagnosticStage::RequestDb,
3407 DiagnosticCode::new("db.decode.type").unwrap(),
3408 ),
3409 ))
3410 .into_reference();
3411 assert_eq!(
3412 stage
3413 .finish_request_failure(None, &axes, &reference)
3414 .boundary_submission(),
3415 DiagnosticSubmission::OutputUnavailable
3416 );
3417 }
3418 assert_eq!(
3419 observer
3420 .metrics_snapshot()
3421 .stage_latency(crate::Stage::Database)
3422 .iter()
3423 .sum::<u64>(),
3424 8
3425 );
3426 call.succeed();
3427 let _ = observer.shutdown();
3428 }
3429 #[test]
3430 fn bounded_diagnostic_admitted_no_escape_full_closed() {
3431 use saddle_core::*;
3432 use std::{
3433 future::Future,
3434 task::{Context, Poll, Waker},
3435 };
3436 let (sender, receiver) = mpsc::sync_channel(1);
3437 let handle = EmergencyDiagnosticHandle {
3438 sender,
3439 status: Arc::new(Status::default()),
3440 };
3441 let observer =
3442 crate::Observer::with_writer(crate::ObserverConfig::default(), io::sink()).unwrap();
3443 let (call, _) = observer
3444 .start_external_call_checked("app", "module", "service", "query", None)
3445 .unwrap();
3446 let stage = observer.start_stage(
3447 call.context(),
3448 crate::Stage::Database,
3449 crate::EventContext::new(
3450 crate::RequestIdentity::new("r1").unwrap(),
3451 crate::RouteIdentity::new("query").unwrap(),
3452 1,
3453 )
3454 .unwrap(),
3455 );
3456 let diagnostic_event = crate::EventContext::new(
3457 crate::RequestIdentity::new("r1").unwrap(),
3458 crate::RouteIdentity::new("/query").unwrap(),
3459 1,
3460 )
3461 .unwrap();
3462 let diagnostic_call = call.context().clone();
3463 let success_stage = observer.start_stage(
3464 call.context(),
3465 crate::Stage::Database,
3466 diagnostic_event.clone(),
3467 );
3468 let receipt_stage = observer.start_stage(
3469 call.context(),
3470 crate::Stage::Database,
3471 diagnostic_event.clone(),
3472 );
3473 let unavailable_stage = observer.start_stage(
3474 call.context(),
3475 crate::Stage::Database,
3476 diagnostic_event.clone(),
3477 );
3478 let ledger = saddle_admission::ProcessLedger::new(saddle_admission::ResourceConfig {
3479 managed_capacity: 1024 * 1024,
3480 entry_reserve: 1024 * 1024,
3481 framework_reserve: 1024 * 1024,
3482 task_reserve: 1024 * 1024,
3483 process_state_reserve: 1024 * 1024,
3484 system_estimate: 1024 * 1024,
3485 safety_margin: 1024 * 1024,
3486 process_limit: 8 * 1024 * 1024,
3487 max_active_requests: 1,
3488 })
3489 .unwrap();
3490 let future = ledger
3491 .try_envelope(65536, 65536, |_| async {
3492 assert!(matches!(
3493 success_stage.finish_nonfailure_bounded(
3494 None,
3495 &DiagnosticOutcomeAxes {
3496 operation: OperationOutcome::Succeeded,
3497 ..Default::default()
3498 }
3499 ),
3500 Ok(DiagnosticSubmission::OutputUnavailable)
3501 ));
3502 observer.observe_database_disposition(crate::DatabaseDisposition::Returned);
3503 let cause = BoundedDiagnosticCause::new(
3504 DiagnosticStage::RequestDb,
3505 DiagnosticCode::new("db.decode.type").unwrap(),
3506 )
3507 .with_column(Some(0), Some(3))
3508 .with_types(
3509 Some(InlineDiagnosticText::metadata("Option<i64>")),
3510 Some(InlineDiagnosticText::metadata("BIGINT")),
3511 None,
3512 );
3513 let primary = BoundedDiagnostic::capture(
3514 DiagnosticCategory::UnexpectedError,
3515 CaptureSite::FirstObserved,
3516 cause,
3517 );
3518 let axes = DiagnosticOutcomeAxes {
3519 operation: OperationOutcome::Failed,
3520 physical: PhysicalDispositionFact::Returned,
3521 ..Default::default()
3522 };
3523 assert_eq!(
3524 handle.submit_bounded(Some(&primary), &axes, None),
3525 DiagnosticSubmission::Enqueued
3526 );
3527 assert_eq!(
3528 handle.submit_bounded(Some(&primary), &axes, None),
3529 DiagnosticSubmission::Full
3530 );
3531 assert_eq!(
3532 stage.finish_bounded(&handle, &axes, Some(&primary)),
3533 DiagnosticSubmission::Full
3534 );
3535 let controlled = crate::RequestDiagnosticScope::established(
3536 &handle,
3537 &diagnostic_call,
3538 &diagnostic_event,
3539 )
3540 .with_zone(crate::DiagnosticZone::from_validated_ingress("zone-a").unwrap())
3541 .with_db_operation(
3542 crate::DiagnosticDbOperation::from_registered("AgentByCode").unwrap(),
3543 );
3544 let failure = controlled.fail(17u32, DiagnosticCategory::UnexpectedError, cause);
3545 assert_eq!(failure.submission(), DiagnosticSubmission::Full);
3546 let (original, delivery) = failure.finish_boundary(&handle, &axes);
3547 assert_eq!(original, 17);
3548 assert_eq!(delivery.boundary_submission(), DiagnosticSubmission::Full);
3549 let reference = controlled.capture_required(primary).into_reference();
3550 let delivery =
3551 receipt_stage.finish_request_failure(Some(&handle), &axes, &reference);
3552 assert_eq!(delivery.source_submission(), DiagnosticSubmission::Full);
3553 assert_eq!(delivery.boundary_submission(), DiagnosticSubmission::Full);
3554 let delivery = unavailable_stage.finish_request_failure(None, &axes, &reference);
3555 assert_eq!(
3556 delivery.boundary_submission(),
3557 DiagnosticSubmission::OutputUnavailable
3558 );
3559 handle.status.closed.store(true, Ordering::Release);
3560 assert_eq!(
3561 handle.submit_bounded(Some(reference.source_diagnostic()), &axes, None),
3562 DiagnosticSubmission::Closed
3563 );
3564 reference.source_diagnostic().id()
3565 })
3566 .unwrap();
3567 let mut future = std::pin::pin!(future);
3568 let result = future
3569 .as_mut()
3570 .poll(&mut Context::from_waker(Waker::noop()));
3571 assert!(
3572 matches!(result, Poll::Ready(Ok(_))),
3573 "admitted capture/submit must not escape: {result:?}"
3574 );
3575 let packet = receiver.try_recv().unwrap();
3576 let row: serde_json::Value = serde_json::from_slice(packet.bytes()).unwrap();
3577 assert_eq!(row["axes"]["operation"], "failed");
3578 assert_eq!(row["axes"]["physical"], "returned");
3579 assert_eq!(handle.snapshot().dropped, 7);
3580 assert_eq!(
3581 observer
3582 .metrics_snapshot()
3583 .database(crate::DatabaseDisposition::Returned),
3584 1
3585 );
3586 assert_eq!(
3587 observer
3588 .metrics_snapshot()
3589 .database(crate::DatabaseDisposition::Discarded),
3590 0
3591 );
3592 assert_eq!(
3593 observer
3594 .metrics_snapshot()
3595 .database(crate::DatabaseDisposition::NotUsed),
3596 0
3597 );
3598 assert_eq!(
3599 observer
3600 .metrics_snapshot()
3601 .stage_latency(crate::Stage::Database)
3602 .iter()
3603 .sum::<u64>(),
3604 4
3605 );
3606 call.succeed();
3607 let _ = observer.shutdown();
3608 }
3609 #[test]
3610 fn bounded_disconnected_and_oversized_context_are_explicit() {
3611 let (sender, receiver) = mpsc::sync_channel(1);
3612 drop(receiver);
3613 let handle = EmergencyDiagnosticHandle {
3614 sender,
3615 status: Arc::new(Status::default()),
3616 };
3617 let axes = saddle_core::DiagnosticOutcomeAxes::default();
3618 assert_eq!(
3619 handle.submit_bounded(None, &axes, None),
3620 DiagnosticSubmission::Closed
3621 );
3622 let observer =
3623 crate::Observer::with_writer(crate::ObserverConfig::default(), io::sink()).unwrap();
3624 let (call, _) = observer
3625 .start_external_call_checked("app", "module", "service", "x".repeat(257), None)
3626 .unwrap();
3627 assert_eq!(
3628 handle.submit_bounded_parts(
3629 None,
3630 &axes,
3631 Some((call.context(), "request", "route")),
3632 None
3633 ),
3634 DiagnosticSubmission::EncodingFailed
3635 );
3636 assert_eq!(handle.snapshot().written, 0);
3637 assert_eq!(handle.snapshot().dropped, 2);
3638 call.succeed();
3639 let _ = observer.shutdown();
3640 }
3641 #[test]
3642 fn diagnostic_protocol_rpc_is_not_internal_span() {
3643 let observer =
3644 crate::Observer::with_writer(crate::ObserverConfig::default(), io::sink()).unwrap();
3645 let (call, _) = observer
3646 .start_external_call_with_rpc(
3647 "test",
3648 "test",
3649 "test",
3650 "query",
3651 Some("opaque-trace"),
3652 saddle_core::RpcCorrelationId::new("0.7").unwrap(),
3653 )
3654 .unwrap();
3655 let event = crate::EventContext::new(
3656 crate::RequestIdentity::new("request-7").unwrap(),
3657 crate::RouteIdentity::new("bill.query").unwrap(),
3658 1,
3659 )
3660 .unwrap();
3661 let diagnostic = sample();
3662 let row = serde_json::to_value(Envelope::new(&diagnostic, Some((call.context(), &event))))
3663 .unwrap();
3664 assert_eq!(row["rpc_id"], "0.7");
3665 assert_eq!(row["span_id"], call.context().span_id().to_string());
3666 assert_ne!(row["rpc_id"], row["span_id"]);
3667 assert_eq!(row["trace_id"], "opaque-trace");
3668 assert_eq!(row["operation"], "query");
3669 assert_eq!(row["request"], "request-7");
3670 assert!(!row.to_string().contains("DO_NOT_LOG_SECRET"));
3671 let missing = serde_json::to_value(Envelope::new(&diagnostic, None)).unwrap();
3672 for key in [
3673 "trace_id",
3674 "rpc_id",
3675 "span_id",
3676 "operation",
3677 "request",
3678 "route",
3679 ] {
3680 assert!(missing[key].is_null());
3681 }
3682 call.succeed();
3683 let _ = observer.shutdown();
3684 }
3685 fn sample() -> Diagnostic {
3686 Diagnostic::capture(
3687 DiagnosticCategory::UnexpectedError,
3688 CaptureSite::FirstObserved,
3689 DiagnosticCause::new(
3690 DiagnosticStage::RequestDb,
3691 DiagnosticCode::new("db.connection_failed").unwrap(),
3692 )
3693 .with_io(&io::Error::other("DO_NOT_LOG_SECRET"))
3694 .with_driver_details(saddle_core::DiagnosticDriverDetails::new(
3695 Some(0),
3696 Some(4),
3697 saddle_core::DiagnosticTypeName::from_metadata("core::option::Option<i64>"),
3698 saddle_core::DiagnosticTypeName::from_metadata("BIGINT"),
3699 ))
3700 .with_input_location(
3701 saddle_core::DiagnosticInputLocation::new(Some(17), Some(3))
3702 .with_file(
3703 saddle_core::DiagnosticLocator::from_projection(
3704 "mapping.json",
3705 false,
3706 false,
3707 )
3708 .unwrap(),
3709 )
3710 .with_column(
3711 saddle_core::DiagnosticLocator::from_projection(
3712 "DO_NOT_LOG_SECRET",
3713 true,
3714 true,
3715 )
3716 .unwrap(),
3717 ),
3718 ),
3719 )
3720 }
3721 struct Failed;
3722 impl Write for Failed {
3723 fn write(&mut self, _: &[u8]) -> io::Result<usize> {
3724 Err(io::Error::from_raw_os_error(28))
3725 }
3726 fn flush(&mut self) -> io::Result<()> {
3727 Ok(())
3728 }
3729 }
3730 #[test]
3731 fn diagnostic_independent_file_after_main_failure() {
3732 let _serial = FILE_TEST.lock().unwrap();
3733 let directory = std::env::temp_dir().join(format!(
3734 "saddle-emergency-{}-{}",
3735 std::process::id(),
3736 sample().id()
3737 ));
3738 let config = FileLoggingConfig::new(&directory, crate::Rotation::Daily);
3739 let mut emergency = EmergencyDiagnostics::start(&config).unwrap();
3740 struct MarkFailure(Arc<AtomicU64>);
3741 impl Write for MarkFailure {
3742 fn write(&mut self, _: &[u8]) -> io::Result<usize> {
3743 self.0.fetch_add(1, Ordering::Release);
3744 Err(io::Error::from_raw_os_error(28))
3745 }
3746 fn flush(&mut self) -> io::Result<()> {
3747 Ok(())
3748 }
3749 }
3750 let main_failed = Arc::new(AtomicU64::new(0));
3751 let observer = crate::Observer::with_writer(
3752 crate::ObserverConfig::default(),
3753 MarkFailure(main_failed.clone()),
3754 )
3755 .unwrap();
3756 let diagnostic = sample();
3757 let (call, _) = observer
3758 .start_external_call_checked("test", "test", "test", "operation", Some("safe-trace"))
3759 .unwrap();
3760 let event = crate::EventContext::new(
3761 crate::RequestIdentity::new("request-1").unwrap(),
3762 crate::RouteIdentity::new("route.test").unwrap(),
3763 1,
3764 )
3765 .unwrap();
3766 let end = Instant::now() + Duration::from_secs(3);
3767 while main_failed.load(Ordering::Acquire) == 0 && Instant::now() < end {
3768 thread::sleep(Duration::from_millis(1));
3769 }
3770 assert!(
3771 main_failed.load(Ordering::Acquire) > 0,
3772 "primary writer failed before diagnostic submission"
3773 );
3774 assert_eq!(
3775 observer.record_diagnostic(
3776 &diagnostic,
3777 &emergency.handle(),
3778 Some((call.context(), &event))
3779 ),
3780 DiagnosticSubmission::Enqueued
3781 );
3782 let end = Instant::now() + Duration::from_secs(3);
3783 while emergency.snapshot().written != 1 && Instant::now() < end {
3784 thread::sleep(Duration::from_millis(1));
3785 }
3786 assert_eq!(emergency.snapshot().written, 1);
3787 let text = fs::read_to_string(emergency.target()).unwrap();
3788 let row: serde_json::Value = serde_json::from_str(text.trim()).unwrap();
3789 assert_eq!(row["trace_id"], "safe-trace");
3790 assert_eq!(row["request"], "request-1");
3791 assert!(row["rpc_id"].is_null());
3792 assert_eq!(row["span_id"], call.context().span_id().to_string());
3793 assert_eq!(row["operation"], "operation");
3794 assert_eq!(row["diagnostic"]["diagnostic_id"], diagnostic.id());
3795 let driver = &row["diagnostic"]["causes"][0]["driver_details"];
3796 assert_eq!(driver["column_index"], 0);
3797 assert_eq!(driver["column_count"], 4);
3798 assert_eq!(
3799 driver["target_rust_type"]["value"],
3800 "core::option::Option<i64>"
3801 );
3802 assert_eq!(driver["actual_db_type"]["value"], "BIGINT");
3803 let input = &row["diagnostic"]["causes"][0]["input_location"];
3804 assert_eq!(input["json_line"], 17);
3805 assert_eq!(input["json_column"], 3);
3806 assert_eq!(input["file"]["value"], "mapping.json");
3807 assert_eq!(input["column"]["value"], "[redacted]");
3808 assert_eq!(input["locator_truncated"], true);
3809 assert_eq!(input["locator_redacted"], true);
3810 assert!(!text.contains("DO_NOT_LOG_SECRET"));
3811 let bounded = saddle_core::BoundedDiagnostic::capture(
3812 DiagnosticCategory::UnexpectedError,
3813 CaptureSite::FirstObserved,
3814 saddle_core::BoundedDiagnosticCause::new(
3815 DiagnosticStage::RequestDb,
3816 DiagnosticCode::new("db.decode.failed").unwrap(),
3817 ),
3818 );
3819 let axes = saddle_core::DiagnosticOutcomeAxes {
3820 operation: saddle_core::OperationOutcome::Failed,
3821 physical: saddle_core::PhysicalDispositionFact::Returned,
3822 business: saddle_core::BusinessOutcome::Failure,
3823 business_code: saddle_core::RegisteredDiagnosticCode::from_registered(
3824 "INVALID_ROW",
3825 &["INVALID_ROW"],
3826 ),
3827 delivery: saddle_core::ResponseDelivery::LocalWriteComplete,
3828 cleanup: saddle_core::CleanupOutcome::Succeeded,
3829 bytes_written: Some(100),
3830 };
3831 assert_eq!(
3832 emergency.handle().submit_bounded(
3833 Some(&bounded),
3834 &axes,
3835 Some((call.context(), &event))
3836 ),
3837 DiagnosticSubmission::Enqueued
3838 );
3839 let until = Instant::now() + Duration::from_secs(3);
3840 while emergency.snapshot().written != 2 && Instant::now() < until {
3841 thread::sleep(Duration::from_millis(1));
3842 }
3843 assert_eq!(emergency.snapshot().written, 2);
3844 let content = fs::read_to_string(emergency.target()).unwrap();
3845 let second: serde_json::Value =
3846 serde_json::from_str(content.lines().nth(1).unwrap()).unwrap();
3847 assert_eq!(second["axes"]["operation"], "failed");
3848 assert_eq!(second["axes"]["physical"], "returned");
3849 assert_eq!(second["axes"]["business_code"], "INVALID_ROW");
3850 assert_eq!(second["axes"]["delivery"], "local_write_complete");
3851 assert_eq!(second["axes"]["cleanup"], "succeeded");
3852 assert_eq!(second["trace_id"], "safe-trace");
3853 let end = Instant::now() + Duration::from_secs(3);
3854 while emergency.shutdown() == DiagnosticShutdown::Pending && Instant::now() < end {
3855 thread::sleep(Duration::from_millis(1));
3856 }
3857 assert_eq!(emergency.shutdown(), DiagnosticShutdown::Finished);
3858 assert_eq!(
3859 emergency.handle().submit(&diagnostic),
3860 DiagnosticSubmission::Closed
3861 );
3862 call.succeed();
3863 let _shutdown = observer.shutdown();
3864 fs::remove_file(emergency.target()).unwrap();
3865 fs::remove_dir(directory).unwrap();
3866 }
3867 #[test]
3868 fn diagnostic_directory_failure_has_real_os_cause() {
3869 let _serial = FILE_TEST.lock().unwrap();
3870 let path = std::env::temp_dir().join(format!(
3871 "saddle-emergency-file-{}-{}",
3872 std::process::id(),
3873 sample().id()
3874 ));
3875 fs::write(&path, b"not a directory").unwrap();
3876 let mut owner =
3877 EmergencyDiagnostics::start(&FileLoggingConfig::new(&path, crate::Rotation::Daily))
3878 .unwrap();
3879 let end = Instant::now() + Duration::from_secs(3);
3880 while !owner.snapshot().initialized && Instant::now() < end {
3881 thread::sleep(Duration::from_millis(1));
3882 }
3883 let failure = owner.snapshot().first_failure.unwrap();
3884 assert_eq!(failure.stage, "initialize");
3885 assert!(failure.os_code.is_some());
3886 assert_eq!(owner.target(), path.join(EMERGENCY_FILE_NAME));
3887 while owner.shutdown() == DiagnosticShutdown::Pending && Instant::now() < end {
3888 thread::sleep(Duration::from_millis(1));
3889 }
3890 assert_eq!(owner.shutdown(), DiagnosticShutdown::Finished);
3891 fs::remove_file(path).unwrap();
3892 }
3893 #[test]
3894 fn diagnostic_stuck_worker_shutdown_never_joins_early() {
3895 let (sender, _receiver) = mpsc::sync_channel(1);
3896 let (release, wait) = mpsc::channel();
3897 let (started, reached) = mpsc::channel();
3898 let worker = thread::spawn(move || {
3899 started.send(()).unwrap();
3900 wait.recv().unwrap();
3901 });
3902 reached.recv_timeout(Duration::from_secs(2)).unwrap();
3903 let mut owner = EmergencyDiagnostics {
3904 handle: EmergencyDiagnosticHandle {
3905 sender,
3906 status: Arc::new(Status::default()),
3907 },
3908 worker: Some(worker),
3909 target: PathBuf::from("unused"),
3910 source_target_preopened: false,
3911 };
3912 assert_eq!(owner.shutdown(), DiagnosticShutdown::Pending);
3913 release.send(()).unwrap();
3914 let end = Instant::now() + Duration::from_secs(3);
3915 while owner.shutdown() == DiagnosticShutdown::Pending && Instant::now() < end {
3916 thread::yield_now();
3917 }
3918 assert_eq!(owner.shutdown(), DiagnosticShutdown::Finished);
3919 }
3920 #[test]
3921 fn diagnostic_double_failure_is_counted_without_recursion() {
3922 let status = Status::default();
3923 let mut output: io::Result<Failed> = Err(io::Error::from_raw_os_error(13));
3924 let mut stderr = Vec::new();
3925 let mut attempted = false;
3926 deliver(
3927 &mut output,
3928 &mut stderr,
3929 std::path::Path::new("/safe/logs/saddle.emergency.log"),
3930 b"{\"original\":true}\n",
3931 &status,
3932 &mut attempted,
3933 );
3934 let text = String::from_utf8(stderr).unwrap();
3935 assert!(text.contains("\"os_code\":13") && text.contains("\"original\":true"));
3936 attempted = false;
3937 deliver(
3938 &mut output,
3939 &mut Failed,
3940 std::path::Path::new("./logs"),
3941 b"{}\n",
3942 &status,
3943 &mut attempted,
3944 );
3945 for _ in 0..10 {
3946 deliver(
3947 &mut output,
3948 &mut Failed,
3949 std::path::Path::new("./logs"),
3950 b"{}\n",
3951 &status,
3952 &mut attempted,
3953 );
3954 }
3955 assert_eq!(status.snapshot().stderr_failed, 1);
3956 assert_eq!(status.snapshot().stderr_suppressed, 10);
3957 assert_eq!(status.snapshot().output_failed, 12);
3958 }
3959 #[test]
3960 fn ingress_deadline_queue_and_writer_failure_are_not_success_evidence() {
3961 let (sender, receiver) = mpsc::sync_channel(1);
3962 let status = Arc::new(Status::default());
3963 let handle = EmergencyDiagnosticHandle { sender, status: status.clone() };
3964 let observer = crate::Observer::with_writer(Default::default(), io::sink()).unwrap();
3965 let (call, _) = observer.start_external_call_with_rpc("app", "profusegw", "profusegw", "route",
3966 Some("trace"), saddle_core::RpcCorrelationId::new("0").unwrap()).unwrap();
3967 let event = crate::EventContext::new(crate::RequestIdentity::new("request-a").unwrap(),
3968 crate::RouteIdentity::new("route").unwrap(), 1).unwrap();
3969 let scope = crate::RequestDiagnosticScope::established(&handle, call.context(), &event);
3970 assert_eq!(scope.ingress_deadline_bound(12345), DiagnosticSubmission::Enqueued);
3971 assert_eq!(scope.ingress_deadline_bound(12345), DiagnosticSubmission::Full);
3972 let packet = receiver.try_recv().unwrap();
3973 let mut output = Ok(Failed);
3974 let mut attempted = false;
3975 deliver(&mut output, &mut Failed, std::path::Path::new("./logs"),
3976 packet.bytes(), &status, &mut attempted);
3977 assert_eq!(status.snapshot().written, 0);
3978 assert_eq!(status.snapshot().output_failed, 1);
3979 assert!(status.snapshot().first_failure.is_some());
3980 assert_eq!(handle.snapshot().dropped, 1);
3981 }
3982
3983 #[test]
3984 fn diagnostic_full_queue_submission_does_not_wait() {
3985 let (sender, _receiver) = mpsc::sync_channel(1);
3986 let status = Arc::new(Status::default());
3987 let handle = EmergencyDiagnosticHandle { sender, status };
3988 let sample = sample();
3989 assert_eq!(handle.submit(&sample), DiagnosticSubmission::Enqueued);
3990 assert_eq!(handle.submit(&sample), DiagnosticSubmission::Full);
3991 assert_eq!(handle.snapshot().dropped, 1);
3992 assert_eq!(handle.snapshot().written, 0);
3993 }
3994}
3995
3996#[cfg(test)]
3997mod rpc_stage_frame_tests {
3998 use super::*;
3999 #[test]
4000 fn rpc_stage_native_frame_keeps_actual_call_and_original_slot_identity_with_loss() {
4001 use saddle_core::request_context::{ContextFact, ContextLabel, RequestLocalFacts,
4002 RequestRootPublisher, RequestViewPhase};
4003 let publisher = RequestRootPublisher::create(ContextLabel::checked("rpc-stage").unwrap(),
4004 ContextFact::NotEstablished).unwrap();
4005 let view = publisher.reference().view(RequestLocalFacts::new(RequestViewPhase::Reading));
4006 let (sender, receiver) = mpsc::sync_channel(1);
4007 let status = Arc::new(Status::default());
4008 let mut call_id = [0; 256];
4009 call_id[..12].copy_from_slice(b"actual-child");
4010 let observer = DetachedRpcStageRelease { context: view.diagnostic_snapshot(),
4011 output: EmergencyDiagnosticHandle {sender, status: status.clone()},
4012 process_instance: 77, call_id, call_id_len: 12 };
4013 let fact = saddle_admission::RpcStageReleaseFact { ledger_instance: 23,
4014 account: saddle_admission::AccountIdentity {account_slot: 0, account_generation: 7, audit_generation: 11},
4015 stage_slot: 3, stage_generation: 9 };
4016 saddle_admission::RpcStageReleaseObserver::released(&observer, fact);
4017 saddle_admission::RpcStageReleaseObserver::released(&observer, fact);
4018 assert_eq!(status.dropped.load(Ordering::Relaxed), 1);
4019 let DiagnosticPacket::Admitted(bytes) = receiver.recv().unwrap() else { panic!("native frame") };
4020 let row: serde_json::Value = serde_json::from_slice(&bytes.bytes[..bytes.len]).unwrap();
4021 assert_eq!(row["schema"], "request-rpc-stage/1");
4022 assert_eq!(row["event"], "rpc_stage_credit_released");
4023 assert_eq!(row["call_id"], "actual-child");
4024 assert_eq!(row["stage"]["stage_generation"], 9);
4025 assert_eq!(row["stage"]["account"]["audit_generation"], 11);
4026 assert_eq!(row["local_request"], row["context"]["local_request"]);
4027 assert!(row.get("physical_driver_destroyed").is_none());
4028 assert_eq!(status.written.load(Ordering::Relaxed), 0);
4029 }
4030}
4031
4032#[cfg(test)]
4033mod request_stop_decision_tests {
4034 use super::*;
4035 #[test]
4036 fn request_stop_decision_frame_preserves_actual_branch_domain_and_rpc_clock() {
4037 use saddle_core::request_context::{ContextFact, ContextLabel, RequestLocalFacts,
4038 RequestRootPublisher, RequestViewPhase};
4039 let publisher = RequestRootPublisher::create(ContextLabel::checked("stop-decision").unwrap(),
4040 ContextFact::NotEstablished).unwrap();
4041 let view = publisher.reference().view(RequestLocalFacts::new(RequestViewPhase::Reading));
4042 let (sender, receiver) = mpsc::sync_channel(2);
4043 let status = Arc::new(Status::default());
4044 let output = EmergencyDiagnosticHandle {sender, status: status.clone()};
4045 output.record_request_stop_decision(view.diagnostic_snapshot(), "peer_disconnected");
4046 output.record_request_stop_decision(view.diagnostic_snapshot(), "shutdown-guessed");
4047 output.record_request_stop_decision(view.diagnostic_snapshot(), "original_deadline");
4048 let mut rows = Vec::new();
4049 for _ in 0..2 {
4050 let DiagnosticPacket::Admitted(bytes) = receiver.recv().unwrap() else { panic!("fixed native frame") };
4051 rows.push(serde_json::from_slice::<serde_json::Value>(&bytes.bytes[..bytes.len]).unwrap());
4052 }
4053 assert_eq!(rows[0]["cause"], "peer_disconnected");
4054 assert_eq!(rows[1]["cause"], "original_deadline");
4055 assert_eq!(rows[0]["decision_point"], "handler_select_before_dispatch_drop");
4056 assert_eq!(rows[0]["schema"], "request-stop/1");
4057 assert_eq!(rows[0]["local_request"], rows[0]["context"]["local_request"]);
4058 assert!(rows[1]["sequence"].as_u64().unwrap() > rows[0]["sequence"].as_u64().unwrap());
4059 let (sequence, monotonic_ns) = rpc_lifecycle_stamp();
4060 assert!(sequence > rows[1]["sequence"].as_u64().unwrap());
4061 assert!(monotonic_ns >= rows[1]["monotonic_ns"].as_u64().unwrap() as u128);
4062 assert!(receiver.try_recv().is_err());
4063 assert_eq!(status.dropped.load(Ordering::Relaxed), 0);
4064 }
4065}
4066
4067#[cfg(test)]
4068mod lifecycle_boundary_frame_tests {
4069 use super::*;
4070 #[test]
4071 fn lifecycle_boundary_actual_fixed_frames_share_clock_and_preserve_queue_loss() {
4072 let (sender, receiver) = mpsc::sync_channel(3);
4073 let status = Arc::new(Status::default());
4074 let output = EmergencyDiagnosticHandle { sender, status: status.clone() };
4075 for (event, phase) in [("shutdown_future_completed", 1), ("claim_cutoff", 2), ("claim_published", 2)] {
4076 output.record_request_lifecycle("boundary-app", event, phase, 1);
4077 }
4078 assert_eq!(status.dropped.load(Ordering::Relaxed), 0);
4079 output.record_request_lifecycle("boundary-app", "claim_published", 2, 1);
4080 assert_eq!(status.dropped.load(Ordering::Relaxed), 1);
4081 let mut previous = 0;
4082 for event in ["shutdown_future_completed", "claim_cutoff", "claim_published"] {
4083 let DiagnosticPacket::Admitted(bytes) = receiver.recv().unwrap() else { panic!("fixed native frame") };
4084 let row: serde_json::Value = serde_json::from_slice(&bytes.bytes[..bytes.len]).unwrap();
4085 assert_eq!(row["schema"], "request-lifecycle-boundary/1");
4086 assert_eq!(row["application"], "boundary-app");
4087 assert_eq!(row["event"], event);
4088 assert!(row["sequence"].as_u64().unwrap() > previous);
4089 previous = row["sequence"].as_u64().unwrap();
4090 assert_eq!(row["process_instance"], serde_json::json!(finalization_process_instance()));
4091 assert!(row.get("request_id").is_none());
4092 assert!(row.get("signal_arrival").is_none());
4093 }
4094 let (sequence, _) = rpc_lifecycle_stamp();
4095 assert!(sequence > previous);
4096 assert!(receiver.try_recv().is_err());
4097 }
4098}
4099
4100#[cfg(test)]
4101mod read_owner_frame_tests {
4102 use super::*;
4103 #[test]
4104 fn read_owner_frame_binds_unpublished_task_to_actual_scalar_peer() {
4105 use saddle_core::request_context::{ContextFact,ContextLabel,RequestLocalFacts,RequestRootPublisher,RequestViewPhase};
4106 let publisher=RequestRootPublisher::create(ContextLabel::checked("read-owner").unwrap(),ContextFact::NotEstablished).unwrap();
4107 let view=publisher.reference().view(RequestLocalFacts::new(RequestViewPhase::Reading).with_task(ContextFact::Present(9)));
4108 let (sender,receiver)=mpsc::sync_channel(1);
4109 let status=Arc::new(Status::default());
4110 let output=EmergencyDiagnosticHandle{sender,status:status.clone()};
4111 output.record_request_read_owner(view.diagnostic_snapshot(),3,
4112 saddle_admission::AccountIdentity{account_slot:0,account_generation:1,audit_generation:2},
4113 std::net::SocketAddrV4::new(std::net::Ipv4Addr::LOCALHOST,23456));
4114 let DiagnosticPacket::Admitted(bytes)=receiver.recv().unwrap() else {panic!("native fixed frame")};
4115 let row:serde_json::Value=serde_json::from_slice(&bytes.bytes[..bytes.len]).unwrap();
4116 assert_eq!(row["schema"],"request-read-owner/1");
4117 assert_eq!(row["event"],"recognition_read_head_entered");
4118 assert_eq!(row["context"]["publication"],0);
4119 assert_eq!(row["context"]["task"],serde_json::json!({"state":"present","value":9}));
4120 assert_eq!(row["peer_address"],serde_json::json!([127,0,0,1]));
4121 assert_eq!(row["peer_port"],23456);
4122 assert_eq!(row["local_request"],row["context"]["local_request"]);
4123 assert!(row.get("task_reclaimed").is_none());
4124 assert_eq!(status.dropped.load(Ordering::Relaxed),0);
4125 }
4126}
4127
4128#[cfg(test)]
4129mod task_owner_frame_tests {
4130 use super::*;
4131 #[test]
4132 fn owner_binding_is_scalar_creation_not_reclamation() {
4133 use saddle_core::request_context::{ContextFact,ContextLabel,RequestLocalFacts,RequestRootPublisher,RequestViewPhase};
4134 let publisher=RequestRootPublisher::create(ContextLabel::checked("task-owner").unwrap(),ContextFact::NotEstablished).unwrap();
4135 let view=publisher.reference().view(RequestLocalFacts::new(RequestViewPhase::Reading).with_task(ContextFact::Present(9)));
4136 let (sender,receiver)=mpsc::sync_channel(1);
4137 let status=Arc::new(Status::default());
4138 let output=EmergencyDiagnosticHandle{sender,status:status.clone()};
4139 output.record_request_task_owner(view.diagnostic_snapshot(),3,
4140 saddle_admission::AccountIdentity{account_slot:0,account_generation:1,audit_generation:2}, "business");
4141 let DiagnosticPacket::Admitted(bytes)=receiver.recv().unwrap() else {panic!("native frame")};
4142 let row:serde_json::Value=serde_json::from_slice(&bytes.bytes[..bytes.len]).unwrap();
4143 assert_eq!(row["schema"],"request-task-roster/2");
4144 assert_eq!(row["task_role"],"business");
4145 assert_eq!(row["event"],"task_owner_bound");
4146 assert_eq!(row["context"]["publication"],0);
4147 assert_eq!(row["context"]["task"],serde_json::json!({"state":"present","value":9}));
4148 assert_eq!(row["ledger_instance"],3);
4149 assert!(row.get("completion").is_none());
4150 assert_eq!(status.dropped.load(Ordering::Relaxed),0);
4151 drop(receiver);
4152 output.record_request_task_owner(view.diagnostic_snapshot(),3,
4153 saddle_admission::AccountIdentity{account_slot:0,account_generation:1,audit_generation:2}, "recognition");
4154 assert_eq!(status.dropped.load(Ordering::Relaxed),1);
4155 }
4156}
4157
4158#[cfg(test)]
4159mod database_admission_frame_tests {
4160 use super::*;
4161 #[test]
4162 fn actual_database_attempt_records_typed_decision_without_claiming_release() {
4163 use saddle_core::request_context::{ContextFact,ContextLabel,RequestLocalFacts,RequestRootPublisher,RequestViewPhase};
4164 let publisher=RequestRootPublisher::create(ContextLabel::checked("database-admission").unwrap(),ContextFact::NotEstablished).unwrap();
4165 let view=publisher.reference().view(RequestLocalFacts::new(RequestViewPhase::Database).with_task(ContextFact::Present(9)));
4166 let (sender,receiver)=mpsc::sync_channel(2);
4167 let status=Arc::new(Status::default());
4168 let output=EmergencyDiagnosticHandle{sender,status:status.clone()};
4169 let account=saddle_admission::AccountIdentity{account_slot:0,account_generation:1,audit_generation:2};
4170 output.record_request_database_admission(view.diagnostic_snapshot(),3,account,true,"accepted");
4171 output.record_request_database_admission(view.diagnostic_snapshot(),3,account,false,"db_credits_exhausted");
4172 let mut previous=0;
4173 for accepted in [true,false] {
4174 let DiagnosticPacket::Admitted(bytes)=receiver.recv().unwrap() else {panic!("native frame")};
4175 let row:serde_json::Value=serde_json::from_slice(&bytes.bytes[..bytes.len]).unwrap();
4176 assert_eq!(row["schema"],"request-database-admission/1");
4177 assert_eq!(row["accepted"],accepted);
4178 assert_eq!(row["reason"],if accepted {"accepted"} else {"db_credits_exhausted"});
4179 assert_eq!(row["ledger_instance"],3);
4180 let sequence=row["sequence"].as_u64().unwrap();assert!(sequence>previous);previous=sequence;
4181 assert!(row.get("physical_release").is_none());
4182 }
4183 assert_eq!(status.dropped.load(Ordering::Relaxed),0);
4184 }
4185}
4186
4187#[cfg(test)]
4188mod fifo_clock_frame_tests {
4189 use super::*;
4190 #[test]
4191 fn actual_fixed_frame_binds_effective_deadline_and_recording_point_only() {
4192 let (sender,receiver)=mpsc::sync_channel(1);
4193 let status=Arc::new(Status::default());
4194 let output=EmergencyDiagnosticHandle{sender,status:status.clone()};
4195 let before=std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos();
4196 output.record_ingress_fifo_clock(Some(saddle_core::request_context::ContextIdentity::checked("queued-R1").unwrap()),7,"removed","deadline",12345);
4197 let after=std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos();
4198 let DiagnosticPacket::Admitted(bytes)=receiver.recv().unwrap() else {panic!("native frame")};
4199 let row:serde_json::Value=serde_json::from_slice(&bytes.bytes[..bytes.len]).unwrap();
4200 assert_eq!(row["schema"],"ingress-fifo-clock/1");
4201 assert_eq!(row["request_id"],"queued-R1");
4202 assert_eq!(row["queue_sequence"],7);
4203 assert_eq!(row["effective_deadline_unix_ms"],12345);
4204 let recorded=row["recorded_unix_ns"].as_u64().unwrap() as u128;
4205 assert!(before<=recorded && recorded<=after);
4206 assert!(row.get("physical_removal_unix_ns").is_none());
4207 assert_eq!(status.dropped.load(Ordering::Relaxed),0);
4208 }
4209}
4210
4211#[cfg(test)]
4212mod signal_receipt_frame_tests {
4213 use super::*;
4214 #[test]
4215 fn signal_receipt_actual_frames_preserve_kind_closed_stream_and_loss() {
4216 let (sender, receiver) = mpsc::sync_channel(3);
4217 let status = Arc::new(Status::default());
4218 let output = EmergencyDiagnosticHandle { sender, status: status.clone() };
4219 let app = saddle_core::ContextLabel::checked("signal-app").unwrap();
4220 for (kind, received) in [("SIGTERM", true), ("SIGINT", true), ("SIGTERM", false)] {
4221 output.record_shutdown_signal_received(app, kind, received);
4222 }
4223 output.record_shutdown_signal_received(app, "SIGINT", false);
4224 assert_eq!(status.dropped.load(Ordering::Relaxed), 1);
4225 let mut previous = 0;
4226 for (kind, received) in [("SIGTERM", true), ("SIGINT", true), ("SIGTERM", false)] {
4227 let DiagnosticPacket::Admitted(bytes) = receiver.recv().unwrap() else { panic!("fixed frame") };
4228 let row: serde_json::Value = serde_json::from_slice(&bytes.bytes[..bytes.len]).unwrap();
4229 assert_eq!(row["schema"], "process-signal-receipt/1");
4230 assert_eq!(row["application"], "signal-app");
4231 assert_eq!(row["signal"], kind);
4232 assert_eq!(row["received"], received);
4233 assert_eq!(row["process_instance"], serde_json::json!(finalization_process_instance()));
4234 assert!(row["sequence"].as_u64().unwrap() > previous);
4235 previous = row["sequence"].as_u64().unwrap();
4236 assert!(row.get("kernel_arrival").is_none());
4237 }
4238 assert!(receiver.try_recv().is_err());
4239 }
4240}
4241
4242struct DetachedHandlerPoll {
4243 context: saddle_core::request_context::RequestContextSnapshot,
4244 output: EmergencyDiagnosticHandle,
4245 process_instance: u128,
4246}
4247impl saddle_admission::HandlerPollObserver for DetachedHandlerPoll {
4248 fn before_first_poll(&self) {
4249 #[derive(Serialize)]
4250 struct Record<'a> {
4251 schema: &'static str, event: &'static str, pid: u32, process_instance: u128,
4252 sequence: u64, monotonic_ns: u128,
4253 context: &'a saddle_core::request_context::RequestContextSnapshot,
4254 }
4255 let (sequence, monotonic_ns) = rpc_lifecycle_stamp();
4256 let _ = self.output.submit_fixed_record(&Record {
4257 schema: "request-handler-poll/1", event: "before_first_typed_handler_poll",
4258 pid: std::process::id(), process_instance: self.process_instance,
4259 sequence, monotonic_ns, context: &self.context,
4260 });
4261 }
4262}
4263impl EmergencyDiagnosticHandle {
4264 #[doc(hidden)]
4265 pub fn prepare_handler_poll(&self, context: saddle_core::request_context::RequestContextSnapshot,
4266 storage: &saddle_admission::ProcessLogStorage) -> Option<saddle_admission::HandlerPollObservation> {
4267 saddle_admission::HandlerPollObservation::prepare(storage, DetachedHandlerPoll {
4268 context, output: self.clone(), process_instance: finalization_process_instance(),
4269 }).ok()
4270 }
4271}
4272
4273#[cfg(test)]
4274mod handler_poll_frame_tests {
4275 use super::*;
4276 use saddle_admission::HandlerPollObserver;
4277 #[test]
4278 fn handler_poll_actual_detached_frame_has_native_context_and_bounded_loss() {
4279 use saddle_core::request_context::{ContextLabel,ContextFact,RequestRootPublisher,RequestLocalFacts,RequestViewPhase};
4280 let root=RequestRootPublisher::create(ContextLabel::checked("handler-frame-app").unwrap(),ContextFact::NotEstablished).unwrap();
4281 let view=root.reference().view(RequestLocalFacts::new(RequestViewPhase::Handler).with_task(ContextFact::Present(9)));
4282 let (sender,receiver)=mpsc::sync_channel(1);
4283 let status=Arc::new(Status::default());
4284 let observer=DetachedHandlerPoll{context:view.diagnostic_snapshot(),
4285 output:EmergencyDiagnosticHandle{sender,status:status.clone()},process_instance:finalization_process_instance()};
4286 observer.before_first_poll();
4287 observer.before_first_poll(); assert_eq!(status.dropped.load(Ordering::Relaxed),1);
4289 let DiagnosticPacket::Admitted(bytes)=receiver.recv().unwrap() else {panic!("fixed frame")};
4290 let row:serde_json::Value=serde_json::from_slice(&bytes.bytes[..bytes.len]).unwrap();
4291 assert_eq!(row["schema"],"request-handler-poll/1");
4292 assert_eq!(row["event"],"before_first_typed_handler_poll");
4293 assert_eq!(row["context"]["application"]["value"],"handler-frame-app");
4294 assert_eq!(row["context"]["task"]["value"],9);
4295 assert_eq!(row["context"]["publication"],0);
4296 assert_eq!(row["process_instance"],serde_json::json!(finalization_process_instance()));
4297 assert!(row["sequence"].as_u64().unwrap()>0);
4298 assert!(row.get("handler_success").is_none());
4299 }
4300}
4301
4302#[doc(hidden)]
4304pub struct DbOwnerObservation {
4305 state: saddle_admission::DbOwnerObservationStorage<DbOwnerObservationState>,
4306}
4307struct DbOwnerObservationState {
4308 context: saddle_core::request_context::RequestContextSnapshot,
4309 output: EmergencyDiagnosticHandle,
4310 account: saddle_admission::AccountIdentity,
4311 ledger_instance: u64,
4312 owner_id: u64,
4313 finished: std::sync::atomic::AtomicBool,
4314 acquired: std::sync::atomic::AtomicBool,
4315 acknowledged: std::sync::atomic::AtomicBool,
4316 handed_off: std::sync::atomic::AtomicBool,
4317 unavailable: std::sync::atomic::AtomicBool,
4318}
4319#[doc(hidden)]
4320#[derive(Clone, Copy)]
4321pub enum DbOwnerDisposition { Returned, Discarded }
4322impl DbOwnerObservation {
4323 fn emit(&self, event: &'static str) {
4324 let state = self.state.get();
4325 if state.unavailable.load(Ordering::Acquire) { return; }
4326 #[derive(Serialize)]
4327 struct Record<'a> {
4328 schema: &'static str, pid: u32, process_instance: u128,
4329 local_request: u64, ledger_instance: u64,
4330 account: saddle_admission::AccountIdentity,
4331 context: &'a saddle_core::request_context::RequestContextSnapshot,
4332 owner_id: u64, event: &'static str, sequence: u64, monotonic_ns: u128,
4333 }
4334 let (sequence, monotonic_ns) = rpc_lifecycle_stamp();
4335 if sequence == 0 { state.unavailable.store(true, Ordering::Release); return; }
4336 let result = state.output.submit_fixed_record(&Record {
4337 schema: "request-db-owner/1", pid: std::process::id(),
4338 process_instance: finalization_process_instance(),
4339 local_request: state.context.local_request_id(), ledger_instance: state.ledger_instance,
4340 account: state.account, context: &state.context, owner_id: state.owner_id,
4341 event, sequence, monotonic_ns,
4342 });
4343 if result != DiagnosticSubmission::Enqueued { state.unavailable.store(true, Ordering::Release); }
4344 }
4345 pub fn acquired(&self) {
4346 let s=self.state.get();
4347 if !s.finished.load(Ordering::Acquire) && !s.acquired.swap(true, Ordering::AcqRel) { self.emit("acquired"); }
4348 }
4349 pub fn handoff(&self) {
4350 let s=self.state.get();
4351 if s.acquired.load(Ordering::Acquire) && !s.finished.load(Ordering::Acquire) && !s.handed_off.swap(true, Ordering::AcqRel) { self.emit("finalization_handoff"); }
4352 }
4353 pub fn commit_ack(&self) {
4354 let s=self.state.get();
4355 if s.acquired.load(Ordering::Acquire) && !s.finished.load(Ordering::Acquire) && !s.acknowledged.swap(true, Ordering::AcqRel) { self.emit("commit_ack"); }
4356 }
4357 pub fn finish(&mut self, disposition: DbOwnerDisposition) {
4358 if self.state.get().acquired.load(Ordering::Acquire) && !self.state.get().finished.swap(true, Ordering::AcqRel) {
4359 self.emit(match disposition { DbOwnerDisposition::Returned => "returned", DbOwnerDisposition::Discarded => "discarded" });
4360 }
4361 }
4362}
4363impl EmergencyDiagnosticHandle {
4364 #[doc(hidden)]
4365 pub fn prepare_db_owner_observation(&self,
4366 context: saddle_core::request_context::RequestContextSnapshot,
4367 ledger_instance: u64, account: saddle_admission::AccountIdentity,
4368 storage: &saddle_admission::ProcessLogStorage) -> Option<DbOwnerObservation> {
4369 static NEXT_OWNER: AtomicU64 = AtomicU64::new(1);
4370 let owner_id = NEXT_OWNER.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |id| id.checked_add(1)).ok()?;
4371 Some(DbOwnerObservation { state: saddle_admission::DbOwnerObservationStorage::prepare(storage, DbOwnerObservationState {
4372 context, output: self.clone(), account, ledger_instance, owner_id,
4373 finished: std::sync::atomic::AtomicBool::new(false),
4374 acquired: std::sync::atomic::AtomicBool::new(false),
4375 acknowledged: std::sync::atomic::AtomicBool::new(false),
4376 handed_off: std::sync::atomic::AtomicBool::new(false),
4377 unavailable: std::sync::atomic::AtomicBool::new(false),
4378 }).ok()? })
4379 }
4380}
4381
4382#[cfg(test)]
4383mod db_owner_observation_tests {
4384 use super::*;
4385 static TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
4386 fn ledger() -> saddle_admission::ProfuseGwLightweightProcessOwner {
4387 use saddle_admission::{freeze_deployment_resource_budget,bind_deployment_resource_budget_bootstrap,prepare_profusegw_lightweight_profile};
4388 use saddle_core::{BootstrapRendezvousIssuer,GeneratedApplicationFreezeSource,ListenerStartupFreezeSource,pair_bootstrap_rendezvous};
4389 let pending=freeze_deployment_resource_budget(1,32,5000,1,1,1000000,1000000,1000000,1000000).unwrap().reserve_known_database_memory(0,0,0).ok().unwrap();
4390 let (application,listener)=BootstrapRendezvousIssuer::issue().freeze_application(GeneratedApplicationFreezeSource::new("db-owner",b"descriptor", &["route"])).unwrap();
4391 let listener=listener.freeze_listener(ListenerStartupFreezeSource::new("db-owner","127.0.0.1:8000".parse().unwrap(),"127.0.0.1:9000".parse().unwrap(),std::time::Duration::from_millis(5000))).ok().unwrap();
4392 let (whole,receipt)=pair_bootstrap_rendezvous(application,listener).ok().unwrap();
4393 prepare_profusegw_lightweight_profile(bind_deployment_resource_budget_bootstrap(pending,whole,receipt).ok().unwrap()).ok().unwrap()
4394 }
4395 fn context() -> saddle_core::request_context::RequestContextSnapshot {
4396 use saddle_core::request_context::*;
4397 RequestRootPublisher::create(ContextLabel::checked("db-owner").unwrap(),ContextFact::NotEstablished).unwrap()
4398 .reference().view(RequestLocalFacts::new(RequestViewPhase::Reading).with_task(ContextFact::Present(9))).diagnostic_snapshot()
4399 }
4400 fn account() -> saddle_admission::AccountIdentity {
4401 saddle_admission::AccountIdentity{account_slot:0,account_generation:1,audit_generation:2}
4402 }
4403 #[test]
4404 fn prepaid_detached_move_exact_charge_terminal_once_same_domain() {
4405 let _guard=TEST_LOCK.lock().unwrap();
4406 let ledger=ledger();let root=ledger.try_process_storage(saddle_admission::StorageDemand::separate(&[]).unwrap()).unwrap();let storage=root.release_log_storage();let before=ledger.ownership_snapshot();
4407 let (sender,receiver)=mpsc::sync_channel(8);let status=Arc::new(Status::default());
4408 let output=EmergencyDiagnosticHandle{sender,status:status.clone()};
4409 let snapshot=context();let owner=output.prepare_db_owner_observation(snapshot,17,account(),&storage).unwrap();
4410 let expected=saddle_admission::StorageDemand::embedded(std::alloc::Layout::new::<DbOwnerObservationState>(),std::alloc::Layout::new::<()>(),&[]).unwrap().bytes();
4411 assert_eq!(ledger.ownership_snapshot().process_storage_bytes-before.process_storage_bytes,expected);
4412 owner.acquired();owner.acquired();owner.commit_ack();owner.commit_ack();
4413 let mut moved=owner;moved.handoff();moved.handoff();moved.finish(DbOwnerDisposition::Returned);
4414 moved.finish(DbOwnerDisposition::Discarded);moved.commit_ack();moved.handoff();
4415 let mut previous=0;let mut owner_id=None;
4416 for event in ["acquired","commit_ack","finalization_handoff","returned"] {
4417 let DiagnosticPacket::Admitted(bytes)=receiver.recv().unwrap() else {panic!("fixed record")};
4418 let row:serde_json::Value=serde_json::from_slice(&bytes.bytes[..bytes.len]).unwrap();
4419 assert_eq!(row["schema"],"request-db-owner/1");assert_eq!(row["event"],event);
4420 assert_eq!(row["ledger_instance"],17);assert_eq!(row["account"],serde_json::to_value(account()).unwrap());
4421 assert_eq!(row["local_request"],row["context"]["local_request"]);
4422 let id=row["owner_id"].as_u64().unwrap();assert!(id>0);assert_eq!(*owner_id.get_or_insert(id),id);
4423 let sequence=row["sequence"].as_u64().unwrap();assert!(sequence>previous);previous=sequence;
4424 }
4425 assert!(rpc_lifecycle_stamp().0>previous);assert!(receiver.try_recv().is_err());
4426 drop(moved);assert_eq!(ledger.ownership_snapshot().process_storage_bytes,before.process_storage_bytes);
4427 println!("db_owner_state_layout={} handle_layout={} charged={}",std::mem::size_of::<DbOwnerObservationState>(),std::mem::size_of::<DbOwnerObservation>(),expected);
4428 }
4429 #[test]
4430 fn closed_or_full_output_and_failed_prep_are_observation_only() {
4431 let _guard=TEST_LOCK.lock().unwrap();
4432 let ledger=ledger();let root=ledger.try_process_storage(saddle_admission::StorageDemand::separate(&[]).unwrap()).unwrap();let storage=root.release_log_storage();let before=ledger.ownership_snapshot();
4433 for closed in [false,true] {
4434 let (sender,receiver)=mpsc::sync_channel(0);let status=Arc::new(Status::default());status.closed.store(closed,Ordering::Release);
4435 let output=EmergencyDiagnosticHandle{sender,status:status.clone()};
4436 let mut owner=output.prepare_db_owner_observation(context(),17,account(),&storage).unwrap();
4437 owner.acquired();owner.commit_ack();owner.handoff();owner.finish(DbOwnerDisposition::Discarded);
4438 assert!(owner.state.get().unavailable.load(Ordering::Acquire));assert_eq!(status.dropped.load(Ordering::Relaxed),1);
4439 assert!(receiver.try_recv().is_err());drop(owner);
4440 }
4441 assert_eq!(ledger.ownership_snapshot().process_storage_bytes,before.process_storage_bytes);
4442 let (sender,_receiver)=mpsc::sync_channel(1);let output=EmergencyDiagnosticHandle{sender,status:Arc::new(Status::default())};
4443 let layout=std::alloc::Layout::from_size_align(1024*1024*1024,8).unwrap();
4444 assert!(storage.try_reserve(layout).is_err());
4445 drop(root);assert!(output.prepare_db_owner_observation(context(),17,account(),&storage).is_none());
4446 }
4447}