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