Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cache with time-to-keep expiry #191

Open
wants to merge 7 commits into
base: master
Choose a base branch
from

Conversation

mrflip
Copy link
Contributor

@mrflip mrflip commented Aug 8, 2022

This adds time-to-keep expiration to the LRUCacheWithDelete.

Besides the other ledgers of the backing cache, on every write operation the update time is saved in a fixed-sized array. No other speed or memory overhead is required for regular operations: most importantly, that means there is no age verification on read. Customer must periodically call the expire method, which evicts items older than the time-to-keep.

Our use case is a 15-minute time-to-keep on caches of 64k capacity, where we can tolerate a modest slop in expiration and small periodic expiry sweeps. The benchmark script demonstrates a 1M-element cache with a 10s ttk being expired every 2 seconds, spending 50-60ms on each expiry sweep. I don't know what use case would need those extremes but there you go.

Limitations:

  • The time-to-live (maximum age of any record returned in a read operation) has only the weak guarantee of (time-to-keep + maximum-delay-between-expires).

  • The expire operation must be done as a whole, and this does not offer to do it in a separate thread or make any attempt to be thread safe. However, the benchmarks in benchmark/lru-cache show that a full delete of every item in a 30,000 element cache runs in less than 10ms on a 2019 Macbook Pro.

  • Having two expire operations scheduled in the same thread should be harmless but would give no speedup, so if you found yourself in a situation where the expire was taking significant time it could be big trouble.

  • Due to floating-point shenanigans a custom clock returning fractional times may behave unexpectedly

Alternatives considered and discarded:

  • Rather than walking the full length of the age ledger (or linked list), keep a data structure (heap?) to reveal only the expirable records. This could shorten expiry times in general but I'd worry that if there was a large batch to expire the time spent grooming the heap could swamp the typical-case savings.
  • Maintain two or more generations of cached items, rotating then at ttl/2. (write to both, read from the new generation falling back to the old generation, and on each expire discard the oldest generation and add a new empty cache). This would make expiry O(1), with low impact on individual operations but a notable tradeoff in cache efficiency.
  • Checking the age on read would significantly impact the read performance.

Potential Opportunities for improvement:

  • Reduce the memory footprint of the age ledger by chunking the timestamps to bytes or words. In the case of byte (256 age bins), a 10-minute ttk, and otherwise default parameters, an expire operation would discard everything older than (ttk - ttk/64) (guaranteeing the ttk but reaping an additional 1.5% of records). Expire must be called at least once every 2 * ttk (20 minutes) or the newest records would be indistinguishable from the oldest records. (Everything would work but it would be a damn shame for cache efficiency). A word-sized (64k bins) ledger makes the tradeoffs minimal, but offering both choices shouldn't be a problem.

  • For every record it expires we independently call delete, which doctors the read-age linked list. It may make more sense to walk the linked list. I stubbed that out but would value guidance on how to do surgery on the list as it is traversed.

The commit history here is a hot mess that includes and crosses with the other PR on profiling. I'll reorganize with squashed commits once I button things up.

mrflip added 6 commits August 8, 2022 18:14
* LRUCache and family .inspect limits its output -- showing the youngest items, and ellipsis, and the oldest item. Options allow dumping the raw object or controlling the size of output (and the number of items a console.log will mindlessly iterate over).
* LRUCache and family all have inspect wired up to the magic 'nodejs.util.inspect.custom' symbol property that drives console.log output
* LRUCache and family all have a summaryString method returning eg 'LRUCache[8/200]' for a cache with size 8 and capacity 200, wired to the magic Symbol.toStringTag property that drives string interpolation (partially addresses Yomguithereal#129).
@mrflip mrflip marked this pull request as ready for review August 9, 2022 04:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant