1#[derive(Clone, Copy, Debug, PartialEq, Eq)]
6pub enum FlowState {
7 Running,
9 Throttled,
11 Paused,
13 Draining,
15}
16
17#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
19pub enum FlowPriority {
20 Low = 0,
21 Normal = 1,
22 High = 2,
23 Critical = 3,
24}
25
26#[derive(Clone, Debug, PartialEq, Eq)]
28pub struct FlowItem {
29 pub item_id: u64,
30 pub tensor_id: u64,
31 pub priority: FlowPriority,
32 pub size_bytes: u64,
33 pub enqueued_at_tick: u64,
34}
35
36#[derive(Clone, Debug)]
38pub struct FlowControllerConfig {
39 pub max_queue_size: usize,
41 pub max_bytes_per_tick: u64,
43 pub backpressure_threshold: f64,
45}
46
47impl Default for FlowControllerConfig {
48 fn default() -> Self {
49 Self {
50 max_queue_size: 256,
51 max_bytes_per_tick: 1_048_576,
52 backpressure_threshold: 0.8,
53 }
54 }
55}
56
57#[derive(Clone, Debug, Default)]
59pub struct FlowStats {
60 pub total_admitted: u64,
61 pub total_dropped: u64,
62 pub total_processed: u64,
63 pub total_bytes_processed: u64,
64}
65
66impl FlowStats {
67 pub fn drop_rate(&self) -> f64 {
69 let total = self.total_admitted + self.total_dropped;
70 if total == 0 {
71 0.0
72 } else {
73 self.total_dropped as f64 / total as f64
74 }
75 }
76}
77
78pub struct TensorFlowController {
83 queue: Vec<FlowItem>,
84 state: FlowState,
85 config: FlowControllerConfig,
86 stats: FlowStats,
87}
88
89impl TensorFlowController {
90 pub fn new(config: FlowControllerConfig) -> Self {
92 Self {
93 queue: Vec::new(),
94 state: FlowState::Running,
95 config,
96 stats: FlowStats::default(),
97 }
98 }
99
100 pub fn admit(&mut self, item: FlowItem) -> bool {
104 if self.state == FlowState::Paused || self.state == FlowState::Draining {
106 self.stats.total_dropped += 1;
107 return false;
108 }
109
110 if self.queue.len() >= self.config.max_queue_size {
112 self.stats.total_dropped += 1;
113 return false;
114 }
115
116 let pos = self.queue.partition_point(|existing| {
118 existing.priority > item.priority
119 || (existing.priority == item.priority
120 && existing.enqueued_at_tick <= item.enqueued_at_tick)
121 });
122 self.queue.insert(pos, item);
123 self.stats.total_admitted += 1;
124
125 self.state = self.compute_state_after_admit();
127 true
128 }
129
130 pub fn process_tick(&mut self) -> Vec<FlowItem> {
134 if self.state == FlowState::Paused {
135 return Vec::new();
136 }
137
138 let mut processed = Vec::new();
139 let mut bytes_remaining = self.config.max_bytes_per_tick;
140
141 while let Some(front) = self.queue.first() {
142 if front.size_bytes > bytes_remaining {
143 break;
144 }
145 bytes_remaining -= front.size_bytes;
146 let item = self.queue.remove(0);
147 self.stats.total_processed += 1;
148 self.stats.total_bytes_processed += item.size_bytes;
149 processed.push(item);
150 }
151
152 self.state = self.compute_state_after_tick();
154 processed
155 }
156
157 pub fn pause(&mut self) {
159 self.state = FlowState::Paused;
160 }
161
162 pub fn resume(&mut self) {
164 if self.state == FlowState::Paused {
165 self.state = self.fill_based_state();
166 }
167 }
168
169 pub fn drain(&mut self) {
171 self.state = FlowState::Draining;
172 }
173
174 pub fn queue_len(&self) -> usize {
176 self.queue.len()
177 }
178
179 pub fn stats(&self) -> &FlowStats {
181 &self.stats
182 }
183
184 fn compute_state_after_admit(&self) -> FlowState {
188 let fill = self.queue.len() as f64 / self.config.max_queue_size as f64;
189 if fill >= self.config.backpressure_threshold {
190 FlowState::Throttled
191 } else {
192 FlowState::Running
193 }
194 }
195
196 fn compute_state_after_tick(&self) -> FlowState {
198 if self.state == FlowState::Draining && self.queue.is_empty() {
199 return FlowState::Paused;
200 }
201 self.fill_based_state()
202 }
203
204 fn fill_based_state(&self) -> FlowState {
206 let fill = self.queue.len() as f64 / self.config.max_queue_size as f64;
207 if fill >= self.config.backpressure_threshold {
208 FlowState::Throttled
209 } else {
210 FlowState::Running
211 }
212 }
213}
214
215#[cfg(test)]
218mod tests {
219 use super::*;
220
221 fn default_controller() -> TensorFlowController {
222 TensorFlowController::new(FlowControllerConfig::default())
223 }
224
225 fn make_item(item_id: u64, priority: FlowPriority, size_bytes: u64, tick: u64) -> FlowItem {
226 FlowItem {
227 item_id,
228 tensor_id: item_id * 10,
229 priority,
230 size_bytes,
231 enqueued_at_tick: tick,
232 }
233 }
234
235 #[test]
237 fn test_new_starts_running() {
238 let ctrl = default_controller();
239 assert_eq!(ctrl.state, FlowState::Running);
240 assert_eq!(ctrl.queue_len(), 0);
241 }
242
243 #[test]
245 fn test_admit_running_succeeds() {
246 let mut ctrl = default_controller();
247 let admitted = ctrl.admit(make_item(1, FlowPriority::Normal, 100, 0));
248 assert!(admitted);
249 assert_eq!(ctrl.queue_len(), 1);
250 }
251
252 #[test]
254 fn test_admit_paused_drops() {
255 let mut ctrl = default_controller();
256 ctrl.pause();
257 let admitted = ctrl.admit(make_item(1, FlowPriority::Normal, 100, 0));
258 assert!(!admitted);
259 assert_eq!(ctrl.stats().total_dropped, 1);
260 assert_eq!(ctrl.queue_len(), 0);
261 }
262
263 #[test]
265 fn test_admit_draining_drops() {
266 let mut ctrl = default_controller();
267 ctrl.drain();
268 let admitted = ctrl.admit(make_item(1, FlowPriority::Normal, 100, 0));
269 assert!(!admitted);
270 assert_eq!(ctrl.stats().total_dropped, 1);
271 assert_eq!(ctrl.queue_len(), 0);
272 }
273
274 #[test]
276 fn test_admit_at_capacity_drops() {
277 let config = FlowControllerConfig {
278 max_queue_size: 2,
279 max_bytes_per_tick: 1_048_576,
280 backpressure_threshold: 0.99,
281 };
282 let mut ctrl = TensorFlowController::new(config);
283 assert!(ctrl.admit(make_item(1, FlowPriority::Normal, 100, 0)));
284 assert!(ctrl.admit(make_item(2, FlowPriority::Normal, 100, 1)));
285 let admitted = ctrl.admit(make_item(3, FlowPriority::Normal, 100, 2));
286 assert!(!admitted);
287 assert_eq!(ctrl.stats().total_dropped, 1);
288 }
289
290 #[test]
292 fn test_backpressure_threshold_throttled() {
293 let config = FlowControllerConfig {
294 max_queue_size: 10,
295 max_bytes_per_tick: 1_048_576,
296 backpressure_threshold: 0.8,
297 };
298 let mut ctrl = TensorFlowController::new(config);
299 for i in 0..8 {
301 ctrl.admit(make_item(i, FlowPriority::Normal, 100, i));
302 }
303 assert_eq!(ctrl.state, FlowState::Throttled);
304 }
305
306 #[test]
308 fn test_below_threshold_stays_running() {
309 let config = FlowControllerConfig {
310 max_queue_size: 10,
311 max_bytes_per_tick: 1_048_576,
312 backpressure_threshold: 0.8,
313 };
314 let mut ctrl = TensorFlowController::new(config);
315 for i in 0..7 {
317 ctrl.admit(make_item(i, FlowPriority::Normal, 100, i));
318 }
319 assert_eq!(ctrl.state, FlowState::Running);
320 }
321
322 #[test]
324 fn test_process_tick_respects_byte_budget() {
325 let config = FlowControllerConfig {
326 max_queue_size: 256,
327 max_bytes_per_tick: 300,
328 backpressure_threshold: 0.8,
329 };
330 let mut ctrl = TensorFlowController::new(config);
331 ctrl.admit(make_item(1, FlowPriority::Normal, 100, 0));
332 ctrl.admit(make_item(2, FlowPriority::Normal, 100, 1));
333 ctrl.admit(make_item(3, FlowPriority::Normal, 100, 2));
334 ctrl.admit(make_item(4, FlowPriority::Normal, 100, 3));
335 let processed = ctrl.process_tick();
336 assert_eq!(processed.len(), 3);
338 assert_eq!(ctrl.queue_len(), 1);
339 }
340
341 #[test]
343 fn test_process_tick_paused_returns_empty() {
344 let mut ctrl = default_controller();
345 ctrl.admit(make_item(1, FlowPriority::Normal, 100, 0));
346 ctrl.pause();
347 let processed = ctrl.process_tick();
348 assert!(processed.is_empty());
349 assert_eq!(ctrl.queue_len(), 1);
350 }
351
352 #[test]
354 fn test_process_tick_priority_order() {
355 let config = FlowControllerConfig {
356 max_queue_size: 256,
357 max_bytes_per_tick: 200,
358 backpressure_threshold: 0.8,
359 };
360 let mut ctrl = TensorFlowController::new(config);
361 ctrl.admit(make_item(1, FlowPriority::Low, 100, 0));
363 ctrl.admit(make_item(2, FlowPriority::Critical, 100, 1));
364 let processed = ctrl.process_tick();
365 assert_eq!(processed.len(), 2);
366 assert_eq!(processed[0].priority, FlowPriority::Critical);
368 assert_eq!(processed[1].priority, FlowPriority::Low);
369 }
370
371 #[test]
373 fn test_process_tick_fifo_within_priority() {
374 let config = FlowControllerConfig {
375 max_queue_size: 256,
376 max_bytes_per_tick: 300,
377 backpressure_threshold: 0.8,
378 };
379 let mut ctrl = TensorFlowController::new(config);
380 ctrl.admit(make_item(10, FlowPriority::Normal, 100, 5));
381 ctrl.admit(make_item(20, FlowPriority::Normal, 100, 3));
382 ctrl.admit(make_item(30, FlowPriority::Normal, 100, 7));
383 let processed = ctrl.process_tick();
384 assert_eq!(processed.len(), 3);
385 assert_eq!(processed[0].enqueued_at_tick, 3);
386 assert_eq!(processed[1].enqueued_at_tick, 5);
387 assert_eq!(processed[2].enqueued_at_tick, 7);
388 }
389
390 #[test]
392 fn test_process_tick_updates_total_processed() {
393 let mut ctrl = default_controller();
394 ctrl.admit(make_item(1, FlowPriority::Normal, 100, 0));
395 ctrl.admit(make_item(2, FlowPriority::Normal, 100, 1));
396 ctrl.process_tick();
397 assert_eq!(ctrl.stats().total_processed, 2);
398 }
399
400 #[test]
402 fn test_process_tick_updates_total_bytes() {
403 let mut ctrl = default_controller();
404 ctrl.admit(make_item(1, FlowPriority::Normal, 400, 0));
405 ctrl.admit(make_item(2, FlowPriority::Normal, 600, 1));
406 ctrl.process_tick();
407 assert_eq!(ctrl.stats().total_bytes_processed, 1000);
408 }
409
410 #[test]
412 fn test_draining_empty_queue_becomes_paused() {
413 let mut ctrl = default_controller();
414 ctrl.admit(make_item(1, FlowPriority::Normal, 100, 0));
415 ctrl.drain();
416 assert_eq!(ctrl.state, FlowState::Draining);
417 ctrl.process_tick();
418 assert_eq!(ctrl.state, FlowState::Paused);
419 }
420
421 #[test]
423 fn test_pause_sets_paused() {
424 let mut ctrl = default_controller();
425 ctrl.pause();
426 assert_eq!(ctrl.state, FlowState::Paused);
427 }
428
429 #[test]
431 fn test_resume_from_paused_sets_running() {
432 let mut ctrl = default_controller();
433 ctrl.pause();
434 ctrl.resume();
435 assert_eq!(ctrl.state, FlowState::Running);
436 }
437
438 #[test]
440 fn test_resume_from_paused_heavy_queue_sets_throttled() {
441 let config = FlowControllerConfig {
442 max_queue_size: 10,
443 max_bytes_per_tick: 1_048_576,
444 backpressure_threshold: 0.5,
445 };
446 let mut ctrl = TensorFlowController::new(config);
447 for i in 0..5 {
449 ctrl.admit(make_item(i, FlowPriority::Normal, 100, i));
450 }
451 ctrl.pause();
452 ctrl.resume();
453 assert_eq!(ctrl.state, FlowState::Throttled);
454 }
455
456 #[test]
458 fn test_drain_sets_draining() {
459 let mut ctrl = default_controller();
460 ctrl.drain();
461 assert_eq!(ctrl.state, FlowState::Draining);
462 }
463
464 #[test]
466 fn test_drop_rate_zero_when_nothing_dropped() {
467 let ctrl = default_controller();
468 assert_eq!(ctrl.stats().drop_rate(), 0.0);
469 }
470
471 #[test]
473 fn test_drop_rate_correct() {
474 let config = FlowControllerConfig {
475 max_queue_size: 1,
476 max_bytes_per_tick: 1_048_576,
477 backpressure_threshold: 0.99,
478 };
479 let mut ctrl = TensorFlowController::new(config);
480 ctrl.admit(make_item(1, FlowPriority::Normal, 100, 0)); ctrl.admit(make_item(2, FlowPriority::Normal, 100, 1)); let rate = ctrl.stats().drop_rate();
484 assert!((rate - 0.5).abs() < f64::EPSILON);
485 }
486
487 #[test]
489 fn test_total_admitted_increments() {
490 let mut ctrl = default_controller();
491 ctrl.admit(make_item(1, FlowPriority::Normal, 100, 0));
492 ctrl.admit(make_item(2, FlowPriority::Normal, 100, 1));
493 ctrl.admit(make_item(3, FlowPriority::Normal, 100, 2));
494 assert_eq!(ctrl.stats().total_admitted, 3);
495 }
496
497 #[test]
499 fn test_total_dropped_increments() {
500 let mut ctrl = default_controller();
501 ctrl.pause();
502 ctrl.admit(make_item(1, FlowPriority::Normal, 100, 0));
503 ctrl.admit(make_item(2, FlowPriority::Normal, 100, 1));
504 assert_eq!(ctrl.stats().total_dropped, 2);
505 }
506
507 #[test]
509 fn test_queue_len_reflects_size() {
510 let mut ctrl = default_controller();
511 assert_eq!(ctrl.queue_len(), 0);
512 ctrl.admit(make_item(1, FlowPriority::Normal, 100, 0));
513 assert_eq!(ctrl.queue_len(), 1);
514 ctrl.admit(make_item(2, FlowPriority::Normal, 100, 1));
515 assert_eq!(ctrl.queue_len(), 2);
516 ctrl.process_tick();
517 assert_eq!(ctrl.queue_len(), 0);
518 }
519
520 #[test]
522 fn test_mixed_priority_ordering() {
523 let config = FlowControllerConfig {
524 max_queue_size: 256,
525 max_bytes_per_tick: 1_048_576,
526 backpressure_threshold: 0.8,
527 };
528 let mut ctrl = TensorFlowController::new(config);
529 ctrl.admit(make_item(1, FlowPriority::Low, 10, 0));
530 ctrl.admit(make_item(2, FlowPriority::High, 10, 1));
531 ctrl.admit(make_item(3, FlowPriority::Normal, 10, 2));
532 ctrl.admit(make_item(4, FlowPriority::Critical, 10, 3));
533 let processed = ctrl.process_tick();
534 assert_eq!(processed[0].priority, FlowPriority::Critical);
535 assert_eq!(processed[1].priority, FlowPriority::High);
536 assert_eq!(processed[2].priority, FlowPriority::Normal);
537 assert_eq!(processed[3].priority, FlowPriority::Low);
538 }
539
540 #[test]
542 fn test_draining_processes_existing_items() {
543 let mut ctrl = default_controller();
544 ctrl.admit(make_item(1, FlowPriority::Normal, 100, 0));
545 ctrl.admit(make_item(2, FlowPriority::Normal, 100, 1));
546 ctrl.drain();
547 let processed = ctrl.process_tick();
548 assert_eq!(processed.len(), 2);
549 assert_eq!(ctrl.stats().total_processed, 2);
550 }
551}