Skip to main content

rusteron_client/
lib.rs

1#![allow(improper_ctypes_definitions)]
2#![allow(non_upper_case_globals)]
3#![allow(non_camel_case_types)]
4#![allow(non_snake_case)]
5#![allow(clippy::all)]
6#![allow(unused_unsafe)]
7#![allow(unused_variables)]
8#![doc = include_str!("../README.md")]
9//! # Features
10//!
11//! - **`static`**: When enabled, this feature statically links the Aeron C code.
12//!   By default, the library uses dynamic linking to the Aeron C libraries.
13//! - **`backtrace`**: When enabled will log a backtrace for each AeronCError
14//! - **`extra-logging`**: When enabled will log when resource is created and destroyed. Useful if you're seeing a segfault due to a resource being closed
15//! - **`log-c-bindings`**: When enabled will log every C binding call with arguments and return values. Useful for debugging FFI interactions
16//! - **`precompile`**: When enabled will use precompiled C code instead of requiring cmake and java to be installed
17
18#[allow(improper_ctypes_definitions)]
19#[allow(unpredictable_function_pointer_comparisons)]
20pub mod bindings {
21    include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
22}
23
24use bindings::*;
25use std::time::Duration;
26
27/// Result codes returned by [`AeronPublication::offer`] / `try_claim` (Aeron `aeronc.h`). A
28/// positive value is the resulting log position; the negatives classify the failure.
29///
30/// **Fatal** (stop offering): [`PUBLICATION_CLOSED`], [`PUBLICATION_MAX_POSITION_EXCEEDED`],
31/// [`PUBLICATION_ERROR`]. **Transient** (retry, ideally with an idle strategy):
32/// [`PUBLICATION_BACK_PRESSURED`], [`PUBLICATION_NOT_CONNECTED`], [`PUBLICATION_ADMIN_ACTION`].
33/// Mirrors how Aeron's own samples (`BasicPublisher` / `basic_publisher.c`) classify offer results.
34pub const PUBLICATION_NOT_CONNECTED: i64 = bindings::AERON_PUBLICATION_NOT_CONNECTED as i64;
35pub const PUBLICATION_BACK_PRESSURED: i64 = bindings::AERON_PUBLICATION_BACK_PRESSURED as i64;
36pub const PUBLICATION_ADMIN_ACTION: i64 = bindings::AERON_PUBLICATION_ADMIN_ACTION as i64;
37pub const PUBLICATION_CLOSED: i64 = bindings::AERON_PUBLICATION_CLOSED as i64;
38pub const PUBLICATION_MAX_POSITION_EXCEEDED: i64 = bindings::AERON_PUBLICATION_MAX_POSITION_EXCEEDED as i64;
39pub const PUBLICATION_ERROR: i64 = bindings::AERON_PUBLICATION_ERROR as i64;
40
41include!(concat!(env!("OUT_DIR"), "/aeron.rs"));
42include!(concat!(env!("OUT_DIR"), "/aeron_custom.rs"));
43
44// ---------------------------------------------------------------------------
45// Idle strategies for poll loops. Pure-Rust port of Aeron's `IdleStrategy`
46// (Java/C++): pass the work count from the last `poll` to `idle(work_count)`; it returns
47// immediately when work was done, otherwise backs off the CPU (spin / yield / sleep).
48// ---------------------------------------------------------------------------
49
50/// Back off a poll loop when the last cycle did no work. Mirrors Aeron's `IdleStrategy`:
51/// `idle(work_count)` returns immediately when `work_count > 0`, otherwise spins / yields /
52/// sleeps depending on the implementation.
53pub trait IdleStrategy {
54    /// Called with the work/fragment count from the last operation. Implementations return
55    /// immediately when `work_count > 0` (work was done) and back off otherwise.
56    fn idle(&mut self, work_count: i32);
57
58    /// Reset accumulated backoff state after a productive period. Default: no-op.
59    fn reset(&mut self) {}
60}
61
62/// Spin with a CPU pause hint. Lowest latency, pins a core. (Aeron `BusySpinIdleStrategy`.)
63pub struct BusySpinIdleStrategy;
64impl BusySpinIdleStrategy {
65    pub fn new() -> Self {
66        Self
67    }
68}
69impl Default for BusySpinIdleStrategy {
70    fn default() -> Self {
71        Self::new()
72    }
73}
74impl IdleStrategy for BusySpinIdleStrategy {
75    #[inline]
76    fn idle(&mut self, work_count: i32) {
77        if work_count > 0 {
78            return;
79        }
80        std::hint::spin_loop();
81    }
82}
83
84/// No-op — never yields the core. (Aeron `NoOpIdleStrategy`.)
85pub struct NoOpIdleStrategy;
86impl NoOpIdleStrategy {
87    pub fn new() -> Self {
88        Self
89    }
90}
91impl Default for NoOpIdleStrategy {
92    fn default() -> Self {
93        Self::new()
94    }
95}
96impl IdleStrategy for NoOpIdleStrategy {
97    #[inline]
98    fn idle(&mut self, _work_count: i32) {}
99}
100
101/// Yield the OS thread when idle. Lower CPU than busy-spin, slightly higher latency.
102/// (Aeron `YieldingIdleStrategy`.)
103pub struct YieldingIdleStrategy;
104impl YieldingIdleStrategy {
105    pub fn new() -> Self {
106        Self
107    }
108}
109impl Default for YieldingIdleStrategy {
110    fn default() -> Self {
111        Self::new()
112    }
113}
114impl IdleStrategy for YieldingIdleStrategy {
115    #[inline]
116    fn idle(&mut self, work_count: i32) {
117        if work_count > 0 {
118            return;
119        }
120        std::thread::yield_now();
121    }
122}
123
124/// Sleep for a fixed duration when idle. (Aeron `SleepingIdleStrategy`.)
125pub struct SleepingIdleStrategy {
126    duration: Duration,
127}
128impl SleepingIdleStrategy {
129    pub fn new(duration: Duration) -> Self {
130        Self { duration }
131    }
132}
133impl IdleStrategy for SleepingIdleStrategy {
134    #[inline]
135    fn idle(&mut self, work_count: i32) {
136        if work_count == 0 {
137            std::thread::sleep(self.duration);
138        }
139    }
140}
141
142/// Adaptive backoff: spin a few times, then yield a few times, then sleep with an exponentially
143/// growing park up to a max. A good general-purpose strategy. (Aeron `BackoffIdleStrategy`.)
144///
145/// Defaults match Aeron: `max_spins = 10`, `max_yields = 20`, `min_park = 1µs`, `max_park = 1ms`.
146pub struct BackoffIdleStrategy {
147    max_spins: i64,
148    max_yields: i64,
149    min_park: Duration,
150    max_park: Duration,
151    spins: i64,
152    yields: i64,
153    park: Duration,
154    state: u8,
155}
156
157const BACKOFF_NOT_IDLE: u8 = 0;
158const BACKOFF_SPINNING: u8 = 1;
159const BACKOFF_YIELDING: u8 = 2;
160const BACKOFF_PARKING: u8 = 3;
161
162impl BackoffIdleStrategy {
163    /// Defaults match Aeron: 10 spins, 20 yields, park 1µs..1ms.
164    pub fn new() -> Self {
165        Self::with(10, 20, Duration::from_micros(1), Duration::from_millis(1))
166    }
167
168    /// Full control over the backoff parameters.
169    pub fn with(max_spins: i64, max_yields: i64, min_park: Duration, max_park: Duration) -> Self {
170        Self {
171            max_spins,
172            max_yields,
173            min_park,
174            max_park,
175            spins: 0,
176            yields: 0,
177            park: min_park,
178            state: BACKOFF_NOT_IDLE,
179        }
180    }
181
182    #[inline]
183    fn idle_one(&mut self) {
184        match self.state {
185            BACKOFF_NOT_IDLE => {
186                self.state = BACKOFF_SPINNING;
187                self.spins += 1;
188            }
189            BACKOFF_SPINNING => {
190                std::hint::spin_loop();
191                self.spins += 1;
192                if self.spins > self.max_spins {
193                    self.state = BACKOFF_YIELDING;
194                    self.yields = 0;
195                }
196            }
197            BACKOFF_YIELDING => {
198                self.yields += 1;
199                if self.yields > self.max_yields {
200                    self.state = BACKOFF_PARKING;
201                    self.park = self.min_park;
202                } else {
203                    std::thread::yield_now();
204                }
205            }
206            _ => {
207                // PARKING — sleep then double the park period up to the max.
208                std::thread::sleep(self.park);
209                self.park = std::cmp::min(self.park.saturating_mul(2), self.max_park);
210            }
211        }
212    }
213}
214
215impl Default for BackoffIdleStrategy {
216    fn default() -> Self {
217        Self::new()
218    }
219}
220
221impl IdleStrategy for BackoffIdleStrategy {
222    #[inline]
223    fn idle(&mut self, work_count: i32) {
224        if work_count > 0 {
225            self.reset();
226        } else {
227            self.idle_one();
228        }
229    }
230
231    fn reset(&mut self) {
232        self.spins = 0;
233        self.yields = 0;
234        self.park = self.min_park;
235        self.state = BACKOFF_NOT_IDLE;
236    }
237}
238
239#[cfg(test)]
240mod idle_strategy_tests {
241    use super::*;
242    use std::time::Instant;
243
244    #[test]
245    fn idle_strategy_kind_round_trips_through_context() {
246        let ctx = AeronContext::new().unwrap();
247        for (kind, name) in [
248            (AeronIdleStrategyKind::Sleeping, "sleeping"),
249            (AeronIdleStrategyKind::Yielding, "yield"),
250            (AeronIdleStrategyKind::BusySpin, "spin"),
251            (AeronIdleStrategyKind::NoOp, "noop"),
252            (AeronIdleStrategyKind::Backoff, "backoff"),
253        ] {
254            ctx.set_idle_strategy_kind(kind).unwrap();
255            assert_eq!(name, ctx.get_idle_strategy(), "kind {kind:?}");
256        }
257    }
258
259    #[test]
260    fn rust_backoff_idles_correctly() {
261        let mut idle = BackoffIdleStrategy::new();
262        // work done -> effectively free
263        idle.idle(1);
264        // no work: walk spin -> yield -> park without crashing; parking must take real time
265        let start = Instant::now();
266        for _ in 0..100 {
267            idle.idle(0);
268        }
269        assert!(
270            start.elapsed() >= Duration::from_micros(50),
271            "backoff should have parked"
272        );
273    }
274
275    #[test]
276    fn rust_stateless_strategies_are_safe() {
277        let strategies: Vec<Box<dyn IdleStrategy>> = vec![
278            Box::new(BusySpinIdleStrategy::default()),
279            Box::new(YieldingIdleStrategy::default()),
280            Box::new(NoOpIdleStrategy::default()),
281        ];
282        for mut s in strategies {
283            s.idle(1);
284            s.idle(0);
285        }
286    }
287
288    #[test]
289    fn rust_backoff_matches_c_defaults() {
290        // Verify BackoffIdleStrategy uses Aeron's canonical backoff defaults
291        // (AERON_IDLE_STRATEGY_BACKOFF_*: 10 spins, 20 yields, park 1µs..1ms).
292        let idle = BackoffIdleStrategy::new();
293        assert_eq!(idle.max_spins, 10, "max_spins should be 10");
294        assert_eq!(idle.max_yields, 20, "max_yields should be 20");
295        assert_eq!(idle.min_park, Duration::from_micros(1), "min_park should be 1µs");
296        assert_eq!(idle.max_park, Duration::from_millis(1), "max_park should be 1ms");
297    }
298
299    #[test]
300    fn busy_spin_and_yield_return_on_work() {
301        let mut s = BusySpinIdleStrategy;
302        s.idle(5); // work done -> must not block
303        s.idle(0); // no work -> just a pause hint, returns immediately
304
305        let mut y = YieldingIdleStrategy;
306        y.idle(3); // work done
307        y.idle(0); // yields once, returns
308    }
309
310    #[test]
311    fn no_op_never_blocks() {
312        let mut s = NoOpIdleStrategy;
313        for _ in 0..1000 {
314            s.idle(0);
315        }
316    }
317
318    #[test]
319    fn sleeping_idle_only_sleeps_when_idle() {
320        let mut s = SleepingIdleStrategy::new(Duration::from_micros(10));
321        let t = std::time::Instant::now();
322        s.idle(1); // work done -> no sleep
323        assert!(t.elapsed() < Duration::from_millis(1));
324
325        let t = std::time::Instant::now();
326        s.idle(0); // idle -> sleeps ~10µs
327        assert!(t.elapsed() >= Duration::from_micros(10));
328    }
329
330    #[test]
331    fn backoff_resets_on_work_and_progresses_to_park() {
332        let mut s = BackoffIdleStrategy::with(2, 2, Duration::from_micros(1), Duration::from_millis(1));
333        // Work done -> resets state (no backoff progression).
334        s.idle(1);
335        assert_eq!(s.state, BACKOFF_NOT_IDLE);
336        assert_eq!(s.spins, 0);
337
338        // Spin a few, then yield, then park: driving it enough with 0 work must reach PARKING.
339        for _ in 0..1000 {
340            s.idle(0);
341        }
342        assert_eq!(s.state, BACKOFF_PARKING);
343        // Park period grows but is capped at max_park.
344        assert!(s.park <= Duration::from_millis(1));
345    }
346}
347
348#[cfg(test)]
349mod tests {
350    use super::*;
351    use crate::test_alloc::current_allocs;
352    use hdrhistogram::Histogram;
353    use log::{error, info};
354    use rusteron_media_driver::AeronDriverContext;
355    use serial_test::serial;
356    use std::error;
357    use std::error::Error;
358    use std::io::Write;
359    use std::os::raw::c_int;
360    use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
361    use std::sync::Arc;
362    use std::thread::{sleep, JoinHandle};
363    use std::time::{Duration, Instant};
364
365    #[derive(Default, Debug)]
366    struct ErrorCount {
367        error_count: usize,
368    }
369
370    impl AeronErrorHandlerCallback for ErrorCount {
371        fn handle_aeron_error_handler(&mut self, error_code: c_int, msg: &str) {
372            error!("Aeron error {}: {}", error_code, msg);
373            self.error_count += 1;
374        }
375    }
376
377    struct CloseNotificationCount {
378        count: Arc<AtomicUsize>,
379    }
380
381    impl AeronNotificationCallback for CloseNotificationCount {
382        fn handle_aeron_notification(&mut self) {
383            self.count.fetch_add(1, Ordering::SeqCst);
384        }
385    }
386
387    fn running_under_valgrind() -> bool {
388        std::env::var_os("RUSTERON_VALGRIND").is_some()
389    }
390
391    #[test]
392    #[serial]
393    fn version_check() -> Result<(), Box<dyn error::Error>> {
394        unsafe {
395            aeron_randomised_int32();
396        }
397        // Let background teardown from previous #[serial] tests settle before taking
398        // the baseline: a driver thread still stopping can emit a captured log line
399        // inside our window, which counts as a live allocation and skews the check.
400        let settle_start = Instant::now();
401        let mut alloc_count = current_allocs();
402        loop {
403            sleep(Duration::from_millis(50));
404            let now = current_allocs();
405            if now == alloc_count || settle_start.elapsed() > Duration::from_secs(2) {
406                alloc_count = now;
407                break;
408            }
409            alloc_count = now;
410        }
411
412        {
413            let major = unsafe { crate::aeron_version_major() };
414            let minor = unsafe { crate::aeron_version_minor() };
415            let patch = unsafe { crate::aeron_version_patch() };
416
417            let cargo_version = "1.52.0";
418            let aeron_version = format!("{}.{}.{}", major, minor, patch);
419            assert_eq!(aeron_version, cargo_version);
420
421            let ctx = AeronContext::new()?;
422            let handler = Handler::new(ErrorCount::default());
423            ctx.set_error_handler(Some(handler.clone()))?;
424
425            assert!(Aeron::epoch_clock() > 0);
426            // the context holds a clone of the handler; dropping both frees the
427            // callback value exactly once (verified by the alloc-count check below)
428        }
429
430        assert!(
431            current_allocs() <= alloc_count,
432            "allocations {} > {alloc_count}",
433            current_allocs()
434        );
435
436        Ok(())
437    }
438
439    #[test]
440    #[serial]
441    fn async_publication_invalid_interface_poll_then_drop() -> Result<(), Box<dyn error::Error>> {
442        rusteron_code_gen::test_logger::init(log::LevelFilter::Info);
443
444        let media_driver_ctx = rusteron_media_driver::AeronDriverContext::new()?;
445        media_driver_ctx.set_dir_delete_on_shutdown(true)?;
446        media_driver_ctx.set_dir_delete_on_start(true)?;
447        media_driver_ctx.set_dir(&format!("{}{}", media_driver_ctx.get_dir(), Aeron::epoch_clock()).into_c_string())?;
448        let (stop, driver_handle) =
449            rusteron_media_driver::AeronDriver::launch_embedded(media_driver_ctx.clone(), false);
450
451        let ctx = AeronContext::new()?;
452        ctx.set_dir(&media_driver_ctx.get_dir().into_c_string())?;
453        let error_handler = Handler::new(ErrorCount::default());
454        ctx.set_error_handler(Some(error_handler.clone()))?;
455        let aeron = Aeron::new(&ctx)?;
456        aeron.start()?;
457
458        let channel = String::from("aeron:udp?endpoint=203.0.113.1:54321");
459
460        // Create async publication and subscription pollers on the same invalid channel and
461        // attempt to resolve them. If both are created, try a small send/receive cycle and then exit.
462        let pub_poller = aeron.async_add_publication(&channel.clone().into_c_string(), 4321)?;
463        let sub_poller =
464            aeron.async_add_subscription(&channel.into_c_string(), 4321, Handlers::NONE, Handlers::NONE)?;
465
466        let mut publication: Option<AeronPublication> = None;
467        let mut subscription: Option<AeronSubscription> = None;
468        let start = Instant::now();
469        while start.elapsed() < Duration::from_secs(2) {
470            if publication.is_none() {
471                match pub_poller.poll() {
472                    Ok(Some(p)) => publication = Some(p),
473                    Ok(None) | Err(_) => {}
474                }
475            }
476            if subscription.is_none() {
477                match sub_poller.poll() {
478                    Ok(Some(s)) => subscription = Some(s),
479                    Ok(None) | Err(_) => {}
480                }
481            }
482            if publication.is_some() && subscription.is_some() {
483                break;
484            }
485            #[cfg(debug_assertions)]
486            std::thread::sleep(Duration::from_millis(10));
487        }
488
489        info!("publication: {:?}", publication);
490        info!("subscription: {:?}", subscription);
491
492        if let (Some(publisher), Some(subscription)) = (publication, subscription) {
493            let payload = b"hello-aeron";
494            let send_start = Instant::now();
495            let mut sent = false;
496            while send_start.elapsed() < Duration::from_millis(500) {
497                let res = publisher.offer_raw(payload, Handlers::NONE);
498                if res >= payload.len() as i64 {
499                    sent = true;
500                    info!("sent {:?}", payload);
501                    break;
502                }
503                std::thread::sleep(Duration::from_millis(10));
504            }
505
506            if sent {
507                let mut got = false;
508                let read_start = Instant::now();
509                while read_start.elapsed() < Duration::from_millis(500) {
510                    let _ = subscription.poll_fn(
511                        |msg, _hdr| {
512                            if msg == payload {
513                                got = true;
514                                info!("received {:?}", payload);
515                            }
516                        },
517                        1024,
518                    );
519                    if got {
520                        break;
521                    }
522                    std::thread::sleep(Duration::from_millis(10));
523                }
524                // We don't assert on got, just exercise the path.
525            }
526        }
527
528        // Shutdown
529        stop.store(true, Ordering::SeqCst);
530        let _ = driver_handle.join().unwrap();
531        Ok(())
532    }
533
534    /// Exercises the additive ergonomics API end-to-end:
535    /// `offer_result_simple`, `try_claim_owned` + `commit`, the `session_id` /
536    /// `stream_id` header accessors, and `status()` on publication/subscription.
537    #[test]
538    #[serial]
539    fn offer_result_and_claim_roundtrip_with_header_accessors() -> Result<(), Box<dyn error::Error>> {
540        rusteron_code_gen::test_logger::init(log::LevelFilter::Info);
541
542        let media_driver_ctx = AeronDriverContext::new()?;
543        media_driver_ctx.set_dir_delete_on_shutdown(true)?;
544        media_driver_ctx.set_dir_delete_on_start(true)?;
545        media_driver_ctx.set_dir(&format!("{}{}", media_driver_ctx.get_dir(), Aeron::epoch_clock()).into_c_string())?;
546        let (stop, driver_handle) =
547            rusteron_media_driver::AeronDriver::launch_embedded(media_driver_ctx.clone(), false);
548
549        let ctx = AeronContext::new()?;
550        ctx.set_dir(&media_driver_ctx.get_dir().into_c_string())?;
551        let error_handler = Handler::new(ErrorCount::default());
552        ctx.set_error_handler(Some(error_handler.clone()))?;
553        let aeron = Aeron::new(&ctx)?;
554        aeron.start()?;
555
556        let channel = String::from("aeron:ipc");
557        let stream_id: i32 = 9123;
558        let pub_poller = aeron.async_add_publication(&channel.clone().into_c_string(), stream_id)?;
559        let sub_poller =
560            aeron.async_add_subscription(&channel.into_c_string(), stream_id, Handlers::NONE, Handlers::NONE)?;
561
562        let mut publication: Option<AeronPublication> = None;
563        let mut subscription: Option<AeronSubscription> = None;
564        let start = Instant::now();
565        while start.elapsed() < Duration::from_secs(2) {
566            if publication.is_none() {
567                if let Ok(Some(p)) = pub_poller.poll() {
568                    publication = Some(p);
569                }
570            }
571            if subscription.is_none() {
572                if let Ok(Some(s)) = sub_poller.poll() {
573                    subscription = Some(s);
574                }
575            }
576            if publication.is_some() && subscription.is_some() {
577                break;
578            }
579            #[cfg(debug_assertions)]
580            sleep(Duration::from_millis(10));
581        }
582
583        let (publisher, subscription) = match (publication, subscription) {
584            (Some(p), Some(s)) => (p, s),
585            _ => panic!("publication/subscription did not come up"),
586        };
587
588        // Wait for the IPC images to connect before asserting status.
589        let conn_start = Instant::now();
590        while !publisher.is_connected() && conn_start.elapsed() < Duration::from_secs(2) {
591            #[cfg(debug_assertions)]
592            sleep(Duration::from_millis(10));
593        }
594        assert_eq!(publisher.status(), AeronStatus::Connected);
595
596        // 1) Publish via the Result-returning offer variant.
597        let payload = b"hello-result";
598        let offer_start = Instant::now();
599        let mut offered = false;
600        while offer_start.elapsed() < Duration::from_secs(2) {
601            if let Ok(pos) = publisher.offer(payload) {
602                if pos >= payload.len() as i64 {
603                    offered = true;
604                    break;
605                }
606            }
607            #[cfg(debug_assertions)]
608            sleep(Duration::from_millis(10));
609        }
610        assert!(offered, "offer_result_simple never succeeded");
611
612        // 2) Publish via the RAII claim: write into the claimed buffer and commit.
613        let claim_payload = b"hello-claim";
614        let claim_start = Instant::now();
615        let mut committed = false;
616        while claim_start.elapsed() < Duration::from_secs(2) {
617            if let Ok(mut claim) = publisher.try_claim_owned(claim_payload.len()) {
618                claim.data()[..claim_payload.len()].copy_from_slice(claim_payload);
619                if claim.commit().is_ok() {
620                    committed = true;
621                    break;
622                }
623            }
624            #[cfg(debug_assertions)]
625            sleep(Duration::from_millis(10));
626        }
627        assert!(committed, "try_claim_owned + commit never succeeded");
628
629        // 3) Publish via the zero-alloc gathering offer: header + payload parts must
630        // arrive as ONE contiguous message.
631        let parts_header = b"hdr:";
632        let parts_payload = b"gathered-body";
633        let parts_expected: Vec<u8> = [parts_header.as_slice(), parts_payload.as_slice()].concat();
634        let parts_start = Instant::now();
635        let mut parts_offered = false;
636        while parts_start.elapsed() < Duration::from_secs(2) {
637            if publisher.offer_parts(&[parts_header, parts_payload]).is_ok() {
638                parts_offered = true;
639                break;
640            }
641            #[cfg(debug_assertions)]
642            sleep(Duration::from_millis(10));
643        }
644        assert!(parts_offered, "offer_parts never succeeded");
645        // more parts than the stack iovec capacity must be rejected, not truncated
646        let too_many = [b"x".as_slice(); MAX_OFFER_PARTS + 1];
647        assert!(
648            publisher.offer_parts(&too_many).is_err(),
649            "over-capacity offer_parts must fail"
650        );
651
652        // 4) Receive all three and assert the header accessors.
653        let received_offer = std::cell::Cell::new(false);
654        let received_claim = std::cell::Cell::new(false);
655        let received_parts = std::cell::Cell::new(false);
656        let header_ids = std::cell::Cell::new(Option::<(i32, i32)>::None);
657        let read_start = Instant::now();
658        while read_start.elapsed() < Duration::from_secs(2)
659            && !(received_offer.get() && received_claim.get() && received_parts.get())
660        {
661            let _ = subscription.poll_fn(
662                |msg, header| {
663                    header_ids.set(Some((
664                        header.session_id().unwrap_or(0),
665                        header.stream_id().unwrap_or(0),
666                    )));
667                    if msg == payload {
668                        received_offer.set(true);
669                    }
670                    if msg == claim_payload {
671                        received_claim.set(true);
672                    }
673                    if msg == parts_expected.as_slice() {
674                        received_parts.set(true);
675                    }
676                },
677                1024,
678            );
679            #[cfg(debug_assertions)]
680            sleep(Duration::from_millis(10));
681        }
682        assert!(received_offer.get(), "did not receive offer_result message");
683        assert!(received_claim.get(), "did not receive claim message");
684        assert!(
685            received_parts.get(),
686            "did not receive offer_parts message as one contiguous body"
687        );
688        let (session_id, recv_stream_id) = header_ids.get().expect("no header captured");
689        assert_eq!(recv_stream_id, stream_id, "header stream_id should match");
690        assert_ne!(session_id, 0, "session_id should be populated");
691
692        assert_eq!(subscription.status(), AeronStatus::Connected);
693
694        // Shutdown
695        stop.store(true, Ordering::SeqCst);
696        let _ = driver_handle.join().unwrap();
697        Ok(())
698    }
699    /// C5: test AeronClaim RAII lifecycle — commit round-trip is received.
700    /// Exercises the explicit commit path and position() accessor.
701    #[test]
702    #[serial]
703    fn aeron_claim_commit_roundtrip_is_received() -> Result<(), Box<dyn error::Error>> {
704        rusteron_code_gen::test_logger::init(log::LevelFilter::Info);
705
706        let media_driver_ctx = AeronDriverContext::new()?;
707        media_driver_ctx.set_dir_delete_on_shutdown(true)?;
708        media_driver_ctx.set_dir_delete_on_start(true)?;
709        media_driver_ctx.set_dir(&format!("{}{}", media_driver_ctx.get_dir(), Aeron::epoch_clock()).into_c_string())?;
710        let (stop, driver_handle) =
711            rusteron_media_driver::AeronDriver::launch_embedded(media_driver_ctx.clone(), false);
712
713        let ctx = AeronContext::new()?;
714        ctx.set_dir(&media_driver_ctx.get_dir().into_c_string())?;
715        let error_handler = Handler::new(ErrorCount::default());
716        ctx.set_error_handler(Some(error_handler.clone()))?;
717        let aeron = Aeron::new(&ctx)?;
718        aeron.start()?;
719
720        let channel = String::from("aeron:ipc");
721        let stream_id: i32 = 9124;
722        let pub_poller = aeron.async_add_publication(&channel.clone().into_c_string(), stream_id)?;
723        let sub_poller =
724            aeron.async_add_subscription(&channel.into_c_string(), stream_id, Handlers::NONE, Handlers::NONE)?;
725
726        let mut publication: Option<AeronPublication> = None;
727        let mut subscription: Option<AeronSubscription> = None;
728        let start = Instant::now();
729        while start.elapsed() < Duration::from_secs(2) {
730            if publication.is_none() {
731                if let Ok(Some(p)) = pub_poller.poll() {
732                    publication = Some(p);
733                }
734            }
735            if subscription.is_none() {
736                if let Ok(Some(s)) = sub_poller.poll() {
737                    subscription = Some(s);
738                }
739            }
740            if publication.is_some() && subscription.is_some() {
741                break;
742            }
743            #[cfg(debug_assertions)]
744            sleep(Duration::from_millis(10));
745        }
746
747        let (publisher, subscription) = match (publication, subscription) {
748            (Some(p), Some(s)) => (p, s),
749            _ => panic!("publication/subscription did not come up"),
750        };
751
752        // Wait for the IPC images to connect before asserting status.
753        let conn_start = Instant::now();
754        while !publisher.is_connected() && conn_start.elapsed() < Duration::from_secs(2) {
755            #[cfg(debug_assertions)]
756            sleep(Duration::from_millis(10));
757        }
758        assert_eq!(publisher.status(), AeronStatus::Connected);
759
760        // Claim a buffer, write into it, and commit.
761        let claim_payload = b"claim-commit-test";
762        let claim_start = Instant::now();
763        let mut committed_pos = None;
764        while claim_start.elapsed() < Duration::from_secs(2) {
765            if let Ok(mut claim) = publisher.try_claim_owned(claim_payload.len()) {
766                claim.data()[..claim_payload.len()].copy_from_slice(claim_payload);
767                let pos = claim.position();
768                let commit_pos = claim.commit()?;
769                committed_pos = Some((commit_pos, pos));
770                break;
771            }
772            #[cfg(debug_assertions)]
773            sleep(Duration::from_millis(10));
774        }
775        let (commit_pos, claim_pos) = committed_pos.expect("try_claim_owned + commit never succeeded");
776        assert_eq!(commit_pos, claim_pos, "commit() return should match claim.position()");
777
778        // Receive the committed message and assert the exact bytes.
779        let received = std::cell::Cell::new(false);
780        let read_start = Instant::now();
781        while read_start.elapsed() < Duration::from_secs(2) && !received.get() {
782            let _ = subscription.poll_fn(
783                |msg, _header| {
784                    if msg == claim_payload {
785                        received.set(true);
786                    }
787                },
788                1024,
789            );
790            #[cfg(debug_assertions)]
791            sleep(Duration::from_millis(10));
792        }
793        assert!(received.get(), "did not receive claim message");
794
795        assert_eq!(subscription.status(), AeronStatus::Connected);
796
797        // Shutdown
798        stop.store(true, Ordering::SeqCst);
799        let _ = driver_handle.join().unwrap();
800        Ok(())
801    }
802
803    /// C6: test AeronClaim RAII lifecycle — dropped without commit is aborted.
804    /// Verifies that a dropped claim is not delivered and the publication remains usable.
805    #[test]
806    #[serial]
807    fn aeron_claim_dropped_without_commit_is_aborted() -> Result<(), Box<dyn error::Error>> {
808        rusteron_code_gen::test_logger::init(log::LevelFilter::Info);
809
810        let media_driver_ctx = AeronDriverContext::new()?;
811        media_driver_ctx.set_dir_delete_on_shutdown(true)?;
812        media_driver_ctx.set_dir_delete_on_start(true)?;
813        media_driver_ctx.set_dir(&format!("{}{}", media_driver_ctx.get_dir(), Aeron::epoch_clock()).into_c_string())?;
814        let (stop, driver_handle) =
815            rusteron_media_driver::AeronDriver::launch_embedded(media_driver_ctx.clone(), false);
816
817        let ctx = AeronContext::new()?;
818        ctx.set_dir(&media_driver_ctx.get_dir().into_c_string())?;
819        let error_handler = Handler::new(ErrorCount::default());
820        ctx.set_error_handler(Some(error_handler.clone()))?;
821        let aeron = Aeron::new(&ctx)?;
822        aeron.start()?;
823
824        let channel = String::from("aeron:ipc");
825        let stream_id: i32 = 9125;
826        let pub_poller = aeron.async_add_publication(&channel.clone().into_c_string(), stream_id)?;
827        let sub_poller =
828            aeron.async_add_subscription(&channel.into_c_string(), stream_id, Handlers::NONE, Handlers::NONE)?;
829
830        let mut publication: Option<AeronPublication> = None;
831        let mut subscription: Option<AeronSubscription> = None;
832        let start = Instant::now();
833        while start.elapsed() < Duration::from_secs(2) {
834            if publication.is_none() {
835                if let Ok(Some(p)) = pub_poller.poll() {
836                    publication = Some(p);
837                }
838            }
839            if subscription.is_none() {
840                if let Ok(Some(s)) = sub_poller.poll() {
841                    subscription = Some(s);
842                }
843            }
844            if publication.is_some() && subscription.is_some() {
845                break;
846            }
847            #[cfg(debug_assertions)]
848            sleep(Duration::from_millis(10));
849        }
850
851        let (publisher, subscription) = match (publication, subscription) {
852            (Some(p), Some(s)) => (p, s),
853            _ => panic!("publication/subscription did not come up"),
854        };
855
856        // Wait for the IPC images to connect.
857        let conn_start = Instant::now();
858        while !publisher.is_connected() && conn_start.elapsed() < Duration::from_secs(2) {
859            #[cfg(debug_assertions)]
860            sleep(Duration::from_millis(10));
861        }
862        assert_eq!(publisher.status(), AeronStatus::Connected);
863
864        // Claim a buffer, write into it, then DROP without commit/abort.
865        let dropped_payload = b"dropped-claim-payload";
866        let claim_start = Instant::now();
867        let mut claimed = false;
868        while claim_start.elapsed() < Duration::from_secs(2) && !claimed {
869            if let Ok(mut claim) = publisher.try_claim_owned(dropped_payload.len()) {
870                claim.data()[..dropped_payload.len()].copy_from_slice(dropped_payload);
871                // Explicitly drop the claim here — it should be aborted, not committed.
872                drop(claim);
873                claimed = true;
874            }
875            #[cfg(debug_assertions)]
876            sleep(Duration::from_millis(10));
877        }
878        assert!(claimed, "try_claim_owned never succeeded");
879
880        // Publish a second, different message via the standard offer path.
881        let marker_payload = b"marker-after-dropped-claim";
882        let offer_start = Instant::now();
883        let mut offered = false;
884        while offer_start.elapsed() < Duration::from_secs(2) && !offered {
885            if let Ok(pos) = publisher.offer(marker_payload) {
886                if pos >= marker_payload.len() as i64 {
887                    offered = true;
888                }
889            }
890            #[cfg(debug_assertions)]
891            sleep(Duration::from_millis(10));
892        }
893        assert!(offered, "offer_result_simple never succeeded after dropped claim");
894
895        // Receive only the marker — the dropped claim should have been aborted.
896        let received_dropped = std::cell::Cell::new(false);
897        let received_marker = std::cell::Cell::new(false);
898        let read_start = Instant::now();
899        while read_start.elapsed() < Duration::from_secs(2) && !(received_marker.get() || received_dropped.get()) {
900            let _ = subscription.poll_fn(
901                |msg, _header| {
902                    if msg == dropped_payload {
903                        received_dropped.set(true);
904                    }
905                    if msg == marker_payload {
906                        received_marker.set(true);
907                    }
908                },
909                1024,
910            );
911            #[cfg(debug_assertions)]
912            sleep(Duration::from_millis(10));
913        }
914        assert!(!received_dropped.get(), "dropped claim was erroneously delivered");
915        assert!(
916            received_marker.get(),
917            "marker message was not received after dropped claim"
918        );
919
920        // Confirm the publication is still usable by offering one more message.
921        let final_payload = b"final-after-all";
922        let final_start = Instant::now();
923        let mut final_offered = false;
924        while final_start.elapsed() < Duration::from_secs(2) && !final_offered {
925            if let Ok(pos) = publisher.offer(final_payload) {
926                if pos >= final_payload.len() as i64 {
927                    final_offered = true;
928                }
929            }
930            #[cfg(debug_assertions)]
931            sleep(Duration::from_millis(10));
932        }
933        assert!(final_offered, "publication became unusable after dropped claim");
934
935        assert_eq!(subscription.status(), AeronStatus::Connected);
936
937        // Shutdown
938        stop.store(true, Ordering::SeqCst);
939        let _ = driver_handle.join().unwrap();
940        Ok(())
941    }
942
943    /// C7: test try_claim_owned failure path — oversized request returns Err cleanly.
944    /// Verifies the error path does not construct an AeronClaim or call abort.
945    #[test]
946    #[serial]
947    fn try_claim_owned_failure_is_clean() -> Result<(), Box<dyn error::Error>> {
948        rusteron_code_gen::test_logger::init(log::LevelFilter::Info);
949
950        let media_driver_ctx = AeronDriverContext::new()?;
951        media_driver_ctx.set_dir_delete_on_shutdown(true)?;
952        media_driver_ctx.set_dir_delete_on_start(true)?;
953        media_driver_ctx.set_dir(&format!("{}{}", media_driver_ctx.get_dir(), Aeron::epoch_clock()).into_c_string())?;
954        let (stop, driver_handle) =
955            rusteron_media_driver::AeronDriver::launch_embedded(media_driver_ctx.clone(), false);
956
957        let ctx = AeronContext::new()?;
958        ctx.set_dir(&media_driver_ctx.get_dir().into_c_string())?;
959        let error_handler = Handler::new(ErrorCount::default());
960        ctx.set_error_handler(Some(error_handler.clone()))?;
961        let aeron = Aeron::new(&ctx)?;
962        aeron.start()?;
963
964        let channel = String::from("aeron:ipc");
965        let stream_id: i32 = 9126;
966        let pub_poller = aeron.async_add_publication(&channel.clone().into_c_string(), stream_id)?;
967        let sub_poller =
968            aeron.async_add_subscription(&channel.into_c_string(), stream_id, Handlers::NONE, Handlers::NONE)?;
969
970        let mut publication: Option<AeronPublication> = None;
971        let mut subscription: Option<AeronSubscription> = None;
972        let start = Instant::now();
973        while start.elapsed() < Duration::from_secs(2) {
974            if publication.is_none() {
975                if let Ok(Some(p)) = pub_poller.poll() {
976                    publication = Some(p);
977                }
978            }
979            if subscription.is_none() {
980                if let Ok(Some(s)) = sub_poller.poll() {
981                    subscription = Some(s);
982                }
983            }
984            if publication.is_some() && subscription.is_some() {
985                break;
986            }
987            #[cfg(debug_assertions)]
988            sleep(Duration::from_millis(10));
989        }
990
991        let (publisher, _subscription) = match (publication, subscription) {
992            (Some(p), Some(s)) => (p, s),
993            _ => panic!("publication/subscription did not come up"),
994        };
995
996        // Wait for the IPC images to connect.
997        let conn_start = Instant::now();
998        while !publisher.is_connected() && conn_start.elapsed() < Duration::from_secs(2) {
999            #[cfg(debug_assertions)]
1000            sleep(Duration::from_millis(10));
1001        }
1002        assert_eq!(publisher.status(), AeronStatus::Connected);
1003
1004        // Get the max_message_length and attempt to claim more than that.
1005        let constants = publisher.get_constants().expect("publication constants");
1006        let max_len = constants.max_message_length;
1007        assert!(max_len > 0, "max_message_length should be positive");
1008
1009        // Try to claim an absurdly large length — should return Err without panicking.
1010        let oversized_claim = publisher.try_claim_owned(usize::MAX);
1011        assert!(
1012            oversized_claim.is_err(),
1013            "try_claim_owned(usize::MAX) should return Err"
1014        );
1015
1016        // Try to claim exactly one byte over the limit — should also return Err.
1017        let over_limit_claim = publisher.try_claim_owned(max_len as usize + 1);
1018        assert!(
1019            over_limit_claim.is_err(),
1020            "try_claim_owned(max_len + 1) should return Err"
1021        );
1022
1023        // Verify the publication is still usable after the failed claims.
1024        let valid_payload = b"valid-after-failed-claim";
1025        let offer_start = Instant::now();
1026        let mut offered = false;
1027        while offer_start.elapsed() < Duration::from_secs(2) && !offered {
1028            if let Ok(pos) = publisher.offer(valid_payload) {
1029                if pos >= valid_payload.len() as i64 {
1030                    offered = true;
1031                }
1032            }
1033            #[cfg(debug_assertions)]
1034            sleep(Duration::from_millis(10));
1035        }
1036        assert!(offered, "publication became unusable after failed try_claim_owned");
1037
1038        // Shutdown
1039        stop.store(true, Ordering::SeqCst);
1040        let _ = driver_handle.join().unwrap();
1041        Ok(())
1042    }
1043
1044    /// C4: regression guard for the latency prime directive — a tight loop on the
1045    /// publish hot path (`offer` with the static no-op reserved-value supplier)
1046    /// must stay allocation-free. If a future change adds a stray `to_string` /
1047    /// `Vec` / clone on the offer path, this fails CI.
1048    #[test]
1049    #[serial]
1050    fn publish_hot_path_is_allocation_free() -> Result<(), Box<dyn error::Error>> {
1051        let media_driver_ctx = AeronDriverContext::new()?;
1052        media_driver_ctx.set_dir_delete_on_shutdown(true)?;
1053        media_driver_ctx.set_dir_delete_on_start(true)?;
1054        media_driver_ctx.set_dir(&format!("{}alloc-guard", media_driver_ctx.get_dir()).into_c_string())?;
1055        let (stop, driver_handle) =
1056            rusteron_media_driver::AeronDriver::launch_embedded(media_driver_ctx.clone(), false);
1057
1058        let ctx = AeronContext::new()?;
1059        ctx.set_dir(&media_driver_ctx.get_dir().into_c_string())?;
1060        let error_handler = Handler::new(ErrorCount::default());
1061        ctx.set_error_handler(Some(error_handler.clone()))?;
1062        let aeron = Aeron::new(&ctx)?;
1063        aeron.start()?;
1064
1065        let channel = String::from("aeron:ipc");
1066        let stream_id: i32 = 7777;
1067        let pub_poller = aeron.async_add_publication(&channel.clone().into_c_string(), stream_id)?;
1068
1069        // Bring the publication up + connected before measuring.
1070        let publisher: AeronPublication = {
1071            let start = Instant::now();
1072            loop {
1073                if let Ok(Some(p)) = pub_poller.poll() {
1074                    let conn_start = Instant::now();
1075                    while !p.is_connected() && conn_start.elapsed() < Duration::from_secs(2) {
1076                        #[cfg(debug_assertions)]
1077                        sleep(Duration::from_millis(10));
1078                    }
1079                    break p;
1080                }
1081                if start.elapsed() > Duration::from_secs(3) {
1082                    panic!("publication did not come up");
1083                }
1084                #[cfg(debug_assertions)]
1085                sleep(Duration::from_millis(10));
1086            }
1087        };
1088
1089        let payload = b"alloc-guard-payload";
1090        // Warm up (first offer may touch lazy publication state).
1091        let _ = publisher.offer_raw(payload, Handlers::NONE);
1092
1093        // Assert a tight offer loop is allocation-free (publish hot path).
1094        crate::test_alloc::assert_no_allocation(|| {
1095            for _ in 0..200 {
1096                let _ = publisher.offer_raw(payload, Handlers::NONE);
1097            }
1098        });
1099
1100        stop.store(true, Ordering::SeqCst);
1101        let _ = driver_handle.join().unwrap();
1102        Ok(())
1103    }
1104
1105    /// `from_code` + `Clone` must stay allocation-free so retry loops that keep
1106    /// failing on `-1` don't tax the hot path. Message capture is opt-in.
1107    #[test]
1108    #[serial]
1109    fn repeated_c_errors_are_allocation_free() {
1110        crate::test_alloc::assert_no_allocation(|| {
1111            for _ in 0..200 {
1112                let err = AeronCError::from_code(-1);
1113                assert_eq!(err.code, -1);
1114                assert!(err.message().is_none());
1115                let cloned = err.clone();
1116                assert_eq!(cloned, err);
1117            }
1118        });
1119    }
1120
1121    #[test]
1122    #[serial]
1123    fn async_pub_sub_invalid_endpoint_create_drop_stress() -> Result<(), Box<dyn error::Error>> {
1124        rusteron_code_gen::test_logger::init(log::LevelFilter::Info);
1125
1126        let media_driver_ctx = rusteron_media_driver::AeronDriverContext::new()?;
1127        media_driver_ctx.set_dir_delete_on_shutdown(true)?;
1128        media_driver_ctx.set_dir_delete_on_start(true)?;
1129        media_driver_ctx.set_dir(&format!("{}{}", media_driver_ctx.get_dir(), Aeron::epoch_clock()).into_c_string())?;
1130        let (stop, driver_handle) =
1131            rusteron_media_driver::AeronDriver::launch_embedded(media_driver_ctx.clone(), false);
1132
1133        let ctx = AeronContext::new()?;
1134        ctx.set_dir(&media_driver_ctx.get_dir().into_c_string())?;
1135        let error_handler = Handler::new(ErrorCount::default());
1136        ctx.set_error_handler(Some(error_handler.clone()))?;
1137        let aeron = Aeron::new(&ctx)?;
1138        aeron.start()?;
1139
1140        const STRESS_ITERS: u16 = 60;
1141        const POLL_TIMEOUT: Duration = Duration::from_secs(10);
1142        const POLL_SLEEP: Duration = Duration::from_millis(10);
1143
1144        // Stress: repeatedly create async pub/sub on an invalid endpoint and drive each
1145        // poller to a terminal state before dropping it.
1146        for i in 0..STRESS_ITERS {
1147            let port = 55000u16 + i;
1148            let channel = format!("aeron:udp?endpoint=203.0.113.1:{}", port);
1149            let pub_poller = aeron.async_add_publication(&channel.clone().into_c_string(), 4500 + i as i32)?;
1150            let sub_poller = aeron.async_add_subscription(
1151                &channel.into_c_string(),
1152                4500 + i as i32,
1153                Handlers::NONE,
1154                Handlers::NONE,
1155            )?;
1156
1157            let start = Instant::now();
1158            let mut publication_done = false;
1159            let mut subscription_done = false;
1160
1161            while !(publication_done && subscription_done) && start.elapsed() < POLL_TIMEOUT {
1162                if !publication_done {
1163                    match pub_poller.poll() {
1164                        Ok(Some(pub_)) => {
1165                            let _ = pub_;
1166                            publication_done = true;
1167                        }
1168                        Ok(None) => {}
1169                        Err(err) => {
1170                            info!("publication async add finished with error on iteration {i}: {err:?}");
1171                            publication_done = true;
1172                        }
1173                    }
1174                }
1175
1176                if !subscription_done {
1177                    match sub_poller.poll() {
1178                        Ok(Some(sub_)) => {
1179                            let _ = sub_;
1180                            subscription_done = true;
1181                        }
1182                        Ok(None) => {}
1183                        Err(err) => {
1184                            info!("subscription async add finished with error on iteration {i}: {err:?}");
1185                            subscription_done = true;
1186                        }
1187                    }
1188                }
1189
1190                if !(publication_done && subscription_done) {
1191                    std::thread::sleep(POLL_SLEEP);
1192                }
1193            }
1194
1195            assert!(
1196                publication_done,
1197                "publication async add did not complete on iteration {i} within {:?}",
1198                POLL_TIMEOUT
1199            );
1200            assert!(
1201                subscription_done,
1202                "subscription async add did not complete on iteration {i} within {:?}",
1203                POLL_TIMEOUT
1204            );
1205        }
1206
1207        drop(aeron);
1208        stop.store(true, Ordering::SeqCst);
1209        let _ = driver_handle.join().unwrap();
1210        Ok(())
1211    }
1212
1213    #[test]
1214    #[serial]
1215    fn async_subscription_invalid_interface_poll_then_drop() -> Result<(), Box<dyn error::Error>> {
1216        rusteron_code_gen::test_logger::init(log::LevelFilter::Info);
1217
1218        let media_driver_ctx = rusteron_media_driver::AeronDriverContext::new()?;
1219        media_driver_ctx.set_dir_delete_on_shutdown(true)?;
1220        media_driver_ctx.set_dir_delete_on_start(true)?;
1221        media_driver_ctx.set_dir(&format!("{}{}", media_driver_ctx.get_dir(), Aeron::epoch_clock()).into_c_string())?;
1222        let (stop, driver_handle) =
1223            rusteron_media_driver::AeronDriver::launch_embedded(media_driver_ctx.clone(), false);
1224
1225        let ctx = AeronContext::new()?;
1226        ctx.set_dir(&media_driver_ctx.get_dir().into_c_string())?;
1227        let error_handler = Handler::new(ErrorCount::default());
1228        ctx.set_error_handler(Some(error_handler.clone()))?;
1229        let aeron = Aeron::new(&ctx)?;
1230        aeron.start()?;
1231
1232        // Invalid remote endpoint only (no interface)
1233        let channel = String::from("aeron:udp?endpoint=203.0.113.1:54323");
1234
1235        let poller = aeron.async_add_subscription(&channel.into_c_string(), 4323, Handlers::NONE, Handlers::NONE)?;
1236
1237        let start = Instant::now();
1238        while start.elapsed() < Duration::from_millis(250) {
1239            let _ = poller.poll();
1240            #[cfg(debug_assertions)]
1241            std::thread::sleep(Duration::from_millis(10));
1242        }
1243
1244        stop.store(true, Ordering::SeqCst);
1245        let _ = driver_handle.join().unwrap();
1246        Ok(())
1247    }
1248
1249    #[test]
1250    #[serial]
1251    fn blocking_add_subscription_invalid_interface_timeout() -> Result<(), Box<dyn error::Error>> {
1252        rusteron_code_gen::test_logger::init(log::LevelFilter::Info);
1253
1254        let media_driver_ctx = rusteron_media_driver::AeronDriverContext::new()?;
1255        media_driver_ctx.set_dir_delete_on_shutdown(true)?;
1256        media_driver_ctx.set_dir_delete_on_start(true)?;
1257        media_driver_ctx.set_dir(&format!("{}{}", media_driver_ctx.get_dir(), Aeron::epoch_clock()).into_c_string())?;
1258        let (stop, driver_handle) =
1259            rusteron_media_driver::AeronDriver::launch_embedded(media_driver_ctx.clone(), false);
1260
1261        let ctx = AeronContext::new()?;
1262        ctx.set_dir(&media_driver_ctx.get_dir().into_c_string())?;
1263        let error_handler = Handler::new(ErrorCount::default());
1264        ctx.set_error_handler(Some(error_handler.clone()))?;
1265        let aeron = Aeron::new(&ctx)?;
1266        aeron.start()?;
1267
1268        let channel = String::from("aeron:udp?endpoint=203.0.113.1:54324");
1269
1270        let result = aeron.add_subscription(
1271            &channel.into_c_string(),
1272            4324,
1273            Handlers::NONE,
1274            Handlers::NONE,
1275            Duration::from_millis(300),
1276        );
1277
1278        assert!(result.is_err(), "expected error for invalid interface");
1279        drop(aeron);
1280        stop.store(true, Ordering::SeqCst);
1281        let _ = driver_handle.join().unwrap();
1282        Ok(())
1283    }
1284
1285    #[test]
1286    #[serial]
1287    fn async_publication_invalid_bind_poll_then_drop() -> Result<(), Box<dyn error::Error>> {
1288        rusteron_code_gen::test_logger::init(log::LevelFilter::Info);
1289
1290        let media_driver_ctx = rusteron_media_driver::AeronDriverContext::new()?;
1291        media_driver_ctx.set_dir_delete_on_shutdown(true)?;
1292        media_driver_ctx.set_dir_delete_on_start(true)?;
1293        media_driver_ctx.set_dir(&format!("{}{}", media_driver_ctx.get_dir(), Aeron::epoch_clock()).into_c_string())?;
1294        let (stop, driver_handle) =
1295            rusteron_media_driver::AeronDriver::launch_embedded(media_driver_ctx.clone(), false);
1296
1297        let ctx = AeronContext::new()?;
1298        ctx.set_dir(&media_driver_ctx.get_dir().into_c_string())?;
1299        let error_handler = Handler::new(ErrorCount::default());
1300        ctx.set_error_handler(Some(error_handler.clone()))?;
1301        let aeron = Aeron::new(&ctx)?;
1302        aeron.start()?;
1303
1304        // Use an invalid bind on publication (bind is not valid for publication, and the IP is unowned).
1305        let channel = format!("aeron:udp?endpoint=127.0.0.1:54330|bind=203.0.113.1:60000");
1306
1307        let poller = aeron.async_add_publication(&channel.into_c_string(), 4330)?;
1308        let start = Instant::now();
1309        while start.elapsed() < Duration::from_millis(250) {
1310            let _ = poller.poll();
1311            #[cfg(debug_assertions)]
1312            std::thread::sleep(Duration::from_millis(10));
1313        }
1314        stop.store(true, Ordering::SeqCst);
1315        let _ = driver_handle.join().unwrap();
1316        Ok(())
1317    }
1318
1319    #[test]
1320    #[serial]
1321    pub fn simple_large_send() -> Result<(), Box<dyn error::Error>> {
1322        rusteron_code_gen::test_logger::init(log::LevelFilter::Info);
1323        let media_driver_ctx = rusteron_media_driver::AeronDriverContext::new()?;
1324        media_driver_ctx.set_dir_delete_on_shutdown(true)?;
1325        media_driver_ctx.set_dir_delete_on_start(true)?;
1326        media_driver_ctx.set_dir(&format!("{}{}", media_driver_ctx.get_dir(), Aeron::epoch_clock()).into_c_string())?;
1327        let (liveness_ns, unblock_ns, driver_timeout_ms) = if running_under_valgrind() {
1328            (180_000_000_000u64, 185_000_000_000u64, 180_000)
1329        } else {
1330            (60_000_000_000u64, 65_000_000_000u64, 60_000)
1331        };
1332        media_driver_ctx.set_client_liveness_timeout_ns(liveness_ns)?;
1333        media_driver_ctx.set_image_liveness_timeout_ns(liveness_ns)?;
1334        media_driver_ctx.set_publication_unblock_timeout_ns(unblock_ns)?;
1335        media_driver_ctx.set_driver_timeout_ms(driver_timeout_ms)?;
1336        let (stop, driver_handle) =
1337            rusteron_media_driver::AeronDriver::launch_embedded(media_driver_ctx.clone(), false);
1338
1339        let ctx = AeronContext::new()?;
1340        ctx.set_dir(&media_driver_ctx.get_dir().into_c_string())?;
1341        assert_eq!(media_driver_ctx.get_dir(), ctx.get_dir());
1342        // Keep client-side keepalive threshold aligned with the slow Valgrind environment.
1343        ctx.set_driver_timeout_ms(driver_timeout_ms)?;
1344        // Handlers are reference-counted: the context/resources keep clones alive,
1345        // and the values are freed automatically when the last reference drops.
1346        let error_handler = Handler::new(ErrorCount::default());
1347        let new_pub_handler = Handler::new(AeronNewPublicationLogger);
1348        let avail_counter_handler1 = Handler::new(AeronAvailableCounterLogger);
1349        let close_client_handler = Handler::new(AeronCloseClientLogger);
1350        let new_sub_handler = Handler::new(AeronNewSubscriptionLogger);
1351        let unavail_counter_handler = Handler::new(AeronUnavailableCounterLogger);
1352        let avail_counter_handler2 = Handler::new(AeronAvailableCounterLogger);
1353        let excl_pub_handler = Handler::new(AeronNewPublicationLogger);
1354        ctx.set_error_handler(Some(error_handler.clone()))?;
1355        ctx.set_on_new_publication(Some(new_pub_handler.clone()))?;
1356        ctx.set_on_available_counter(Some(avail_counter_handler1.clone()))?;
1357        ctx.set_on_close_client(Some(close_client_handler.clone()))?;
1358        ctx.set_on_new_subscription(Some(new_sub_handler.clone()))?;
1359        ctx.set_on_unavailable_counter(Some(unavail_counter_handler.clone()))?;
1360        ctx.set_on_available_counter(Some(avail_counter_handler2.clone()))?;
1361        ctx.set_on_new_exclusive_publication(Some(excl_pub_handler.clone()))?;
1362
1363        info!("creating client [simple_large_send test]");
1364        let aeron = Aeron::new(&ctx)?;
1365        info!("starting client");
1366
1367        aeron.start()?;
1368        info!("client started");
1369        let publisher = aeron.add_publication(AERON_IPC_STREAM, 123, Duration::from_secs(5))?;
1370        info!("created publisher");
1371
1372        assert!(AeronCncMetadata::load_from_file(ctx.get_dir())?.pid > 0);
1373        let cstr = std::ffi::CString::new(ctx.get_dir()).unwrap();
1374        AeronCncMetadata::read_from_file(&cstr, |cnc| {
1375            assert!(cnc.pid > 0);
1376        })?;
1377        assert!(AeronCnc::open(&ctx.get_dir().into_c_string())?.get_to_driver_heartbeat_ms()? > 0);
1378        let cstr = std::ffi::CString::new(ctx.get_dir()).unwrap();
1379        for _ in 0..50 {
1380            AeronCnc::read(&cstr, |cnc| {
1381                assert!(cnc.get_to_driver_heartbeat_ms().unwrap() > 0);
1382            })?;
1383        }
1384
1385        let subscription = aeron.add_subscription(
1386            AERON_IPC_STREAM,
1387            123,
1388            Handlers::NONE,
1389            Handlers::NONE,
1390            Duration::from_secs(5),
1391        )?;
1392        info!("created subscription");
1393
1394        subscription.poll_fn(|msg, header| println!("foo"), 1024).unwrap();
1395
1396        // pick a large enough size to confirm fragement assembler is working
1397        let string_len = media_driver_ctx.ipc_mtu_length * 100;
1398        info!("string length: {}", string_len);
1399
1400        let stop_publisher = Arc::new(AtomicBool::new(false));
1401
1402        let publisher_handler = {
1403            let stop_publisher = stop_publisher.clone();
1404            std::thread::spawn(move || {
1405                let binding = "1".repeat(string_len);
1406                let large_msg = binding.as_bytes();
1407                loop {
1408                    if stop_publisher.load(Ordering::Acquire) || publisher.is_closed() {
1409                        break;
1410                    }
1411                    let result = publisher.offer_raw(large_msg, Handlers::NONE);
1412
1413                    assert_eq!(123, publisher.get_constants().unwrap().stream_id);
1414
1415                    if result < large_msg.len() as i64 {
1416                        let error = AeronCError::from_code(result as i32);
1417                        match error.kind() {
1418                            AeronErrorType::PublicationBackPressured | AeronErrorType::PublicationAdminAction => {
1419                                // ignore
1420                            }
1421                            _ => {
1422                                error!(
1423                                    "ERROR: failed to send message {:?}",
1424                                    AeronCError::from_code(result as i32)
1425                                );
1426                            }
1427                        }
1428                        sleep(Duration::from_millis(500));
1429                    }
1430                }
1431                info!("stopping publisher thread");
1432            })
1433        };
1434
1435        let mut assembler = AeronFragmentClosureAssembler::new()?;
1436
1437        struct Context {
1438            count: Arc<AtomicUsize>,
1439            stop: Arc<AtomicBool>,
1440            string_len: usize,
1441        }
1442
1443        let count = Arc::new(AtomicUsize::new(0usize));
1444        let mut context = Context {
1445            count: count.clone(),
1446            stop: stop.clone(),
1447            string_len,
1448        };
1449
1450        // Start the timer
1451        let start_time = Instant::now();
1452
1453        // Use break-with-value so cleanup (handler release, driver stop) always runs.
1454        // 120-second timeout: under Valgrind execution is ~10× slower, so 30 s is too tight.
1455        let loop_result: Result<(), Box<dyn error::Error>> = loop {
1456            if start_time.elapsed() > Duration::from_secs(120) {
1457                info!("Failed: exceeded 120-second timeout");
1458                break Err(Box::new(std::io::Error::new(
1459                    std::io::ErrorKind::TimedOut,
1460                    "Timeout exceeded",
1461                )));
1462            }
1463            let c = count.load(Ordering::SeqCst);
1464            if c > 100 {
1465                break Ok(());
1466            }
1467
1468            fn process_msg(ctx: &mut Context, buffer: &[u8], header: AeronHeader) {
1469                ctx.count.fetch_add(1, Ordering::SeqCst);
1470
1471                let values = header.get_values().unwrap();
1472                assert_ne!(values.frame.session_id, 0);
1473
1474                if buffer.len() != ctx.string_len {
1475                    ctx.stop.store(true, Ordering::SeqCst);
1476                    error!(
1477                        "ERROR: message was {} but was expecting {} [header={:?}]",
1478                        buffer.len(),
1479                        ctx.string_len,
1480                        header
1481                    );
1482                    sleep(Duration::from_secs(1));
1483                }
1484
1485                assert_eq!(buffer.len(), ctx.string_len);
1486                assert_eq!(buffer, "1".repeat(ctx.string_len).as_bytes());
1487            }
1488
1489            assembler.poll(&subscription, &mut context, process_msg, 128)?;
1490            assert_eq!(123, subscription.stream_id().unwrap());
1491        };
1492
1493        subscription.close()?;
1494
1495        info!("stopping client");
1496        stop_publisher.store(true, Ordering::SeqCst);
1497
1498        let _ = publisher_handler.join().unwrap();
1499        drop(aeron);
1500
1501        stop.store(true, Ordering::SeqCst);
1502        let _ = driver_handle.join().unwrap();
1503
1504        // Release all context handlers now that Aeron and the driver are fully stopped.
1505
1506        let cnc = AeronCnc::open(&ctx.get_dir().into_c_string())?;
1507        cnc.counters_reader()
1508            .foreach_counter_fn(|value: i64, id: i32, type_id: i32, key: &[u8], label: &str| {
1509                println!(
1510                    "counter reader id={id}, type_id={type_id}, key={key:?}, label={label}, value={value} [type={:?}]",
1511                    AeronSystemCounterType::try_from(type_id)
1512                );
1513            });
1514        cnc.error_log_read_fn(| observation_count: i32,
1515                                     first_observation_timestamp: i64,
1516                                     last_observation_timestamp: i64,
1517                                     error: &str| {
1518            println!("error: {error} observationCount={observation_count}, first_observation_timestamp={first_observation_timestamp}, last_observation_timestamp={last_observation_timestamp}");
1519        }, 0);
1520        cnc.loss_reporter_read_fn(|    observation_count: i64,
1521                                    total_bytes_lost: i64,
1522                                    first_observation_timestamp: i64,
1523                                    last_observation_timestamp: i64,
1524                                    session_id: i32,
1525                                    stream_id: i32,
1526                                    channel: &str,
1527                                    source: &str,| {
1528            println!("loss reporter observationCount={observation_count}, totalBytesLost={total_bytes_lost}, first_observed={first_observation_timestamp}, last_observed={last_observation_timestamp}, session_id={session_id}, stream_id={stream_id}, channel={channel} source={source}");
1529        })?;
1530
1531        loop_result?;
1532        Ok(())
1533    }
1534
1535    #[test]
1536    #[serial]
1537    pub fn try_claim() -> Result<(), Box<dyn error::Error>> {
1538        rusteron_code_gen::test_logger::init(log::LevelFilter::Info);
1539        let media_driver_ctx = rusteron_media_driver::AeronDriverContext::new()?;
1540        media_driver_ctx.set_dir_delete_on_shutdown(true)?;
1541        media_driver_ctx.set_dir_delete_on_start(true)?;
1542        media_driver_ctx.set_dir(&format!("{}{}", media_driver_ctx.get_dir(), Aeron::epoch_clock()).into_c_string())?;
1543        let (liveness_ns, unblock_ns, driver_timeout_ms) = if running_under_valgrind() {
1544            (180_000_000_000u64, 185_000_000_000u64, 180_000)
1545        } else {
1546            (60_000_000_000u64, 65_000_000_000u64, 60_000)
1547        };
1548        media_driver_ctx.set_client_liveness_timeout_ns(liveness_ns)?;
1549        media_driver_ctx.set_image_liveness_timeout_ns(liveness_ns)?;
1550        media_driver_ctx.set_publication_unblock_timeout_ns(unblock_ns)?;
1551        media_driver_ctx.set_driver_timeout_ms(driver_timeout_ms)?;
1552        let (stop, driver_handle) =
1553            rusteron_media_driver::AeronDriver::launch_embedded(media_driver_ctx.clone(), false);
1554
1555        let ctx = AeronContext::new()?;
1556        ctx.set_dir(&media_driver_ctx.get_dir().into_c_string())?;
1557        assert_eq!(media_driver_ctx.get_dir(), ctx.get_dir());
1558        // Keep client-side keepalive threshold aligned with slow Valgrind environment.
1559        ctx.set_driver_timeout_ms(driver_timeout_ms)?;
1560        let error_handler = Handler::new(ErrorCount::default());
1561        ctx.set_error_handler(Some(error_handler.clone()))?;
1562
1563        info!("creating client [try_claim test]");
1564        let aeron = Aeron::new(&ctx)?;
1565        info!("starting client");
1566
1567        aeron.start()?;
1568        info!("client started");
1569        const STREAM_ID: i32 = 123;
1570        let publisher = aeron.add_publication(AERON_IPC_STREAM, STREAM_ID, Duration::from_secs(5))?;
1571        info!("created publisher");
1572
1573        let subscription = aeron.add_subscription(
1574            AERON_IPC_STREAM,
1575            STREAM_ID,
1576            Handlers::NONE,
1577            Handlers::NONE,
1578            Duration::from_secs(5),
1579        )?;
1580        info!("created subscription");
1581
1582        // pick a large enough size to confirm fragement assembler is working
1583        let string_len = 156;
1584        info!("string length: {}", string_len);
1585
1586        let stop_publisher = Arc::new(AtomicBool::new(false));
1587
1588        let publisher_handler = {
1589            let stop_publisher = stop_publisher.clone();
1590            std::thread::spawn(move || {
1591                let binding = "1".repeat(string_len);
1592                let msg = binding.as_bytes();
1593                let buffer = AeronBufferClaim::default();
1594                loop {
1595                    if stop_publisher.load(Ordering::Acquire) || publisher.is_closed() {
1596                        break;
1597                    }
1598
1599                    let result = publisher.try_claim_raw(string_len, &buffer);
1600
1601                    if result < msg.len() as i64 {
1602                        error!(
1603                            "ERROR: failed to send message {:?}",
1604                            AeronCError::from_code(result as i32)
1605                        );
1606                    } else {
1607                        buffer.data().write_all(&msg).unwrap();
1608                        buffer.commit().unwrap();
1609                    }
1610                }
1611                info!("stopping publisher thread");
1612            })
1613        };
1614
1615        let count = Arc::new(AtomicUsize::new(0usize));
1616        let count_copy = Arc::clone(&count);
1617        let stop2 = stop.clone();
1618
1619        struct FragmentHandler {
1620            count_copy: Arc<AtomicUsize>,
1621            stop2: Arc<AtomicBool>,
1622            string_len: usize,
1623        }
1624
1625        impl AeronFragmentHandlerCallback for FragmentHandler {
1626            fn handle_aeron_fragment_handler(&mut self, buffer: &[u8], header: AeronHeader) {
1627                assert_eq!(STREAM_ID, header.get_values().unwrap().frame.stream_id);
1628                let header = header.get_values().unwrap();
1629                let frame = header.frame();
1630                let stream_id = frame.stream_id();
1631                assert_eq!(STREAM_ID, stream_id);
1632
1633                self.count_copy.fetch_add(1, Ordering::SeqCst);
1634
1635                if buffer.len() != self.string_len {
1636                    self.stop2.store(true, Ordering::SeqCst);
1637                    error!(
1638                        "ERROR: message was {} but was expecting {} [header={:?}]",
1639                        buffer.len(),
1640                        self.string_len,
1641                        header
1642                    );
1643                    sleep(Duration::from_secs(1));
1644                }
1645
1646                assert_eq!(buffer.len(), self.string_len);
1647                assert_eq!(buffer, "1".repeat(self.string_len).as_bytes());
1648            }
1649        }
1650
1651        let (closure, inner_handler) = Handler::with_fragment_assembler(FragmentHandler {
1652            count_copy,
1653            stop2,
1654            string_len,
1655        })?;
1656        let loop_result: Result<(), Box<dyn error::Error>> = {
1657            let start_time = Instant::now();
1658            loop {
1659                if start_time.elapsed() > Duration::from_secs(120) {
1660                    info!("Failed: exceeded 120-second timeout");
1661                    break Err(Box::new(std::io::Error::new(
1662                        std::io::ErrorKind::TimedOut,
1663                        "Timeout exceeded",
1664                    )));
1665                }
1666                let c = count.load(Ordering::SeqCst);
1667                if c > 100 {
1668                    break Ok(());
1669                }
1670                subscription.poll(Some(&closure), 128)?;
1671            }
1672        };
1673
1674        info!("stopping client");
1675
1676        stop_publisher.store(true, Ordering::SeqCst);
1677
1678        let _ = publisher_handler.join().unwrap();
1679
1680        stop.store(true, Ordering::SeqCst);
1681        let _ = driver_handle.join().unwrap();
1682        loop_result?;
1683        Ok(())
1684    }
1685
1686    #[test]
1687    #[serial]
1688    pub fn counters() -> Result<(), Box<dyn error::Error>> {
1689        rusteron_code_gen::test_logger::init(log::LevelFilter::Info);
1690        let media_driver_ctx = rusteron_media_driver::AeronDriverContext::new()?;
1691        media_driver_ctx.set_dir_delete_on_shutdown(true)?;
1692        media_driver_ctx.set_dir_delete_on_start(true)?;
1693        media_driver_ctx.set_dir(&format!("{}{}", media_driver_ctx.get_dir(), Aeron::epoch_clock()).into_c_string())?;
1694        let (stop, driver_handle) =
1695            rusteron_media_driver::AeronDriver::launch_embedded(media_driver_ctx.clone(), false);
1696
1697        let ctx = AeronContext::new()?;
1698        ctx.set_dir(&media_driver_ctx.get_dir().into_c_string())?;
1699        assert_eq!(media_driver_ctx.get_dir(), ctx.get_dir());
1700        let error_handler = Handler::new(ErrorCount::default());
1701        ctx.set_error_handler(Some(error_handler.clone()))?;
1702        let unavailable_counter_handler = Handler::new(AeronUnavailableCounterLogger);
1703        ctx.set_on_unavailable_counter(Some(unavailable_counter_handler.clone()))?;
1704
1705        struct AvailableCounterHandler {
1706            found_counter: bool,
1707        }
1708
1709        impl AeronAvailableCounterCallback for AvailableCounterHandler {
1710            fn handle_aeron_on_available_counter(
1711                &mut self,
1712                counters_reader: AeronCountersReader,
1713                registration_id: i64,
1714                counter_id: i32,
1715            ) -> () {
1716                info!(
1717            "on counter key={:?}, label={:?} registration_id={registration_id}, counter_id={counter_id}, value={}, {counters_reader:?}",
1718            String::from_utf8(counters_reader.get_counter_key(counter_id).unwrap()),
1719            counters_reader.get_counter_label(counter_id, 1000),
1720            counters_reader.addr(counter_id)
1721        );
1722
1723                assert_eq!(
1724                    counters_reader.counter_registration_id(counter_id).unwrap(),
1725                    registration_id
1726                );
1727
1728                if let Ok(label) = counters_reader.get_counter_label(counter_id, 1000) {
1729                    if label == "label_buffer" {
1730                        self.found_counter = true;
1731                        assert_eq!(&counters_reader.get_counter_key(counter_id).unwrap(), "key".as_bytes());
1732                    }
1733                }
1734            }
1735        }
1736
1737        let available_counter_handler = Handler::new(AvailableCounterHandler { found_counter: false });
1738        ctx.set_on_available_counter(Some(available_counter_handler.clone()))?;
1739
1740        info!("creating client");
1741        let aeron = Aeron::new(&ctx)?;
1742        info!("starting client");
1743
1744        aeron.start()?;
1745        info!("client started [counters test]");
1746
1747        let counter = aeron.add_counter(123, "key".as_bytes(), "label_buffer", Duration::from_secs(5))?;
1748        let constants = counter.get_constants()?;
1749        let counter_id = constants.counter_id;
1750
1751        let stop_publisher = Arc::new(AtomicBool::new(false));
1752
1753        let publisher_handler = {
1754            let stop_publisher = stop_publisher.clone();
1755            let counter = counter.clone();
1756            std::thread::spawn(move || {
1757                for _ in 0..150 {
1758                    if stop_publisher.load(Ordering::Acquire) || counter.is_closed() {
1759                        break;
1760                    }
1761                    counter.addr_atomic().fetch_add(1, Ordering::SeqCst);
1762                }
1763                info!("stopping publisher thread");
1764            })
1765        };
1766
1767        let now = Instant::now();
1768        while counter.addr_atomic().load(Ordering::SeqCst) < 100 && now.elapsed() < Duration::from_secs(10) {
1769            sleep(Duration::from_micros(10));
1770        }
1771
1772        assert!(now.elapsed() < Duration::from_secs(10));
1773
1774        info!("counter is {}", counter.addr_atomic().load(Ordering::SeqCst));
1775
1776        info!("stopping client");
1777
1778        #[cfg(not(target_os = "windows"))] // not sure why windows version doesn't fire event
1779        assert!(available_counter_handler.found_counter);
1780
1781        let reader = aeron.counters_reader();
1782        assert_eq!(reader.get_counter_label(counter_id, 256)?, "label_buffer");
1783        assert_eq!(reader.get_counter_key(counter_id)?, "key".as_bytes());
1784        let buffers = AeronCountersReaderBuffers::default();
1785        reader.get_buffers(&buffers)?;
1786
1787        stop_publisher.store(true, Ordering::SeqCst);
1788
1789        let _ = publisher_handler.join().unwrap();
1790
1791        stop.store(true, Ordering::SeqCst);
1792        let _ = driver_handle.join().unwrap();
1793        Ok(())
1794    }
1795
1796    /// A simple error counter for testing error callback invocation.
1797    #[derive(Default, Debug)]
1798    struct TestErrorCount {
1799        pub error_count: usize,
1800    }
1801
1802    impl Drop for TestErrorCount {
1803        fn drop(&mut self) {
1804            info!("TestErrorCount dropped with {} errors", self.error_count);
1805        }
1806    }
1807
1808    impl AeronErrorHandlerCallback for TestErrorCount {
1809        fn handle_aeron_error_handler(&mut self, error_code: c_int, msg: &str) {
1810            error!("Aeron error {}: {}", error_code, msg);
1811            self.error_count += 1;
1812        }
1813    }
1814
1815    #[test]
1816    #[serial]
1817    pub fn backpressure_recovery_test() -> Result<(), Box<dyn error::Error>> {
1818        rusteron_code_gen::test_logger::init(log::LevelFilter::Info);
1819
1820        let under_valgrind = running_under_valgrind();
1821        let driver_timeout_ms = if under_valgrind { 180_000 } else { 60_000 };
1822        let liveness_timeout_ns = if under_valgrind {
1823            180_000_000_000
1824        } else {
1825            60_000_000_000
1826        };
1827        let poll_timeout = Duration::from_millis(driver_timeout_ms as u64);
1828
1829        let media_driver_ctx = rusteron_media_driver::AeronDriverContext::new()?;
1830        media_driver_ctx.set_dir_delete_on_shutdown(true)?;
1831        media_driver_ctx.set_dir_delete_on_start(true)?;
1832        media_driver_ctx.set_dir(&format!("{}{}", media_driver_ctx.get_dir(), Aeron::epoch_clock()).into_c_string())?;
1833        media_driver_ctx.set_client_liveness_timeout_ns(liveness_timeout_ns)?;
1834        media_driver_ctx.set_image_liveness_timeout_ns(liveness_timeout_ns)?;
1835        media_driver_ctx.set_publication_unblock_timeout_ns(liveness_timeout_ns + 5_000_000_000)?;
1836        media_driver_ctx.set_driver_timeout_ms(driver_timeout_ms)?;
1837        let (stop, driver_handle) =
1838            rusteron_media_driver::AeronDriver::launch_embedded(media_driver_ctx.clone(), false);
1839
1840        let ctx = AeronContext::new()?;
1841        ctx.set_dir(&media_driver_ctx.get_dir().into_c_string())?;
1842        ctx.set_driver_timeout_ms(driver_timeout_ms)?;
1843        let error_handler = Handler::new(TestErrorCount::default());
1844        ctx.set_error_handler(Some(error_handler.clone()))?;
1845
1846        let aeron = Aeron::new(&ctx)?;
1847        aeron.start()?;
1848
1849        let publisher = aeron.add_publication(AERON_IPC_STREAM, 123, Duration::from_secs(5))?;
1850        let subscription = aeron.add_subscription(
1851            AERON_IPC_STREAM,
1852            123,
1853            Handlers::NONE,
1854            Handlers::NONE,
1855            Duration::from_secs(5),
1856        )?;
1857
1858        let count = Arc::new(AtomicUsize::new(0));
1859        let start_time = Instant::now();
1860
1861        let stop_publisher = Arc::new(AtomicBool::new(false));
1862
1863        // Spawn a publisher thread that repeatedly sends "test" messages.
1864        let publisher_thread = {
1865            let stop_publisher = stop_publisher.clone();
1866            std::thread::spawn(move || {
1867                while !stop_publisher.load(Ordering::Acquire) {
1868                    let msg = b"test";
1869                    let result = publisher.offer_raw(msg, Handlers::NONE);
1870                    // If backpressure is encountered, sleep a bit.
1871                    if result == AeronErrorType::PublicationBackPressured.code() as i64 {
1872                        sleep(Duration::from_millis(50));
1873                    }
1874                    if publisher.is_closed() {
1875                        break;
1876                    }
1877                }
1878            })
1879        };
1880
1881        // Poll using the inline closure via poll_fn until we receive at least 50 messages.
1882        while count.load(Ordering::SeqCst) < 50 && start_time.elapsed() < poll_timeout {
1883            let _ = subscription.poll_fn(
1884                |_msg, _header| {
1885                    count.fetch_add(1, Ordering::SeqCst);
1886                },
1887                128,
1888            )?;
1889        }
1890
1891        stop_publisher.store(true, Ordering::SeqCst);
1892        publisher_thread.join().unwrap();
1893        stop.store(true, Ordering::SeqCst);
1894        let _ = driver_handle.join().unwrap();
1895
1896        assert!(
1897            count.load(Ordering::SeqCst) >= 50,
1898            "Expected at least 50 messages received"
1899        );
1900        Ok(())
1901    }
1902
1903    #[test]
1904    #[serial]
1905    pub fn multi_subscription_test() -> Result<(), Box<dyn error::Error>> {
1906        rusteron_code_gen::test_logger::init(log::LevelFilter::Info);
1907
1908        let media_driver_ctx = rusteron_media_driver::AeronDriverContext::new()?;
1909        media_driver_ctx.set_dir_delete_on_shutdown(true)?;
1910        media_driver_ctx.set_dir_delete_on_start(true)?;
1911        media_driver_ctx.set_dir(&format!("{}{}", media_driver_ctx.get_dir(), Aeron::epoch_clock()).into_c_string())?;
1912        media_driver_ctx.set_client_liveness_timeout_ns(60_000_000_000)?;
1913        media_driver_ctx.set_image_liveness_timeout_ns(60_000_000_000)?;
1914        media_driver_ctx.set_publication_unblock_timeout_ns(65_000_000_000)?;
1915        media_driver_ctx.set_driver_timeout_ms(60_000)?;
1916        let (_stop, driver_handle) =
1917            rusteron_media_driver::AeronDriver::launch_embedded(media_driver_ctx.clone(), false);
1918
1919        let ctx = AeronContext::new()?;
1920        ctx.set_dir(&media_driver_ctx.get_dir().into_c_string())?;
1921        let error_handler = Handler::new(TestErrorCount::default());
1922        ctx.set_error_handler(Some(error_handler.clone()))?;
1923
1924        let aeron = Aeron::new(&ctx)?;
1925        aeron.start()?;
1926        let publisher = aeron.add_publication(AERON_IPC_STREAM, 123, Duration::from_secs(5))?;
1927
1928        // Create two subscriptions on the same channel.
1929        let subscription1 = aeron.add_subscription(
1930            AERON_IPC_STREAM,
1931            123,
1932            Handlers::NONE,
1933            Handlers::NONE,
1934            Duration::from_secs(5),
1935        )?;
1936        let subscription2 = aeron.add_subscription(
1937            AERON_IPC_STREAM,
1938            123,
1939            Handlers::NONE,
1940            Handlers::NONE,
1941            Duration::from_secs(5),
1942        )?;
1943
1944        let count1 = Arc::new(AtomicUsize::new(0));
1945        let count2 = Arc::new(AtomicUsize::new(0));
1946
1947        // Publish a single message.
1948        let msg = b"hello multi-subscription";
1949        let result = publisher.offer_raw(msg, Handlers::NONE);
1950        assert!(result >= msg.len() as i64, "Message should be sent successfully");
1951
1952        let start_time = Instant::now();
1953        // Poll both subscriptions with inline closures until each has received at least one message.
1954        while (count1.load(Ordering::SeqCst) < 1 || count2.load(Ordering::SeqCst) < 1)
1955            && start_time.elapsed() < Duration::from_secs(5)
1956        {
1957            let _ = subscription1.poll_fn(
1958                |_msg, _header| {
1959                    count1.fetch_add(1, Ordering::SeqCst);
1960                },
1961                128,
1962            )?;
1963            let _ = subscription2.poll_fn(
1964                |_msg, _header| {
1965                    count2.fetch_add(1, Ordering::SeqCst);
1966                },
1967                128,
1968            )?;
1969        }
1970
1971        assert!(
1972            count1.load(Ordering::SeqCst) >= 1,
1973            "Subscription 1 did not receive the message"
1974        );
1975        assert!(
1976            count2.load(Ordering::SeqCst) >= 1,
1977            "Subscription 2 did not receive the message"
1978        );
1979
1980        drop(subscription2);
1981        drop(subscription1);
1982        drop(publisher);
1983        drop(aeron);
1984        _stop.store(true, Ordering::SeqCst);
1985        let _ = driver_handle.join().unwrap();
1986        Ok(())
1987    }
1988
1989    #[test]
1990    #[serial]
1991    pub fn should_be_able_to_drop_after_close_manually_being_closed() -> Result<(), Box<dyn error::Error>> {
1992        rusteron_code_gen::test_logger::init(log::LevelFilter::Info);
1993
1994        let media_driver_ctx = rusteron_media_driver::AeronDriverContext::new()?;
1995        media_driver_ctx.set_dir_delete_on_shutdown(true)?;
1996        media_driver_ctx.set_dir_delete_on_start(true)?;
1997        media_driver_ctx.set_dir(&format!("{}{}", media_driver_ctx.get_dir(), Aeron::epoch_clock()).into_c_string())?;
1998        let (_stop, driver_handle) =
1999            rusteron_media_driver::AeronDriver::launch_embedded(media_driver_ctx.clone(), false);
2000
2001        let ctx = AeronContext::new()?;
2002        ctx.set_dir(&media_driver_ctx.get_dir().into_c_string())?;
2003        let error_handler = Handler::new(AeronErrorHandlerLogger);
2004        ctx.set_error_handler(Some(error_handler.clone()))?;
2005
2006        let aeron = Aeron::new(&ctx)?;
2007        aeron.start()?;
2008
2009        {
2010            let publisher = aeron.add_publication(AERON_IPC_STREAM, 123, Duration::from_secs(5))?;
2011            info!("created publication [sessionId={}]", publisher.session_id());
2012            publisher.close()?;
2013        }
2014
2015        {
2016            let publisher = aeron.add_publication(AERON_IPC_STREAM, 124, Duration::from_secs(5))?;
2017            info!("created publication [sessionId={}]", publisher.session_id());
2018            publisher.close()?;
2019        }
2020
2021        {
2022            let publisher = aeron.add_publication(AERON_IPC_STREAM, 125, Duration::from_secs(5))?;
2023            info!("created publication [sessionId={}]", publisher.session_id());
2024            publisher.close()?;
2025        }
2026
2027        drop(aeron);
2028        _stop.store(true, Ordering::SeqCst);
2029        let _ = driver_handle.join().unwrap();
2030        Ok(())
2031    }
2032
2033    #[test]
2034    #[serial]
2035    pub fn structural_close_safety_test() -> Result<(), Box<dyn error::Error>> {
2036        // Under the new design, close(self) consumes the handle — use-after-close
2037        // is a compile error, not a runtime check.  This test verifies that
2038        // structural teardown (the ManagedCResource::Drop path) properly frees
2039        // the C resource without errors.
2040        rusteron_code_gen::test_logger::init(log::LevelFilter::Info);
2041
2042        let media_driver_ctx = rusteron_media_driver::AeronDriverContext::new()?;
2043        media_driver_ctx.set_dir_delete_on_shutdown(true)?;
2044        media_driver_ctx.set_dir_delete_on_start(true)?;
2045        media_driver_ctx.set_dir(&format!("{}{}", media_driver_ctx.get_dir(), Aeron::epoch_clock()).into_c_string())?;
2046        let (_stop, driver_handle) =
2047            rusteron_media_driver::AeronDriver::launch_embedded(media_driver_ctx.clone(), false);
2048
2049        let ctx = AeronContext::new()?;
2050        ctx.set_dir(&media_driver_ctx.get_dir().into_c_string())?;
2051        let error_handler = Handler::new(TestErrorCount::default());
2052        ctx.set_error_handler(Some(error_handler.clone()))?;
2053
2054        let aeron = Aeron::new(&ctx)?;
2055        aeron.start()?;
2056        let publisher = aeron.add_publication(AERON_IPC_STREAM, 123, Duration::from_secs(5))?;
2057
2058        // Consume the publication handle — close(self) drops it, and the cleanup
2059        // closure (wired at construction) calls aeron_publication_close on Drop
2060        // of the last Rc.  No further use of `publisher` is possible at compile
2061        // time (the binding is moved into close()).
2062        publisher.close()?;
2063
2064        drop(aeron);
2065        _stop.store(true, Ordering::SeqCst);
2066        let _ = driver_handle.join().unwrap();
2067        Ok(())
2068    }
2069
2070    /// Test sending and receiving an empty (zero-length) message using inline closures with poll_fn.
2071    #[test]
2072    #[serial]
2073    pub fn empty_message_test() -> Result<(), Box<dyn error::Error>> {
2074        rusteron_code_gen::test_logger::init(log::LevelFilter::Info);
2075
2076        let media_driver_ctx = rusteron_media_driver::AeronDriverContext::new()?;
2077        media_driver_ctx.set_dir_delete_on_shutdown(true)?;
2078        media_driver_ctx.set_dir_delete_on_start(true)?;
2079        media_driver_ctx.set_dir(&format!("{}{}", media_driver_ctx.get_dir(), Aeron::epoch_clock()).into_c_string())?;
2080        let (_stop, driver_handle) =
2081            rusteron_media_driver::AeronDriver::launch_embedded(media_driver_ctx.clone(), false);
2082
2083        let ctx = AeronContext::new()?;
2084        ctx.set_dir(&media_driver_ctx.get_dir().into_c_string())?;
2085        let error_handler = Handler::new(TestErrorCount::default());
2086        ctx.set_error_handler(Some(error_handler.clone()))?;
2087
2088        let aeron = Aeron::new(&ctx)?;
2089        aeron.start()?;
2090        let publisher = aeron.add_publication(AERON_IPC_STREAM, 123, Duration::from_secs(5))?;
2091        let subscription = aeron.add_subscription(
2092            AERON_IPC_STREAM,
2093            123,
2094            Handlers::NONE,
2095            Handlers::NONE,
2096            Duration::from_secs(5),
2097        )?;
2098
2099        let empty_received = Arc::new(AtomicBool::new(false));
2100        let start_time = Instant::now();
2101
2102        let result = publisher.offer_raw(b"", Handlers::NONE);
2103        assert!(result > 0);
2104
2105        while !empty_received.load(Ordering::SeqCst) && start_time.elapsed() < Duration::from_secs(5) {
2106            let _ = subscription.poll_fn(
2107                |msg, _header| {
2108                    if msg.is_empty() {
2109                        empty_received.store(true, Ordering::SeqCst);
2110                    }
2111                },
2112                128,
2113            )?;
2114        }
2115
2116        assert!(empty_received.load(Ordering::SeqCst), "Empty message was not received");
2117        drop(subscription);
2118        drop(publisher);
2119        drop(aeron);
2120        _stop.store(true, Ordering::SeqCst);
2121        let _ = driver_handle.join().unwrap();
2122        Ok(())
2123    }
2124
2125    #[derive(Default, Debug)]
2126    struct MdcTotals {
2127        gap_events: u64,
2128        missing_messages: u64,
2129        received_messages: u64,
2130    }
2131
2132    #[derive(Debug)]
2133    struct MdcWindowStats {
2134        expected_seq: Option<u64>,
2135        gap_events: u64,
2136        missing_messages: u64,
2137        received_messages: u64,
2138        histogram: Histogram<u64>,
2139    }
2140
2141    impl MdcWindowStats {
2142        fn new() -> Result<Self, Box<dyn error::Error>> {
2143            Ok(Self {
2144                expected_seq: None,
2145                gap_events: 0,
2146                missing_messages: 0,
2147                received_messages: 0,
2148                histogram: Histogram::new(3)?,
2149            })
2150        }
2151
2152        fn observe(&mut self, seq: u64, sent_ts_ns: u64) {
2153            self.received_messages += 1;
2154
2155            match self.expected_seq {
2156                None => self.expected_seq = Some(seq.saturating_add(1)),
2157                Some(expected) if seq > expected => {
2158                    self.gap_events += 1;
2159                    self.missing_messages += seq - expected;
2160                    self.expected_seq = Some(seq.saturating_add(1));
2161                }
2162                Some(expected) if seq == expected => {
2163                    self.expected_seq = Some(expected.saturating_add(1));
2164                }
2165                Some(_) => {
2166                    // Ignore out-of-order/late packets for gap counting in this window.
2167                }
2168            }
2169
2170            let now_ns = Aeron::nano_clock().max(0) as u64;
2171            let latency_ns = now_ns.saturating_sub(sent_ts_ns);
2172            let _ = self.histogram.record(latency_ns);
2173        }
2174
2175        fn print_and_reset(&mut self, window_number: usize, interval: Duration, totals: &mut MdcTotals) {
2176            totals.gap_events += self.gap_events;
2177            totals.missing_messages += self.missing_messages;
2178            totals.received_messages += self.received_messages;
2179
2180            if self.histogram.len() > 0 {
2181                let min_us = self.histogram.min() / 1_000;
2182                let p50_us = self.histogram.value_at_quantile(0.50) / 1_000;
2183                let p99_us = self.histogram.value_at_quantile(0.99) / 1_000;
2184                let max_us = self.histogram.max() / 1_000;
2185                println!(
2186                    "[mdc-window-{window_number}] interval={interval:?} received={} gaps={} missing={} latency_us[min={}, p50={}, p99={}, max={}]",
2187                    self.received_messages, self.gap_events, self.missing_messages, min_us, p50_us, p99_us, max_us,
2188                );
2189            } else {
2190                println!(
2191                    "[mdc-window-{window_number}] interval={interval:?} received=0 gaps=0 missing=0 latency_us[min=n/a, p50=n/a, p99=n/a, max=n/a]"
2192                );
2193            }
2194
2195            self.expected_seq = None;
2196            self.gap_events = 0;
2197            self.missing_messages = 0;
2198            self.received_messages = 0;
2199            self.histogram.reset();
2200        }
2201    }
2202
2203    /// Run with loss profile on macOS:
2204    /// `just mdc-loss-run 120 10 0.10`
2205    ///
2206    /// The recipe handles PF setup and cleanup automatically.
2207    ///
2208    /// Manual cleanup (if needed):
2209    /// `sudo pfctl -a com.apple/rusteron-mdc-loss -F all`
2210    /// `sudo dnctl -q flush`
2211    /// `sudo pfctl -f /etc/pf.conf`
2212    #[test]
2213    #[serial]
2214    #[ignore] // Long-running diagnostics test for manual MDC with rolling latency/gap reports.
2215    pub fn mdc_unreliable_gap_latency_histogram_report() -> Result<(), Box<dyn error::Error>> {
2216        rusteron_code_gen::test_logger::init(log::LevelFilter::Info);
2217
2218        const STREAM_ID: i32 = 32931;
2219        const CONTROL_PORT: u16 = 32929;
2220        const SUBSCRIBER_PORT: u16 = 32930;
2221        const MESSAGE_LEN: usize = 130;
2222
2223        let report_interval = Duration::from_secs(
2224            std::env::var("RUSTERON_MDC_REPORT_INTERVAL_SECS")
2225                .ok()
2226                .and_then(|value| value.parse::<u64>().ok())
2227                .unwrap_or(10),
2228        );
2229        let test_duration = Duration::from_secs(
2230            std::env::var("RUSTERON_MDC_TEST_DURATION_SECS")
2231                .ok()
2232                .and_then(|value| value.parse::<u64>().ok())
2233                .unwrap_or(300),
2234        );
2235        let mdc_host = std::env::var("RUSTERON_MDC_HOST").unwrap_or("127.0.0.1".to_string());
2236
2237        let publication_channel = format!("aeron:udp?control-mode=manual|control={}:{CONTROL_PORT}", mdc_host);
2238        // - group=false: do not apply MDC receiver-group semantics.
2239        // - nak-delay=100us: shorten unreliable-stream gap-fill decision latency.
2240        let subscription_channel = format!(
2241            "aeron:udp?endpoint={}:{SUBSCRIBER_PORT}|reliable=false|tether=false|group=false|nak-delay=500us",
2242            mdc_host
2243        );
2244        let destination_uri = format!("aeron:udp?endpoint={}:{SUBSCRIBER_PORT}", mdc_host);
2245
2246        let (media_driver_ctx, stop_driver, driver_handle) = start_media_driver(32930)?;
2247        let aeron_dir = media_driver_ctx.get_dir().to_string();
2248
2249        println!(
2250            "[mdc-start] publication={} subscription={} duration={:?} report_interval={:?}",
2251            publication_channel, subscription_channel, test_duration, report_interval
2252        );
2253
2254        let running = Arc::new(AtomicBool::new(true));
2255
2256        let subscriber_dir = aeron_dir.clone();
2257        let subscriber_channel = subscription_channel.clone();
2258        let subscriber_running = Arc::clone(&running);
2259        let subscriber_thread = std::thread::spawn(move || -> MdcTotals {
2260            let (_ctx, aeron) =
2261                create_client_for_dir(&subscriber_dir).expect("failed to create subscriber aeron client");
2262
2263            let subscription = aeron
2264                .add_subscription(
2265                    &subscriber_channel.into_c_string(),
2266                    STREAM_ID,
2267                    Handlers::NONE,
2268                    Handlers::NONE,
2269                    Duration::from_secs(5),
2270                )
2271                .expect("failed to create subscriber");
2272
2273            let mut totals = MdcTotals::default();
2274            let mut window_stats = MdcWindowStats::new().expect("failed to create histogram");
2275            let test_start = Instant::now();
2276            let mut window_start = test_start;
2277            let mut window_number = 1usize;
2278
2279            while test_start.elapsed() < test_duration {
2280                let _ = subscription
2281                    .poll_fn(
2282                        |msg, _header| {
2283                            if msg.len() < 16 {
2284                                return;
2285                            }
2286
2287                            let seq = u64::from_le_bytes(msg[0..8].try_into().unwrap());
2288                            let sent_ts_ns = u64::from_le_bytes(msg[8..16].try_into().unwrap());
2289                            window_stats.observe(seq, sent_ts_ns);
2290                        },
2291                        10_000,
2292                    )
2293                    .expect("subscriber poll failed");
2294
2295                if window_start.elapsed() >= report_interval {
2296                    window_stats.print_and_reset(window_number, report_interval, &mut totals);
2297                    window_number += 1;
2298                    window_start = Instant::now();
2299                }
2300            }
2301
2302            window_stats.print_and_reset(window_number, report_interval, &mut totals);
2303            subscriber_running.store(false, Ordering::SeqCst);
2304            totals
2305        });
2306
2307        // Ensure subscriber has started before publisher setup.
2308        sleep(Duration::from_millis(250));
2309
2310        let publisher_dir = aeron_dir;
2311        let publisher_channel = publication_channel.clone();
2312        let publisher_destination = destination_uri.clone();
2313        let publisher_running = Arc::clone(&running);
2314        let publisher_thread = std::thread::spawn(move || -> u64 {
2315            let (_ctx, aeron) = create_client_for_dir(&publisher_dir).expect("failed to create publisher aeron client");
2316
2317            let publication = aeron
2318                .add_exclusive_publication(&publisher_channel.into_c_string(), STREAM_ID, Duration::from_secs(5))
2319                .expect("failed to create publication");
2320
2321            let add_destination = AeronAsyncDestination::aeron_exclusive_publication_async_add_destination(
2322                &aeron,
2323                &publication,
2324                &publisher_destination.into_c_string(),
2325            )
2326            .expect("failed to add manual MDC destination");
2327
2328            let add_destination_start = Instant::now();
2329            while add_destination
2330                .aeron_exclusive_publication_async_destination_poll()
2331                .expect("destination add poll failed")
2332                == 0
2333            {
2334                assert!(
2335                    add_destination_start.elapsed() <= Duration::from_secs(5),
2336                    "Timed out adding manual MDC destination"
2337                );
2338                sleep(Duration::from_millis(10));
2339            }
2340
2341            let connect_start = Instant::now();
2342            while !publication.is_connected() && connect_start.elapsed() < Duration::from_secs(5) {
2343                sleep(Duration::from_millis(10));
2344            }
2345            assert!(
2346                publication.is_connected(),
2347                "manual MDC publication did not connect to subscriber destination"
2348            );
2349
2350            let mut seq: u64 = 0;
2351            let mut payload = [0u8; MESSAGE_LEN];
2352            while publisher_running.load(Ordering::Acquire) {
2353                payload[0..8].copy_from_slice(&seq.to_le_bytes());
2354                let ts_ns = Aeron::nano_clock().max(0) as u64;
2355                payload[8..16].copy_from_slice(&ts_ns.to_le_bytes());
2356
2357                let result = publication.offer_raw(&payload, Handlers::NONE);
2358                if result > 0 {
2359                    seq = seq.wrapping_add(1);
2360                }
2361                sleep(Duration::from_millis(1));
2362            }
2363            seq
2364        });
2365
2366        let totals = subscriber_thread.join().unwrap();
2367        running.store(false, Ordering::SeqCst);
2368        let sent_messages = publisher_thread.join().unwrap();
2369        stop_driver.store(true, Ordering::SeqCst);
2370        let _ = driver_handle.join().unwrap();
2371
2372        println!(
2373            "[mdc-summary] sent={} received={} total_gaps={} total_missing={}",
2374            sent_messages, totals.received_messages, totals.gap_events, totals.missing_messages
2375        );
2376
2377        assert!(sent_messages > 0, "publisher failed to send any messages");
2378        assert!(totals.received_messages > 0, "subscriber did not receive any messages");
2379        Ok(())
2380    }
2381
2382    #[test]
2383    #[serial]
2384    #[ignore] // need to work to get tags working properly, its more of testing issue then tag issue
2385    pub fn tags() -> Result<(), Box<dyn error::Error>> {
2386        rusteron_code_gen::test_logger::init(log::LevelFilter::Debug);
2387
2388        let (md_ctx, stop, md) = start_media_driver(1)?;
2389
2390        let (_a_ctx2, aeron_sub) = create_client(&md_ctx)?;
2391
2392        info!("creating suscriber 1");
2393        let sub = aeron_sub
2394            .add_subscription(
2395                c"aeron:udp?tags=100",
2396                123,
2397                Handlers::NONE,
2398                Handlers::NONE,
2399                Duration::from_secs(50),
2400            )
2401            .map_err(|e| {
2402                error!("aeron error={}", Aeron::errmsg());
2403                e
2404            })?;
2405
2406        let ctx = AeronContext::new()?;
2407        ctx.set_dir(&aeron_sub.context().get_dir().into_c_string())?;
2408        let aeron = Aeron::new(&ctx)?;
2409        aeron.start()?;
2410
2411        info!("creating suscriber 2");
2412        let sub2 = aeron_sub.add_subscription(
2413            c"aeron:udp?tags=100",
2414            123,
2415            Handlers::NONE,
2416            Handlers::NONE,
2417            Duration::from_secs(50),
2418        )?;
2419
2420        let (_a_ctx1, aeron_pub) = create_client(&md_ctx)?;
2421        info!("creating publisher");
2422        assert!(!aeron_pub.is_closed());
2423        let publisher = aeron_pub
2424            .add_publication(
2425                c"aeron:udp?endpoint=localhost:4040|alias=test|tags=100",
2426                123,
2427                Duration::from_secs(5),
2428            )
2429            .map_err(|e| {
2430                error!("aeron error={}", Aeron::errmsg());
2431                e
2432            })?;
2433
2434        info!("publishing msg");
2435
2436        loop {
2437            let result = publisher.offer_raw("213".as_bytes(), Handlers::NONE);
2438            if result < 0 {
2439                error!("failed to publish {:?}", AeronCError::from_code(result as i32));
2440            } else {
2441                break;
2442            }
2443        }
2444
2445        sub.poll_fn(
2446            |msg, _header| {
2447                println!("Received message: {:?}", msg);
2448            },
2449            128,
2450        )?;
2451        sub2.poll_fn(
2452            |msg, _header| {
2453                println!("Received message: {:?}", msg);
2454            },
2455            128,
2456        )?;
2457
2458        stop.store(true, Ordering::SeqCst);
2459
2460        Ok(())
2461    }
2462
2463    fn create_client_for_dir(dir: &str) -> Result<(AeronContext, Aeron), Box<dyn Error>> {
2464        info!("creating aeron client [dir={}]", dir);
2465        let ctx = AeronContext::new()?;
2466        ctx.set_dir(&dir.into_c_string())?;
2467        let aeron = Aeron::new(&ctx)?;
2468        aeron.start()?;
2469        Ok((ctx, aeron))
2470    }
2471
2472    fn create_client(media_driver_ctx: &AeronDriverContext) -> Result<(AeronContext, Aeron), Box<dyn Error>> {
2473        let dir = media_driver_ctx.get_dir().to_string();
2474        create_client_for_dir(&dir)
2475    }
2476
2477    fn start_media_driver(
2478        instance: u64,
2479    ) -> Result<
2480        (
2481            AeronDriverContext,
2482            Arc<AtomicBool>,
2483            JoinHandle<Result<(), rusteron_media_driver::AeronCError>>,
2484        ),
2485        Box<dyn Error>,
2486    > {
2487        let media_driver_ctx = rusteron_media_driver::AeronDriverContext::new()?;
2488        media_driver_ctx.set_dir_delete_on_shutdown(true)?;
2489        media_driver_ctx.set_dir_delete_on_start(true)?;
2490        media_driver_ctx
2491            .set_dir(&format!("{}{}-{}", media_driver_ctx.get_dir(), Aeron::epoch_clock(), instance).into_c_string())?;
2492        let (stop, driver_handle) =
2493            rusteron_media_driver::AeronDriver::launch_embedded(media_driver_ctx.clone(), false);
2494        Ok((media_driver_ctx, stop, driver_handle))
2495    }
2496
2497    /// C1: exercise the generated `AeronExclusivePublication` surface (the
2498    /// `offer_result` / `try_claim_owned` parity added for non-exclusive pubs)
2499    /// plus the generated `AeronPublicationConstants` accessors — a slice of the
2500    /// generated API that no other test touches end-to-end.
2501    #[test]
2502    #[serial]
2503    fn exclusive_publication_result_variants_and_constants() -> Result<(), Box<dyn error::Error>> {
2504        let (media_driver_ctx, stop, _driver_handle) = start_media_driver(8)?;
2505        let (_ctx, aeron) = create_client(&media_driver_ctx)?;
2506
2507        let stream_id: i32 = 4321;
2508        let exclusive = aeron.add_exclusive_publication(AERON_IPC_STREAM, stream_id, Duration::from_secs(5))?;
2509
2510        // Add a matching subscription so the IPC image connects and offers succeed.
2511        let sub_poller = aeron.async_add_subscription(
2512            &"aeron:ipc".to_string().into_c_string(),
2513            stream_id,
2514            Handlers::NONE,
2515            Handlers::NONE,
2516        )?;
2517        let mut subscription: Option<AeronSubscription> = None;
2518        let sub_start = Instant::now();
2519        while sub_start.elapsed() < Duration::from_secs(2) && subscription.is_none() {
2520            if let Ok(Some(s)) = sub_poller.poll() {
2521                subscription = Some(s);
2522            }
2523            #[cfg(debug_assertions)]
2524            sleep(Duration::from_millis(10));
2525        }
2526        let _subscription = subscription.expect("subscription did not come up");
2527
2528        let conn = Instant::now();
2529        while !exclusive.is_connected() && conn.elapsed() < Duration::from_secs(2) {
2530            #[cfg(debug_assertions)]
2531            sleep(Duration::from_millis(10));
2532        }
2533        assert_eq!(exclusive.status(), AeronStatus::Connected);
2534
2535        // 1) offer_result_simple (typed Result variant on the exclusive pub).
2536        let payload = b"exclusive-result";
2537        let mut offered_pos = None;
2538        let offer_start = Instant::now();
2539        while offer_start.elapsed() < Duration::from_secs(2) && offered_pos.is_none() {
2540            if let Ok(pos) = exclusive.offer(payload) {
2541                offered_pos = Some(pos);
2542            }
2543            #[cfg(debug_assertions)]
2544            sleep(Duration::from_millis(10));
2545        }
2546        let pos = offered_pos.expect("exclusive offer_result_simple never succeeded");
2547        assert!(pos >= payload.len() as i64);
2548
2549        // 2) RAII zero-copy claim on the exclusive pub.
2550        let claim_payload = b"exclusive-claim";
2551        let mut committed = false;
2552        let claim_start = Instant::now();
2553        while claim_start.elapsed() < Duration::from_secs(2) && !committed {
2554            if let Ok(mut claim) = exclusive.try_claim_owned(claim_payload.len()) {
2555                claim.data()[..claim_payload.len()].copy_from_slice(claim_payload);
2556                committed = claim.commit().is_ok();
2557            }
2558            #[cfg(debug_assertions)]
2559            sleep(Duration::from_millis(10));
2560        }
2561        assert!(committed, "exclusive try_claim_owned + commit never succeeded");
2562
2563        // 3) Generated constants accessors return sane, consistent values.
2564        let constants = exclusive.get_constants().expect("publication constants");
2565        assert_eq!(constants.stream_id, stream_id);
2566        // channel is a *const c_char into the C struct; just assert it's a valid C string.
2567        assert!(!constants.channel.is_null());
2568        assert_eq!(
2569            unsafe { std::ffi::CStr::from_ptr(constants.channel) }.to_str().unwrap(),
2570            "aeron:ipc"
2571        );
2572        assert!(constants.max_possible_position > 0);
2573        assert!(constants.term_buffer_length > 0);
2574        assert!(constants.max_message_length > 0);
2575        assert!(constants.max_payload_length > 0);
2576        // position() advances with the offers.
2577        assert!(exclusive.position() >= pos);
2578
2579        stop.store(true, Ordering::SeqCst);
2580        Ok(())
2581    }
2582
2583    /// C2: property tests for the pure-Rust (no driver) logic. Fast and
2584    /// deterministic — they fuzz the invariants the unit tests only sample.
2585    mod property_tests {
2586        use crate::{
2587            validate_endpoint_for_aeron_udp, AeronCError, AeronErrorType, AeronOfferError, AeronStatus,
2588            AeronStatusTracker,
2589        };
2590        use proptest::prelude::*;
2591
2592        /// Fuzz: arbitrary endpoint strings must never panic — only Ok/Err.
2593        #[test]
2594        fn validate_endpoint_never_panics_on_arbitrary_input() {
2595            proptest!(|(s in ".{0,40}")| {
2596                let _ = validate_endpoint_for_aeron_udp(&s);
2597            });
2598        }
2599
2600        /// Any hostname/IPv4 + port in 0..=65535 is accepted.
2601        #[test]
2602        fn validate_endpoint_accepts_well_formed_host_port() {
2603            let host = "[a-z][a-z0-9-]{0,20}(\\.[a-z0-9-]{1,20}){0,3}";
2604            proptest!(|(host in host, port in 0u16..=65535)| {
2605                let ep = format!("{host}:{port}");
2606                prop_assert!(
2607                    validate_endpoint_for_aeron_udp(&ep).is_ok(),
2608                    "expected ok for {ep}"
2609                );
2610            });
2611        }
2612
2613        /// Any valid-looking host with a port > 65535 is rejected.
2614        #[test]
2615        fn validate_endpoint_rejects_out_of_range_port() {
2616            proptest!(|(port in 65536u32..=200_000)| {
2617                let ep = format!("localhost:{port}");
2618                prop_assert!(validate_endpoint_for_aeron_udp(&ep).is_err());
2619            });
2620        }
2621
2622        /// Tracker emits iff the status differs from the previous one, and
2623        /// `last_status` is always the most recently observed.
2624        #[test]
2625        fn tracker_emits_iff_transition_and_tracks_last() {
2626            let status = prop::sample::select(vec![
2627                AeronStatus::Disconnected,
2628                AeronStatus::Connected,
2629                AeronStatus::BackPressured,
2630                AeronStatus::Closed,
2631            ]);
2632            proptest!(|(seq in prop::collection::vec(status, 0..40))| {
2633                let mut t = AeronStatusTracker::new();
2634                let mut prev: Option<AeronStatus> = None;
2635                for &s in &seq {
2636                    let emitted = t.observe(s);
2637                    let expected_emits = prev != Some(s);
2638                    prop_assert_eq!(emitted.is_some(), expected_emits);
2639                    if let Some(e) = emitted {
2640                        prop_assert_eq!(e, s);
2641                    }
2642                    prev = Some(s);
2643                }
2644                prop_assert_eq!(t.last_status(), seq.last().copied());
2645            });
2646        }
2647
2648        /// `AeronOfferError::from_position`: non-negative → Ok(position); the five
2649        /// documented sentinels map to their variants; any other negative keeps its
2650        /// code inside `Error`. Every error is classified retryable xor fatal.
2651        #[test]
2652        fn from_position_and_classification_are_consistent() {
2653            proptest!(|(pos in -1_000_000i64..=1_000_000)| {
2654                match AeronOfferError::from_position(pos) {
2655                    Ok(p) => prop_assert!(p >= 0 && p == pos),
2656                    Err(e) => {
2657                        prop_assert!(pos < 0);
2658                        prop_assert!(e.is_retryable() ^ e.is_fatal());
2659                        match (pos, &e) {
2660                            (-1, AeronOfferError::NotConnected)
2661                            | (-2, AeronOfferError::BackPressured)
2662                            | (-3, AeronOfferError::AdminAction)
2663                            | (-4, AeronOfferError::Closed)
2664                            | (-5, AeronOfferError::MaxPositionExceeded) => {}
2665                            (p, AeronOfferError::Error(inner)) if p < -5 || p == 0 => {
2666                                prop_assert_eq!(inner.code, p as i32);
2667                            }
2668                            (p, other) => prop_assert!(false, "unexpected mapping {} -> {:?}", p, other),
2669                        }
2670                    }
2671                }
2672            });
2673        }
2674
2675        /// Sanity: every documented Aeron error code maps back to itself.
2676        #[test]
2677        fn known_error_codes_round_trip() {
2678            for code in [-1, -2, -3, -4, -5, -6, -1000, -1001, -1002, -1003] {
2679                let err = AeronCError::from_code(code);
2680                assert_eq!(err.code, code);
2681                assert_eq!(AeronErrorType::from_code(code).code(), code);
2682            }
2683        }
2684    }
2685
2686    #[doc = include_str!("../../README.md")]
2687    mod readme_tests {}
2688
2689    #[cfg(test)]
2690    mod spin_poll_tests {
2691        use super::*;
2692        use crate::test_alloc::assert_no_allocation;
2693        use rusteron_media_driver::AeronDriverContext;
2694        use serial_test::serial;
2695
2696        /// Tests the `poll_fn` closure-poll on `AeronSubscription`.
2697        /// Verifies that it receives all published messages without
2698        /// allocations on the hot path.
2699        #[test]
2700        #[serial]
2701        fn poll_fn_receives_all_messages_no_alloc() -> Result<(), Box<dyn error::Error>> {
2702            rusteron_code_gen::test_logger::init(log::LevelFilter::Info);
2703
2704            let media_driver_ctx = AeronDriverContext::new()?;
2705            media_driver_ctx.set_dir_delete_on_shutdown(true)?;
2706            media_driver_ctx.set_dir_delete_on_start(true)?;
2707            media_driver_ctx
2708                .set_dir(&format!("{}{}", media_driver_ctx.get_dir(), Aeron::epoch_clock()).into_c_string())?;
2709            let (stop, driver_handle) =
2710                rusteron_media_driver::AeronDriver::launch_embedded(media_driver_ctx.clone(), false);
2711
2712            let ctx = AeronContext::new()?;
2713            ctx.set_dir(&media_driver_ctx.get_dir().into_c_string())?;
2714            let error_handler = Handler::new(ErrorCount::default());
2715            ctx.set_error_handler(Some(error_handler.clone()))?;
2716            let aeron = Aeron::new(&ctx)?;
2717            aeron.start()?;
2718
2719            let channel = String::from("aeron:ipc");
2720            let stream_id: i32 = 9999;
2721
2722            let pub_poller = aeron.async_add_publication(&channel.clone().into_c_string(), stream_id)?;
2723            let sub_poller =
2724                aeron.async_add_subscription(&channel.into_c_string(), stream_id, Handlers::NONE, Handlers::NONE)?;
2725
2726            let mut publication: Option<AeronPublication> = None;
2727            let mut subscription: Option<AeronSubscription> = None;
2728            let start = Instant::now();
2729            while start.elapsed() < Duration::from_secs(2) {
2730                if publication.is_none() {
2731                    if let Ok(Some(p)) = pub_poller.poll() {
2732                        publication = Some(p);
2733                    }
2734                }
2735                if subscription.is_none() {
2736                    if let Ok(Some(s)) = sub_poller.poll() {
2737                        subscription = Some(s);
2738                    }
2739                }
2740                if publication.is_some() && subscription.is_some() {
2741                    break;
2742                }
2743                #[cfg(debug_assertions)]
2744                sleep(Duration::from_millis(10));
2745            }
2746
2747            let (publisher, subscription) = match (publication, subscription) {
2748                (Some(p), Some(s)) => (p, s),
2749                _ => panic!("publication/subscription did not come up"),
2750            };
2751
2752            // Wait for IPC images to connect
2753            let conn_start = Instant::now();
2754            while !publisher.is_connected() && conn_start.elapsed() < Duration::from_secs(2) {
2755                #[cfg(debug_assertions)]
2756                sleep(Duration::from_millis(10));
2757            }
2758
2759            // Publish N distinct messages
2760            const NUM_MESSAGES: usize = 10;
2761            let payloads: Vec<Vec<u8>> = (0..NUM_MESSAGES)
2762                .map(|i| format!("message-{}", i).into_bytes())
2763                .collect();
2764
2765            for payload in &payloads {
2766                let offer_start = Instant::now();
2767                let mut offered = false;
2768                while offer_start.elapsed() < Duration::from_secs(2) {
2769                    if let Ok(pos) = publisher.offer(payload) {
2770                        if pos >= payload.len() as i64 {
2771                            offered = true;
2772                            break;
2773                        }
2774                    }
2775                    #[cfg(debug_assertions)]
2776                    sleep(Duration::from_millis(10));
2777                }
2778                assert!(offered, "Failed to offer message");
2779            }
2780
2781            // Use poll_fn to receive all messages (spin-poll pattern)
2782            let mut received_count = 0;
2783            let max_iterations = 1000;
2784
2785            for _ in 0..max_iterations {
2786                let fragments = subscription.poll_fn(
2787                    |data, _header| {
2788                        received_count += 1;
2789                        info!("Received fragment {} bytes", data.len());
2790                    },
2791                    1024,
2792                )?;
2793
2794                if fragments > 0 {
2795                    // Got some messages, exit spin
2796                    break;
2797                }
2798
2799                #[cfg(debug_assertions)]
2800                sleep(Duration::from_micros(100));
2801            }
2802
2803            assert_eq!(
2804                received_count, NUM_MESSAGES,
2805                "Expected to receive {} messages, got {}",
2806                NUM_MESSAGES, received_count
2807            );
2808
2809            // Now test that the hot path is allocation-free
2810            let alloc_before = current_allocs();
2811
2812            let mut alloc_free_count = 0;
2813            let spin_alloc_test = || {
2814                for _ in 0..10 {
2815                    let _ = subscription.poll_fn(
2816                        |data, _header| {
2817                            alloc_free_count += 1;
2818                        },
2819                        1024,
2820                    );
2821                }
2822            };
2823
2824            assert_no_allocation(spin_alloc_test);
2825
2826            let alloc_after = current_allocs();
2827            assert!(
2828                (alloc_after - alloc_before).abs() < 10,
2829                "Expected no net allocation in poll_fn hot path"
2830            );
2831
2832            // Cleanup
2833            stop.store(true, Ordering::SeqCst);
2834            let _ = driver_handle.join().unwrap();
2835
2836            Ok(())
2837        }
2838    }
2839
2840    // ── Use-after-free tests: aeron dropped before children ───────────────
2841    //
2842    // When `drop(aeron)` fires first, the C side calls `aeron_close()` which
2843    // frees every registered resource (publications, subscriptions, counters,
2844    // exclusive publications) via `aeron_client_conductor_on_close()`.
2845    //
2846    // The Rust wrappers don't know this happened — their `close_already_called`
2847    // flag is still `false`, so any subsequent method call that goes through
2848    // `get_inner()` dereferences a dangling pointer.
2849    //
2850    // These tests save the raw C pointer before `drop(aeron)`, then call C
2851    // functions directly on it afterwards to eliminate intermediate Drop
2852    // machinery that could trigger re-allocation.
2853
2854    /// Helper: set up an embedded media driver + Aeron client.
2855    fn setup_aeron_for_uaf_test() -> (
2856        Aeron,
2857        rusteron_media_driver::testing::EmbeddedDriver,
2858        Handler<TestErrorCount>,
2859    ) {
2860        rusteron_code_gen::test_logger::init(log::LevelFilter::Info);
2861
2862        let driver = rusteron_media_driver::testing::EmbeddedDriver::launch().unwrap();
2863
2864        let ctx = AeronContext::new().unwrap();
2865        ctx.set_dir(&driver.dir().into_c_string()).unwrap();
2866        let error_handler = Handler::new(TestErrorCount::default());
2867        ctx.set_error_handler(Some(error_handler.clone())).unwrap();
2868
2869        let aeron = Aeron::new(&ctx).unwrap();
2870        aeron.start().unwrap();
2871
2872        (aeron, driver, error_handler)
2873    }
2874
2875    fn teardown_aeron_after_uaf_test(
2876        driver: rusteron_media_driver::testing::EmbeddedDriver,
2877        error_handler: Handler<TestErrorCount>,
2878    ) {
2879        drop(driver); // stops + joins on Drop
2880    }
2881
2882    /// `drop(aeron)` while children hold `Rc<Aeron>` references must not call
2883    /// `aeron_close()` until the last child drops — the raw C pointer reads below
2884    /// stay valid because close is deferred.
2885    #[test]
2886    #[serial]
2887    fn drop_client_before_children_is_safe_test() {
2888        let (aeron, driver, error_handler) = setup_aeron_for_uaf_test();
2889
2890        // Create all resource types.
2891        let publisher = aeron
2892            .add_publication(AERON_IPC_STREAM, 1001, Duration::from_secs(5))
2893            .unwrap();
2894        let subscription = aeron
2895            .add_subscription(
2896                AERON_IPC_STREAM,
2897                1002,
2898                Handlers::NONE,
2899                Handlers::NONE,
2900                Duration::from_secs(5),
2901            )
2902            .unwrap();
2903        let counter = aeron
2904            .add_counter(1003, &[1u8, 2, 3, 4], "test counter", Duration::from_secs(5))
2905            .unwrap();
2906        let excl_pub = aeron
2907            .add_exclusive_publication(AERON_IPC_STREAM, 1004, Duration::from_secs(5))
2908            .unwrap();
2909
2910        // Save raw C pointers while still valid.
2911        let pub_ptr: *mut aeron_publication_t = publisher.get_inner();
2912        let sub_ptr: *mut aeron_subscription_t = subscription.get_inner();
2913        let counter_ptr: *mut aeron_counter_t = counter.get_inner();
2914        let excl_pub_ptr: *mut aeron_exclusive_publication_t = excl_pub.get_inner();
2915
2916        // SAFE: drop(aeron) does NOT call aeron_close() — children still
2917        // hold Rc references, keeping the C client alive.
2918        drop(aeron);
2919
2920        // Read raw pointers — these access VALID (not freed) memory.
2921        let _closed = unsafe { aeron_publication_is_closed(pub_ptr) };
2922        let _connected = unsafe { aeron_subscription_is_connected(sub_ptr) };
2923        let _counter_closed = unsafe { aeron_counter_is_closed(counter_ptr) };
2924        let _excl_closed = unsafe { aeron_exclusive_publication_is_closed(excl_pub_ptr) };
2925        let _channel = unsafe { aeron_publication_channel(pub_ptr) };
2926
2927        // Reaching here proves structural safety: no UAF after drop(aeron).
2928
2929        // Now drop children properly — last one releases Rc<Aeron> →
2930        // aeron_close() fires in correct order.
2931        drop(publisher);
2932        drop(subscription);
2933        drop(counter);
2934        drop(excl_pub);
2935
2936        // Teardown.
2937        teardown_aeron_after_uaf_test(driver, error_handler);
2938    }
2939
2940    #[test]
2941    #[serial]
2942    fn cloned_leaf_handles_close_once_and_null_all_clones() {
2943        let (aeron, driver, error_handler) = setup_aeron_for_uaf_test();
2944
2945        let publisher = aeron
2946            .add_publication(AERON_IPC_STREAM, 1101, Duration::from_secs(5))
2947            .unwrap();
2948        let publisher_clone = publisher.clone();
2949        assert!(!publisher_clone.get_inner().is_null());
2950        assert!(publisher.close().is_ok());
2951        assert!(publisher_clone.get_inner().is_null());
2952        assert!(publisher_clone.close().is_ok());
2953
2954        let subscription = aeron
2955            .add_subscription(
2956                AERON_IPC_STREAM,
2957                1102,
2958                Handlers::NONE,
2959                Handlers::NONE,
2960                Duration::from_secs(5),
2961            )
2962            .unwrap();
2963        let subscription_clone = subscription.clone();
2964        assert!(!subscription_clone.get_inner().is_null());
2965        assert!(subscription.close().is_ok());
2966        assert!(subscription_clone.get_inner().is_null());
2967        assert!(subscription_clone.close().is_ok());
2968
2969        let counter = aeron
2970            .add_counter(1103, &[1u8, 2, 3, 4], "close clone counter", Duration::from_secs(5))
2971            .unwrap();
2972        let counter_clone = counter.clone();
2973        assert!(!counter_clone.get_inner().is_null());
2974        assert!(counter.close().is_ok());
2975        assert!(counter_clone.get_inner().is_null());
2976        assert!(counter_clone.close().is_ok());
2977
2978        let exclusive_publisher = aeron
2979            .add_exclusive_publication(AERON_IPC_STREAM, 1104, Duration::from_secs(5))
2980            .unwrap();
2981        let exclusive_publisher_clone = exclusive_publisher.clone();
2982        assert!(!exclusive_publisher_clone.get_inner().is_null());
2983        assert!(exclusive_publisher.close().is_ok());
2984        assert!(exclusive_publisher_clone.get_inner().is_null());
2985        assert!(exclusive_publisher_clone.close().is_ok());
2986
2987        assert!(aeron.close().is_ok());
2988        teardown_aeron_after_uaf_test(driver, error_handler);
2989    }
2990
2991    #[test]
2992    #[serial]
2993    fn close_with_handler_invokes_publication_close_notification_and_nulls_clones() {
2994        let (aeron, driver, error_handler) = setup_aeron_for_uaf_test();
2995
2996        let notification_count = Arc::new(AtomicUsize::new(0));
2997        let close_handler = Handler::new(CloseNotificationCount {
2998            count: notification_count.clone(),
2999        });
3000
3001        let publisher = aeron
3002            .add_publication(AERON_IPC_STREAM, 1151, Duration::from_secs(5))
3003            .unwrap();
3004        let publisher_clone = publisher.clone();
3005
3006        assert!(publisher.close_with_handler(Some(&close_handler)).is_ok());
3007        assert!(publisher_clone.get_inner().is_null());
3008
3009        let start = Instant::now();
3010        while notification_count.load(Ordering::SeqCst) == 0 && start.elapsed() < Duration::from_secs(5) {
3011            sleep(Duration::from_millis(10));
3012        }
3013        assert_eq!(1, notification_count.load(Ordering::SeqCst));
3014
3015        assert!(publisher_clone.close_with_handler(Some(&close_handler)).is_ok());
3016        assert_eq!(1, notification_count.load(Ordering::SeqCst));
3017
3018        assert!(aeron.close().is_ok());
3019        teardown_aeron_after_uaf_test(driver, error_handler);
3020    }
3021
3022    #[test]
3023    #[serial]
3024    fn explicit_client_close_defers_while_children_are_alive() {
3025        let (aeron, driver, error_handler) = setup_aeron_for_uaf_test();
3026
3027        let publisher = aeron
3028            .add_publication(AERON_IPC_STREAM, 1201, Duration::from_secs(5))
3029            .unwrap();
3030        let subscription = aeron
3031            .add_subscription(
3032                AERON_IPC_STREAM,
3033                1202,
3034                Handlers::NONE,
3035                Handlers::NONE,
3036                Duration::from_secs(5),
3037            )
3038            .unwrap();
3039        let counter = aeron
3040            .add_counter(
3041                1203,
3042                &[5u8, 6, 7, 8],
3043                "deferred client close counter",
3044                Duration::from_secs(5),
3045            )
3046            .unwrap();
3047        let exclusive_publisher = aeron
3048            .add_exclusive_publication(AERON_IPC_STREAM, 1204, Duration::from_secs(5))
3049            .unwrap();
3050
3051        let pub_ptr = publisher.get_inner();
3052        let sub_ptr = subscription.get_inner();
3053        let counter_ptr = counter.get_inner();
3054        let excl_ptr = exclusive_publisher.get_inner();
3055
3056        assert!(aeron.clone().close().is_ok());
3057
3058        assert!(!publisher.get_inner().is_null());
3059        assert!(!subscription.get_inner().is_null());
3060        assert!(!counter.get_inner().is_null());
3061        assert!(!exclusive_publisher.get_inner().is_null());
3062
3063        let _ = unsafe { aeron_publication_is_closed(pub_ptr) };
3064        let _ = unsafe { aeron_subscription_is_connected(sub_ptr) };
3065        let _ = unsafe { aeron_counter_is_closed(counter_ptr) };
3066        let _ = unsafe { aeron_exclusive_publication_is_closed(excl_ptr) };
3067
3068        drop(publisher);
3069        drop(subscription);
3070        drop(counter);
3071        drop(exclusive_publisher);
3072
3073        assert!(aeron.close().is_ok());
3074        teardown_aeron_after_uaf_test(driver, error_handler);
3075    }
3076
3077    #[test]
3078    #[serial]
3079    fn explicit_client_clone_close_defers_and_original_remains_usable() {
3080        let (aeron, driver, error_handler) = setup_aeron_for_uaf_test();
3081
3082        let aeron_clone = aeron.clone();
3083        assert!(aeron_clone.close().is_ok());
3084
3085        let publisher = aeron
3086            .add_publication(AERON_IPC_STREAM, 1301, Duration::from_secs(5))
3087            .expect("original client should remain usable after closing a clone");
3088        assert!(!publisher.get_inner().is_null());
3089        assert!(publisher.close().is_ok());
3090
3091        assert!(aeron.close().is_ok());
3092        teardown_aeron_after_uaf_test(driver, error_handler);
3093    }
3094
3095    #[test]
3096    #[serial]
3097    fn closing_leaf_resource_does_not_poison_client_or_siblings() {
3098        let (aeron, driver, error_handler) = setup_aeron_for_uaf_test();
3099
3100        let publisher = aeron
3101            .add_publication(AERON_IPC_STREAM, 1401, Duration::from_secs(5))
3102            .unwrap();
3103        let subscription = aeron
3104            .add_subscription(
3105                AERON_IPC_STREAM,
3106                1402,
3107                Handlers::NONE,
3108                Handlers::NONE,
3109                Duration::from_secs(5),
3110            )
3111            .unwrap();
3112
3113        assert!(publisher.close().is_ok());
3114        assert!(!subscription.get_inner().is_null());
3115        let _ = unsafe { aeron_subscription_is_connected(subscription.get_inner()) };
3116
3117        let next_publisher = aeron
3118            .add_publication(AERON_IPC_STREAM, 1403, Duration::from_secs(5))
3119            .expect("client should create new resources after a leaf close");
3120        assert!(!next_publisher.get_inner().is_null());
3121
3122        assert!(subscription.close().is_ok());
3123        assert!(next_publisher.close().is_ok());
3124        assert!(aeron.close().is_ok());
3125        teardown_aeron_after_uaf_test(driver, error_handler);
3126    }
3127
3128    /// After `aeron.close()` the surviving handles must remain fully usable —
3129    /// not merely safe to drop. A complete offer/poll roundtrip is the strongest
3130    /// proof that the deferred close left the C client fully operational.
3131    #[test]
3132    #[serial]
3133    fn client_close_defers_and_children_remain_fully_usable() {
3134        let (aeron, driver, error_handler) = setup_aeron_for_uaf_test();
3135
3136        let publisher = aeron
3137            .add_publication(AERON_IPC_STREAM, 1501, Duration::from_secs(5))
3138            .unwrap();
3139        let subscription = aeron
3140            .add_subscription(
3141                AERON_IPC_STREAM,
3142                1501,
3143                Handlers::NONE,
3144                Handlers::NONE,
3145                Duration::from_secs(5),
3146            )
3147            .unwrap();
3148
3149        assert!(aeron.clone().close().is_ok());
3150
3151        let payload = b"post-close-roundtrip";
3152        let send_start = Instant::now();
3153        let mut sent = false;
3154        while send_start.elapsed() < Duration::from_secs(5) {
3155            if publisher.offer_raw(payload, Handlers::NONE) >= payload.len() as i64 {
3156                sent = true;
3157                break;
3158            }
3159            sleep(Duration::from_millis(10));
3160        }
3161        assert!(sent, "offer() must still work after a deferred client close");
3162
3163        let received = Arc::new(AtomicUsize::new(0));
3164        let received_copy = received.clone();
3165        let read_start = Instant::now();
3166        while received.load(Ordering::SeqCst) == 0 && read_start.elapsed() < Duration::from_secs(5) {
3167            subscription
3168                .poll_fn(
3169                    |buffer, _header| {
3170                        assert_eq!(buffer, payload);
3171                        received_copy.fetch_add(1, Ordering::SeqCst);
3172                    },
3173                    16,
3174                )
3175                .unwrap();
3176            sleep(Duration::from_millis(10));
3177        }
3178        assert_eq!(
3179            1,
3180            received.load(Ordering::SeqCst),
3181            "poll() must still deliver after a deferred client close"
3182        );
3183
3184        drop(publisher);
3185        drop(subscription);
3186        assert!(aeron.close().is_ok());
3187        teardown_aeron_after_uaf_test(driver, error_handler);
3188    }
3189
3190    struct CountingAvailableImageHandler {
3191        available: Arc<AtomicUsize>,
3192        drops: Arc<AtomicUsize>,
3193    }
3194
3195    impl AeronAvailableImageCallback for CountingAvailableImageHandler {
3196        fn handle_aeron_on_available_image(&mut self, _subscription: AeronSubscription, _image: AeronImage) {
3197            self.available.fetch_add(1, Ordering::SeqCst);
3198        }
3199    }
3200
3201    impl Drop for CountingAvailableImageHandler {
3202        fn drop(&mut self) {
3203            self.drops.fetch_add(1, Ordering::SeqCst);
3204        }
3205    }
3206
3207    /// A retained callback must stay alive as long as the C client can invoke it,
3208    /// even when the caller drops their `Handler` immediately after registration:
3209    /// the subscription owns a clone via its dependency list. It must be freed
3210    /// exactly once when the subscription (and its async poller) are gone.
3211    #[test]
3212    #[serial]
3213    fn retained_image_handler_outlives_callers_drop_and_frees_exactly_fn() {
3214        let (aeron, driver, error_handler) = setup_aeron_for_uaf_test();
3215
3216        let available = Arc::new(AtomicUsize::new(0));
3217        let drops = Arc::new(AtomicUsize::new(0));
3218
3219        let subscription = {
3220            let handler = Handler::new(CountingAvailableImageHandler {
3221                available: available.clone(),
3222                drops: drops.clone(),
3223            });
3224            let subscription = aeron
3225                .add_subscription(
3226                    AERON_IPC_STREAM,
3227                    1601,
3228                    Some(&handler),
3229                    Handlers::NONE,
3230                    Duration::from_secs(5),
3231                )
3232                .unwrap();
3233            // caller's reference goes away here — the subscription's dependency
3234            // clone must keep the callback value alive for the conductor thread
3235            drop(handler);
3236            subscription
3237        };
3238        assert_eq!(
3239            0,
3240            drops.load(Ordering::SeqCst),
3241            "handler freed while C can still call it"
3242        );
3243
3244        let publisher = aeron
3245            .add_publication(AERON_IPC_STREAM, 1601, Duration::from_secs(5))
3246            .unwrap();
3247
3248        let start = Instant::now();
3249        while available.load(Ordering::SeqCst) == 0 && start.elapsed() < Duration::from_secs(5) {
3250            let _ = publisher.offer_raw(b"wake", Handlers::NONE);
3251            sleep(Duration::from_millis(10));
3252        }
3253        assert!(
3254            available.load(Ordering::SeqCst) > 0,
3255            "image-available callback should have fired after the caller dropped its Handler"
3256        );
3257        assert_eq!(0, drops.load(Ordering::SeqCst));
3258
3259        drop(publisher);
3260        drop(subscription);
3261        drop(aeron);
3262        assert_eq!(
3263            1,
3264            drops.load(Ordering::SeqCst),
3265            "handler must be freed exactly once after its owning resources close"
3266        );
3267        teardown_aeron_after_uaf_test(driver, error_handler);
3268    }
3269
3270    /// Dropping an async poller *without polling it* must not free its retained
3271    /// callbacks: the C conductor completes the add in the background and will
3272    /// invoke the image handler when a publication connects. The handler is
3273    /// anchored to the client, whose lifetime matches the conductor's.
3274    #[test]
3275    #[serial]
3276    fn unpolled_async_subscription_drop_keeps_image_handler_alive() {
3277        let (aeron, driver, error_handler) = setup_aeron_for_uaf_test();
3278
3279        let available = Arc::new(AtomicUsize::new(0));
3280        let drops = Arc::new(AtomicUsize::new(0));
3281
3282        {
3283            let handler = Handler::new(CountingAvailableImageHandler {
3284                available: available.clone(),
3285                drops: drops.clone(),
3286            });
3287            let poller = aeron
3288                .async_add_subscription(AERON_IPC_STREAM, 1611, Some(&handler), Handlers::NONE)
3289                .unwrap();
3290            // both the caller's handler and the never-polled poller go away here
3291        }
3292        assert_eq!(
3293            0,
3294            drops.load(Ordering::SeqCst),
3295            "handler freed while the conductor can still invoke it (UAF)"
3296        );
3297
3298        // the conductor completed the add internally; connecting a publication
3299        // fires the image-available callback into the (still alive) handler
3300        let publisher = aeron
3301            .add_publication(AERON_IPC_STREAM, 1611, Duration::from_secs(5))
3302            .unwrap();
3303        let start = Instant::now();
3304        while available.load(Ordering::SeqCst) == 0 && start.elapsed() < Duration::from_secs(5) {
3305            let _ = publisher.offer_raw(b"wake", Handlers::NONE);
3306            sleep(Duration::from_millis(10));
3307        }
3308        assert!(
3309            available.load(Ordering::SeqCst) > 0,
3310            "conductor should have invoked the image handler after the unpolled poller dropped"
3311        );
3312        assert_eq!(0, drops.load(Ordering::SeqCst));
3313
3314        drop(publisher);
3315        drop(aeron);
3316        assert_eq!(1, drops.load(Ordering::SeqCst), "freed exactly once at client close");
3317        teardown_aeron_after_uaf_test(driver, error_handler);
3318    }
3319
3320    /// A failed async add (invalid URI) must fail cleanly: the error surfaces from
3321    /// new()/poll(), later polls are inert (the C client freed the async struct on
3322    /// the errored poll), and the client stays fully usable afterwards.
3323    #[test]
3324    #[serial]
3325    fn async_add_subscription_invalid_uri_fails_cleanly() {
3326        let (aeron, driver, error_handler) = setup_aeron_for_uaf_test();
3327
3328        let bad_uri = c"aeron:udp?endpoint=not-a-real-host:0|interface=500.500.500.500";
3329        match aeron.async_add_subscription(bad_uri, 1621, Handlers::NONE, Handlers::NONE) {
3330            Err(_) => {} // rejected synchronously — fine
3331            Ok(poller) => {
3332                let mut saw_error = false;
3333                let start = Instant::now();
3334                while start.elapsed() < Duration::from_secs(5) {
3335                    match poller.poll() {
3336                        Err(_) => {
3337                            saw_error = true;
3338                            break;
3339                        }
3340                        Ok(Some(_)) => panic!("subscription must not be created for an invalid uri"),
3341                        Ok(None) => sleep(Duration::from_millis(10)),
3342                    }
3343                }
3344                assert!(saw_error, "poll should surface the async add error");
3345                // the C client freed the async struct on the errored poll; further
3346                // polls must be inert, not use-after-free
3347                assert!(matches!(poller.poll(), Ok(None)));
3348                assert!(matches!(poller.poll(), Ok(None)));
3349                drop(poller);
3350            }
3351        }
3352
3353        // client unaffected: normal roundtrip still works
3354        let publisher = aeron
3355            .add_publication(AERON_IPC_STREAM, 1622, Duration::from_secs(5))
3356            .unwrap();
3357        assert!(!publisher.get_inner().is_null());
3358        assert!(publisher.close().is_ok());
3359        assert!(aeron.close().is_ok());
3360        teardown_aeron_after_uaf_test(driver, error_handler);
3361    }
3362
3363    /// The full dependency graph at once — context (with error handler), client + clone,
3364    /// publication + clone, exclusive publication, subscription with a retained image
3365    /// handler, counter, and an unpolled async poller — closed/dropped in an adversarial
3366    /// order, with every surviving handle exercised after each step. Deferred close must
3367    /// keep the C client alive until the *last* reference, and every retained handler must
3368    /// be freed exactly once at the end.
3369    #[test]
3370    #[serial]
3371    fn complex_object_graph_close_is_safe_in_any_order() {
3372        rusteron_code_gen::test_logger::init(log::LevelFilter::Info);
3373
3374        let media_driver_ctx = AeronDriverContext::new().unwrap();
3375        media_driver_ctx.set_dir_delete_on_shutdown(true).unwrap();
3376        media_driver_ctx.set_dir_delete_on_start(true).unwrap();
3377        media_driver_ctx
3378            .set_dir(&format!("{}{}", media_driver_ctx.get_dir(), Aeron::epoch_clock()).into_c_string())
3379            .unwrap();
3380        let (stop, driver_handle) =
3381            rusteron_media_driver::AeronDriver::launch_embedded(media_driver_ctx.clone(), false);
3382
3383        let errors = Arc::new(AtomicUsize::new(0));
3384        let image_drops = Arc::new(AtomicUsize::new(0));
3385        let unpolled_drops = Arc::new(AtomicUsize::new(0));
3386        let available = Arc::new(AtomicUsize::new(0));
3387
3388        let ctx = AeronContext::new().unwrap();
3389        ctx.set_dir(&media_driver_ctx.get_dir().into_c_string()).unwrap();
3390        let errors_cb = errors.clone();
3391        ctx.set_error_handler(Some(move |code: i32, msg: &str| {
3392            errors_cb.fetch_add(1, Ordering::SeqCst);
3393            log::error!("client error {code}: {msg}");
3394        }))
3395        .unwrap();
3396        let aeron = Aeron::new(&ctx).unwrap();
3397        aeron.start().unwrap();
3398        let aeron_clone = aeron.clone();
3399        drop(ctx); // the client's internal clone keeps the C context alive
3400
3401        // children of every kind
3402        let publication = aeron
3403            .add_publication(AERON_IPC_STREAM, 1801, Duration::from_secs(5))
3404            .unwrap();
3405        let publication_clone = publication.clone();
3406        let exclusive = aeron
3407            .add_exclusive_publication(AERON_IPC_STREAM, 1801, Duration::from_secs(5))
3408            .unwrap();
3409        let image_handler = Handler::new(CountingAvailableImageHandler {
3410            available: available.clone(),
3411            drops: image_drops.clone(),
3412        });
3413        let subscription = aeron
3414            .add_subscription(
3415                AERON_IPC_STREAM,
3416                1801,
3417                Some(&image_handler),
3418                Handlers::NONE,
3419                Duration::from_secs(5),
3420            )
3421            .unwrap();
3422        drop(image_handler); // subscription + client keep it alive
3423        let counter = aeron
3424            .add_counter(1802, &[1, 2, 3], "graph counter", Duration::from_secs(5))
3425            .unwrap();
3426        let unpolled_handler = Handler::new(CountingAvailableImageHandler {
3427            available: available.clone(),
3428            drops: unpolled_drops.clone(),
3429        });
3430        let unpolled_poller = aeron
3431            .async_add_subscription(AERON_IPC_STREAM, 1803, Some(&unpolled_handler), Handlers::NONE)
3432            .unwrap();
3433        drop(unpolled_handler);
3434        drop(unpolled_poller); // never polled — its handler must stay alive via the client
3435
3436        // adversarial order: close the client FIRST (defers), then use everything
3437        assert!(aeron.close().is_ok());
3438
3439        let payload = b"graph-roundtrip";
3440        let start = Instant::now();
3441        let mut sent = false;
3442        while start.elapsed() < Duration::from_secs(5) {
3443            if publication.offer(payload).is_ok() {
3444                sent = true;
3445                break;
3446            }
3447            sleep(Duration::from_millis(5));
3448        }
3449        assert!(sent, "publication must work after deferred client close");
3450        assert!(exclusive.offer(payload).is_ok() || !exclusive.is_connected());
3451        let received = Arc::new(AtomicUsize::new(0));
3452        let received_copy = received.clone();
3453        let read_start = Instant::now();
3454        while received.load(Ordering::SeqCst) == 0 && read_start.elapsed() < Duration::from_secs(5) {
3455            subscription
3456                .poll_fn(
3457                    |_buf, _hdr| {
3458                        received_copy.fetch_add(1, Ordering::SeqCst);
3459                    },
3460                    16,
3461                )
3462                .unwrap();
3463            sleep(Duration::from_millis(5));
3464        }
3465        assert!(
3466            received.load(Ordering::SeqCst) >= 1,
3467            "subscription must deliver after deferred close"
3468        );
3469        counter.addr_atomic().store(42, std::sync::atomic::Ordering::SeqCst);
3470        assert_eq!(42, counter.addr_atomic().load(std::sync::atomic::Ordering::SeqCst));
3471
3472        // close a leaf, then its clone must be inert but safe
3473        assert!(publication.close().is_ok());
3474        assert!(publication_clone.get_inner().is_null());
3475        assert!(publication_clone.close().is_ok());
3476
3477        // remaining teardown in shuffled order, exercising the clone last
3478        drop(subscription);
3479        assert_eq!(
3480            0,
3481            image_drops.load(Ordering::SeqCst),
3482            "client anchor still holds the image handler"
3483        );
3484        drop(exclusive);
3485        assert!(counter.close().is_ok());
3486        assert!(aeron_clone.close().is_ok()); // the LAST reference — the real aeron_close runs here
3487
3488        assert_eq!(
3489            1,
3490            image_drops.load(Ordering::SeqCst),
3491            "image handler freed exactly once"
3492        );
3493        assert_eq!(
3494            1,
3495            unpolled_drops.load(Ordering::SeqCst),
3496            "unpolled poller's handler freed exactly once"
3497        );
3498        assert_eq!(
3499            0,
3500            errors.load(Ordering::SeqCst),
3501            "no client errors during graph teardown"
3502        );
3503
3504        stop.store(true, Ordering::SeqCst);
3505        let _ = driver_handle.join();
3506    }
3507
3508    /// Typed counter navigation: publisher-limit and subscriber-position counters are
3509    /// found by (type id, registration id), mirroring Java's CountersReader lookups.
3510    #[test]
3511    #[serial]
3512    fn counters_can_be_found_by_type_and_registration_id() {
3513        let (aeron, driver, error_handler) = setup_aeron_for_uaf_test();
3514
3515        let publisher = aeron
3516            .add_publication(AERON_IPC_STREAM, 1951, Duration::from_secs(5))
3517            .unwrap();
3518        let subscription = aeron
3519            .add_subscription(
3520                AERON_IPC_STREAM,
3521                1951,
3522                Handlers::NONE,
3523                Handlers::NONE,
3524                Duration::from_secs(5),
3525            )
3526            .unwrap();
3527        let start = Instant::now();
3528        while !publisher.is_connected() && start.elapsed() < Duration::from_secs(5) {
3529            sleep(Duration::from_millis(10));
3530        }
3531
3532        let counters = aeron.counters_reader();
3533        let pub_registration_id = publisher.get_constants().unwrap().registration_id;
3534        let limit_counter = counters
3535            .find_by_type_and_registration_id(AERON_COUNTER_PUBLISHER_LIMIT_TYPE_ID as i32, pub_registration_id)
3536            .expect("publisher limit counter");
3537        assert!(
3538            counters.get_counter_value(limit_counter) > 0,
3539            "publisher limit should be positive once connected"
3540        );
3541
3542        let sub_registration_id = subscription.get_constants().unwrap().registration_id();
3543        let position_counter = counters
3544            .find_by_type_and_registration_id(AERON_COUNTER_SUBSCRIPTION_POSITION_TYPE_ID as i32, sub_registration_id)
3545            .expect("subscriber position counter");
3546        assert!(counters.get_counter_value(position_counter) >= 0);
3547
3548        assert!(counters
3549            .find_by_type_and_registration_id(AERON_COUNTER_PUBLISHER_LIMIT_TYPE_ID as i32, -12345)
3550            .is_none());
3551
3552        drop(publisher);
3553        drop(subscription);
3554        drop(aeron);
3555        teardown_aeron_after_uaf_test(driver, error_handler);
3556    }
3557
3558    /// Retained images are released back to the subscription automatically when the
3559    /// handle drops — in either order: image-then-subscription (normal release) or
3560    /// subscription-then-image (release skipped; the C client already reclaimed it).
3561    #[test]
3562    #[serial]
3563    fn retained_images_release_automatically_in_any_drop_order() {
3564        let (aeron, driver, error_handler) = setup_aeron_for_uaf_test();
3565
3566        let publisher = aeron
3567            .add_publication(AERON_IPC_STREAM, 1901, Duration::from_secs(5))
3568            .unwrap();
3569        let subscription = aeron
3570            .add_subscription(
3571                AERON_IPC_STREAM,
3572                1901,
3573                Handlers::NONE,
3574                Handlers::NONE,
3575                Duration::from_secs(5),
3576            )
3577            .unwrap();
3578        let start = Instant::now();
3579        while subscription.image_count().unwrap_or(0) == 0 && start.elapsed() < Duration::from_secs(5) {
3580            let _ = publisher.offer(b"image-wake");
3581            sleep(Duration::from_millis(10));
3582        }
3583        assert!(subscription.image_count().unwrap() >= 1);
3584
3585        // borrow-scoped iteration: no bookkeeping
3586        let mut seen = 0;
3587        subscription.for_each_image(|image| {
3588            assert!(!image.get_inner().is_null());
3589            seen += 1;
3590        });
3591        assert!(seen >= 1);
3592
3593        // normal order: image handle dropped while the subscription is alive
3594        {
3595            let image = subscription.image_at_index(0).expect("image at 0");
3596            let session_id = image.get_constants().unwrap().session_id();
3597            assert!(subscription.image_by_session_id(session_id).is_some());
3598            assert!(subscription.image_by_session_id(session_id ^ 0x5555_5555).is_none());
3599            drop(image); // releases via aeron_subscription_image_release
3600        }
3601        // still healthy afterwards: roundtrip works
3602        assert!(subscription.poll_fn(|_, _| {}, 4).is_ok());
3603
3604        // adversarial order: subscription closed while a retained image handle lives
3605        let image = subscription.image_at_index(0).expect("image at 0");
3606        drop(subscription);
3607        drop(image); // must be a safe no-op, not a UAF release
3608
3609        drop(publisher);
3610        drop(aeron);
3611        teardown_aeron_after_uaf_test(driver, error_handler);
3612    }
3613
3614    /// `close_now` is the unsafe escape hatch: the C close runs immediately.
3615    /// Clones of the client handle share the nulled pointer, so they stay safe
3616    /// to drop or close (children would dangle — none exist here).
3617    #[test]
3618    #[serial]
3619    fn close_now_with_clones_only_is_safe() {
3620        let (aeron, driver, error_handler) = setup_aeron_for_uaf_test();
3621
3622        let clone = aeron.clone();
3623        unsafe {
3624            assert!(aeron.close_now().is_ok());
3625        }
3626        assert!(clone.get_inner().is_null(), "clones must see the nulled pointer");
3627        assert!(clone.close().is_ok(), "closing an already-closed clone is a no-op");
3628        teardown_aeron_after_uaf_test(driver, error_handler);
3629    }
3630
3631    /// `close_now` with a complex object graph where Rust drops children before
3632    /// the parent. In 0.2's deferred-close model, children close immediately when
3633    /// dropped, so calling `close_now()` on a parent after all children have been
3634    /// dropped must be safe — the C client handles already-closed resources
3635    /// gracefully. This test verifies no segfaults or double-frees (CI runs this
3636    /// under ASan which catches use-after-free).
3637    ///
3638    /// WARNING: if any child handle survives `close_now()`, its C resources are
3639    /// freed and the handle becomes unsafe to use (dangling pointer). You MUST
3640    /// drop all children before calling `close_now()`, or explicitly `std::mem::forget`
3641    /// them and accept the UB.
3642    #[test]
3643    #[serial]
3644    fn close_now_after_all_children_dropped_is_safe() {
3645        rusteron_code_gen::test_logger::init(log::LevelFilter::Info);
3646        let (aeron, driver, error_handler) = setup_aeron_for_uaf_test();
3647
3648        // Build a complex object graph: publications, subscriptions, exclusive publications
3649        let pub1 = aeron
3650            .add_publication(AERON_IPC_STREAM, 1001, Duration::from_secs(5))
3651            .expect("add publication 1");
3652        let pub2 = aeron
3653            .add_publication(AERON_IPC_STREAM, 1002, Duration::from_secs(5))
3654            .expect("add publication 2");
3655
3656        let sub1 = aeron
3657            .add_subscription(
3658                AERON_IPC_STREAM,
3659                1001,
3660                Handlers::NONE,
3661                Handlers::NONE,
3662                Duration::from_secs(5),
3663            )
3664            .expect("add subscription 1");
3665        let sub2 = aeron
3666            .add_subscription(
3667                AERON_IPC_STREAM,
3668                1002,
3669                Handlers::NONE,
3670                Handlers::NONE,
3671                Duration::from_secs(5),
3672            )
3673            .expect("add subscription 2");
3674
3675        let excl_pub = aeron
3676            .add_exclusive_publication(AERON_IPC_STREAM, 1003, Duration::from_secs(5))
3677            .expect("add exclusive publication");
3678
3679        // All children must be dropped before close_now - their C resources are freed
3680        drop(pub1);
3681        drop(pub2);
3682        drop(sub1);
3683        drop(sub2);
3684        drop(excl_pub);
3685
3686        // Call close_now on the parent — must not segfault or double-free
3687        unsafe {
3688            assert!(
3689                aeron.close_now().is_ok(),
3690                "close_now should succeed after all children dropped"
3691            );
3692        }
3693
3694        teardown_aeron_after_uaf_test(driver, error_handler);
3695        // Valgrind will catch any double-frees or use-after-free from this test
3696    }
3697
3698    /// DANGER: This test documents the UB scenario where children survive
3699    /// `close_now()`. This is commented-out because it WILL segfault (ASan would
3700    /// catch it as use-after-free). This proves you MUST drop all children before
3701    /// calling `close_now()`.
3702    ///
3703    /// What happens:
3704    /// 1. `aeron.close_now()` frees the Aeron C client
3705    /// 2. All publications/subscriptions are freed in C
3706    /// 3. But `pub2` still holds a non-null pointer to freed memory
3707    /// 4. `drop(pub2)` calls `aeron_publication_close()` on dangling pointer → UB
3708    ///
3709    /// DO NOT call `close_now()` with surviving children — it's fundamentally unsafe.
3710    #[test]
3711    #[serial]
3712    #[ignore = "this test WILL segfault - it documents the UB scenario"]
3713    fn close_now_with_surviving_children_is_ub_and_segfaults() {
3714        rusteron_code_gen::test_logger::init(log::LevelFilter::Info);
3715        let (aeron, driver, error_handler) = setup_aeron_for_uaf_test();
3716
3717        let pub2 = aeron
3718            .add_publication(AERON_IPC_STREAM, 1001, Duration::from_secs(5))
3719            .expect("add publication");
3720
3721        // Call close_now while pub2 is still alive — UNSAFE
3722        unsafe {
3723            let _ = aeron.close_now();
3724        }
3725
3726        // pub2 now holds a dangling pointer. Dropping it calls
3727        // aeron_publication_close() on freed memory → SEGFAULT
3728        drop(pub2); // <-- BOOM: use-after-free
3729
3730        // Never reached
3731        teardown_aeron_after_uaf_test(driver, error_handler);
3732    }
3733
3734    /// The client stores the raw context pointer in C for its entire life, so the
3735    /// context must outlive the client. Dropping the user's `AeronContext` handle
3736    /// only releases a reference — the client's internal clone keeps the C context
3737    /// alive, and a full roundtrip must still work afterwards.
3738    #[test]
3739    #[serial]
3740    fn context_drop_while_client_alive_is_safe() {
3741        rusteron_code_gen::test_logger::init(log::LevelFilter::Info);
3742
3743        let media_driver_ctx = AeronDriverContext::new().unwrap();
3744        media_driver_ctx.set_dir_delete_on_shutdown(true).unwrap();
3745        media_driver_ctx.set_dir_delete_on_start(true).unwrap();
3746        media_driver_ctx
3747            .set_dir(&format!("{}{}", media_driver_ctx.get_dir(), Aeron::epoch_clock()).into_c_string())
3748            .unwrap();
3749        let (stop, driver_handle) =
3750            rusteron_media_driver::AeronDriver::launch_embedded(media_driver_ctx.clone(), false);
3751
3752        let aeron = {
3753            let ctx = AeronContext::new().unwrap();
3754            ctx.set_dir(&media_driver_ctx.get_dir().into_c_string()).unwrap();
3755            let aeron = Aeron::new(&ctx).unwrap();
3756            drop(ctx); // user's handle gone; the client's clone keeps the C context alive
3757            aeron
3758        };
3759        aeron.start().unwrap();
3760
3761        let publisher = aeron
3762            .add_publication(AERON_IPC_STREAM, 1631, Duration::from_secs(5))
3763            .unwrap();
3764        let subscription = aeron
3765            .add_subscription(
3766                AERON_IPC_STREAM,
3767                1631,
3768                Handlers::NONE,
3769                Handlers::NONE,
3770                Duration::from_secs(5),
3771            )
3772            .unwrap();
3773
3774        let payload = b"ctx-dropped-roundtrip";
3775        let start = Instant::now();
3776        let mut sent = false;
3777        while start.elapsed() < Duration::from_secs(5) {
3778            if publisher.offer_raw(payload, Handlers::NONE) >= payload.len() as i64 {
3779                sent = true;
3780                break;
3781            }
3782            sleep(Duration::from_millis(10));
3783        }
3784        assert!(sent);
3785
3786        let received = Arc::new(AtomicUsize::new(0));
3787        let received_copy = received.clone();
3788        let read_start = Instant::now();
3789        while received.load(Ordering::SeqCst) == 0 && read_start.elapsed() < Duration::from_secs(5) {
3790            subscription
3791                .poll_fn(
3792                    |buffer, _| {
3793                        assert_eq!(buffer, payload);
3794                        received_copy.fetch_add(1, Ordering::SeqCst);
3795                    },
3796                    16,
3797                )
3798                .unwrap();
3799            sleep(Duration::from_millis(10));
3800        }
3801        assert_eq!(1, received.load(Ordering::SeqCst));
3802
3803        stop.store(true, Ordering::SeqCst);
3804        let _ = driver_handle.join();
3805    }
3806
3807    /// Media driver dies mid-flight: the client must surface the failure through
3808    /// the error handler (driver keepalive timeout) and every handle must stay
3809    /// memory-safe — offers turn into errors, never UB — and teardown completes.
3810    #[test]
3811    #[serial]
3812    fn media_driver_shutdown_surfaces_errors_and_client_stays_safe() {
3813        rusteron_code_gen::test_logger::init(log::LevelFilter::Info);
3814
3815        let media_driver_ctx = AeronDriverContext::new().unwrap();
3816        media_driver_ctx.set_dir_delete_on_shutdown(true).unwrap();
3817        media_driver_ctx.set_dir_delete_on_start(true).unwrap();
3818        media_driver_ctx
3819            .set_dir(&format!("{}{}", media_driver_ctx.get_dir(), Aeron::epoch_clock()).into_c_string())
3820            .unwrap();
3821        let (stop, driver_handle) =
3822            rusteron_media_driver::AeronDriver::launch_embedded(media_driver_ctx.clone(), false);
3823
3824        let errors = Arc::new(AtomicUsize::new(0));
3825        let ctx = AeronContext::new().unwrap();
3826        ctx.set_dir(&media_driver_ctx.get_dir().into_c_string()).unwrap();
3827        // short keepalive so the driver loss is detected quickly
3828        ctx.set_driver_timeout_ms(2_000).unwrap();
3829        let errors_cb = errors.clone();
3830        ctx.set_error_handler(Some(move |code: i32, msg: &str| {
3831            errors_cb.fetch_add(1, Ordering::SeqCst);
3832            log::info!("client error {code}: {msg}");
3833        }))
3834        .unwrap();
3835        let aeron = Aeron::new(&ctx).unwrap();
3836        aeron.start().unwrap();
3837
3838        let publisher = aeron
3839            .add_publication(AERON_IPC_STREAM, 1701, Duration::from_secs(5))
3840            .unwrap();
3841        let subscription = aeron
3842            .add_subscription(
3843                AERON_IPC_STREAM,
3844                1701,
3845                Handlers::NONE,
3846                Handlers::NONE,
3847                Duration::from_secs(5),
3848            )
3849            .unwrap();
3850
3851        // sanity roundtrip while the driver is up
3852        let send_start = Instant::now();
3853        while publisher.offer(b"pre-shutdown").is_err() && send_start.elapsed() < Duration::from_secs(5) {
3854            sleep(Duration::from_millis(10));
3855        }
3856
3857        // kill the driver
3858        stop.store(true, Ordering::SeqCst);
3859        let _ = driver_handle.join();
3860
3861        // the conductor must notice the driver is gone and invoke the error handler
3862        let start = Instant::now();
3863        while errors.load(Ordering::SeqCst) == 0 && start.elapsed() < Duration::from_secs(15) {
3864            // keep exercising the handles: must return errors, never crash
3865            let _ = publisher.offer(b"post-shutdown");
3866            let _ = subscription.poll_fn(|_, _| {}, 4);
3867            sleep(Duration::from_millis(50));
3868        }
3869        assert!(
3870            errors.load(Ordering::SeqCst) > 0,
3871            "client error handler should have reported the driver keepalive timeout"
3872        );
3873
3874        // handles remain safe after the client flags the failure
3875        match publisher.offer(b"after-error") {
3876            Ok(_) => {}
3877            Err(e) => assert!(e.is_retryable() || e.is_fatal()), // typed, not UB
3878        }
3879    }
3880
3881    // ── Handler dependency tracking tests ────────────────────────────────
3882    //
3883    // These tests verify that handlers registered via add_dependency()
3884    // are kept alive correctly. Handlers must survive aeron.close() and
3885    // only be released when the Aeron client itself is dropped.
3886    //
3887    // NOTE: Handler<T> wraps Arc<UnsafeCell<T>>, and UnsafeCell deliberately
3888    // doesn't implement Drop (C code mutates handlers through raw pointers).
3889    // We track handler lifetime via Arc::strong_count instead of drop counters.
3890
3891    /// Test that a single error handler survives handler clone drops
3892    /// and persists via dependency tracking.
3893    #[test]
3894    #[serial]
3895    fn handler_dependency_tracking() {
3896        rusteron_code_gen::test_logger::init(log::LevelFilter::Info);
3897        let (aeron, driver, _error_handler) = setup_aeron_for_uaf_test();
3898
3899        // Create a test handler
3900        let handler = Handler::new(move |code: i32, msg: &str| {
3901            log::info!("Error handler called: code={}, msg={}", code, msg);
3902        });
3903
3904        // Store a clone to track strong_count
3905        let handler1 = handler.clone();
3906
3907        // Verify initial strong_count is 2 (handler and handler1)
3908        assert_eq!(
3909            Arc::strong_count(&handler1.inner),
3910            2,
3911            "Handler should be held by handler and handler1"
3912        );
3913
3914        // Register the handler with the context
3915        let ctx = aeron.context();
3916        ctx.set_error_handler(Some(handler)).unwrap();
3917
3918        // After registration, handler1 is the only external reference
3919        // (the original handler was moved into the context's dependencies)
3920        assert_eq!(
3921            Arc::strong_count(&handler1.inner),
3922            1,
3923            "Handler should be held only by handler1 (original stored in context)"
3924        );
3925
3926        // Explicitly close the Aeron client
3927        aeron.close().unwrap();
3928
3929        // The handler should STILL be alive because it's stored in context
3930        assert_eq!(
3931            Arc::strong_count(&handler1.inner),
3932            1,
3933            "Handler must survive aeron.close() - still tracked by context"
3934        );
3935
3936        // Drop handler1 - now only context holds the handler
3937        drop(handler1);
3938
3939        teardown_aeron_after_uaf_test(driver, _error_handler);
3940    }
3941
3942    /// Test that subscription image handlers are properly tracked
3943    /// and survive subscription drops.
3944    #[test]
3945    #[serial]
3946    fn subscription_handler_dependency_tracking() {
3947        rusteron_code_gen::test_logger::init(log::LevelFilter::Info);
3948        let (aeron, driver, _error_handler) = setup_aeron_for_uaf_test();
3949
3950        let avail_handler = Handler::new(move |subscription: AeronSubscription, image: AeronImage| {
3951            log::info!("Available image callback invoked");
3952        });
3953
3954        let local_avail = avail_handler.clone();
3955
3956        // Create subscription with handlers
3957        let sub = aeron
3958            .add_subscription(
3959                AERON_IPC_STREAM,
3960                2001,
3961                Some(&avail_handler),
3962                None::<&Handler<AeronUnavailableImageLogger>>,
3963                Duration::from_secs(5),
3964            )
3965            .unwrap();
3966
3967        // Handler is held by avail_handler, local_avail, and stored in subscription
3968        let initial_count = Arc::strong_count(&local_avail.inner);
3969        assert!(
3970            initial_count >= 2,
3971            "Handler should be held by at least avail_handler and local_avail"
3972        );
3973
3974        // Close the subscription
3975        drop(sub);
3976
3977        // Handler should STILL be alive - tracked by dependencies
3978        let after_sub_drop = Arc::strong_count(&local_avail.inner);
3979        assert!(
3980            after_sub_drop >= 1,
3981            "Handler must survive subscription drop - still tracked"
3982        );
3983
3984        // Drop the Aeron wrapper
3985        drop(aeron);
3986
3987        teardown_aeron_after_uaf_test(driver, _error_handler);
3988    }
3989
3990    // ── Structural teardown verification via memory protection ────────
3991    //
3992    // Under the new design, the Rc dependency graph ensures correct
3993    // teardown order: `aeron_close()` can only fire once every child's
3994    // Rc<Aeron> is released.  This section proves that:
3995    //
3996    //  1. `drop(aeron)` is SAFE while children are alive (Rc keeps the
3997    //     C client alive).
3998    //  2. `drop(publisher)` triggers `aeron_close()` (when its Rc<Aeron>
3999    //     is the last handle), which frees the publication — UAF after
4000    //     the publisher drops is the *correct* C-level behaviour.
4001    //
4002    // To prove (2) we use `mprotect(PROT_NONE)` on the page containing
4003    // the dangling pointer after `aeron_close()`.  Any access then
4004    // triggers SIGBUS/SIGSEGV, proving the pointer targets freed memory.
4005    //
4006    // On Linux/macOS `mprotect` on malloc'd heap memory works at the page
4007    // level.  This test uses `fork()` so the dangerous mprotect runs in a
4008    // child process — the parent test runner is never at risk.
4009    //
4010    // The test passes when the child exits with SIGABRT (teardown proven).
4011
4012    #[cfg(any(target_os = "linux", target_os = "macos"))]
4013    const PROT_NONE: i32 = 0;
4014
4015    #[cfg(target_os = "macos")]
4016    const SYS_PAGESIZE: i32 = 29;
4017    #[cfg(target_os = "linux")]
4018    const SYS_PAGESIZE: i32 = 30;
4019
4020    extern "C" {
4021        fn mprotect(addr: *mut core::ffi::c_void, len: usize, prot: i32) -> i32;
4022        fn sysconf(name: i32) -> isize;
4023        fn write(fd: i32, buf: *const core::ffi::c_void, count: usize) -> isize;
4024    }
4025
4026    /// Async-signal-safe write of a fixed message to stderr.
4027    unsafe fn uaf_write_msg(msg: &[u8]) {
4028        write(2, msg.as_ptr() as *const core::ffi::c_void, msg.len());
4029    }
4030
4031    /// Signal handler for SIGBUS / SIGSEGV caught during the mprotect test.
4032    /// Converts the crash into a clean abort with a diagnostic message.
4033    extern "C" fn uaf_sigbus_handler(_sig: i32) {
4034        unsafe {
4035            uaf_write_msg(b"\n\n*** CORRECT TEARDOWN *** aeron_close() freed the publication memory\n");
4036            uaf_write_msg(b"*** Rc graph teardown confirmed: the publication's page was protected\n");
4037            uaf_write_msg(b"*** with mprotect(PROT_NONE) after the publisher dropped, proving the\n");
4038            uaf_write_msg(b"*** pointer is dangling because aeron_close freed it, not before.\n\n");
4039        }
4040        std::process::abort();
4041    }
4042
4043    /// Prove the Rc graph teardown frees C memory: after the last child handle
4044    /// drops (triggering deferred `aeron_close()`), `mprotect(PROT_NONE)` on the
4045    /// publication's page causes a SIGBUS on access, confirming the memory was freed.
4046    ///
4047    /// This test is `#[ignore]` because it uses `mprotect(PROT_NONE)` which
4048    /// affects an entire VM page.  If other live heap allocations share the
4049    /// same page as the freed Aeron resource the process may crash during
4050    /// teardown too, so we call `std::process::abort` on detection and
4051    /// `std::process::exit(0)` on the no-crash path.
4052    ///
4053    /// Uses `fork()` to isolate the dangerous mprotect + access into
4054    /// a child process.  The parent waits for the child's exit status:
4055    ///
4056    /// | Child exit | Meaning | Test result |
4057    /// |---|---|---|
4058    /// | `_exit(0)` | `mprotect` unsupported on this platform | pass (skip) |
4059    /// | SIGABRT | freed memory detected via signal handler | **pass (teardown proven)** |
4060    /// | other signal or non-zero exit | unexpected failure | fail |
4061    #[test]
4062    #[serial]
4063    fn prove_rc_teardown_frees_via_mprotect() {
4064        extern "C" {
4065            fn signal(sig: i32, handler: unsafe extern "C" fn(i32)) -> usize;
4066            fn fork() -> i32;
4067            fn waitpid(pid: i32, status: *mut i32, options: i32) -> i32;
4068            fn _exit(status: i32) -> !;
4069        }
4070        #[cfg(target_os = "macos")]
4071        const SIGBUS: i32 = 10;
4072        #[cfg(not(target_os = "macos"))]
4073        const SIGBUS: i32 = 7;
4074        const SIGSEGV: i32 = 11;
4075
4076        // This test deliberately forks, mprotects, and `mem::forget`s the driver
4077        // to PROVE Rc teardown frees the C resource — it is not a memory-hygiene
4078        // target, and its non-standard teardown (native-executed child + unusual
4079        // close ordering) confuses Valgrind's leak accounting. Skip under Valgrind.
4080        if running_under_valgrind() {
4081            return;
4082        }
4083
4084        unsafe {
4085            let pid = fork();
4086            assert!(pid >= 0, "fork failed");
4087            if pid == 0 {
4088                // ── CHILD: proof of correct Rc graph teardown ──
4089                // The parent is unaffected (fork makes the child's
4090                // mprotect page-private via copy-on-write).
4091
4092                signal(SIGBUS, uaf_sigbus_handler);
4093                signal(SIGSEGV, uaf_sigbus_handler);
4094
4095                let (aeron, driver, error_handler) = setup_aeron_for_uaf_test();
4096                let publisher = aeron
4097                    .add_publication(AERON_IPC_STREAM, 1006, Duration::from_secs(5))
4098                    .unwrap();
4099
4100                let raw_ptr: *mut aeron_publication_t = publisher.get_inner();
4101
4102                // Step 1: drop(aeron) — SAFE under new design.  The
4103                // publisher still holds an Rc<Aeron>, so aeron_close()
4104                // is deferred.
4105                drop(aeron);
4106
4107                // Step 2: drop(publisher) — the Rc<Aeron> refcount drops
4108                // to 0, triggering aeron_close().  aeron_close() frees all
4109                // C resources including the publication at raw_ptr.
4110                drop(publisher);
4111
4112                // Step 3: mprotect the page.  raw_ptr is now dangling.
4113                let page_size = sysconf(SYS_PAGESIZE) as usize;
4114                let page = (raw_ptr as usize) & !(page_size - 1);
4115                let rc = mprotect(page as *mut core::ffi::c_void, page_size, PROT_NONE);
4116
4117                if rc == 0 {
4118                    // Page is PROT_NONE — accessing raw_ptr triggers SIGBUS →
4119                    // signal handler → "teardown confirmed" → abort() (SIGABRT).
4120                    let _closed = aeron_publication_is_closed(raw_ptr);
4121                    // If we reach here, mprotect worked but nothing crashed.
4122                    uaf_write_msg(b"*** FAIL: mprotect succeeded but freed access did not crash\n");
4123                    std::process::abort();
4124                }
4125
4126                // mprotect not supported on this platform — clean exit.
4127                drop(error_handler);
4128                std::mem::forget(driver); // child process: no clean driver teardown needed
4129                _exit(0);
4130            }
4131
4132            // ── PARENT: wait for child's result ──
4133            let mut status: i32 = 0;
4134            let waited = waitpid(pid, &mut status as *mut i32, 0);
4135            assert!(waited != -1, "waitpid failed");
4136
4137            if status & 0x7f == 0 {
4138                // Child exited normally (WIFEXITED).
4139                let code = (status >> 8) & 0xff;
4140                if code == 0 {
4141                    eprintln!(
4142                        "note: mprotect(PROT_NONE) not supported on heap \
4143                         memory on this platform — teardown proof skipped"
4144                    );
4145                    return;
4146                }
4147                panic!("child exited with code {} (unexpected — setup error?)", code);
4148            }
4149
4150            // Child was signalled (WIFSIGNALED).
4151            let sig = status & 0x7f;
4152            if sig == 6 {
4153                // SIGABRT from uaf_sigbus_handler → teardown PROVEN.
4154                eprintln!(
4155                    "*** PASS: Rc graph teardown proven — child confirmed freed \
4156                     memory after aeron_close() via mprotect"
4157                );
4158                return;
4159            }
4160
4161            panic!("child killed by signal {} (expected SIGABRT=6 for teardown proof)", sig);
4162        }
4163    }
4164}