DefaultCache.keys() returns records.keySet() directly:
@Override
public Set<K> keys() {
return records.keySet();
}
records is a ConcurrentHashMap, so the returned value is a KeySetView backed by that map rather than a snapshot. Two consequences follow for any caller:
- The set is live. A set obtained before a
set or remove call reflects the change afterwards. A caller that holds the result and iterates it later does not see the cache as it stood when keys() was called.
- The set is mutable, and mutating it mutates the cache.
cache.keys().remove(someKey) and cache.keys().clear() evict entries from the cache itself, bypassing the Cache interface entirely. Nothing in the interface signals that keys() hands out write access to the cache's contents.
COMMANDS.md documents the method as "Returns the set of all keys currently in the cache", which reads as a snapshot and does not describe either behaviour.
Suggested resolution
Returning Set.copyOf(records.keySet()) or Collections.unmodifiableSet(records.keySet()) would close the write path. The former also makes the result a snapshot; the latter keeps it live but read-only. Either is a behaviour change for any downstream consumer relying on the current semantics, so it is worth deciding deliberately — ponder-cache is published to repo.dansplugins.com and consumed by other projects.
Origin
This was surfaced while characterization tests were being written for ponder-cache in #134. Under that cycle's test-expansion rules production code is not changed and current behaviour is not cemented by assertion, so the leak is neither fixed nor asserted there; it is filed here instead.
This issue body was drafted during a Gardener session (https://github.com/Stephenson-Software/gardener).
drafted by Claude on behalf of Daniel Stephenson
DefaultCache.keys()returnsrecords.keySet()directly:recordsis aConcurrentHashMap, so the returned value is aKeySetViewbacked by that map rather than a snapshot. Two consequences follow for any caller:setorremovecall reflects the change afterwards. A caller that holds the result and iterates it later does not see the cache as it stood whenkeys()was called.cache.keys().remove(someKey)andcache.keys().clear()evict entries from the cache itself, bypassing theCacheinterface entirely. Nothing in the interface signals thatkeys()hands out write access to the cache's contents.COMMANDS.mddocuments the method as "Returns the set of all keys currently in the cache", which reads as a snapshot and does not describe either behaviour.Suggested resolution
Returning
Set.copyOf(records.keySet())orCollections.unmodifiableSet(records.keySet())would close the write path. The former also makes the result a snapshot; the latter keeps it live but read-only. Either is a behaviour change for any downstream consumer relying on the current semantics, so it is worth deciding deliberately —ponder-cacheis published torepo.dansplugins.comand consumed by other projects.Origin
This was surfaced while characterization tests were being written for
ponder-cachein #134. Under that cycle's test-expansion rules production code is not changed and current behaviour is not cemented by assertion, so the leak is neither fixed nor asserted there; it is filed here instead.This issue body was drafted during a Gardener session (https://github.com/Stephenson-Software/gardener).
drafted by Claude on behalf of Daniel Stephenson