1use std::collections::HashMap;
14use std::fmt;
15use std::sync::atomic::{AtomicI64, AtomicU8, AtomicU16, AtomicU64, Ordering};
16use std::sync::{Arc, LazyLock, Mutex, MutexGuard, Weak};
17
18use orbit_core::Fleet;
19#[cfg(unix)]
20use orbit_core::shm::{ShmRegion, ring_segment_name};
21
22pub const COUNTER_STATE_KIND: u8 = 230;
24pub const COUNTER_CAPACITY: usize =
30 orbit_core::compile::usize_from_env(option_env!("ORBIT_COUNTER_CAPACITY"), 1_024);
31pub const COUNTER_KEY_MAX: usize =
36 orbit_core::compile::usize_from_env(option_env!("ORBIT_COUNTER_KEY_MAX"), 240);
37
38const SLOT_EMPTY: u8 = 0;
39const SLOT_OCCUPIED: u8 = 1;
40const FNV_OFFSET_BASIS: u64 = 0xcbf2_9ce4_8422_2325;
41const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
42
43#[cfg(unix)]
44const STATE_MAGIC: u32 = 0x43_43_4E_54; #[cfg(unix)]
46const STATE_VERSION: u16 = 1;
47
48pub type Result<T, E = Error> = std::result::Result<T, E>;
49
50#[derive(Debug)]
51pub enum Error {
52 KeyEmpty,
53 KeyTooLarge { len: usize, max: usize },
54 NegativeAmount(i64),
55 Overflow,
56 StateFull { capacity: usize },
57 Io(std::io::Error),
58}
59
60impl fmt::Display for Error {
61 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62 match self {
63 Self::KeyEmpty => f.write_str("counter key must not be empty"),
64 Self::KeyTooLarge { len, max } => {
65 write!(f, "counter key is too large: len={len} max={max}")
66 }
67 Self::NegativeAmount(value) => {
68 write!(f, "counter amount must not be negative: {value}")
69 }
70 Self::Overflow => f.write_str("counter value is outside the signed 64-bit range"),
71 Self::StateFull { capacity } => {
72 write!(f, "counter state table is full: capacity={capacity}")
73 }
74 Self::Io(error) => write!(f, "Orbit counter io error: {error}"),
75 }
76 }
77}
78
79impl std::error::Error for Error {
80 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
81 match self {
82 Self::Io(error) => Some(error),
83 _ => None,
84 }
85 }
86}
87
88impl From<std::io::Error> for Error {
89 fn from(value: std::io::Error) -> Self {
90 Self::Io(value)
91 }
92}
93
94#[derive(Clone)]
100pub struct Counter {
101 backend: CounterBackend,
102}
103
104#[derive(Clone)]
105enum CounterBackend {
106 InMemory(Arc<MemoryCounterTable>),
107 #[cfg(unix)]
108 Shm(Arc<ShmCounterTable>),
109}
110
111impl Counter {
112 pub fn new(fleet: Arc<Fleet>) -> Result<Self> {
113 let backend = if fleet.is_shm() {
114 #[cfg(unix)]
115 {
116 CounterBackend::Shm(Arc::new(ShmCounterTable::open_or_create(
117 &ring_segment_name(fleet.name(), COUNTER_STATE_KIND),
118 )?))
119 }
120 #[cfg(not(unix))]
121 unreachable!("non-Unix fleets cannot use POSIX SHM")
122 } else {
123 CounterBackend::InMemory(memory_table(&fleet))
124 };
125 Ok(Self { backend })
126 }
127
128 pub fn get(&self, key: &str) -> Result<Option<i64>> {
129 validate_key(key)?;
130 let hash = counter_hash(key.as_bytes());
131 match &self.backend {
132 CounterBackend::InMemory(table) => {
133 let slots = lock_unpoisoned(&table.slots);
134 Ok(find_slot(&slots, hash, key.as_bytes())
135 .map(|slot| slot.value.load(Ordering::Acquire)))
136 }
137 #[cfg(unix)]
138 CounterBackend::Shm(table) => Ok(find_slot(table.slots(), hash, key.as_bytes())
139 .map(|slot| slot.value.load(Ordering::Acquire))),
140 }
141 }
142
143 pub fn increment(&self, key: &str, by: i64) -> Result<i64> {
144 validate_amount(by)?;
145 self.update(key, by)
146 }
147
148 pub fn decrement(&self, key: &str, by: i64) -> Result<i64> {
149 validate_amount(by)?;
150 self.update(key, -by)
151 }
152
153 pub fn reset(&self, key: &str) -> Result<()> {
155 validate_key(key)?;
156 let hash = counter_hash(key.as_bytes());
157 match &self.backend {
158 CounterBackend::InMemory(table) => {
159 let slots = lock_unpoisoned(&table.slots);
160 reset_or_install(&slots, hash, key.as_bytes())
161 }
162 #[cfg(unix)]
163 CounterBackend::Shm(table) => {
164 if let Some(slot) = find_slot(table.slots(), hash, key.as_bytes()) {
165 slot.value.store(0, Ordering::Release);
166 return Ok(());
167 }
168 table.with_structure(|slots| reset_or_install(slots, hash, key.as_bytes()))
169 }
170 }
171 }
172
173 pub fn reset_all(&self) -> Result<()> {
175 match &self.backend {
176 CounterBackend::InMemory(table) => {
177 let slots = lock_unpoisoned(&table.slots);
178 clear_slots(&slots);
179 Ok(())
180 }
181 #[cfg(unix)]
182 CounterBackend::Shm(table) => table.with_structure(|slots| {
183 clear_slots(slots);
184 Ok(())
185 }),
186 }
187 }
188
189 #[cfg(unix)]
192 pub fn unlink(&self) -> Result<()> {
193 match &self.backend {
194 CounterBackend::InMemory(_) => self.reset_all(),
195 CounterBackend::Shm(table) => table.region.unlink().map_err(Error::Io),
196 }
197 }
198
199 fn update(&self, key: &str, delta: i64) -> Result<i64> {
200 validate_key(key)?;
201 let hash = counter_hash(key.as_bytes());
202 match &self.backend {
203 CounterBackend::InMemory(table) => {
204 let slots = lock_unpoisoned(&table.slots);
205 update_or_install(&slots, hash, key.as_bytes(), delta)
206 }
207 #[cfg(unix)]
208 CounterBackend::Shm(table) => {
209 if let Some(slot) = find_slot(table.slots(), hash, key.as_bytes()) {
210 return update_value(slot, delta);
211 }
212 table.with_structure(|slots| update_or_install(slots, hash, key.as_bytes(), delta))
213 }
214 }
215 }
216}
217
218fn update_or_install(slots: &[CounterSlot], hash: u64, key: &[u8], delta: i64) -> Result<i64> {
219 for offset in 0..slots.len() {
220 let index = probe_index(hash, offset, slots.len());
221 let slot = &slots[index];
222 match slot.state.load(Ordering::Acquire) {
223 SLOT_EMPTY => {
224 slot.install(hash, key, delta);
225 return Ok(delta);
226 }
227 SLOT_OCCUPIED if slot.matches(hash, key) => return update_value(slot, delta),
228 _ => {}
229 }
230 }
231 Err(Error::StateFull {
232 capacity: slots.len(),
233 })
234}
235
236fn find_slot<'a>(slots: &'a [CounterSlot], hash: u64, key: &[u8]) -> Option<&'a CounterSlot> {
237 for offset in 0..slots.len() {
238 let slot = &slots[probe_index(hash, offset, slots.len())];
239 match slot.state.load(Ordering::Acquire) {
240 SLOT_EMPTY => return None,
241 SLOT_OCCUPIED if slot.matches(hash, key) => return Some(slot),
242 _ => {}
243 }
244 }
245 None
246}
247
248fn reset_or_install(slots: &[CounterSlot], hash: u64, key: &[u8]) -> Result<()> {
249 for offset in 0..slots.len() {
250 let index = probe_index(hash, offset, slots.len());
251 let slot = &slots[index];
252 match slot.state.load(Ordering::Acquire) {
253 SLOT_EMPTY => {
254 slot.install(hash, key, 0);
255 return Ok(());
256 }
257 SLOT_OCCUPIED if slot.matches(hash, key) => {
258 slot.value.store(0, Ordering::Release);
259 return Ok(());
260 }
261 _ => {}
262 }
263 }
264 Err(Error::StateFull {
265 capacity: slots.len(),
266 })
267}
268
269fn clear_slots(slots: &[CounterSlot]) {
270 for slot in slots {
271 slot.value.store(0, Ordering::Relaxed);
272 slot.state.store(SLOT_EMPTY, Ordering::Release);
273 }
274}
275
276fn update_value(slot: &CounterSlot, delta: i64) -> Result<i64> {
277 slot.value
278 .try_update(Ordering::AcqRel, Ordering::Acquire, |current| {
279 current.checked_add(delta)
280 })
281 .map(|previous| previous + delta)
282 .map_err(|_| Error::Overflow)
283}
284
285fn validate_key(key: &str) -> Result<()> {
286 if key.is_empty() {
287 return Err(Error::KeyEmpty);
288 }
289 if key.len() > COUNTER_KEY_MAX {
290 return Err(Error::KeyTooLarge {
291 len: key.len(),
292 max: COUNTER_KEY_MAX,
293 });
294 }
295 Ok(())
296}
297
298fn validate_amount(by: i64) -> Result<()> {
299 if by < 0 {
300 return Err(Error::NegativeAmount(by));
301 }
302 Ok(())
303}
304
305fn counter_hash(key: &[u8]) -> u64 {
306 let mut hash = FNV_OFFSET_BASIS;
307 for byte in key {
308 hash ^= u64::from(*byte);
309 hash = hash.wrapping_mul(FNV_PRIME);
310 }
311 hash
312}
313
314fn probe_index(hash: u64, offset: usize, capacity: usize) -> usize {
315 debug_assert!(capacity.is_power_of_two());
316 (hash as usize).wrapping_add(offset) & (capacity - 1)
317}
318
319fn lock_unpoisoned<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
320 mutex.lock().unwrap_or_else(|error| error.into_inner())
321}
322
323struct MemoryCounterTable {
324 slots: Mutex<Vec<CounterSlot>>,
325}
326
327impl MemoryCounterTable {
328 fn new() -> Self {
329 Self {
330 slots: Mutex::new(
331 (0..COUNTER_CAPACITY)
332 .map(|_| CounterSlot::empty())
333 .collect(),
334 ),
335 }
336 }
337}
338
339type MemoryRegistry = HashMap<usize, Weak<MemoryCounterTable>>;
340static MEMORY_TABLES: LazyLock<Mutex<MemoryRegistry>> =
341 LazyLock::new(|| Mutex::new(HashMap::new()));
342
343fn memory_table(fleet: &Arc<Fleet>) -> Arc<MemoryCounterTable> {
344 let fleet_identity = Arc::as_ptr(fleet) as usize;
345 let mut tables = lock_unpoisoned(&MEMORY_TABLES);
346 tables.retain(|_, table| table.strong_count() > 0);
347 if let Some(table) = tables.get(&fleet_identity).and_then(Weak::upgrade) {
348 return table;
349 }
350 let table = Arc::new(MemoryCounterTable::new());
351 tables.insert(fleet_identity, Arc::downgrade(&table));
352 table
353}
354
355#[repr(C, align(64))]
356struct CounterStateHeader {
357 #[cfg(unix)]
358 magic: u32,
359 #[cfg(not(unix))]
360 _magic: u32,
361 #[cfg(unix)]
362 version: u16,
363 #[cfg(not(unix))]
364 _version: u16,
365 header_size: u16,
366 capacity: u32,
367 slot_size: u32,
368 _reserved: [u8; 48],
369}
370
371impl CounterStateHeader {
372 fn new() -> Self {
373 Self {
374 #[cfg(unix)]
375 magic: STATE_MAGIC,
376 #[cfg(not(unix))]
377 _magic: 0,
378 #[cfg(unix)]
379 version: STATE_VERSION,
380 #[cfg(not(unix))]
381 _version: 0,
382 header_size: size_of::<Self>() as u16,
383 capacity: COUNTER_CAPACITY as u32,
384 slot_size: size_of::<CounterSlot>() as u32,
385 _reserved: [0; 48],
386 }
387 }
388}
389
390#[repr(C, align(64))]
403struct CounterSlot {
404 state: AtomicU8,
405 _reserved: [u8; 7],
406 key_hash: AtomicU64,
407 value: AtomicI64,
408 key_len: AtomicU16,
409 _padding: [u8; 6],
410 key: [AtomicU8; COUNTER_KEY_MAX],
411}
412
413impl CounterSlot {
414 fn empty() -> Self {
415 Self {
416 state: AtomicU8::new(SLOT_EMPTY),
417 _reserved: [0; 7],
418 key_hash: AtomicU64::new(0),
419 value: AtomicI64::new(0),
420 key_len: AtomicU16::new(0),
421 _padding: [0; 6],
422 key: std::array::from_fn(|_| AtomicU8::new(0)),
423 }
424 }
425
426 fn matches(&self, hash: u64, key: &[u8]) -> bool {
427 self.state.load(Ordering::Acquire) == SLOT_OCCUPIED
431 && self.key_hash.load(Ordering::Relaxed) == hash
432 && usize::from(self.key_len.load(Ordering::Relaxed)) == key.len()
433 && self.key[..key.len()]
434 .iter()
435 .zip(key)
436 .all(|(stored, expected)| stored.load(Ordering::Relaxed) == *expected)
437 }
438
439 fn install(&self, hash: u64, key: &[u8], value: i64) {
443 debug_assert!(key.len() <= COUNTER_KEY_MAX);
444 self.key_hash.store(hash, Ordering::Relaxed);
445 self.key_len.store(key.len() as u16, Ordering::Relaxed);
446 for (index, byte) in self.key.iter().enumerate() {
447 byte.store(key.get(index).copied().unwrap_or(0), Ordering::Relaxed);
448 }
449 self.value.store(value, Ordering::Relaxed);
450 self.state.store(SLOT_OCCUPIED, Ordering::Release);
452 }
453}
454
455#[cfg(unix)]
456struct ShmCounterTable {
457 region: ShmRegion,
458 structural_lock: Mutex<()>,
459}
460
461#[cfg(unix)]
462impl ShmCounterTable {
463 fn open_or_create(name: &str) -> Result<Self> {
464 use std::{io, ptr};
465
466 let (region, _initialization_lock) =
467 ShmRegion::open_or_create_locked(name, shm_segment_size())?;
468 if region.created() {
469 unsafe {
470 ptr::write(
471 region.as_ptr().cast::<CounterStateHeader>(),
472 CounterStateHeader::new(),
473 );
474 let slots = region.as_ptr().add(size_of::<CounterStateHeader>());
475 ptr::write_bytes(slots, 0, COUNTER_CAPACITY * size_of::<CounterSlot>());
476 }
477 } else {
478 let header = unsafe { &*region.as_ptr().cast::<CounterStateHeader>() };
479 if header.magic != STATE_MAGIC {
480 return Err(Error::Io(io::Error::new(
481 io::ErrorKind::InvalidData,
482 format!(
483 "SHM segment {name} has wrong magic 0x{:08X} (expected 0x{STATE_MAGIC:08X})",
484 header.magic
485 ),
486 )));
487 }
488 if header.version != STATE_VERSION
489 || usize::from(header.header_size) != size_of::<CounterStateHeader>()
490 || header.capacity as usize != COUNTER_CAPACITY
491 || header.slot_size as usize != size_of::<CounterSlot>()
492 {
493 return Err(Error::Io(io::Error::new(
494 io::ErrorKind::InvalidData,
495 format!("SHM segment {name} has an incompatible counter-state layout"),
496 )));
497 }
498 }
499
500 Ok(Self {
501 region,
502 structural_lock: Mutex::new(()),
503 })
504 }
505
506 fn slots(&self) -> &[CounterSlot] {
507 unsafe {
508 std::slice::from_raw_parts(
509 self.region
510 .as_ptr()
511 .add(size_of::<CounterStateHeader>())
512 .cast::<CounterSlot>(),
513 COUNTER_CAPACITY,
514 )
515 }
516 }
517
518 fn with_structure<T>(&self, operation: impl FnOnce(&[CounterSlot]) -> Result<T>) -> Result<T> {
519 let _local = lock_unpoisoned(&self.structural_lock);
520 let _process = self.region.lock_exclusive()?;
521 operation(self.slots())
525 }
526}
527
528#[cfg(unix)]
529fn shm_segment_size() -> usize {
530 size_of::<CounterStateHeader>() + COUNTER_CAPACITY * size_of::<CounterSlot>()
531}
532
533const _: () = assert!(COUNTER_CAPACITY.is_power_of_two());
534const _: () = assert!(COUNTER_KEY_MAX > 0 && COUNTER_KEY_MAX <= u16::MAX as usize);
535const _: () = assert!(size_of::<CounterStateHeader>() == 64);
536const _: () = assert!(size_of::<CounterSlot>() == (32 + COUNTER_KEY_MAX).next_multiple_of(64));