1#[derive(Clone, Debug, PartialEq)]
25pub enum TensorEvent {
26 RuleFired {
28 rule_id: u64,
30 bindings_count: usize,
32 },
33 InferenceComplete {
35 session_id: u64,
37 duration_ms: u64,
39 },
40 TensorUpdated {
42 tensor_id: u64,
44 new_version: u64,
46 },
47 CheckpointSaved {
49 path: String,
51 size_bytes: u64,
53 },
54 GradientApplied {
56 step: u64,
58 loss: f64,
60 },
61}
62
63#[derive(Clone, Debug, PartialEq)]
67pub enum EventFilter {
68 All,
70 RuleEventsOnly,
72 InferenceEventsOnly,
74 TensorEventsOnly,
76}
77
78impl EventFilter {
79 pub fn matches(&self, event: &TensorEvent) -> bool {
81 match self {
82 EventFilter::All => true,
83 EventFilter::RuleEventsOnly => matches!(event, TensorEvent::RuleFired { .. }),
84 EventFilter::InferenceEventsOnly => {
85 matches!(event, TensorEvent::InferenceComplete { .. })
86 }
87 EventFilter::TensorEventsOnly => matches!(event, TensorEvent::TensorUpdated { .. }),
88 }
89 }
90}
91
92#[derive(Debug)]
96pub struct Subscription {
97 pub sub_id: u64,
99 pub filter: EventFilter,
101 pub priority: u32,
103 pub max_queue_size: usize,
105 pub queue: Vec<TensorEvent>,
107 pub delivered: u64,
109 pub dropped: u64,
111}
112
113impl Subscription {
114 fn new(sub_id: u64, filter: EventFilter, priority: u32, max_queue_size: usize) -> Self {
115 Self {
116 sub_id,
117 filter,
118 priority,
119 max_queue_size,
120 queue: Vec::new(),
121 delivered: 0,
122 dropped: 0,
123 }
124 }
125}
126
127#[derive(Clone, Debug, Default, PartialEq)]
131pub struct BusStats {
132 pub total_published: u64,
134 pub total_delivered: u64,
136 pub total_dropped: u64,
138}
139
140impl BusStats {
141 pub fn delivery_rate(&self) -> f64 {
145 if self.total_published == 0 {
146 return 1.0;
147 }
148 self.total_delivered as f64 / self.total_published as f64
155 }
156}
157
158#[derive(Debug)]
167pub struct TensorEventBusV2 {
168 pub subscriptions: Vec<Subscription>,
170 pub dead_letters: Vec<TensorEvent>,
172 pub stats: BusStats,
174 pub next_sub_id: u64,
176}
177
178impl TensorEventBusV2 {
179 pub fn new() -> Self {
181 Self {
182 subscriptions: Vec::new(),
183 dead_letters: Vec::new(),
184 stats: BusStats::default(),
185 next_sub_id: 1,
186 }
187 }
188
189 pub fn subscribe(&mut self, filter: EventFilter, priority: u32, max_queue_size: usize) -> u64 {
194 let sub_id = self.next_sub_id;
195 self.next_sub_id += 1;
196
197 let effective_max = if max_queue_size == 0 {
198 100
199 } else {
200 max_queue_size
201 };
202
203 self.subscriptions
204 .push(Subscription::new(sub_id, filter, priority, effective_max));
205
206 self.subscriptions
208 .sort_unstable_by_key(|s| std::cmp::Reverse(s.priority));
209
210 sub_id
211 }
212
213 pub fn unsubscribe(&mut self, sub_id: u64) -> bool {
217 let before = self.subscriptions.len();
218 self.subscriptions.retain(|s| s.sub_id != sub_id);
219 self.subscriptions.len() < before
220 }
221
222 pub fn publish(&mut self, event: TensorEvent) {
233 self.stats.total_published += 1;
234
235 let mut any_accepted = false;
236
237 for sub in self.subscriptions.iter_mut() {
238 if !sub.filter.matches(&event) {
239 continue;
240 }
241
242 if sub.queue.len() < sub.max_queue_size {
243 sub.queue.push(event.clone());
244 sub.delivered += 1;
245 self.stats.total_delivered += 1;
246 any_accepted = true;
247 } else {
248 sub.dropped += 1;
249 self.stats.total_dropped += 1;
250 any_accepted = true;
252 }
253 }
254
255 if !any_accepted {
256 self.dead_letters.push(event);
257 }
258 }
259
260 pub fn drain(&mut self, sub_id: u64) -> Vec<TensorEvent> {
264 match self.subscriptions.iter_mut().find(|s| s.sub_id == sub_id) {
265 Some(sub) => {
266 let mut out = Vec::with_capacity(sub.queue.len());
267 core::mem::swap(&mut sub.queue, &mut out);
268 out
269 }
270 None => Vec::new(),
271 }
272 }
273
274 pub fn stats(&self) -> &BusStats {
276 &self.stats
277 }
278}
279
280impl Default for TensorEventBusV2 {
281 fn default() -> Self {
282 Self::new()
283 }
284}
285
286#[cfg(test)]
289mod tests {
290 use super::*;
291
292 fn rule_fired(rule_id: u64, bindings_count: usize) -> TensorEvent {
294 TensorEvent::RuleFired {
295 rule_id,
296 bindings_count,
297 }
298 }
299
300 fn inference_complete(session_id: u64, duration_ms: u64) -> TensorEvent {
302 TensorEvent::InferenceComplete {
303 session_id,
304 duration_ms,
305 }
306 }
307
308 fn tensor_updated(tensor_id: u64, new_version: u64) -> TensorEvent {
310 TensorEvent::TensorUpdated {
311 tensor_id,
312 new_version,
313 }
314 }
315
316 fn checkpoint_saved(path: &str, size_bytes: u64) -> TensorEvent {
318 TensorEvent::CheckpointSaved {
319 path: path.to_string(),
320 size_bytes,
321 }
322 }
323
324 fn gradient_applied(step: u64, loss: f64) -> TensorEvent {
326 TensorEvent::GradientApplied { step, loss }
327 }
328
329 #[test]
332 fn subscribe_returns_unique_sub_ids() {
333 let mut bus = TensorEventBusV2::new();
334 let id1 = bus.subscribe(EventFilter::All, 10, 100);
335 let id2 = bus.subscribe(EventFilter::All, 10, 100);
336 let id3 = bus.subscribe(EventFilter::All, 10, 100);
337 assert_ne!(id1, id2);
338 assert_ne!(id2, id3);
339 assert_ne!(id1, id3);
340 }
341
342 #[test]
345 fn subscribe_first_id_nonzero() {
346 let mut bus = TensorEventBusV2::new();
347 let id = bus.subscribe(EventFilter::All, 5, 100);
348 assert!(id > 0, "sub_id must be nonzero");
349 }
350
351 #[test]
354 fn publish_routes_to_subscriber_queue() {
355 let mut bus = TensorEventBusV2::new();
356 let id = bus.subscribe(EventFilter::All, 10, 100);
357 let event = rule_fired(42, 3);
358 bus.publish(event.clone());
359 let drained = bus.drain(id);
360 assert_eq!(drained.len(), 1);
361 assert_eq!(drained[0], event);
362 }
363
364 #[test]
367 fn filter_all_receives_every_variant() {
368 let mut bus = TensorEventBusV2::new();
369 let id = bus.subscribe(EventFilter::All, 5, 100);
370 bus.publish(rule_fired(1, 0));
371 bus.publish(inference_complete(2, 50));
372 bus.publish(tensor_updated(3, 7));
373 bus.publish(checkpoint_saved("/tmp/ckpt", 1024));
374 bus.publish(gradient_applied(10, 0.25));
375 let drained = bus.drain(id);
376 assert_eq!(drained.len(), 5);
377 }
378
379 #[test]
382 fn filter_rule_events_only() {
383 let mut bus = TensorEventBusV2::new();
384 let id = bus.subscribe(EventFilter::RuleEventsOnly, 5, 100);
385 bus.publish(rule_fired(1, 2));
386 bus.publish(inference_complete(2, 10));
387 bus.publish(rule_fired(3, 5));
388 let drained = bus.drain(id);
389 assert_eq!(drained.len(), 2);
390 for e in &drained {
391 assert!(matches!(e, TensorEvent::RuleFired { .. }));
392 }
393 }
394
395 #[test]
398 fn filter_inference_events_only() {
399 let mut bus = TensorEventBusV2::new();
400 let id = bus.subscribe(EventFilter::InferenceEventsOnly, 5, 100);
401 bus.publish(rule_fired(1, 0));
402 bus.publish(inference_complete(7, 100));
403 bus.publish(tensor_updated(3, 2));
404 let drained = bus.drain(id);
405 assert_eq!(drained.len(), 1);
406 assert!(matches!(
407 drained[0],
408 TensorEvent::InferenceComplete { session_id: 7, .. }
409 ));
410 }
411
412 #[test]
415 fn filter_tensor_events_only() {
416 let mut bus = TensorEventBusV2::new();
417 let id = bus.subscribe(EventFilter::TensorEventsOnly, 5, 100);
418 bus.publish(rule_fired(1, 0));
419 bus.publish(tensor_updated(10, 5));
420 bus.publish(gradient_applied(1, 0.1));
421 let drained = bus.drain(id);
422 assert_eq!(drained.len(), 1);
423 assert!(matches!(
424 drained[0],
425 TensorEvent::TensorUpdated { tensor_id: 10, .. }
426 ));
427 }
428
429 #[test]
432 fn priority_ordering_high_to_low() {
433 let mut bus = TensorEventBusV2::new();
435 let id_low = bus.subscribe(EventFilter::All, 1, 100);
437 let id_high = bus.subscribe(EventFilter::All, 99, 100);
438
439 bus.publish(rule_fired(1, 0));
440
441 assert_eq!(bus.drain(id_high).len(), 1);
443 assert_eq!(bus.drain(id_low).len(), 1);
444
445 assert!(bus.subscriptions[0].priority >= bus.subscriptions[1].priority);
447 }
448
449 #[test]
452 fn queue_full_drops_excess_events() {
453 let max = 3_usize;
454 let mut bus = TensorEventBusV2::new();
455 let id = bus.subscribe(EventFilter::All, 5, max);
456
457 for i in 0..5_u64 {
458 bus.publish(rule_fired(i, 0));
459 }
460
461 let sub = bus
462 .subscriptions
463 .iter()
464 .find(|s| s.sub_id == id)
465 .expect("test: should succeed");
466 assert_eq!(sub.queue.len(), max);
467 assert_eq!(sub.dropped, 2);
468 assert_eq!(sub.delivered, max as u64);
469 }
470
471 #[test]
474 fn drain_clears_queue() {
475 let mut bus = TensorEventBusV2::new();
476 let id = bus.subscribe(EventFilter::All, 5, 100);
477 bus.publish(rule_fired(1, 0));
478 bus.publish(rule_fired(2, 0));
479 assert_eq!(bus.drain(id).len(), 2);
480 assert!(bus.drain(id).is_empty());
482 }
483
484 #[test]
487 fn unsubscribe_removes_subscription() {
488 let mut bus = TensorEventBusV2::new();
489 let id = bus.subscribe(EventFilter::All, 5, 100);
490 assert!(bus.unsubscribe(id));
491 assert!(!bus.unsubscribe(id));
493 assert!(bus.subscriptions.is_empty());
494 }
495
496 #[test]
499 fn unsubscribe_nonexistent_returns_false() {
500 let mut bus = TensorEventBusV2::new();
501 assert!(!bus.unsubscribe(999));
502 }
503
504 #[test]
507 fn dead_letters_with_no_subscribers() {
508 let mut bus = TensorEventBusV2::new();
509 let event = rule_fired(1, 0);
510 bus.publish(event.clone());
511 assert_eq!(bus.dead_letters.len(), 1);
512 assert_eq!(bus.dead_letters[0], event);
513 }
514
515 #[test]
518 fn dead_letters_when_no_filter_matches() {
519 let mut bus = TensorEventBusV2::new();
520 let _id = bus.subscribe(EventFilter::RuleEventsOnly, 5, 100);
522 bus.publish(inference_complete(1, 50));
524 assert_eq!(bus.dead_letters.len(), 1);
525 }
526
527 #[test]
530 fn delivery_rate_perfect() {
531 let mut bus = TensorEventBusV2::new();
532 let _id = bus.subscribe(EventFilter::All, 5, 100);
533 bus.publish(rule_fired(1, 0));
534 bus.publish(inference_complete(2, 10));
535 let stats = bus.stats();
536 assert!((stats.delivery_rate() - 1.0_f64).abs() < f64::EPSILON);
538 }
539
540 #[test]
543 fn delivery_rate_empty_bus() {
544 let bus = TensorEventBusV2::new();
545 assert_eq!(bus.stats().delivery_rate(), 1.0);
546 }
547
548 #[test]
551 fn stats_total_dropped_accumulates() {
552 let mut bus = TensorEventBusV2::new();
553 let _id1 = bus.subscribe(EventFilter::All, 5, 1);
555 let _id2 = bus.subscribe(EventFilter::All, 5, 1);
556
557 bus.publish(rule_fired(1, 0));
559 bus.publish(rule_fired(2, 0));
560 bus.publish(rule_fired(3, 0));
561
562 assert_eq!(bus.stats().total_dropped, 4);
564 }
565
566 #[test]
569 fn multiple_subscribers_all_receive() {
570 let mut bus = TensorEventBusV2::new();
571 let id1 = bus.subscribe(EventFilter::All, 10, 100);
572 let id2 = bus.subscribe(EventFilter::All, 5, 100);
573 let id3 = bus.subscribe(EventFilter::All, 1, 100);
574
575 bus.publish(tensor_updated(7, 3));
576
577 assert_eq!(bus.drain(id1).len(), 1);
578 assert_eq!(bus.drain(id2).len(), 1);
579 assert_eq!(bus.drain(id3).len(), 1);
580 }
581
582 #[test]
585 fn partial_subscriber_match() {
586 let mut bus = TensorEventBusV2::new();
587 let id_rule = bus.subscribe(EventFilter::RuleEventsOnly, 10, 100);
588 let id_all = bus.subscribe(EventFilter::All, 5, 100);
589
590 bus.publish(gradient_applied(1, 0.5));
592
593 assert!(bus.drain(id_rule).is_empty());
594 assert_eq!(bus.drain(id_all).len(), 1);
595 assert!(bus.dead_letters.is_empty());
597 }
598
599 #[test]
602 fn checkpoint_saved_event_round_trip() {
603 let mut bus = TensorEventBusV2::new();
604 let id = bus.subscribe(EventFilter::All, 5, 100);
605 let event = checkpoint_saved("/tmp/model.safetensors", 4096);
606 bus.publish(event.clone());
607 let drained = bus.drain(id);
608 assert_eq!(drained.len(), 1);
609 assert_eq!(drained[0], event);
610 }
611
612 #[test]
615 fn gradient_applied_event_round_trip() {
616 let mut bus = TensorEventBusV2::new();
617 let id = bus.subscribe(EventFilter::All, 5, 100);
618 let event = gradient_applied(42, 0.001_f64);
619 bus.publish(event.clone());
620 let drained = bus.drain(id);
621 assert_eq!(drained.len(), 1);
622 if let TensorEvent::GradientApplied { step, loss } = &drained[0] {
623 assert_eq!(*step, 42);
624 assert!((*loss - 0.001_f64).abs() < 1e-12);
625 } else {
626 panic!("unexpected event variant");
627 }
628 }
629
630 #[test]
633 fn stats_consistency() {
634 let mut bus = TensorEventBusV2::new();
635 let _id = bus.subscribe(EventFilter::All, 5, 100);
636 for i in 0..10_u64 {
637 bus.publish(rule_fired(i, 0));
638 }
639 let s = bus.stats();
640 assert_eq!(s.total_published, 10);
641 assert_eq!(s.total_delivered, 10);
642 assert_eq!(s.total_dropped, 0);
643 }
644
645 #[test]
648 fn drain_unknown_sub_id_returns_empty() {
649 let mut bus = TensorEventBusV2::new();
650 assert!(bus.drain(9999).is_empty());
651 }
652
653 #[test]
656 fn default_queue_size_applied_when_zero() {
657 let mut bus = TensorEventBusV2::new();
658 let id = bus.subscribe(EventFilter::All, 5, 0);
659 let sub = bus
660 .subscriptions
661 .iter()
662 .find(|s| s.sub_id == id)
663 .expect("test: should succeed");
664 assert_eq!(sub.max_queue_size, 100);
665 }
666}