A TTL cache makes a stored value eligible only until its recorded expiry time, independently of its eviction order.
Python TTL cache: expire at a defined boundary and use an injected clock
Operation contract
The cache stores at most four scalar receipt values. put validates its key, amount, TTL and clock before changing state. Expired entries are removed before capacity eviction; an overwrite receives a new expiry. get treats now equal to expiry as a miss and does not extend the TTL. The injected clock makes tests deterministic and allows the cache to reject clock reversal. A returned scalar has no mutable alias for the caller to modify.
Failure and ownership boundary
TTL is freshness policy, not durable state or authorization. No background thread removes idle expired records; they remain until the next operation, still bounded by capacity. Python LRU cache project: make misses, replacement and eviction explicit, Python lru_cache: bound retention and include the revision in the key and Python expiry exercise: separate pending, active and expired records at exact boundaries separate expiry from caching and retry decisions.
Working program
from collections import OrderedDict
class ReceiptTTL:
def __init__(self, clock):
self.clock, self.last = clock, -1
self.entries = OrderedDict()
def timestamp(self):
now = self.clock()
if type(now) is not int or not 0 <= now <= 1000000 or now < self.last:
raise ValueError("bounded nondecreasing clock required")
return now
def expire(self, now):
for key in [key for key, (_, until) in self.entries.items() if until <= now]:
del self.entries[key]
self.last = now
def put(self, key, amount, ttl):
if type(key) is not str or not key.isascii() or not key.isalnum() or not 1 <= len(key) <= 16 or type(amount) is not int or not 0 <= amount <= 1000000 or type(ttl) is not int or not 1 <= ttl <= 100:
raise ValueError("bounded receipt fields required")
now = self.timestamp()
self.expire(now)
self.entries[key] = (amount, now + ttl)
self.entries.move_to_end(key)
if len(self.entries) > 4:
self.entries.popitem(last=False)
def get(self, key):
if type(key) is not str or not key.isascii() or not key.isalnum() or not 1 <= len(key) <= 16:
raise ValueError("bounded key required")
now = self.timestamp()
self.expire(now)
record = self.entries.get(key)
return None if record is None else record[0]
ticks = [100]
cache = ReceiptTTL(lambda: ticks[0])
cache.put("R41", 125, 10)
ticks[0] = 109
print("before expiry:", cache.get("R41"))
ticks[0] = 110
print("at expiry:", cache.get("R41"))
cache.put("R41", 250, 5)
ticks[0] = 109
try:
cache.get("R41")
except ValueError:
print("clock reversal rejected")
print("retained:", list(cache.entries))Output
before expiry: 125
at expiry: None
clock reversal rejected
retained: ['R41']Costs and limits
Expiry scanning is O(C) per operation for capacity C, with O(C) storage. The accepted capacity is fixed at four, so this is intentionally simpler than a heap of deadlines. Clock callbacks must be owned, side-effect-controlled code; a lock is still required if multiple threads share this cache.
Common Mistakes
- Specify whether now equal to expiry is accepted.
- Do not confuse write-order eviction with access-order LRU.
Connected lessons
Python LRU cache project: make misses, replacement and eviction explicit, Python OrderedDict: explicit reordering differs from insertion order, Python expiry exercise: separate pending, active and expired records at exact boundaries.
