Skip to main content

saddle_core/
request_context.rs

1//! Request facts only. Allocation permission and cancellation remain with Runtime/Admission.
2//! Root creation and each local allocation must be preceded by their storage reservation.
3use crate::{CallContext, DbScopeDiagnosticIdentity};
4use serde::{Serialize, Serializer, ser::SerializeStruct};
5use std::sync::{
6    Arc, OnceLock,
7    atomic::{AtomicU64, Ordering},
8};
9
10static NEXT_ROOT: AtomicU64 = AtomicU64::new(1);
11
12/// Closed absence vocabulary, separate from output availability.
13#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
14#[serde(tag = "state", content = "value", rename_all = "snake_case")]
15pub enum ContextFact<T> {
16    Present(T),
17    NotApplicable,
18    NotEstablished,
19    Unavailable,
20}
21
22/// Exact bounded protocol identity. Not an authorization or a user-data container.
23#[derive(Clone, Copy, Eq, PartialEq)]
24pub struct ContextIdentity {
25    bytes: [u8; 256],
26    len: u16,
27}
28impl ContextIdentity {
29    pub fn checked(value: &str) -> Result<Self, ContextConflict> {
30        if value.is_empty() || value.len() > 256 || value.chars().any(char::is_control) {
31            return Err(ContextConflict::InvalidIdentity);
32        }
33        let mut out = Self {
34            bytes: [0; 256],
35            len: value.len() as u16,
36        };
37        out.bytes[..value.len()].copy_from_slice(value.as_bytes());
38        Ok(out)
39    }
40    fn as_str(&self) -> &str {
41        std::str::from_utf8(&self.bytes[..usize::from(self.len)]).expect("validated UTF-8")
42    }
43}
44impl Serialize for ContextIdentity {
45    fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
46        s.serialize_str(self.as_str())
47    }
48}
49
50/// Safe metadata, bounded and exact; credentials/URL/query syntax is rejected.
51#[derive(Clone, Copy, Eq, PartialEq, Serialize)]
52#[serde(transparent)]
53pub struct ContextLabel(ContextIdentity);
54impl ContextLabel {
55    pub fn checked(value: &str) -> Result<Self, ContextConflict> {
56        let value = ContextIdentity::checked(value)?;
57        if value.as_str().contains("://")
58            || !value
59                .as_str()
60                .chars()
61                .all(|c| c.is_alphanumeric() || "_.:/{}*-".contains(c))
62        {
63            return Err(ContextConflict::UnsafeMetadata);
64        }
65        Ok(Self(value))
66    }
67}
68
69#[derive(Clone, Copy, Debug, Eq, PartialEq)]
70pub enum ContextConflict {
71    InvalidIdentity,
72    UnsafeMetadata,
73    IdentityGroup,
74    ForeignRoot,
75    ChildRelation,
76    CounterExhausted,
77}
78
79/// Complete validated identity group. Consumed atomically by the entry publisher.
80/// No partial-field setters; rejected publication returns this same group.
81#[derive(Eq, PartialEq)]
82pub struct RequestIdentityGroup {
83    application: ContextLabel,
84    module: ContextLabel,
85    service: ContextLabel,
86    operation: ContextLabel,
87    trace: ContextIdentity,
88    rpc: ContextFact<ContextIdentity>,
89    span: u64,
90    request: ContextIdentity,
91    route: ContextLabel,
92    attempt: u32,
93    zone: ContextFact<ContextLabel>,
94}
95impl RequestIdentityGroup {
96    pub fn from_validated(
97        call: &CallContext,
98        request: &str,
99        route: &str,
100        attempt: u32,
101        zone: ContextFact<ContextLabel>,
102    ) -> Result<Self, ContextConflict> {
103        if attempt == 0 {
104            return Err(ContextConflict::InvalidIdentity);
105        }
106        Ok(Self {
107            application: ContextLabel::checked(call.application().as_str())?,
108            module: ContextLabel::checked(call.module().as_str())?,
109            service: ContextLabel::checked(call.service().as_str())?,
110            operation: ContextLabel::checked(call.operation().as_str())?,
111            trace: ContextIdentity::checked(call.trace_correlation_id().as_str())?,
112            rpc: match call.rpc_correlation_id() {
113                Some(id) => ContextFact::Present(ContextIdentity::checked(id.as_str())?),
114                None => ContextFact::Unavailable,
115            },
116            span: call.span_id().as_u64(),
117            request: ContextIdentity::checked(request)?,
118            route: ContextLabel::checked(route)?,
119            attempt,
120            zone,
121        })
122    }
123}
124
125struct RequestRoot {
126    local: u64,
127    application: ContextLabel,
128    initial: ContextFact<()>,
129    identity: OnceLock<RequestIdentityGroup>,
130}
131impl Drop for RequestRoot {
132    fn drop(&mut self) {
133        observe(self.local, "root_drop");
134    }
135}
136
137/// Unique entry binding capability. Does not own an account or output handle.
138///
139/// ```compile_fail
140/// use saddle_core::RequestRootPublisher;
141/// fn duplicate(p: RequestRootPublisher) { let _ = p.clone(); }
142/// ```
143pub struct RequestRootPublisher {
144    root: Arc<RequestRoot>,
145}
146
147/// Read-only shared reference; cloning shares one allocation, never root fields.
148pub struct RequestRootRef {
149    root: Arc<RequestRoot>,
150}
151impl Clone for RequestRootRef {
152    fn clone(&self) -> Self {
153        observe(self.root.local, "root_share");
154        Self {
155            root: Arc::clone(&self.root),
156        }
157    }
158}
159impl Drop for RequestRootRef {
160    fn drop(&mut self) {
161        observe(self.root.local, "root_release");
162    }
163}
164
165impl RequestRootPublisher {
166    /// Storage-only constructor, not admission. Caller must reserve before calling;
167    /// C supplies layout, R0/A/R supply and enforce the actual reservation seam.
168    pub fn create(
169        application: ContextLabel,
170        initial: ContextFact<()>,
171    ) -> Result<Self, ContextConflict> {
172        if matches!(initial, ContextFact::Present(())) {
173            return Err(ContextConflict::InvalidIdentity);
174        }
175        let local = NEXT_ROOT
176            .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |n| n.checked_add(1))
177            .map_err(|_| ContextConflict::CounterExhausted)?;
178        let root = Arc::new(RequestRoot {
179            local,
180            application,
181            initial,
182            identity: OnceLock::new(),
183        });
184        observe(local, "root_create");
185        Ok(Self { root })
186    }
187    pub fn reference(&self) -> RequestRootRef {
188        observe(self.root.local, "root_share");
189        RequestRootRef {
190            root: Arc::clone(&self.root),
191        }
192    }
193    #[allow(clippy::result_large_err)] // Recover the fixed input without allocating on rejection.
194    pub fn publish(
195        &mut self,
196        group: RequestIdentityGroup,
197    ) -> Result<(), (ContextConflict, RequestIdentityGroup)> {
198        if group.application != self.root.application {
199            return Err((ContextConflict::IdentityGroup, group));
200        }
201        if let Some(old) = self.root.identity.get() {
202            return if old == &group {
203                Ok(())
204            } else {
205                Err((ContextConflict::IdentityGroup, group))
206            };
207        }
208        self.root
209            .identity
210            .set(group)
211            .map_err(|group| (ContextConflict::IdentityGroup, group))
212    }
213}
214
215/// Closed lifecycle metadata, not a state machine controlling the request.
216#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
217#[serde(rename_all = "snake_case")]
218pub enum RequestViewPhase {
219    SocketAccepted,
220    Reading,
221    Admitted,
222    Dispatch,
223    Handler,
224    Database,
225    Outbound,
226    Response,
227    Finalizing,
228    Finished,
229}
230
231/// Static registered operation labels are borrowed forever, never copied as text.
232#[derive(Clone, Copy, Eq, PartialEq, Serialize)]
233pub struct RegisteredContextOperation(&'static str);
234impl RegisteredContextOperation {
235    pub fn checked(value: &'static str) -> Result<Self, ContextConflict> {
236        ContextLabel::checked(value)?;
237        Ok(Self(value))
238    }
239}
240
241#[derive(Clone, Copy)]
242pub struct RequestLocalFacts {
243    task: ContextFact<u64>,
244    scope: ContextFact<DbScopeDiagnosticIdentity>,
245    db_operation: ContextFact<RegisteredContextOperation>,
246    phase: RequestViewPhase,
247}
248impl RequestLocalFacts {
249    pub fn new(phase: RequestViewPhase) -> Self {
250        Self {
251            task: ContextFact::NotEstablished,
252            scope: ContextFact::NotEstablished,
253            db_operation: ContextFact::NotApplicable,
254            phase,
255        }
256    }
257    /// Numeric observation supplied by the real task owner; not execution authority.
258    pub fn with_task(mut self, task: ContextFact<u64>) -> Self {
259        self.task = task;
260        self
261    }
262    pub fn without_scope(mut self, state: ContextFact<()>) -> Self {
263        self.scope = absent(state);
264        self
265    }
266    pub fn with_db_operation(mut self, operation: RegisteredContextOperation) -> Self {
267        self.db_operation = ContextFact::Present(operation);
268        self
269    }
270}
271
272// Only a real child needs variable local identities. Sibling views share this
273// allocation; the root's original request/trace/zone never appear in this object.
274struct ChildCall {
275    #[cfg(test)]
276    observation: LocalObservation,
277    application: ContextLabel,
278    module: ContextLabel,
279    service: ContextLabel,
280    operation: ContextLabel,
281    rpc: ContextIdentity,
282    span: u64,
283    route: ContextLabel,
284    attempt: u32,
285}
286struct LocalView {
287    #[cfg(test)]
288    observation: LocalObservation,
289    root: Arc<RequestRoot>,
290    published: bool,
291    facts: RequestLocalFacts,
292    child: Option<Arc<ChildCall>>,
293}
294impl Drop for LocalView {
295    fn drop(&mut self) {
296        observe(self.root.local, "view_drop");
297        #[cfg(test)]
298        self.observation.record("destroy");
299    }
300}
301impl Drop for ChildCall {
302    fn drop(&mut self) {
303        #[cfg(test)]
304        self.observation.record("destroy");
305    }
306}
307
308/// Immutable local facts + a frozen visibility stage, sharing the single root.
309/// Source receipts retain this object, not another projection or diagnostic body.
310///
311/// ```compile_fail
312/// use saddle_core::RequestExecutionView;
313/// fn rewrite(view: RequestExecutionView) { view.inner.published = false; }
314/// ```
315pub struct RequestExecutionView {
316    inner: Arc<LocalView>,
317}
318
319/// Fixed diagnostic facts, with no root reference or resource authority.
320/// Capturing this value cannot extend an account's lifetime.
321#[derive(Clone, Copy, Serialize)]
322pub struct RequestContextSnapshot {
323    schema_version: u8,
324    local_request: u64,
325    publication: u8,
326    application: ContextFact<ContextLabel>,
327    call_application: ContextFact<ContextLabel>,
328    module: ContextFact<ContextLabel>,
329    service: ContextFact<ContextLabel>,
330    operation: ContextFact<ContextLabel>,
331    trace_id: ContextFact<ContextIdentity>,
332    request: ContextFact<ContextIdentity>,
333    span_id: ContextFact<SpanProjection>,
334    route: ContextFact<ContextLabel>,
335    attempt: ContextFact<u32>,
336    rpc_id: ContextFact<ContextIdentity>,
337    zone: ContextFact<ContextLabel>,
338    db_operation: ContextFact<RegisteredContextOperation>,
339    scope: ContextFact<DbScopeDiagnosticIdentity>,
340    task: ContextFact<u64>,
341    lifecycle: ContextFact<RequestViewPhase>,
342    target: ContextFact<ContextLabel>,
343}
344impl RequestContextSnapshot {
345    #[doc(hidden)]
346    pub fn local_request_id(&self) -> u64 { self.local_request }
347}
348/// A borrowed source-record projection of the actual outbound call target.
349/// It does not change the request identity or retain the target.
350pub struct OutboundSourceContext<'a> {
351    view: &'a RequestExecutionView,
352    business_unit: &'a str,
353}
354impl Clone for RequestExecutionView {
355    fn clone(&self) -> Self {
356        #[cfg(test)]
357        self.inner.observation.record("share");
358        Self {
359            inner: Arc::clone(&self.inner),
360        }
361    }
362}
363impl Drop for RequestExecutionView {
364    fn drop(&mut self) {
365        #[cfg(test)]
366        self.inner.observation.record("release");
367    }
368}
369impl RequestRootRef {
370    pub fn view(&self, facts: RequestLocalFacts) -> RequestExecutionView {
371        RequestExecutionView::allocate(
372            Arc::clone(&self.root),
373            self.root.identity.get().is_some(),
374            facts,
375            None,
376        )
377    }
378    pub fn same_request(&self, view: &RequestExecutionView) -> bool {
379        Arc::ptr_eq(&self.root, &view.inner.root)
380    }
381}
382impl RequestExecutionView {
383    pub fn diagnostic_snapshot(&self) -> RequestContextSnapshot {
384        let identity = self.identity();
385        let child = self.inner.child.as_deref();
386        let initial = self.inner.root.initial;
387        macro_rules! root {
388            ($field:ident) => { identity.map_or_else(|| absent(initial), |g| ContextFact::Present(g.$field)) };
389        }
390        macro_rules! call {
391            ($field:ident) => { child.map(|c| c.$field).or_else(|| identity.map(|g| g.$field))
392                .map_or_else(|| absent(initial), ContextFact::Present) };
393        }
394        RequestContextSnapshot {
395            schema_version: 2,
396            local_request: self.inner.root.local,
397            publication: u8::from(self.inner.published),
398            application: ContextFact::Present(self.inner.root.application),
399            call_application: ContextFact::Present(child.map_or(self.inner.root.application, |c| c.application)),
400            module: call!(module), service: call!(service), operation: call!(operation),
401            trace_id: root!(trace), request: root!(request),
402            span_id: child.map(|c| c.span).or_else(|| identity.map(|g| g.span))
403                .map_or_else(|| absent(initial), |span| ContextFact::Present(SpanProjection(span))),
404            route: call!(route), attempt: call!(attempt),
405            rpc_id: child.map(|c| ContextFact::Present(c.rpc)).or_else(|| identity.map(|g| g.rpc))
406                .unwrap_or_else(|| absent(initial)),
407            zone: identity.map_or_else(|| absent(initial), |g| g.zone),
408            db_operation: self.inner.facts.db_operation, scope: self.inner.facts.scope,
409            task: self.inner.facts.task, lifecycle: ContextFact::Present(self.inner.facts.phase),
410            target: child.map_or(ContextFact::NotApplicable, |c| ContextFact::Present(c.route)),
411        }
412    }
413    pub fn outbound_source_context<'a>(&'a self, business_unit: &'a str) -> OutboundSourceContext<'a> {
414        OutboundSourceContext { view: self, business_unit }
415    }
416    fn allocate(
417        root: Arc<RequestRoot>,
418        published: bool,
419        facts: RequestLocalFacts,
420        child: Option<Arc<ChildCall>>,
421    ) -> Self {
422        observe(root.local, "view_create");
423        Self {
424            inner: Arc::new(LocalView {
425                #[cfg(test)]
426                observation: LocalObservation::create(root.local, "view"),
427                root,
428                published,
429                facts,
430                child,
431            }),
432        }
433    }
434    /// New operation, no root-text copy or mutation of an earlier view.
435    fn local(&self, facts: RequestLocalFacts) -> Self {
436        Self::allocate(
437            Arc::clone(&self.inner.root),
438            self.inner.published,
439            facts,
440            self.inner.child.clone(),
441        )
442    }
443    pub fn with_phase(&self, phase: RequestViewPhase) -> Self {
444        let mut facts = self.inner.facts;
445        facts.phase = phase;
446        self.local(facts)
447    }
448    pub fn with_db_operation(&self, operation: RegisteredContextOperation) -> Self {
449        self.local(self.inner.facts.with_db_operation(operation))
450    }
451    /// Derive the observation for the actual task selected by the runtime. The
452    /// numeric task is metadata, never a task permit or a sequence minted here.
453    pub fn in_task(&self, task: u64) -> Self {
454        self.local(self.inner.facts.with_task(ContextFact::Present(task)))
455    }
456    pub fn in_db_scope(
457        &self,
458        scope: &crate::DbScopeDiagnosticContext<Self>,
459    ) -> Result<Self, ContextConflict> {
460        let (original, identity) = scope.diagnostic_context();
461        if !self.same_request(original) {
462            return Err(ContextConflict::ForeignRoot);
463        }
464        let mut facts = self.inner.facts;
465        facts.scope = ContextFact::Present(identity);
466        Ok(self.local(facts))
467    }
468    pub fn observed_db_scope(
469        &self,
470        scope: &crate::DbScopeObservation<Self>,
471    ) -> Result<Self, ContextConflict> {
472        let (original, identity) = scope.diagnostic_context();
473        if !self.same_request(original) {
474            return Err(ContextConflict::ForeignRoot);
475        }
476        let mut facts = self.inner.facts;
477        facts.scope = ContextFact::Present(identity);
478        Ok(self.local(facts))
479    }
480    pub fn same_request(&self, other: &Self) -> bool {
481        Arc::ptr_eq(&self.inner.root, &other.inner.root)
482    }
483    pub fn same_view(&self, other: &Self) -> bool {
484        Arc::ptr_eq(&self.inner, &other.inner)
485    }
486    /// Only the existing entry root can authorize a later visibility stage.
487    pub fn refresh(&self, root: &RequestRootRef) -> Result<Self, ContextConflict> {
488        if !root.same_request(self) {
489            return Err(ContextConflict::ForeignRoot);
490        }
491        Ok(Self::allocate(
492            Arc::clone(&self.inner.root),
493            self.inner.root.identity.get().is_some(),
494            self.inner.facts,
495            self.inner.child.clone(),
496        ))
497    }
498    pub fn child(
499        &self,
500        call: &CallContext,
501        request: &str,
502        route: &str,
503        attempt: u32,
504    ) -> Result<Self, ContextConflict> {
505        let identity = self.identity().ok_or(ContextConflict::ChildRelation)?;
506        if identity.trace.as_str() != call.trace_correlation_id().as_str()
507            || identity.request.as_str() != request
508        {
509            return Err(ContextConflict::ForeignRoot);
510        }
511        let parent_rpc = if let Some(child) = &self.inner.child {
512            &child.rpc
513        } else if let ContextFact::Present(rpc) = &identity.rpc {
514            rpc
515        } else {
516            return Err(ContextConflict::ChildRelation);
517        };
518        let rpc = call
519            .rpc_correlation_id()
520            .ok_or(ContextConflict::ChildRelation)?;
521        let suffix = rpc
522            .as_str()
523            .strip_prefix(parent_rpc.as_str())
524            .and_then(|s| s.strip_prefix('.'))
525            .ok_or(ContextConflict::ChildRelation)?;
526        let span = self
527            .inner
528            .child
529            .as_ref()
530            .map_or(identity.span, |child| child.span);
531        if suffix.is_empty()
532            || !suffix.bytes().all(|b| b.is_ascii_digit())
533            || span == call.span_id().as_u64()
534            || attempt == 0
535        {
536            return Err(ContextConflict::ChildRelation);
537        }
538        let child = Arc::new(ChildCall {
539            application: ContextLabel::checked(call.application().as_str())?,
540            module: ContextLabel::checked(call.module().as_str())?,
541            service: ContextLabel::checked(call.service().as_str())?,
542            operation: ContextLabel::checked(call.operation().as_str())?,
543            rpc: ContextIdentity::checked(rpc.as_str())?,
544            span: call.span_id().as_u64(),
545            route: ContextLabel::checked(route)?,
546            attempt,
547            #[cfg(test)]
548            observation: LocalObservation::create(self.inner.root.local, "child"),
549        });
550        Ok(Self::allocate(
551            Arc::clone(&self.inner.root),
552            self.inner.published,
553            self.inner.facts,
554            Some(child),
555        ))
556    }
557    fn identity(&self) -> Option<&RequestIdentityGroup> {
558        self.inner
559            .published
560            .then(|| self.inner.root.identity.get())
561            .flatten()
562    }
563}
564
565fn absent<T>(state: ContextFact<()>) -> ContextFact<T> {
566    match state {
567        ContextFact::NotApplicable => ContextFact::NotApplicable,
568        ContextFact::NotEstablished => ContextFact::NotEstablished,
569        _ => ContextFact::Unavailable,
570    }
571}
572impl Serialize for RequestExecutionView {
573    fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
574        self.serialize_context(s, None)
575    }
576}
577impl Serialize for OutboundSourceContext<'_> {
578    fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
579        self.view.serialize_context(s, Some(self.business_unit))
580    }
581}
582impl RequestExecutionView {
583    fn serialize_context<S: Serializer>(&self, s: S, business_unit: Option<&str>) -> Result<S::Ok, S::Error> {
584        let mut out = s.serialize_struct("RequestContext", 21 + usize::from(business_unit.is_some()))?;
585        let identity = self.identity();
586        let child = self.inner.child.as_deref();
587        let initial = self.inner.root.initial;
588        out.serialize_field("schema_version", &2u8)?;
589        out.serialize_field("local_request", &self.inner.root.local)?;
590        out.serialize_field("publication", &u8::from(self.inner.published))?;
591        out.serialize_field(
592            "application",
593            &ContextFact::Present(&self.inner.root.application),
594        )?;
595        out.serialize_field(
596            "call_application",
597            &ContextFact::Present(child.map_or(&self.inner.root.application, |c| &c.application)),
598        )?;
599        macro_rules! root_field {
600            ($name:literal, $field:ident) => {
601                out.serialize_field(
602                    $name,
603                    &identity.map_or_else(|| absent(initial), |g| ContextFact::Present(&g.$field)),
604                )?;
605            };
606        }
607        macro_rules! call_field {
608            ($name:literal, $field:ident) => {
609                out.serialize_field(
610                    $name,
611                    &child
612                        .map(|c| &c.$field)
613                        .or_else(|| identity.map(|g| &g.$field))
614                        .map_or_else(|| absent(initial), ContextFact::Present),
615                )?;
616            };
617        }
618        call_field!("module", module);
619        call_field!("service", service);
620        call_field!("operation", operation);
621        root_field!("trace_id", trace);
622        root_field!("request", request);
623        let span = child.map(|c| c.span).or_else(|| identity.map(|g| g.span));
624        out.serialize_field(
625            "span_id",
626            &span.map_or_else(
627                || absent(initial),
628                |s| ContextFact::Present(SpanProjection(s)),
629            ),
630        )?;
631        call_field!("route", route);
632        call_field!("attempt", attempt);
633        let rpc = child
634            .map(|c| ContextFact::Present(c.rpc))
635            .or_else(|| identity.map(|g| g.rpc))
636            .unwrap_or_else(|| absent(initial));
637        out.serialize_field("rpc_id", &rpc)?;
638        out.serialize_field(
639            "zone",
640            &identity.map_or_else(|| absent(initial), |g| g.zone),
641        )?;
642        out.serialize_field("db_operation", &self.inner.facts.db_operation)?;
643        out.serialize_field("scope", &self.inner.facts.scope)?;
644        out.serialize_field("task", &self.inner.facts.task)?;
645        out.serialize_field("lifecycle", &ContextFact::Present(self.inner.facts.phase))?;
646        // Target is the registered route, never endpoint/URL or credentials.
647        out.serialize_field(
648            "target",
649            &child.map_or(ContextFact::NotApplicable, |c| {
650                ContextFact::Present(&c.route)
651            }),
652        )?;
653        if let Some(business_unit) = business_unit {
654            out.serialize_field("business_unit", &ContextFact::Present(business_unit))?;
655        }
656        out.end()
657    }
658}
659
660/// Payload and shared allocation layouts, not a reservation or full task charge.
661/// Arc control block calculation follows std's two-AtomicUsize header, with
662/// Layout::extend padding. Allocator metadata is NOT included; R0 must account it.
663pub fn request_context_layouts() -> [(std::alloc::Layout, std::alloc::Layout); 3] {
664    fn pair<T>() -> (std::alloc::Layout, std::alloc::Layout) {
665        let payload = std::alloc::Layout::new::<T>();
666        let header = std::alloc::Layout::new::<[std::sync::atomic::AtomicUsize; 2]>();
667        (
668            payload,
669            header
670                .extend(payload)
671                .expect("fixed layout")
672                .0
673                .pad_to_align(),
674        )
675    }
676    [
677        pair::<RequestRoot>(),
678        pair::<LocalView>(),
679        pair::<ChildCall>(),
680    ]
681}
682
683#[cfg(not(test))]
684fn observe(_: u64, _: &'static str) {}
685#[cfg(test)]
686fn observe(root: u64, event: &'static str) {
687    EVENTS.lock().unwrap().push((root, event));
688    record_observation(root, root, "root", event);
689}
690#[cfg(test)]
691static EVENTS: std::sync::Mutex<Vec<(u64, &'static str)>> = std::sync::Mutex::new(Vec::new());
692
693#[derive(Clone, Copy)]
694struct SpanProjection(u64);
695impl Serialize for SpanProjection {
696    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
697        let mut bytes = [b'0'; 16];
698        for (i, byte) in bytes.iter_mut().enumerate() {
699            *byte = b"0123456789abcdef"[((self.0 >> ((15 - i) * 4)) & 15) as usize];
700        }
701        serializer.serialize_str(std::str::from_utf8(&bytes).expect("hex"))
702    }
703}
704
705// Private observation holds numbers only, never Arc/Weak, callbacks or objects.
706// Instrumentation storage is test-only and excluded from production layouts.
707#[cfg(test)]
708struct LocalObservation {
709    id: u64,
710    root: u64,
711    kind: &'static str,
712}
713#[cfg(test)]
714static LOCAL_EVENTS: std::sync::Mutex<Vec<(u64, u64, &'static str, &'static str)>> =
715    std::sync::Mutex::new(Vec::new());
716#[cfg(test)]
717impl LocalObservation {
718    fn create(root: u64, kind: &'static str) -> Self {
719        static NEXT: AtomicU64 = AtomicU64::new(1);
720        let observation = Self {
721            id: NEXT.fetch_add(1, Ordering::Relaxed),
722            root,
723            kind,
724        };
725        observation.record("create");
726        observation
727    }
728    fn record(&self, event: &'static str) {
729        LOCAL_EVENTS
730            .lock()
731            .unwrap()
732            .push((self.id, self.root, self.kind, event));
733        record_observation(self.root, self.id, self.kind, event);
734    }
735}
736
737#[cfg(test)]
738type ObservationEvent = (u64, u64, u64, &'static str, &'static str);
739#[cfg(test)]
740static ORDERED_EVENTS: std::sync::Mutex<Vec<ObservationEvent>> = std::sync::Mutex::new(Vec::new());
741#[cfg(test)]
742fn record_observation(root: u64, object: u64, kind: &'static str, event: &'static str) {
743    static SEQUENCE: AtomicU64 = AtomicU64::new(1);
744    let sequence = SEQUENCE.fetch_add(1, Ordering::Relaxed);
745    ORDERED_EVENTS
746        .lock()
747        .unwrap()
748        .push((sequence, root, object, kind, event));
749}
750
751#[cfg(test)]
752mod tests;