use std::cell::Cell;
use std::marker::PhantomData;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::sync::atomic::Ordering;
use crate::adaptive_ring::{AdaptiveRing, ADAPTIVE_SPSC_PAYLOAD_BYTES};
use crate::ordering::{default_stamp_kind, OrderingMode};
use crate::shared_atomic::{SharedAtomicU32, SharedAtomicU64};
use crate::shared_ring::RingError;
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Locale {
Anon = 0,
File = 1,
ShmFs = 2,
}
impl Locale {
fn from_u32(tag: u32) -> Self {
match tag {
0 => Self::Anon,
1 => Self::File,
2 => Self::ShmFs,
_ => panic!("LocaleAdaptiveRing locale_tag corrupted: {tag}"),
}
}
}
pub struct LocaleAdaptiveRing {
locale_tag: Arc<SharedAtomicU32>,
locale_generation: Arc<SharedAtomicU64>,
anon: AdaptiveRing,
file: AdaptiveRing,
shmfs: AdaptiveRing,
base_path: PathBuf,
}
unsafe impl Send for LocaleAdaptiveRing {}
unsafe impl Sync for LocaleAdaptiveRing {}
impl LocaleAdaptiveRing {
pub fn create(
base_path: impl Into<PathBuf>,
max_producers: usize,
max_consumers: usize,
capacity: usize,
) -> Result<Self, RingError> {
let base_path: PathBuf = base_path.into();
let tag_path = with_suffix(&base_path, ".locale.tag.bin");
let gen_path = with_suffix(&base_path, ".locale.gen.bin");
let file_ring_prefix = with_suffix(&base_path, ".locale.file.ring");
let locale_tag = Arc::new(
SharedAtomicU32::create(&tag_path, 0)
.map_err(|_| RingError::PayloadTooLarge)?,
);
let locale_generation = Arc::new(
SharedAtomicU64::create(&gen_path, 0)
.map_err(|_| RingError::PayloadTooLarge)?,
);
let anon =
AdaptiveRing::create_anon(max_producers, max_consumers, capacity)?;
let file = AdaptiveRing::create(
&file_ring_prefix,
max_producers,
max_consumers,
capacity,
)?;
let shmfs_name_prefix = shmfs_name_prefix_for(&base_path);
let shmfs = AdaptiveRing::create_shmfs(
&shmfs_name_prefix,
max_producers,
max_consumers,
capacity,
)?;
Ok(Self {
locale_tag,
locale_generation,
anon,
file,
shmfs,
base_path,
})
}
pub fn create_with_ordering_stamps(
base_path: impl Into<PathBuf>,
max_producers: usize,
max_consumers: usize,
capacity: usize,
) -> Result<Self, RingError> {
let base_path: PathBuf = base_path.into();
let kind = default_stamp_kind();
let tag_path = with_suffix(&base_path, ".locale.tag.bin");
let gen_path = with_suffix(&base_path, ".locale.gen.bin");
let file_ring_prefix = with_suffix(&base_path, ".locale.file.ring");
let locale_tag = Arc::new(
SharedAtomicU32::create(&tag_path, 0)
.map_err(|_| RingError::PayloadTooLarge)?,
);
let locale_generation = Arc::new(
SharedAtomicU64::create(&gen_path, 0)
.map_err(|_| RingError::PayloadTooLarge)?,
);
let anon = AdaptiveRing::create_anon(max_producers, max_consumers, capacity)?
.with_ordering_stamps_kind(kind)?;
let file = AdaptiveRing::create(
&file_ring_prefix, max_producers, max_consumers, capacity,
)?
.with_ordering_stamps_kind(kind)?;
let shmfs_name_prefix = shmfs_name_prefix_for(&base_path);
let shmfs = AdaptiveRing::create_shmfs(
&shmfs_name_prefix, max_producers, max_consumers, capacity,
)?
.with_ordering_stamps_kind(kind)?;
Ok(Self {
locale_tag,
locale_generation,
anon,
file,
shmfs,
base_path,
})
}
pub fn is_stamped(&self) -> bool {
self.anon.is_stamped()
}
pub fn ordering_mode(&self) -> Option<OrderingMode> {
match self.current_locale() {
Locale::Anon => self.anon.ordering_mode(),
Locale::File => self.file.ordering_mode(),
Locale::ShmFs => self.shmfs.ordering_mode(),
}
}
pub fn set_ordering_mode(&self, mode: OrderingMode) -> Result<(), RingError> {
self.anon.set_ordering_mode(mode)?;
self.file.set_ordering_mode(mode)?;
self.shmfs.set_ordering_mode(mode)
}
pub fn inversions(&self) -> u64 {
self.anon.inversions() + self.file.inversions() + self.shmfs.inversions()
}
pub fn current_locale(&self) -> Locale {
Locale::from_u32(self.locale_tag.load(Ordering::Acquire))
}
pub fn locale_generation(&self) -> u64 {
self.locale_generation.load(Ordering::Acquire)
}
pub fn anon_ring(&self) -> &AdaptiveRing { &self.anon }
pub fn file_ring(&self) -> &AdaptiveRing { &self.file }
pub fn shmfs_ring(&self) -> &AdaptiveRing { &self.shmfs }
pub fn register_producer(&self) -> Result<usize, crate::adaptive_ring::AdaptiveError> {
let anon_id = self.anon.register_producer()?;
let file_id = self.file.register_producer()?;
let shmfs_id = self.shmfs.register_producer()?;
assert_eq!(anon_id, file_id,
"LocaleAdaptiveRing producer registrations must stay in lockstep (anon vs file)");
assert_eq!(anon_id, shmfs_id,
"LocaleAdaptiveRing producer registrations must stay in lockstep (anon vs shmfs)");
Ok(anon_id)
}
pub fn register_consumer(&self) -> Result<usize, crate::adaptive_ring::AdaptiveError> {
let anon_id = self.anon.register_consumer()?;
let file_id = self.file.register_consumer()?;
let shmfs_id = self.shmfs.register_consumer()?;
assert_eq!(anon_id, file_id,
"LocaleAdaptiveRing consumer registrations must stay in lockstep (anon vs file)");
assert_eq!(anon_id, shmfs_id,
"LocaleAdaptiveRing consumer registrations must stay in lockstep (anon vs shmfs)");
Ok(anon_id)
}
pub fn try_send(&self, producer_id: usize, payload: &[u8]) -> Result<(), RingError> {
let locale = Locale::from_u32(self.locale_tag.load(Ordering::Acquire));
match locale {
Locale::Anon => self.anon.try_send(producer_id, payload),
Locale::File => self.file.try_send(producer_id, payload),
Locale::ShmFs => self.shmfs.try_send(producer_id, payload),
}
}
pub fn try_recv(&self, consumer_id: usize, out: &mut [u8]) -> Result<usize, RingError> {
let locale = Locale::from_u32(self.locale_tag.load(Ordering::Acquire));
match locale {
Locale::Anon => self.anon.try_recv(consumer_id, out),
Locale::File => self.file.try_recv(consumer_id, out),
Locale::ShmFs => self.shmfs.try_recv(consumer_id, out),
}
}
pub fn pin_current_locale(&self) -> PinnedLocale<'_> {
let captured_gen = self.locale_generation.load(Ordering::Acquire);
let locale = Locale::from_u32(self.locale_tag.load(Ordering::Acquire));
PinnedLocale {
parent: self,
pinned_generation: captured_gen,
locale,
_not_sync: PhantomData,
}
}
pub fn migrate_to(&self, target: Locale) -> Result<(), RingError> {
let current = Locale::from_u32(self.locale_tag.load(Ordering::Acquire));
if current == target {
return Ok(());
}
self.locale_generation.fetch_add(1, Ordering::AcqRel);
self.transfer_items(current, target)?;
self.locale_tag.store(target as u32, Ordering::Release);
Ok(())
}
fn transfer_items(&self, from: Locale, to: Locale) -> Result<(), RingError> {
let mut buf = [0u8; ADAPTIVE_SPSC_PAYLOAD_BYTES];
loop {
let pop = match from {
Locale::Anon => self.anon.try_recv(0, &mut buf),
Locale::File => self.file.try_recv(0, &mut buf),
Locale::ShmFs => self.shmfs.try_recv(0, &mut buf),
};
match pop {
Ok(n) => {
let push = match to {
Locale::Anon => self.anon.try_send(0, &buf[..n]),
Locale::File => self.file.try_send(0, &buf[..n]),
Locale::ShmFs => self.shmfs.try_send(0, &buf[..n]),
};
push?;
}
Err(_) => return Ok(()),
}
}
}
}
impl Drop for LocaleAdaptiveRing {
fn drop(&mut self) {
let tag_path = with_suffix(&self.base_path, ".locale.tag.bin");
let gen_path = with_suffix(&self.base_path, ".locale.gen.bin");
let file_prefix = with_suffix(&self.base_path, ".locale.file.ring");
let max_p = self.file.max_producers();
std::fs::remove_file(&tag_path).ok();
std::fs::remove_file(&gen_path).ok();
std::fs::remove_file(with_suffix(&file_prefix, ".spsc.bin")).ok();
std::fs::remove_file(with_suffix(&file_prefix, ".vyukov.bin")).ok();
std::fs::remove_file(with_suffix(&file_prefix, ".ordering.bin")).ok();
for i in 0..max_p {
std::fs::remove_file(
with_suffix(&file_prefix, &format!(".mpsc.{i}.bin")),
).ok();
std::fs::remove_file(
with_suffix(&file_prefix, &format!(".mpmc.{i}.bin")),
).ok();
}
}
}
fn with_suffix(base: &Path, suffix: &str) -> PathBuf {
let mut s = base.as_os_str().to_owned();
s.push(suffix);
PathBuf::from(s)
}
fn shmfs_name_prefix_for(base: &Path) -> String {
let stem = base.file_name()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_else(|| "locale_ring".to_string());
format!("locale_{stem}")
}
pub struct PinnedLocale<'a> {
parent: &'a LocaleAdaptiveRing,
pinned_generation: u64,
locale: Locale,
_not_sync: PhantomData<Cell<()>>,
}
impl<'a> PinnedLocale<'a> {
pub fn locale(&self) -> Locale { self.locale }
pub fn pinned_generation(&self) -> u64 { self.pinned_generation }
pub fn is_still_valid(&self) -> bool {
self.parent.locale_generation.load(Ordering::Acquire)
== self.pinned_generation
}
pub fn as_anon(&self) -> Option<&AdaptiveRing> {
match self.locale {
Locale::Anon => Some(&self.parent.anon),
_ => None,
}
}
pub fn as_file(&self) -> Option<&AdaptiveRing> {
match self.locale {
Locale::File => Some(&self.parent.file),
_ => None,
}
}
pub fn as_shmfs(&self) -> Option<&AdaptiveRing> {
match self.locale {
Locale::ShmFs => Some(&self.parent.shmfs),
_ => None,
}
}
}
use std::sync::atomic::{AtomicU32 as StdAtomicU32, AtomicU64 as StdAtomicU64};
#[derive(Debug, Clone, Copy)]
pub struct LocalePolicyObservation {
pub current_locale: Locale,
pub requested_locale: Locale,
pub since_last_migrate: std::time::Duration,
}
pub trait LocalePolicy: Send + Sync + 'static {
fn decide(&self, observation: &LocalePolicyObservation) -> Option<Locale>;
}
pub struct DefaultLocalePolicy {
pub hysteresis: std::time::Duration,
}
impl Default for DefaultLocalePolicy {
fn default() -> Self {
Self { hysteresis: std::time::Duration::from_millis(250) }
}
}
impl LocalePolicy for DefaultLocalePolicy {
fn decide(&self, obs: &LocalePolicyObservation) -> Option<Locale> {
if obs.since_last_migrate < self.hysteresis {
return None;
}
if obs.requested_locale == obs.current_locale {
None
} else {
Some(obs.requested_locale)
}
}
}
pub struct LocaleAdaptiveRingSidecar {
handle: Option<std::thread::JoinHandle<()>>,
stop: Arc<std::sync::atomic::AtomicBool>,
requested_locale: Arc<StdAtomicU32>,
migrations_triggered: Arc<StdAtomicU64>,
}
impl LocaleAdaptiveRingSidecar {
pub fn spawn<P: LocalePolicy>(
ring: Arc<LocaleAdaptiveRing>,
policy: P,
scan_interval: std::time::Duration,
) -> Self {
Self::spawn_gated(
ring,
policy,
scan_interval,
crate::policy_gate::GateConfig::default(),
)
}
pub fn spawn_gated<P: LocalePolicy>(
ring: Arc<LocaleAdaptiveRing>,
policy: P,
scan_interval: std::time::Duration,
gate_cfg: crate::policy_gate::GateConfig,
) -> Self {
let stop = Arc::new(std::sync::atomic::AtomicBool::new(false));
let requested_locale = Arc::new(StdAtomicU32::new(ring.current_locale() as u32));
let migrations_triggered = Arc::new(StdAtomicU64::new(0));
let stop_c = Arc::clone(&stop);
let requested_c = Arc::clone(&requested_locale);
let migrations_c = Arc::clone(&migrations_triggered);
let handle = std::thread::spawn(move || {
let mut last_migrate = std::time::Instant::now();
let mut gate = crate::policy_gate::ConfidenceGate::new(gate_cfg);
while !stop_c.load(Ordering::Acquire) {
let req = Locale::from_u32(requested_c.load(Ordering::Acquire));
let obs = LocalePolicyObservation {
current_locale: ring.current_locale(),
requested_locale: req,
since_last_migrate: last_migrate.elapsed(),
};
if let Some(target) = gate.observe(policy.decide(&obs))
&& ring.migrate_to(target).is_ok()
{
last_migrate = std::time::Instant::now();
migrations_c.fetch_add(1, Ordering::Relaxed);
}
std::thread::sleep(scan_interval);
}
});
Self {
handle: Some(handle),
stop,
requested_locale,
migrations_triggered,
}
}
pub fn request_locale(&self, target: Locale) {
self.requested_locale.store(target as u32, Ordering::Release);
}
pub fn migrations_triggered(&self) -> u64 {
self.migrations_triggered.load(Ordering::Relaxed)
}
pub fn shutdown(mut self) {
self.stop.store(true, Ordering::Release);
if let Some(h) = self.handle.take() {
drop(h.join());
}
}
}
impl Drop for LocaleAdaptiveRingSidecar {
fn drop(&mut self) {
self.stop.store(true, Ordering::Release);
if let Some(h) = self.handle.take() {
drop(h.join());
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::RingShape;
fn tmp(name: &str) -> PathBuf {
let mut p = std::env::temp_dir();
let pid = std::process::id();
let nonce = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
p.push(format!("subetha_locale_{pid}_{nonce}_{name}"));
p
}
#[test]
fn create_starts_in_anon_locale() {
let path = tmp("init");
let r = LocaleAdaptiveRing::create(&path, 1, 1, 64).expect("create");
assert_eq!(r.current_locale(), Locale::Anon);
assert_eq!(r.locale_generation(), 0);
}
#[test]
fn round_trip_in_anon_locale() {
let path = tmp("rt_anon");
let r = LocaleAdaptiveRing::create(&path, 1, 1, 64).expect("create");
r.register_producer().expect("p");
r.register_consumer().expect("c");
let payload = 0xDEADBEEFu64.to_le_bytes();
let mut buf = [0u8; ADAPTIVE_SPSC_PAYLOAD_BYTES];
r.try_send(0, &payload).expect("send");
let n = r.try_recv(0, &mut buf).expect("recv");
assert!(n >= 8);
assert_eq!(&buf[..8], &payload);
}
#[test]
fn migrate_invalidates_outstanding_pin() {
let path = tmp("migrate_invalidate");
let r = LocaleAdaptiveRing::create(&path, 1, 1, 64).expect("create");
r.register_producer().expect("p");
r.register_consumer().expect("c");
let pin = r.pin_current_locale();
assert!(pin.is_still_valid());
assert_eq!(pin.locale(), Locale::Anon);
assert!(pin.as_anon().is_some());
assert!(pin.as_file().is_none());
r.migrate_to(Locale::File).expect("migrate");
assert!(!pin.is_still_valid(), "pin must invalidate on locale flip");
assert_eq!(r.current_locale(), Locale::File);
let pin2 = r.pin_current_locale();
assert!(pin2.is_still_valid());
assert_eq!(pin2.locale(), Locale::File);
assert!(pin2.as_file().is_some());
assert!(pin2.as_anon().is_none());
}
#[test]
fn migrate_to_same_locale_does_not_bump_generation() {
let path = tmp("migrate_noop");
let r = LocaleAdaptiveRing::create(&path, 1, 1, 64).expect("create");
let pin = r.pin_current_locale();
let gen_before = pin.pinned_generation();
r.migrate_to(Locale::Anon).expect("noop migrate");
assert_eq!(r.locale_generation(), gen_before);
assert!(pin.is_still_valid());
}
#[test]
fn migrate_transfers_in_flight_items() {
let path = tmp("migrate_transfer");
let r = LocaleAdaptiveRing::create(&path, 1, 1, 64).expect("create");
r.register_producer().expect("p");
r.register_consumer().expect("c");
for i in 0u64..3 {
r.try_send(0, &i.to_le_bytes()).expect("send");
}
r.migrate_to(Locale::File).expect("migrate");
assert_eq!(r.current_locale(), Locale::File);
let mut got = Vec::new();
let mut buf = [0u8; ADAPTIVE_SPSC_PAYLOAD_BYTES];
for _ in 0..3 {
let n = r.try_recv(0, &mut buf).expect("recv");
assert!(n >= 8);
got.push(u64::from_le_bytes(buf[..8].try_into().unwrap()));
}
got.sort();
assert_eq!(got, vec![0, 1, 2]);
}
#[test]
fn pinned_chain_into_shape_axis() {
let path = tmp("pin_chain");
let r = LocaleAdaptiveRing::create(&path, 1, 1, 64).expect("create");
r.register_producer().expect("p");
r.register_consumer().expect("c");
let pin_locale = r.pin_current_locale();
let ring = pin_locale.as_anon().expect("pinned at anon");
let pin_shape = ring.pin_current_shape();
assert_eq!(pin_shape.shape(), RingShape::Spsc);
assert!(pin_locale.is_still_valid() && pin_shape.is_still_valid());
let payload = 0x12345678u64.to_le_bytes();
pin_shape.spsc_try_push(&payload).expect("native push");
let mut buf = [0u8; ADAPTIVE_SPSC_PAYLOAD_BYTES];
let n = pin_shape.spsc_try_pop(&mut buf).expect("native pop");
assert!(n >= 8);
assert_eq!(&buf[..8], &payload);
}
#[test]
fn migrate_to_shmfs_works() {
let path = tmp("migrate_shmfs");
let r = LocaleAdaptiveRing::create(&path, 1, 1, 64).expect("create");
r.register_producer().expect("p");
r.register_consumer().expect("c");
let pin = r.pin_current_locale();
assert_eq!(pin.locale(), Locale::Anon);
assert!(pin.as_shmfs().is_none());
r.migrate_to(Locale::ShmFs).expect("migrate to shmfs");
assert!(!pin.is_still_valid());
assert_eq!(r.current_locale(), Locale::ShmFs);
let pin2 = r.pin_current_locale();
assert_eq!(pin2.locale(), Locale::ShmFs);
assert!(pin2.as_shmfs().is_some());
assert!(pin2.as_anon().is_none());
assert!(pin2.as_file().is_none());
}
#[test]
fn round_trip_in_shmfs_locale() {
let path = tmp("rt_shmfs");
let r = LocaleAdaptiveRing::create(&path, 1, 1, 64).expect("create");
r.register_producer().expect("p");
r.register_consumer().expect("c");
r.migrate_to(Locale::ShmFs).expect("migrate");
let payload = 0xABCDEF12u64.to_le_bytes();
let mut buf = [0u8; ADAPTIVE_SPSC_PAYLOAD_BYTES];
r.try_send(0, &payload).expect("send");
let n = r.try_recv(0, &mut buf).expect("recv");
assert!(n >= 8);
assert_eq!(&buf[..8], &payload);
}
#[test]
fn migrate_anon_to_shmfs_to_file_transfers_items() {
let path = tmp("triple_morph");
let r = LocaleAdaptiveRing::create(&path, 1, 1, 64).expect("create");
r.register_producer().expect("p");
r.register_consumer().expect("c");
for i in 0u64..3 {
r.try_send(0, &i.to_le_bytes()).expect("anon send");
}
r.migrate_to(Locale::ShmFs).expect("anon -> shmfs");
for i in 100u64..103 {
r.try_send(0, &i.to_le_bytes()).expect("shmfs send");
}
r.migrate_to(Locale::File).expect("shmfs -> file");
let mut got = Vec::new();
let mut buf = [0u8; ADAPTIVE_SPSC_PAYLOAD_BYTES];
for _ in 0..6 {
let n = r.try_recv(0, &mut buf).expect("recv");
assert!(n >= 8);
got.push(u64::from_le_bytes(buf[..8].try_into().unwrap()));
}
got.sort();
assert_eq!(got, vec![0, 1, 2, 100, 101, 102]);
}
#[test]
fn stamped_locale_ring_mode_follows_migrations() {
let path = tmp("stamped_locale");
let r = LocaleAdaptiveRing::create_with_ordering_stamps(&path, 1, 1, 64)
.expect("create stamped");
assert!(r.is_stamped());
r.register_producer().expect("p");
r.register_consumer().expect("c");
r.set_ordering_mode(OrderingMode::MergeByStamp).expect("mode");
assert_eq!(r.ordering_mode(), Some(OrderingMode::MergeByStamp));
let payload = 0xFEEDu64.to_le_bytes();
let mut buf = [0u8; ADAPTIVE_SPSC_PAYLOAD_BYTES];
r.try_send(0, &payload).expect("send");
let n = r.try_recv(0, &mut buf).expect("recv");
assert_eq!(n, crate::ordering::STAMPED_PAYLOAD_BYTES);
assert_eq!(&buf[..8], &payload);
for i in 0u64..3 {
r.try_send(0, &i.to_le_bytes()).expect("send pre-migrate");
}
r.migrate_to(Locale::ShmFs).expect("migrate");
assert_eq!(r.ordering_mode(), Some(OrderingMode::MergeByStamp),
"mode must follow the ring to the new locale");
let mut got = Vec::new();
for _ in 0..3 {
let n = r.try_recv(0, &mut buf).expect("recv post-migrate");
assert_eq!(n, crate::ordering::STAMPED_PAYLOAD_BYTES);
got.push(u64::from_le_bytes(buf[..8].try_into().unwrap()));
}
assert_eq!(got, vec![0, 1, 2],
"merge-mode drain order must survive the locale transfer");
}
#[test]
fn four_axis_pin_chain_at_shmfs_locale() {
let path = tmp("four_axis_shmfs");
let r = LocaleAdaptiveRing::create(&path, 1, 1, 64).expect("create");
r.register_producer().expect("p");
r.register_consumer().expect("c");
r.migrate_to(Locale::ShmFs).expect("migrate");
let pin_locale = r.pin_current_locale();
let adaptive = pin_locale.as_shmfs().expect("pinned at shmfs");
let pin_shape = adaptive.pin_current_shape();
assert_eq!(pin_shape.shape(), RingShape::Spsc);
assert!(pin_locale.is_still_valid() && pin_shape.is_still_valid());
let payload = 0xCAFEBABEu64.to_le_bytes();
pin_shape.spsc_try_push(&payload).expect("native push");
let mut buf = [0u8; ADAPTIVE_SPSC_PAYLOAD_BYTES];
let n = pin_shape.spsc_try_pop(&mut buf).expect("native pop");
assert!(n >= 8);
assert_eq!(&buf[..8], &payload);
}
}