tiny-counter 0.1.0

Track event counts across time windows with fixed memory and fast queries
Documentation
use std::sync::{Arc, Mutex};

use chrono::{DateTime, Utc};
use dashmap::DashMap;

use crate::{config_converter::convert_if_needed, Storage};
use crate::{sync_time::synchronized_start, Clock, EventCounterConfig, SingleEventCounter};
use crate::{Error, Result};

/// Top-level event store managing multiple event counters.
///
/// Internal state for EventStore, wrapped in Arc for cheap cloning.
pub(crate) struct EventStoreInner {
    pub(crate) events: DashMap<String, Arc<Mutex<SingleEventCounter>>>,
    pub(crate) storage: Option<Arc<Mutex<Box<dyn Storage>>>>,
    pub(crate) formatter: Option<Arc<dyn crate::Formatter>>,
    pub(crate) clock: Arc<dyn Clock>,
    pub(crate) config: EventCounterConfig,
}

impl EventStoreInner {
    /// Gets current time from clock.
    pub(crate) fn clock_now(&self) -> DateTime<Utc> {
        self.clock.now()
    }

    /// Gets or creates a counter for recording - always returns a counter.
    pub(crate) fn get_counter_for_record(&self, event_id: &str) -> Arc<Mutex<SingleEventCounter>> {
        // Use atomic entry API to avoid TOCTOU race.
        // Previously: contains_key + insert could race with or_insert_with,
        // causing the insert to overwrite a counter that had recorded events.
        //
        // We must load BEFORE entering the closure to avoid deadlock (load_event_if_needed
        // calls contains_key, which would try to read while or_insert_with holds write lock).
        let preloaded = if !self.events.contains_key(event_id) {
            self.load_event_if_needed(event_id).ok().flatten()
        } else {
            None
        };

        self.events
            .entry(event_id.to_string())
            .or_insert_with(|| {
                // Use preloaded counter if available, otherwise create new
                if let Some(counter) = preloaded {
                    return Arc::new(Mutex::new(counter));
                }
                Arc::new(Mutex::new(self.create_counter()))
            })
            .value()
            .clone()
    }

    /// Gets a counter for querying - handles lazy loading from storage if needed.
    ///
    /// Returns None if the counter doesn't exist in memory or storage.
    pub(crate) fn get_counter_for_query(
        &self,
        event_id: &str,
    ) -> Option<Arc<Mutex<SingleEventCounter>>> {
        // Return if in memory
        if let Some(entry) = self.events.get(event_id) {
            return Some(entry.value().clone());
        }

        // Try loading from storage
        if let Ok(Some(counter)) = self.load_event_if_needed(event_id) {
            let arc = Arc::new(Mutex::new(counter));
            self.events.insert(event_id.to_string(), arc.clone());
            return Some(arc);
        }

        None
    }

    fn create_counter(&self) -> SingleEventCounter {
        let now = self.clock.now();
        let starting_instant = synchronized_start(now);
        self.config.create_counter(starting_instant)
    }

    /// Loads an event counter from storage if needed.
    ///
    /// Returns Ok(None) if the event is already in memory or doesn't exist in storage.
    /// Returns Ok(Some(counter)) if the event was successfully loaded and aligned.
    /// Returns Err if deserialization or storage access fails.
    fn load_event_if_needed(&self, event_id: &str) -> Result<Option<SingleEventCounter>> {
        // Return None if already in memory
        if self.events.contains_key(event_id) {
            return Ok(None);
        }

        // Return None if no storage configured
        let storage = match &self.storage {
            Some(s) => s,
            None => return Ok(None),
        };

        // Load from storage
        let storage_guard = storage.lock().unwrap();
        let data = match storage_guard.load(event_id)? {
            Some(d) => d,
            None => return Ok(None),
        };
        drop(storage_guard);

        // Deserialize
        let formatter = self
            .formatter
            .as_ref()
            .ok_or_else(|| Error::Serialization("No formatter configured".to_string()))?;
        let mut counter: SingleEventCounter = formatter.deserialize(&data)?;

        // Align to current time and config
        let now = self.clock.now();
        counter.advance_if_needed(now);
        let counter = convert_if_needed(counter, &self.config);

        Ok(Some(counter))
    }
}