PolicyEngine / PolicyEngine/policyengine.py

LRUCache.add silently keeps the old value for an existing key

Open
#487 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Python
Stars
7
Forks
9
Avg merge
15h 51m
Merged PRs (30d)
9

Description

Problem

LRUCache.add in policyengine/core/cache.py (observed in 4.22.0) reads like a setter but is not one:

def add(self, key: str, value: T) -> None:
    """Add item to cache with LRU eviction when full."""
    if key in self._cache:
        self._cache.move_to_end(key)
    else:
        self._cache[key] = value
        ...

For an existing key it only bumps recency — the value argument is silently discarded. Any caller using add to refresh an entry keeps the stale object with no error and no signal. The class also exposes no remove/replace, so there is no public way to update an entry at all.

How this bit us

In the sim API's baseline-artifact work (PolicyEngine/policyengine-sim-api#644), a Simulation subclass validates what ensure() loads and recomputes when the cached/loaded output is missing requested columns. After the recompute it called _cache.add(self.id, self) to replace the stale entry — a no-op, because the key already existed. Consequences in a warm worker:

  • the process-wide cache never converged: every later request needing the richer column set cache-hit the stale entry and re-ran the full model, indefinitely;
  • within a single request, a second ensure() re-hit the stale entry, clobbered the freshly computed output, and ran the full model a second time.

Related: Simulation.ensure()'s cache-hit path returns early without re-adding, so entries are only ever written by the compute/load path — combined with add's keep-old-value behavior, a cached simulation object is effectively immutable for the life of the process.

Workaround in use

Evicting through the private OrderedDict before re-adding:

_cache._cache.pop(self.id, None)
_cache.add(self.id, self)

Contained, but it reaches into private state and will break if the internal representation changes.

Suggested fix

Either:

  1. give add real setter semantics (update the stored value on existing keys as well as bumping recency) — likely what every caller already assumes; or
  2. keep add insert-only but add a public replace(key, value) (and/or remove(key)) and document that add ignores the value for existing keys.

Option 1 is the least-surprise fix; we could not find a caller that benefits from the current keep-old-value behavior.

🤖 Generated with Claude Code

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in policyengine/core/cache.py at LRUCache.add and trace the callers described in Simulation.ensure. Compare the proposed setter semantics with the insert-only alternative, then add coverage for an existing key; the work is done when refreshing a key no longer leaves the stale value and the intended public behavior is documented or tested.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
backend
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.