Skip to content

Proxy floods panics and kills client sessions: foyer-memory 0.22.3 LRU high-priority pool unwrap on None #73

Description

@xbmc4lyfe

Summary

Under sustained client traffic the proxy starts emitting a continuous flood of identical panics (tens per second) originating in the foyer-memory dependency's in-memory LRU eviction. The process stays "active" (panics are caught at the tokio task boundary), but every client whose request triggers a cache insert into the affected shard has its session killed, producing widespread NNTP failures. The condition does not self-heal and persists until the service is restarted.

Panic

thread 'tokio-rt-worker' panicked at
  foyer-memory-0.22.3/src/eviction/lru.rs:89:62:
called `Option::unwrap()` on a `None` value

ERROR nntp_proxy::runtime: Client session task failed:
  JoinError::Panic(Id(...), "called `Option::unwrap()` on a `None` value", ...)

Observed: ~6,700 panics in a 2-minute window (~55/s), sustained for the life of the process.

Root cause

foyer-memory 0.22.3, src/eviction/lru.rs, Lru::may_overflow_high_priority_pool:

fn may_overflow_high_priority_pool(&mut self) {
    while self.high_priority_weight > self.high_priority_weight_capacity {
        strict_assert!(!self.high_priority_list.is_empty());      // compiled out in release
        let record = self.high_priority_list.pop_front().unwrap(); // line 89 -> panics
        ...

foyer tracks high_priority_weight (a running weight counter) separately from high_priority_list (the actual entries). The loop condition reports excess weight to evict while the list is already empty, so pop_front() returns None and .unwrap() panics. The counter and the list have drifted out of sync — a foyer-internal invariant violation. The strict_assert! guarding this is a no-op in release builds, so it falls through to the panicking unwrap() instead of a clean assertion.

This is an upstream foyer-memory bug, but it is reachable through this project's hybrid-cache configuration, which enables the high-priority pool with a custom weighter.

How this project triggers it

src/cache/hybrid.rs:

let mut builder = HybridCacheBuilder::new()
    .with_name(HYBRID_CACHE_NAME)
    .with_policy(HybridCachePolicy::WriteOnInsertion)
    .memory(memory_capacity_usize)
    .with_shards(config.shards)
    .with_eviction_config(LruConfig {
        high_priority_pool_ratio: 0.1,          // enables the buggy code path
    })
    .with_weighter(|_key: &String, value: &DiskCachedArticle| value.payload_len().get())
    ...

The in-place update path (re-inserting an existing key with a potentially different weight, hybrid.rs:387) is the most likely source of the weight/list desync, though I have not yet isolated a minimal reproducer.

Environment

  • nntp-proxy: running from main (foyer 0.22 -> foyer-memory 0.22.3 locked)
  • rustc 1.95.0, release profile (lto = "thin", codegen-units = 1, panic = "unwind")
  • panic = "unwind" is why the process survives each panic instead of aborting
  • Cache config in use: article_cache_capacity = "12gb", disk capacity = "500gb", shards = 8, ttl 4h; process RSS ~21 GB at time of incident
  • foyer-memory 0.22.3 is the latest published release — no upstream version bump available to pull in a fix

Impact

  • Continuous panic spam in the journal (log volume + noise)
  • Client NNTP sessions killed mid-request whenever they touch the poisoned shard
  • Requires manual systemctl restart to recover; recurs over time

Possible mitigations (for discussion)

  1. Disable the high-priority pool (high_priority_pool_ratio: 0.0) to avoid may_overflow_high_priority_pool entirely — needs verification that ratio 0 fully bypasses the path rather than making every entry overflow.
  2. Switch the memory tier to the moka backend (already supported in src/cache/mod.rs), sidestepping foyer's LRU.
  3. Report/track upstream in foyer and pin past a fixed release once available.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions