Skip to main content

kcode_k1_audio_classification_driver/
lib.rs

1pub use kcode_k1_audio_fragment_runner::FragmentId;
2
3use kcode_k1_audio_fragment_runner as runner;
4use kcode_k1_audio_fragment_transactions::{self as transactions, FragmentStageV1};
5use kcode_k1_objects::K1Objects;
6use kcode_k1_peering::K1Peering;
7use kcode_speaker_v3_analysis::Analyzer;
8use std::collections::{HashMap, VecDeque};
9use std::future::Future;
10use std::pin::Pin;
11use std::sync::atomic::{AtomicBool, Ordering};
12use std::sync::{Arc, Mutex};
13use std::thread;
14
15pub const MAX_ACTIVE_ATTEMPTS: usize = 8;
16pub type EngineFuture<'a> = Pin<Box<dyn Future<Output = Result<(), String>> + 'a>>;
17
18pub trait FragmentEngine: Send + Sync + 'static {
19    fn run<'a>(
20        &'a self,
21        fragment_id: FragmentId,
22        ogg_bytes: &'a [u8],
23        is_active: &'a (dyn Fn() -> bool + Send + Sync),
24    ) -> EngineFuture<'a>;
25}
26
27#[derive(Clone, Copy, Debug, Eq, PartialEq)]
28pub enum StartOutcome {
29    Started,
30    Pending,
31    AlreadyActive,
32}
33
34pub struct AudioClassificationDriver {
35    inner: Arc<Inner>,
36}
37
38impl AudioClassificationDriver {
39    pub fn open(peering: Arc<K1Peering>, objects: Arc<K1Objects>, analyzer: Analyzer) -> Self {
40        let engine: Arc<dyn FragmentEngine> = Arc::new(RunnerEngine {
41            peering: peering.clone(),
42            analyzer: Arc::new(analyzer),
43        });
44        Self::with_engine(peering, objects, engine)
45    }
46
47    pub fn with_engine(
48        peering: Arc<K1Peering>,
49        objects: Arc<K1Objects>,
50        engine: Arc<dyn FragmentEngine>,
51    ) -> Self {
52        Self::with_resources(Arc::new(LiveResources { peering, objects }), engine)
53    }
54
55    pub fn start(&self, id: FragmentId) -> Result<StartOutcome, String> {
56        let (outcome, lane) = self.inner.reserve(id)?;
57        if let Some(lane) = lane {
58            self.inner.launch(lane)?;
59        }
60        Ok(outcome)
61    }
62
63    pub fn abort(&self, id: FragmentId) {
64        let mut state = lock(&self.inner.state);
65        if let Some(entry) = state.current.remove(&id) {
66            entry.active.store(false, Ordering::Release);
67        }
68        state.pending.retain(|(pending, _)| *pending != id);
69    }
70
71    pub fn ensure_healthy(&self) -> Result<(), String> {
72        match &lock(&self.inner.state).fault {
73            Some(error) => Err(format!("audio classification driver is unhealthy: {error}")),
74            None => Ok(()),
75        }
76    }
77
78    pub fn shutdown(&self) {
79        lock(&self.inner.state).stop(None);
80    }
81
82    fn with_resources(
83        resources: Arc<dyn FragmentResources>,
84        engine: Arc<dyn FragmentEngine>,
85    ) -> Self {
86        let state = State {
87            accepting: true,
88            fault: None,
89            next_generation: 0,
90            lanes: 0,
91            current: HashMap::new(),
92            pending: VecDeque::new(),
93        };
94        Self {
95            inner: Arc::new(Inner {
96                resources,
97                engine,
98                state: Mutex::new(state),
99            }),
100        }
101    }
102}
103
104impl Drop for AudioClassificationDriver {
105    fn drop(&mut self) {
106        self.shutdown();
107    }
108}
109
110struct RunnerEngine {
111    peering: Arc<K1Peering>,
112    analyzer: Arc<Analyzer>,
113}
114
115impl FragmentEngine for RunnerEngine {
116    fn run<'a>(
117        &'a self,
118        id: FragmentId,
119        bytes: &'a [u8],
120        active: &'a (dyn Fn() -> bool + Send + Sync),
121    ) -> EngineFuture<'a> {
122        Box::pin(runner::run_while_active(
123            &self.analyzer,
124            &self.peering,
125            id,
126            bytes,
127            active,
128        ))
129    }
130}
131
132trait FragmentResources: Send + Sync + 'static {
133    fn load(&self, id: FragmentId) -> Result<Option<(String, Vec<u8>)>, String>;
134    fn fail_queue(&self, id: FragmentId, error: String) -> Result<(), String>;
135}
136
137struct LiveResources {
138    peering: Arc<K1Peering>,
139    objects: Arc<K1Objects>,
140}
141
142impl FragmentResources for LiveResources {
143    fn load(&self, id: FragmentId) -> Result<Option<(String, Vec<u8>)>, String> {
144        self.objects
145            .load(id)
146            .map(|object| object.map(|object| (object.file_type, object.data)))
147    }
148
149    fn fail_queue(&self, id: FragmentId, error: String) -> Result<(), String> {
150        transactions::submit_failure(&self.peering, id, FragmentStageV1::Queue, None, error)
151            .map(|_| ())
152    }
153}
154
155struct State {
156    accepting: bool,
157    fault: Option<String>,
158    next_generation: u64,
159    lanes: usize,
160    current: HashMap<FragmentId, Entry>,
161    pending: VecDeque<(FragmentId, u64)>,
162}
163
164struct Entry {
165    generation: u64,
166    active: Arc<AtomicBool>,
167}
168
169struct Lane {
170    id: FragmentId,
171    generation: u64,
172    active: Arc<AtomicBool>,
173}
174
175impl State {
176    fn stop(&mut self, fault: Option<String>) {
177        self.accepting = false;
178        self.pending.clear();
179        for entry in self.current.values() {
180            entry.active.store(false, Ordering::Release);
181        }
182        self.current.clear();
183        if self.fault.is_none() {
184            self.fault = fault;
185        }
186    }
187
188    fn next_lane(&mut self) -> Option<Lane> {
189        if !self.accepting || self.lanes == MAX_ACTIVE_ATTEMPTS {
190            return None;
191        }
192        let (id, generation) = self.pending.pop_front()?;
193        let entry = self
194            .current
195            .get(&id)
196            .filter(|entry| entry.generation == generation)?;
197        self.lanes += 1;
198        Some(Lane {
199            id,
200            generation,
201            active: entry.active.clone(),
202        })
203    }
204}
205
206struct Inner {
207    resources: Arc<dyn FragmentResources>,
208    engine: Arc<dyn FragmentEngine>,
209    state: Mutex<State>,
210}
211
212impl Inner {
213    fn reserve(&self, id: FragmentId) -> Result<(StartOutcome, Option<Lane>), String> {
214        let mut state = lock(&self.state);
215        if let Some(error) = &state.fault {
216            return Err(format!("audio classification driver is unhealthy: {error}"));
217        }
218        if !state.accepting {
219            return Err("audio classification driver is shut down".into());
220        }
221        if state.current.contains_key(&id) {
222            return Ok((StartOutcome::AlreadyActive, None));
223        }
224        state.next_generation = state
225            .next_generation
226            .checked_add(1)
227            .ok_or_else(|| "audio classification generation overflow".to_owned())?;
228        let generation = state.next_generation;
229        let active = Arc::new(AtomicBool::new(true));
230        state.current.insert(
231            id,
232            Entry {
233                generation,
234                active: active.clone(),
235            },
236        );
237        if state.lanes == MAX_ACTIVE_ATTEMPTS {
238            state.pending.push_back((id, generation));
239            return Ok((StartOutcome::Pending, None));
240        }
241        state.lanes += 1;
242        Ok((
243            StartOutcome::Started,
244            Some(Lane {
245                id,
246                generation,
247                active,
248            }),
249        ))
250    }
251
252    fn launch(self: &Arc<Self>, lane: Lane) -> Result<(), String> {
253        let id = lane.id;
254        let generation = lane.generation;
255        let inner = self.clone();
256        match thread::Builder::new()
257            .name("k1-audio-fragment".to_owned())
258            .spawn(move || inner.run_lane(lane))
259        {
260            Ok(_) => Ok(()),
261            Err(error) => {
262                let error = format!("start audio classification lane: {error}");
263                self.finished(id, generation, Err(error.clone()));
264                Err(error)
265            }
266        }
267    }
268
269    fn run_lane(self: Arc<Self>, lane: Lane) {
270        let result = tokio::runtime::Builder::new_current_thread()
271            .build()
272            .map_err(|error| format!("create audio classification runtime: {error}"))
273            .and_then(|runtime| {
274                runtime.block_on(run_attempt(
275                    self.resources.as_ref(),
276                    self.engine.as_ref(),
277                    lane.id,
278                    lane.active.clone(),
279                ))
280            });
281        self.finished(lane.id, lane.generation, result);
282    }
283
284    fn finished(self: &Arc<Self>, id: FragmentId, generation: u64, result: Result<(), String>) {
285        let next = {
286            let mut state = lock(&self.state);
287            state.lanes = state.lanes.saturating_sub(1);
288            let active = state.current.get(&id).and_then(|entry| {
289                (entry.generation == generation).then(|| entry.active.load(Ordering::Acquire))
290            });
291            if active.is_some() {
292                state.current.remove(&id);
293            }
294            if let Some(error) = result.err().filter(|_| active == Some(true)) {
295                state.stop(Some(format!("runner persistence failure: {error}")));
296                None
297            } else {
298                state.next_lane()
299            }
300        };
301        if let Some(lane) = next {
302            let _ = self.launch(lane);
303        }
304    }
305}
306
307async fn run_attempt(
308    resources: &dyn FragmentResources,
309    engine: &dyn FragmentEngine,
310    id: FragmentId,
311    active: Arc<AtomicBool>,
312) -> Result<(), String> {
313    let object = match resources.load(id) {
314        Ok(Some(object)) if object.0 == "audio/ogg" => object,
315        Ok(Some(_)) => return fail_if_active(resources, id, &active, "wrong Object media type"),
316        Ok(None) => return fail_if_active(resources, id, &active, "audio Object is unavailable"),
317        Err(error) => {
318            return fail_if_active(
319                resources,
320                id,
321                &active,
322                &format!("load audio Object: {error}"),
323            );
324        }
325    };
326    let is_active = || active.load(Ordering::Acquire);
327    if is_active() {
328        engine.run(id, &object.1, &is_active).await?;
329    }
330    Ok(())
331}
332
333fn fail_if_active(
334    resources: &dyn FragmentResources,
335    id: FragmentId,
336    active: &AtomicBool,
337    error: &str,
338) -> Result<(), String> {
339    if active.load(Ordering::Acquire) {
340        resources.fail_queue(id, error.to_owned())?;
341    }
342    Ok(())
343}
344
345fn lock<T>(mutex: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
346    mutex.lock().unwrap_or_else(|error| error.into_inner())
347}
348
349#[cfg(test)]
350mod tests {
351    use super::*;
352    use std::sync::atomic::{AtomicUsize, Ordering as AtomicOrdering};
353    use std::time::{Duration, Instant};
354
355    type Action = (Option<Arc<AtomicBool>>, Result<(), String>);
356
357    #[derive(Clone)]
358    enum Load {
359        Missing,
360        Wrong,
361        Error,
362        Wait(Arc<AtomicBool>),
363    }
364
365    #[derive(Default)]
366    struct Harness {
367        actions: Mutex<HashMap<FragmentId, VecDeque<Action>>>,
368        loads: Mutex<HashMap<FragmentId, Load>>,
369        load_calls: AtomicUsize,
370        failures: AtomicUsize,
371        polls: AtomicUsize,
372    }
373
374    impl FragmentEngine for Harness {
375        fn run<'a>(
376            &'a self,
377            id: FragmentId,
378            _bytes: &'a [u8],
379            _active: &'a (dyn Fn() -> bool + Send + Sync),
380        ) -> EngineFuture<'a> {
381            let (gate, result) = lock(&self.actions)
382                .get_mut(&id)
383                .and_then(VecDeque::pop_front)
384                .unwrap_or((None, Ok(())));
385            Box::pin(async move {
386                self.polls.fetch_add(1, AtomicOrdering::SeqCst);
387                if let Some(gate) = gate {
388                    wait_gate(&gate);
389                }
390                result
391            })
392        }
393    }
394
395    impl FragmentResources for Harness {
396        fn load(&self, id: FragmentId) -> Result<Option<(String, Vec<u8>)>, String> {
397            self.load_calls.fetch_add(1, AtomicOrdering::SeqCst);
398            let load = lock(&self.loads).get(&id).cloned();
399            if let Some(Load::Wait(gate)) = &load {
400                wait_gate(gate);
401            }
402            match load {
403                None | Some(Load::Wait(_)) => Ok(Some(("audio/ogg".into(), vec![1]))),
404                Some(Load::Missing) => Ok(None),
405                Some(Load::Wrong) => Ok(Some(("text/plain".into(), vec![1]))),
406                Some(Load::Error) => Err("read failed".into()),
407            }
408        }
409
410        fn fail_queue(&self, _id: FragmentId, _error: String) -> Result<(), String> {
411            self.failures.fetch_add(1, AtomicOrdering::SeqCst);
412            Ok(())
413        }
414    }
415
416    fn wait_gate(gate: &AtomicBool) {
417        while !gate.load(Ordering::Acquire) {
418            thread::yield_now();
419        }
420    }
421
422    fn wait_for(condition: impl Fn() -> bool) {
423        let deadline = Instant::now() + Duration::from_secs(2);
424        while !condition() && Instant::now() < deadline {
425            thread::sleep(Duration::from_millis(2));
426        }
427        assert!(condition());
428    }
429
430    #[test]
431    fn isolation_admission_generation_failures_and_shutdown() {
432        let id = |value| FragmentId::from_bytes([value; 12]);
433        let gate = || Arc::new(AtomicBool::new(false));
434        let harness = Arc::new(Harness::default());
435        let driver = AudioClassificationDriver::with_resources(harness.clone(), harness.clone());
436        let (first, rest) = (gate(), gate());
437        lock(&harness.actions).insert(id(1), vec![(Some(first.clone()), Ok(()))].into());
438        for value in 2..=9 {
439            lock(&harness.actions).insert(id(value), vec![(Some(rest.clone()), Ok(()))].into());
440        }
441        assert!((1..=8).all(|value| driver.start(id(value)) == Ok(StartOutcome::Started)));
442        assert_eq!(driver.start(id(9)), Ok(StartOutcome::Pending));
443        wait_for(|| harness.polls.load(AtomicOrdering::SeqCst) == 8);
444        driver.abort(id(1));
445        thread::sleep(Duration::from_millis(10));
446        assert_eq!(harness.polls.load(AtomicOrdering::SeqCst), 8);
447        first.store(true, Ordering::Release);
448        wait_for(|| harness.polls.load(AtomicOrdering::SeqCst) == 9);
449        rest.store(true, Ordering::Release);
450
451        let harness = Arc::new(Harness::default());
452        let driver = AudioClassificationDriver::with_resources(harness.clone(), harness.clone());
453        let (old, new) = (gate(), gate());
454        lock(&harness.actions).insert(
455            id(1),
456            vec![
457                (Some(old.clone()), Err("stale".into())),
458                (Some(new.clone()), Ok(())),
459            ]
460            .into(),
461        );
462        assert_eq!(driver.start(id(1)), Ok(StartOutcome::Started));
463        wait_for(|| harness.polls.load(AtomicOrdering::SeqCst) == 1);
464        driver.abort(id(1));
465        assert_eq!(driver.start(id(1)), Ok(StartOutcome::Started));
466        wait_for(|| harness.polls.load(AtomicOrdering::SeqCst) == 2);
467        old.store(true, Ordering::Release);
468        wait_for(|| lock(&driver.inner.state).lanes == 1);
469        assert_eq!(driver.ensure_healthy(), Ok(()));
470        assert_eq!(driver.start(id(1)), Ok(StartOutcome::AlreadyActive));
471        new.store(true, Ordering::Release);
472        wait_for(|| lock(&driver.inner.state).lanes == 0);
473        lock(&harness.actions).insert(id(2), vec![(None, Err("current".into()))].into());
474        assert_eq!(driver.start(id(2)), Ok(StartOutcome::Started));
475        wait_for(|| driver.ensure_healthy().is_err());
476        assert!(driver.start(id(3)).is_err());
477
478        let harness = Arc::new(Harness::default());
479        let driver = AudioClassificationDriver::with_resources(harness.clone(), harness.clone());
480        for (value, load) in [(1, Load::Missing), (2, Load::Wrong), (3, Load::Error)] {
481            lock(&harness.loads).insert(id(value), load);
482            assert_eq!(driver.start(id(value)), Ok(StartOutcome::Started));
483        }
484        wait_for(|| harness.failures.load(AtomicOrdering::SeqCst) == 3);
485        assert_eq!(harness.polls.load(AtomicOrdering::SeqCst), 0);
486        let load_gate = gate();
487        lock(&harness.loads).insert(id(4), Load::Wait(load_gate.clone()));
488        assert_eq!(driver.start(id(4)), Ok(StartOutcome::Started));
489        wait_for(|| harness.load_calls.load(AtomicOrdering::SeqCst) == 4);
490        assert_eq!(driver.start(id(5)), Ok(StartOutcome::Started));
491        wait_for(|| harness.polls.load(AtomicOrdering::SeqCst) == 1);
492        let started = Instant::now();
493        driver.shutdown();
494        assert!(started.elapsed() < Duration::from_millis(100));
495        assert!(driver.start(id(6)).is_err());
496        load_gate.store(true, Ordering::Release);
497        wait_for(|| lock(&driver.inner.state).lanes == 0);
498    }
499}