skip to content

In a cache whose keys are held weakly so entries drop out by themselves, why must a stored value never reference its own key?

level: middleimportance: should knowfreq 45%

answer

  1. weak key, strong value
  2. the table roots the value
  3. value to key is a live path
  4. entry pins the key it is filed under
  5. copy the fields, not the object

basics

~20 s

The table holds its values strongly, so a value that points back at its own key keeps that key strongly reachable. The weak key is then never cleared, the entry never leaves, and the cache grows without bound.

solid answer

~50 s

A weakly keyed table clears an entry when its **key** stops being strongly reachable from anywhere else. The table itself holds each **value** strongly — otherwise entries would vanish while still in use. So if a value stores a strong reference to the key it is filed under, there is a live path `table -> value -> key`, the key never becomes weakly reachable, and the entry survives for the life of the table. The structure that was supposed to shrink by itself now behaves exactly like an ordinary unbounded map, and the retained key drags its whole object graph along with it. The fixes are to copy out of the key only the plain data the value needs, or to have the value hold the key weakly and treat an empty read as a miss.

code

pseudocode · 12 lines
pseudocode
cache = weakly_keyed_table()        // keys weak, values strong

// the pinning form: the value stores its own key
entry = new record(source: handle, bytes_read: 0)
cache.put(handle, entry)

// what the collector sees once the request path drops `handle`:
//   roots -> cache -> entry -> entry.source == handle   (all strong)
// the weak key is therefore never cleared, and the entry stays for ever

// the safe form: keep only the plain data the value needs
cache.put(handle, new record(doc_id: handle.id, bytes_read: 0))

go deeper

for a junior

Recall the asymmetry: in such a table the key is held weakly and the value strongly, so anything a value points at stays alive, including the key it was filed under.

for a middle

Trace the reachability path from the table through the entry to the key and explain why every link being strong means the weak key is never cleared.

for a senior

Recognise this in a heap snapshot: entries that never leave, each retaining a key that drags a buffer or connection behind it, on a structure documented as self-clearing.

for a principal

Treat the no-path-back-to-the-key rule as an invariant nothing enforces, and decide whether a self-clearing table is worth an unchecked rule compared with an explicit bounded cache.

## What a weakly keyed table actually promises A weakly keyed table is a lookup structure whose **keys** are held through weak references. Its promise is narrow and worth stating precisely: *an entry is eligible for removal once its key object is no longer strongly reachable from anywhere outside the table.* When the collector clears the weak key, the table drops the entry, and the value becomes collectable too. The design is for **metadata attached to objects someone else owns**. In a content-delivery node holding open document handles, the handles are owned by the request path; a table that maps each open handle to derived metadata — parsed headers, a computed digest, a byte offset — should hold exactly as long as the handle does, and no longer. The weak key expresses that without anyone having to remember to delete. ## Why the values cannot also be weak The values must be held strongly, or the cache would be useless: a value that nothing else refers to — which is the normal case for derived metadata — would be reclaimed almost immediately, and every lookup would miss. So the asymmetry is deliberate: - **key**: weak, because someone else owns its lifetime; - **value**: strong, because the table is the owner. That asymmetry is the whole hazard. The table is a root-reachable structure, so everything strongly reachable *from a value* is strongly reachable, full stop. ## The pinning path Suppose the value stores the handle it was computed from, for convenience. The reachability walk then finds: `roots -> table -> entry -> value -> key` Every link on that path is strong. The key is therefore strongly reachable, the weak reference to it is never cleared, and the entry never leaves. The failure is silent: functionally the cache is correct, every lookup hits, nothing throws. It shows up weeks later as a live set that climbs for as long as the process runs. Worse, the retained key is rarely small. A document handle typically pins a buffer, a connection, or a parsed structure behind it, so each pinned entry retains far more than its own size. ## The three fixes, in order of preference 1. **Store only plain data.** Copy the identifier, offset or name the value actually needs into the value, and let the key object itself stay out of the entry. This is almost always the right answer: the value needed a few fields, not the object. 2. **Hold the key weakly from the value too.** If the value genuinely needs the whole key object when it has one, a weak read that comes back empty is a legitimate miss. This keeps the entry collectable, at the cost of a nullable path through the value. 3. **Key by a separate identity object** that nobody else retains, and file the real subject as data. This turns the problem around but is easy to get wrong, because now nothing outside the table keeps the key alive and entries vanish at once. ## Identity versus content for the key A second, subtler trap: weakly keyed tables want **identity** comparison for keys. If keys are compared by content, entry lifetime still follows the one key *object* the entry was filed under, not any equal one. A lookup built from a freshly constructed but equal key can succeed today and miss tomorrow, because the original key object died even though the program still has an equal value in hand. The lifetime rule and the lookup rule then disagree, and the cache behaves non-deterministically under load. ## What this kind of cache still does not give you - **No size bound.** Nothing limits the number of entries; the bound is 'however many keys are currently alive', which under a load spike is exactly when it is largest. - **No timeliness.** Entries leave when a collection notices, not when the key is dropped. On a heap with plenty of headroom, they may not leave for a long time. - **No cost awareness.** Eviction is driven by someone else's reference graph, not by hit rate, entry size, or how expensive the value is to rebuild. - **No protection from a leak elsewhere.** If some other structure retains the keys, the table retains everything with it and the weakness buys nothing. The honest summary is that a weakly keyed table solves exactly one problem — *how do I stop attached metadata outliving the thing it is attached to* — and creates a new invariant you must maintain: **nothing reachable from a value may reach its own key**. That invariant is not checked by any compiler, so it belongs in a comment on the table and in review of anything added to a value.

  • How do you keep the self-shrinking behaviour when the value genuinely needs the whole key object?
    Have the value hold the key weakly rather than strongly, and treat an empty read as a cache miss that recomputes or discards. The entry stays collectable because no strong path runs from the table to the key, and the cost is one extra absence check on a path that could already miss.
  • What goes wrong if keys in such a table are compared by content instead of identity?
    Entry lifetime follows the single key object the entry was filed under, while lookups match any equal key. A lookup with an equal but distinct key can hit today and miss tomorrow, because the original key died. Lifetime and lookup then disagree, which reads as a flaky cache rather than a bug.
  • Does using weak keys mean the cache no longer needs a size limit?
    No. The number of entries tracks how many keys are alive right now, which peaks under exactly the load you were worried about. Weak keys stop metadata outliving its subject; they say nothing about how much is alive at once, so a service with a footprint budget still needs a stated bound.

saying these in an interview costs you the question

  • Thinks weak keys make any cache automatically leak-proof
  • Believes the values in such a table are held weakly too
  • Stores the key inside the value and still expects the entry to vanish
  • Assumes the entry disappears the moment the key is dropped
  • Uses content equality for keys and expects stable entry lifetimes
  • Treats the weak key as a replacement for a size limit