Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@ let parser = NetflowParser::builder()
```

**Cache Behavior:**
- Empty template caches reserve no entry storage; each cache grows as templates arrive
- When the cache is full, the least recently used template is evicted
- Templates are keyed by template ID (per source)
- Each parser instance maintains its own template cache
Expand Down
5 changes: 4 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@ use std::sync::Arc;

/// Count non-expired templates in an LRU cache, respecting TTL if configured.
fn count_valid_templates<T>(
cache: &lru::LruCache<u16, variable_versions::ttl::TemplateWithTtl<T>>,
cache: &variable_versions::lazy_lru::LazyLruCache<
u16,
variable_versions::ttl::TemplateWithTtl<T>,
>,
ttl_config: &Option<variable_versions::ttl::TtlConfig>,
) -> usize {
match ttl_config {
Expand Down
14 changes: 7 additions & 7 deletions src/variable_versions/ipfix/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ use crate::template_store::{
use crate::variable_versions::config::DEFAULT_MAX_RECORDS_PER_FLOWSET;
use crate::variable_versions::enterprise_registry::EnterpriseFieldRegistry;
use crate::variable_versions::field_value::FieldValue;
use crate::variable_versions::lazy_lru::LazyLruCache;
use crate::variable_versions::metrics::CacheMetricsInner;
use crate::variable_versions::template_events::TemplateProtocol;
use crate::variable_versions::ttl::{TemplateWithTtl, TtlConfig};
Expand All @@ -33,7 +34,6 @@ use crate::variable_versions::{
use crate::{NetflowError, NetflowPacket, ParsedNetflow};

use crate::variable_versions::fast_parse::{parse_u8, parse_u16_be};
use lru::LruCache;
use nom::IResult;
use nom::combinator::complete;
use nom::multi::many0;
Expand Down Expand Up @@ -88,10 +88,10 @@ impl IPFixParser {
.transpose()?;

Ok(Self {
templates: LruCache::new(cache_size),
v9_templates: LruCache::new(cache_size),
ipfix_options_templates: LruCache::new(cache_size),
v9_options_templates: LruCache::new(cache_size),
templates: LazyLruCache::new(cache_size),
v9_templates: LazyLruCache::new(cache_size),
ipfix_options_templates: LazyLruCache::new(cache_size),
v9_options_templates: LazyLruCache::new(cache_size),
ttl_config: config.ttl_config,
max_template_cache_size: config.max_template_cache_size,
max_field_count: config.max_field_count,
Expand Down Expand Up @@ -159,7 +159,7 @@ impl IPFixParser {
/// checker reaching for splits.
#[allow(clippy::too_many_arguments)]
fn install_restored<T>(
cache: &mut LruCache<crate::variable_versions::TemplateId, TemplateWithTtl<Arc<T>>>,
cache: &mut LazyLruCache<crate::variable_versions::TemplateId, TemplateWithTtl<Arc<T>>>,
template_id: u16,
arc: &Arc<T>,
ttl_enabled: bool,
Expand Down Expand Up @@ -835,7 +835,7 @@ impl HasTemplateId for V9OptionsTemplate {
/// metrics. Returns the IDs of any entries the LRU evicted to make room so the
/// caller can mirror the eviction into the secondary template store.
fn insert_templates<T: HasTemplateId>(
cache: &mut LruCache<u16, TemplateWithTtl<Arc<T>>>,
cache: &mut LazyLruCache<u16, TemplateWithTtl<Arc<T>>>,
templates: &[T],
ttl_enabled: bool,
metrics: &mut CacheMetricsInner,
Expand Down
10 changes: 5 additions & 5 deletions src/variable_versions/ipfix/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use crate::template_store::TemplateStore;
use crate::variable_versions::PendingFlowCache;
use crate::variable_versions::enterprise_registry::EnterpriseFieldRegistry;
use crate::variable_versions::field_value::FieldValue;
use crate::variable_versions::lazy_lru::LazyLruCache;
use crate::variable_versions::metrics::CacheMetricsInner;
use crate::variable_versions::template_events::TemplateProtocol;
use crate::variable_versions::ttl::{TemplateWithTtl, TtlConfig};
Expand All @@ -24,7 +25,6 @@ use crate::variable_versions::v9::{
Template as V9Template,
};

use lru::LruCache;
use std::sync::Arc;

use crate::variable_versions::TemplateId;
Expand All @@ -37,12 +37,12 @@ pub type IPFixFlowRecord = Vec<IPFixFieldPair>;
/// Supports both native IPFIX templates and V9-style templates embedded in IPFIX messages.
#[derive(Debug)]
pub struct IPFixParser {
pub(crate) templates: LruCache<TemplateId, TemplateWithTtl<Arc<Template>>>,
pub(crate) v9_templates: LruCache<TemplateId, TemplateWithTtl<Arc<V9Template>>>,
pub(crate) templates: LazyLruCache<TemplateId, TemplateWithTtl<Arc<Template>>>,
pub(crate) v9_templates: LazyLruCache<TemplateId, TemplateWithTtl<Arc<V9Template>>>,
pub(crate) ipfix_options_templates:
LruCache<TemplateId, TemplateWithTtl<Arc<OptionsTemplate>>>,
LazyLruCache<TemplateId, TemplateWithTtl<Arc<OptionsTemplate>>>,
pub(crate) v9_options_templates:
LruCache<TemplateId, TemplateWithTtl<Arc<V9OptionsTemplate>>>,
LazyLruCache<TemplateId, TemplateWithTtl<Arc<V9OptionsTemplate>>>,
pub(crate) ttl_config: Option<TtlConfig>,
pub(crate) max_template_cache_size: usize,
pub(crate) max_field_count: usize,
Expand Down
164 changes: 164 additions & 0 deletions src/variable_versions/lazy_lru.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
use lru::LruCache;
use std::borrow::Borrow;
use std::fmt;
use std::hash::Hash;
use std::num::NonZeroUsize;

/// An LRU cache that allocates its backing storage on first insertion.
///
/// `lru::LruCache::new(capacity)` reserves the full configured capacity.
/// Template parsers own several independent caches, most of which remain unused
/// for a given exporter. This wrapper retains the same capacity and eviction
/// semantics without reserving memory for empty caches.
pub(crate) struct LazyLruCache<K: Hash + Eq, V> {
capacity: NonZeroUsize,
cache: Option<LruCache<K, V>>,
}

impl<K: Hash + Eq, V> fmt::Debug for LazyLruCache<K, V> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
// Preserve the parser's existing debug representation.
formatter
.debug_struct("LruCache")
.field("len", &self.len())
.field("cap", &self.capacity)
.finish()
}
}

impl<K: Hash + Eq, V> LazyLruCache<K, V> {
pub(crate) fn new(capacity: NonZeroUsize) -> Self {
Self {
capacity,
cache: None,
}
}

fn cache_mut(&mut self) -> &mut LruCache<K, V> {
let capacity = self.capacity;
self.cache.get_or_insert_with(|| {
// `unbounded()` starts with an empty hash map. Resizing it before
// the first insertion sets the eviction limit without reserving
// storage for every possible entry.
let mut cache = LruCache::unbounded();
cache.resize(capacity);
cache
})
}

fn release_if_empty(&mut self) {
if self.cache.as_ref().is_some_and(LruCache::is_empty) {
self.cache = None;
}
}

pub(crate) fn push(&mut self, key: K, value: V) -> Option<(K, V)> {
self.cache_mut().push(key, value)
}

#[cfg(test)]
pub(crate) fn put(&mut self, key: K, value: V) -> Option<V> {
self.cache_mut().put(key, value)
}

pub(crate) fn peek<Q>(&self, key: &Q) -> Option<&V>
where
K: Borrow<Q>,
Q: Hash + Eq + ?Sized,
{
self.cache.as_ref()?.peek(key)
}

pub(crate) fn pop<Q>(&mut self, key: &Q) -> Option<V>
where
K: Borrow<Q>,
Q: Hash + Eq + ?Sized,
{
let value = self.cache.as_mut()?.pop(key);
self.release_if_empty();
value
}

pub(crate) fn promote<Q>(&mut self, key: &Q) -> bool
where
K: Borrow<Q>,
Q: Hash + Eq + ?Sized,
{
self.cache.as_mut().is_some_and(|cache| cache.promote(key))
}

pub(crate) fn resize(&mut self, capacity: NonZeroUsize) {
self.capacity = capacity;
if let Some(cache) = self.cache.as_mut() {
cache.resize(capacity);
}
self.release_if_empty();
}

pub(crate) fn clear(&mut self) {
self.cache = None;
}

pub(crate) fn len(&self) -> usize {
self.cache.as_ref().map_or(0, LruCache::len)
}

#[cfg(test)]
pub(crate) fn cap(&self) -> NonZeroUsize {
self.capacity
}

pub(crate) fn iter(&self) -> impl Iterator<Item = (&K, &V)> {
self.cache.iter().flat_map(|cache| cache.iter())
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn reads_and_resizes_do_not_allocate_an_empty_cache() {
let mut cache = LazyLruCache::<u16, u16>::new(NonZeroUsize::new(10).unwrap());

assert!(cache.cache.is_none());
assert_eq!(cache.cap().get(), 10);
assert_eq!(cache.len(), 0);
assert_eq!(cache.peek(&1), None);
assert!(!cache.promote(&1));
assert_eq!(cache.pop(&1), None);
assert_eq!(cache.iter().count(), 0);

cache.resize(NonZeroUsize::new(20).unwrap());
assert!(cache.cache.is_none());
assert_eq!(cache.cap().get(), 20);
}

#[test]
fn first_insert_allocates_and_preserves_lru_eviction() {
let mut cache = LazyLruCache::new(NonZeroUsize::new(2).unwrap());

assert_eq!(cache.push(1, "one"), None);
assert!(cache.cache.is_some());
assert_eq!(cache.push(2, "two"), None);
assert!(cache.promote(&1));
assert_eq!(cache.push(3, "three"), Some((2, "two")));
assert_eq!(cache.peek(&1), Some(&"one"));
assert_eq!(cache.peek(&2), None);
assert_eq!(cache.peek(&3), Some(&"three"));
}

#[test]
fn removing_the_last_entry_releases_storage() {
let mut cache = LazyLruCache::new(NonZeroUsize::new(2).unwrap());
cache.put(1, "one");

assert_eq!(cache.pop(&1), Some("one"));
assert!(cache.cache.is_none());

cache.put(2, "two");
cache.clear();
assert!(cache.cache.is_none());
assert_eq!(cache.cap().get(), 2);
}
}
5 changes: 3 additions & 2 deletions src/variable_versions/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ pub(crate) mod fast_parse;
pub mod field_types;
pub mod field_value;
pub mod ipfix;
pub(crate) mod lazy_lru;
pub mod metrics;
pub(crate) mod pending_flows;
pub mod template_events;
Expand Down Expand Up @@ -144,7 +145,7 @@ pub(crate) fn calculate_padding(body_size: usize) -> &'static [u8] {
/// Returns None if the template doesn't exist or has expired.
#[inline]
pub(crate) fn get_valid_template<T: Clone>(
cache: &mut lru::LruCache<TemplateId, ttl::TemplateWithTtl<std::sync::Arc<T>>>,
cache: &mut lazy_lru::LazyLruCache<TemplateId, ttl::TemplateWithTtl<std::sync::Arc<T>>>,
id: &TemplateId,
ttl_config: &Option<TtlConfig>,
metrics: &mut CacheMetricsInner,
Expand All @@ -170,7 +171,7 @@ pub(crate) fn get_valid_template<T: Clone>(
/// Used in replay paths where the parse may fail — promotion should only
/// happen on a successful replay (callers promote manually on success).
pub(crate) fn peek_valid_template<T: Clone>(
cache: &mut lru::LruCache<TemplateId, ttl::TemplateWithTtl<std::sync::Arc<T>>>,
cache: &mut lazy_lru::LazyLruCache<TemplateId, ttl::TemplateWithTtl<std::sync::Arc<T>>>,
id: &TemplateId,
ttl_config: &Option<TtlConfig>,
metrics: &mut CacheMetricsInner,
Expand Down
13 changes: 7 additions & 6 deletions src/variable_versions/v9/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ use crate::template_store::{
use crate::variable_versions::config::DEFAULT_MAX_RECORDS_PER_FLOWSET;
use crate::variable_versions::enterprise_registry::EnterpriseFieldRegistry;
use crate::variable_versions::field_value::FieldValue;
use crate::variable_versions::lazy_lru::LazyLruCache;
use crate::variable_versions::metrics::CacheMetricsInner;
use crate::variable_versions::template_events::TemplateProtocol;
use crate::variable_versions::ttl::{TemplateWithTtl, TtlConfig};
Expand All @@ -28,7 +29,6 @@ use crate::variable_versions::{
};
use crate::{NetflowError, NetflowPacket, ParsedNetflow};

use lru::LruCache;
use nom::IResult;
use nom::bytes::complete::take;
use nom::error::{Error as NomError, ErrorKind};
Expand All @@ -39,8 +39,9 @@ use std::sync::Arc;
/// Stateful NetFlow V9 parser with LRU template caching and optional pending flow support.
#[derive(Debug)]
pub struct V9Parser {
pub(crate) templates: LruCache<TemplateId, TemplateWithTtl<Arc<Template>>>,
pub(crate) options_templates: LruCache<TemplateId, TemplateWithTtl<Arc<OptionsTemplate>>>,
pub(crate) templates: LazyLruCache<TemplateId, TemplateWithTtl<Arc<Template>>>,
pub(crate) options_templates:
LazyLruCache<TemplateId, TemplateWithTtl<Arc<OptionsTemplate>>>,
pub(crate) ttl_config: Option<TtlConfig>,
pub(crate) max_template_cache_size: usize,
pub(crate) max_field_count: usize,
Expand Down Expand Up @@ -108,8 +109,8 @@ impl V9Parser {
.transpose()?;

Ok(Self {
templates: LruCache::new(cache_size),
options_templates: LruCache::new(cache_size),
templates: LazyLruCache::new(cache_size),
options_templates: LazyLruCache::new(cache_size),
ttl_config: config.ttl_config,
max_template_cache_size: config.max_template_cache_size,
max_field_count: config.max_field_count,
Expand Down Expand Up @@ -191,7 +192,7 @@ impl V9Parser {
/// borrows at the call site without re-borrowing the whole parser.
#[allow(clippy::too_many_arguments)]
fn install_restored_template<T>(
cache: &mut LruCache<TemplateId, TemplateWithTtl<Arc<T>>>,
cache: &mut LazyLruCache<TemplateId, TemplateWithTtl<Arc<T>>>,
template_id: u16,
arc: &Arc<T>,
ttl_enabled: bool,
Expand Down
Loading
Loading