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