Skip to main content

ferrum_interfaces/vnext/event/
sink.rs

1use std::error::Error;
2use std::fmt;
3use std::sync::Arc;
4
5use crate::model_executor::ExecutorRequestOrigin;
6
7use super::{
8    has_active, BoundExecutionResourceMaintenance, ExecutionEvent, ExecutionEventCursor,
9    ExecutionEventKind, RequestIdentity, RunId, TrustedExecutionEventContext,
10};
11
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct ExecutionEventSinkError {
14    message: String,
15}
16
17impl ExecutionEventSinkError {
18    pub fn new(message: impl Into<String>) -> Self {
19        Self {
20            message: message.into(),
21        }
22    }
23
24    pub fn message(&self) -> &str {
25        &self.message
26    }
27}
28
29impl fmt::Display for ExecutionEventSinkError {
30    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
31        formatter.write_str(&self.message)
32    }
33}
34
35impl Error for ExecutionEventSinkError {}
36
37mod event_sink_seal {
38    pub struct Seal;
39}
40
41/// Owned capability created only after the emitter has validated the event
42/// against its transactional cursor. Ownership lets asynchronous sinks defer
43/// materialization without cloning the event or extending producer lifetimes.
44pub struct EventEmissionPermit {
45    event: ExecutionEvent,
46    _seal: event_sink_seal::Seal,
47}
48
49impl EventEmissionPermit {
50    pub fn event(&self) -> &ExecutionEvent {
51        &self.event
52    }
53
54    pub fn into_event(self) -> ExecutionEvent {
55        self.event
56    }
57}
58
59/// Owned capability created only after the emitter has validated an ordered
60/// event batch against one transactional cursor.
61pub struct EventBatchEmissionPermit {
62    events: Vec<ExecutionEvent>,
63    _seal: event_sink_seal::Seal,
64}
65
66impl EventBatchEmissionPermit {
67    pub fn events(&self) -> &[ExecutionEvent] {
68        &self.events
69    }
70
71    pub fn into_events(self) -> Vec<ExecutionEvent> {
72        self.events
73    }
74}
75
76#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
77pub enum ExecutionEventCapturePolicy {
78    #[default]
79    AllFrames,
80    FirstFramePerRequest,
81    LifecycleOnly,
82}
83
84impl ExecutionEventCapturePolicy {
85    pub const fn captures_frame(self, completed_frames: u64) -> bool {
86        match self {
87            Self::AllFrames => true,
88            Self::FirstFramePerRequest | Self::LifecycleOnly => completed_frames == 0,
89        }
90    }
91
92    pub const fn records_event(self, kind: ExecutionEventKind) -> bool {
93        match self {
94            Self::AllFrames | Self::FirstFramePerRequest => true,
95            Self::LifecycleOnly => matches!(
96                kind,
97                ExecutionEventKind::RequestAccepted
98                    | ExecutionEventKind::PlanBuilt
99                    | ExecutionEventKind::FailureObserved
100                    | ExecutionEventKind::SequenceCompleted
101                    | ExecutionEventKind::SequenceAborted
102                    | ExecutionEventKind::RequestCompleted
103                    | ExecutionEventKind::RequestFailed
104            ),
105        }
106    }
107
108    pub const fn as_str(self) -> &'static str {
109        match self {
110            Self::AllFrames => "all_frames",
111            Self::FirstFramePerRequest => "first_frame_per_request",
112            Self::LifecycleOnly => "lifecycle_only",
113        }
114    }
115}
116
117#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
118pub enum ExecutionEventSinkEnablement {
119    None,
120    All,
121    #[default]
122    PerKind,
123}
124
125pub trait ExecutionEventSink: Send + Sync {
126    /// Resolves stable event enablement once when an emitter is constructed.
127    ///
128    /// Dynamically filtered sinks keep the conservative `PerKind` default.
129    /// Permanently disabled and all-event sinks select `None` or `All` so the
130    /// event hot path does not perform a virtual enablement query.
131    fn enablement(&self) -> ExecutionEventSinkEnablement {
132        ExecutionEventSinkEnablement::PerKind
133    }
134
135    fn is_enabled(&self, kind: ExecutionEventKind) -> bool;
136
137    fn device_timing_mode(&self) -> super::super::DeviceTimingMode {
138        super::super::DeviceTimingMode::Off
139    }
140
141    fn capture_policy(&self) -> ExecutionEventCapturePolicy {
142        ExecutionEventCapturePolicy::AllFrames
143    }
144
145    fn capture_policy_for_request(
146        &self,
147        _origin: ExecutorRequestOrigin,
148    ) -> ExecutionEventCapturePolicy {
149        self.capture_policy()
150    }
151
152    fn record_device_submission_attribution(
153        &self,
154        _attribution: &super::super::BoundDeviceSubmissionAttribution,
155    ) -> Result<(), ExecutionEventSinkError> {
156        Ok(())
157    }
158
159    fn record_physical_device_submission_timing(
160        &self,
161        _completion: &super::super::OperationCompletionReceipt,
162    ) -> Result<(), ExecutionEventSinkError> {
163        Ok(())
164    }
165
166    /// Resource maintenance is a plan/batch event and therefore does not run
167    /// through a per-request execution cursor. Sinks opt in explicitly so a
168    /// disabled profile path performs no event construction or serialization.
169    fn records_execution_resource_maintenance(&self) -> bool {
170        false
171    }
172
173    fn record_execution_resource_maintenance(
174        &self,
175        _maintenance: BoundExecutionResourceMaintenance,
176    ) -> Result<(), ExecutionEventSinkError> {
177        Ok(())
178    }
179
180    /// Prefix lookup precedes a per-request execution cursor. Opt-in sinks
181    /// receive resource-only decisions through their existing trace transport.
182    fn records_prefix_restore_decisions(&self) -> bool {
183        false
184    }
185
186    fn record_prefix_restore_decision(
187        &self,
188        _observation: &crate::model_executor::PrefixRestoreObservation<'_>,
189    ) -> Result<(), ExecutionEventSinkError> {
190        Ok(())
191    }
192
193    fn record(&self, permit: EventEmissionPermit) -> Result<(), ExecutionEventSinkError>;
194
195    /// Records one cursor-ordered batch. Sinks with a buffered transport should
196    /// override this boundary; the default preserves compatibility and order.
197    fn record_batch(
198        &self,
199        permit: EventBatchEmissionPermit,
200    ) -> Result<(), ExecutionEventSinkError> {
201        for event in permit.into_events() {
202            self.record(EventEmissionPermit {
203                event,
204                _seal: event_sink_seal::Seal,
205            })?;
206        }
207        Ok(())
208    }
209}
210
211enum ExecutionEventSinkHandle<'sink, S>
212where
213    S: ExecutionEventSink + ?Sized,
214{
215    Borrowed(&'sink S),
216    Shared(Arc<S>),
217}
218
219impl<S> ExecutionEventSinkHandle<'_, S>
220where
221    S: ExecutionEventSink + ?Sized,
222{
223    #[inline]
224    fn as_sink(&self) -> &S {
225        match self {
226            Self::Borrowed(sink) => *sink,
227            Self::Shared(sink) => sink.as_ref(),
228        }
229    }
230}
231
232pub struct ExecutionEventEmitter<'sink, S = dyn ExecutionEventSink>
233where
234    S: ExecutionEventSink + ?Sized,
235{
236    sink: ExecutionEventSinkHandle<'sink, S>,
237    cursor: ExecutionEventCursor,
238    capture_policy: ExecutionEventCapturePolicy,
239    sink_enablement: ExecutionEventSinkEnablement,
240    sink_failed: bool,
241}
242
243impl<'sink, S> ExecutionEventEmitter<'sink, S>
244where
245    S: ExecutionEventSink + ?Sized,
246{
247    #[inline]
248    pub fn new(sink: &'sink S, run_id: RunId, request_id: RequestIdentity) -> Self {
249        let sink_enablement = sink.enablement();
250        let capture_policy = if sink_enablement == ExecutionEventSinkEnablement::None {
251            ExecutionEventCapturePolicy::AllFrames
252        } else {
253            sink.capture_policy()
254        };
255        Self {
256            sink: ExecutionEventSinkHandle::Borrowed(sink),
257            cursor: ExecutionEventCursor::new(run_id, request_id),
258            capture_policy,
259            sink_enablement,
260            sink_failed: false,
261        }
262    }
263}
264
265impl ExecutionEventEmitter<'static, dyn ExecutionEventSink> {
266    /// Creates a durable emitter that may be owned by a request/session.
267    ///
268    /// The borrowed constructor remains useful for bounded validation. Product
269    /// runtimes use this form so event authority cannot outlive its sink.
270    pub fn from_shared(
271        sink: Arc<dyn ExecutionEventSink>,
272        run_id: RunId,
273        request_id: RequestIdentity,
274    ) -> ExecutionEventEmitter<'static> {
275        let sink_enablement = sink.enablement();
276        let capture_policy = if sink_enablement == ExecutionEventSinkEnablement::None {
277            ExecutionEventCapturePolicy::AllFrames
278        } else {
279            sink.capture_policy()
280        };
281        Self::from_shared_parts(sink, run_id, request_id, capture_policy, sink_enablement)
282    }
283
284    pub fn from_shared_with_capture_policy(
285        sink: Arc<dyn ExecutionEventSink>,
286        run_id: RunId,
287        request_id: RequestIdentity,
288        capture_policy: ExecutionEventCapturePolicy,
289    ) -> ExecutionEventEmitter<'static> {
290        let sink_enablement = sink.enablement();
291        Self::from_shared_parts(sink, run_id, request_id, capture_policy, sink_enablement)
292    }
293
294    fn from_shared_parts(
295        sink: Arc<dyn ExecutionEventSink>,
296        run_id: RunId,
297        request_id: RequestIdentity,
298        capture_policy: ExecutionEventCapturePolicy,
299        sink_enablement: ExecutionEventSinkEnablement,
300    ) -> ExecutionEventEmitter<'static> {
301        ExecutionEventEmitter {
302            sink: ExecutionEventSinkHandle::Shared(sink),
303            cursor: ExecutionEventCursor::new(run_id, request_id),
304            capture_policy,
305            sink_enablement,
306            sink_failed: false,
307        }
308    }
309}
310
311impl<'sink, S> ExecutionEventEmitter<'sink, S>
312where
313    S: ExecutionEventSink + ?Sized,
314{
315    #[inline]
316    fn records_event(&self, event: &ExecutionEvent) -> bool {
317        self.capture_policy.records_event(event.kind())
318            && match self.sink_enablement {
319                ExecutionEventSinkEnablement::None => false,
320                ExecutionEventSinkEnablement::All => true,
321                ExecutionEventSinkEnablement::PerKind => {
322                    self.sink.as_sink().is_enabled(event.kind())
323                }
324            }
325    }
326
327    #[inline]
328    fn validate_next(
329        cursor: &mut ExecutionEventCursor,
330        event: &ExecutionEvent,
331        context: &TrustedExecutionEventContext<'_>,
332    ) -> Result<(), ExecutionEventSinkError> {
333        match event.kind() {
334            ExecutionEventKind::FrameStarted | ExecutionEventKind::NodeStarted => context
335                .active_binding()
336                .ok_or_else(|| {
337                    ExecutionEventSinkError::new(
338                        "active execution emission lacks live sequence evidence",
339                    )
340                })?
341                .ensure_open_for_emission()
342                .map_err(|error| ExecutionEventSinkError::new(error.to_string()))?,
343            ExecutionEventKind::OperationSubmitted
344            | ExecutionEventKind::NodeRetired
345            | ExecutionEventKind::FrameCompleted => context
346                .active_binding()
347                .ok_or_else(|| {
348                    ExecutionEventSinkError::new(
349                        "active execution emission lacks live sequence evidence",
350                    )
351                })?
352                .ensure_live_for_emission()
353                .map_err(|error| ExecutionEventSinkError::new(error.to_string()))?,
354            ExecutionEventKind::FailureObserved if has_active(event.identity().parts()) => context
355                .active_binding()
356                .ok_or_else(|| {
357                    ExecutionEventSinkError::new(
358                        "active execution emission lacks live sequence evidence",
359                    )
360                })?
361                .ensure_live_for_emission()
362                .map_err(|error| ExecutionEventSinkError::new(error.to_string()))?,
363            _ => {}
364        }
365        cursor
366            .observe_candidate_in_place(event, context)
367            .map_err(|error| ExecutionEventSinkError::new(error.to_string()))
368    }
369
370    #[inline]
371    pub fn emit(
372        &mut self,
373        event: ExecutionEvent,
374        context: &TrustedExecutionEventContext<'_>,
375    ) -> Result<(), ExecutionEventSinkError> {
376        if self.sink_enablement == ExecutionEventSinkEnablement::None {
377            let mut next_cursor = self.cursor.clone();
378            Self::validate_next(&mut next_cursor, &event, context)?;
379            self.cursor = next_cursor;
380            return Ok(());
381        }
382        if self.sink_failed {
383            return Err(ExecutionEventSinkError::new(
384                "execution event emitter is sealed after a sink failure",
385            ));
386        }
387        let mut next_cursor = self.cursor.clone();
388        Self::validate_next(&mut next_cursor, &event, context)?;
389        if self.records_event(&event) {
390            let permit = EventEmissionPermit {
391                event,
392                _seal: event_sink_seal::Seal,
393            };
394            if let Err(error) = self.sink.as_sink().record(permit) {
395                self.sink_failed = true;
396                return Err(error);
397            }
398        }
399        self.cursor = next_cursor;
400        Ok(())
401    }
402
403    pub fn emit_batch(
404        &mut self,
405        events: Vec<ExecutionEvent>,
406        contexts: &[TrustedExecutionEventContext<'_>],
407    ) -> Result<(), ExecutionEventSinkError> {
408        if self.sink_enablement == ExecutionEventSinkEnablement::None {
409            if events.len() != contexts.len() {
410                return Err(ExecutionEventSinkError::new(
411                    "execution event batch context count differs from event count",
412                ));
413            }
414            if events.is_empty() {
415                return Ok(());
416            }
417            let mut next_cursor = self.cursor.clone();
418            for (event, context) in events.iter().zip(contexts) {
419                Self::validate_next(&mut next_cursor, event, context)?;
420            }
421            self.cursor = next_cursor;
422            return Ok(());
423        }
424        if self.sink_failed {
425            return Err(ExecutionEventSinkError::new(
426                "execution event emitter is sealed after a sink failure",
427            ));
428        }
429        if events.len() != contexts.len() {
430            return Err(ExecutionEventSinkError::new(
431                "execution event batch context count differs from event count",
432            ));
433        }
434        if events.is_empty() {
435            return Ok(());
436        }
437
438        let mut next_cursor = self.cursor.clone();
439        for (event, context) in events.iter().zip(contexts) {
440            Self::validate_next(&mut next_cursor, event, context)?;
441        }
442
443        let all_enabled = events.iter().all(|event| self.records_event(event));
444        if all_enabled {
445            let permit = EventBatchEmissionPermit {
446                events,
447                _seal: event_sink_seal::Seal,
448            };
449            if let Err(error) = self.sink.as_sink().record_batch(permit) {
450                self.sink_failed = true;
451                return Err(error);
452            }
453        } else {
454            for event in events {
455                if !self.records_event(&event) {
456                    continue;
457                }
458                let permit = EventEmissionPermit {
459                    event,
460                    _seal: event_sink_seal::Seal,
461                };
462                if let Err(error) = self.sink.as_sink().record(permit) {
463                    self.sink_failed = true;
464                    return Err(error);
465                }
466            }
467        }
468        self.cursor = next_cursor;
469        Ok(())
470    }
471
472    pub fn cursor(&self) -> &ExecutionEventCursor {
473        &self.cursor
474    }
475
476    pub const fn sink_failed(&self) -> bool {
477        self.sink_failed
478    }
479}
480
481#[derive(Debug, Default)]
482pub struct DisabledExecutionEventSink;
483
484impl ExecutionEventSink for DisabledExecutionEventSink {
485    fn enablement(&self) -> ExecutionEventSinkEnablement {
486        ExecutionEventSinkEnablement::None
487    }
488
489    fn is_enabled(&self, _kind: ExecutionEventKind) -> bool {
490        false
491    }
492
493    fn record(&self, _permit: EventEmissionPermit) -> Result<(), ExecutionEventSinkError> {
494        Ok(())
495    }
496}