PolicyEngine / PolicyEngine/policyengine.py
LRUCache.add silently keeps the old value for an existing key
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:
- give
addreal setter semantics (update the stored value on existing keys as well as bumping recency) — likely what every caller already assumes; or - keep
addinsert-only but add a publicreplace(key, value)(and/orremove(key)) and document thataddignores 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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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