Introduction
The FileCache sits at the core of the bankopladerne backend with the following responsibilities:
- Maintain cache of files in the temporary file system while not using too much space
- Ensure often-accessed content is cached
- Ensure an item is only produced once in a high-traffic scenario where the same resource is requested concurrently
Rationale
The bankopladerne.dk site produces different kinds of mainly static content based on users needs.
Primarily the 12 000 PNGs on the free part of the site, each representing a bankoplade (A “bankoplade” is a “card” for playing the English version of Bingo.)
These PNGs are generated when requested and along with the images are also coordinates basically describing where on the PNG there are clickable numbers.
Why not just serve these as static content since they are static in nature?
Well currently there are 12 000 (this is configurable) and each is more than 80KiB so in total that would be more than 1GiB static content to maintain along with the coordinates.
Then there are the PDFs. For each page visited a user can request a PDF containing the same bankoplader (bingo cards) in different setups of 1, 2, 3, 4 or 6 cards per page, with one or 10 pages. The number of combinations are way above what is practical to serve statically.
So the PNGs and the PDFs are generated on the fly when requested, the FileCache helps maintain an LRU cache of files produced. Also HTTP response headers are set to help cache the files in downstream clients and proxies.
The FileCache component
The FileCache component was introduced to help solve the issues of simple caching of practically static content in the temporary file-system without using up too much space in the file-system.
It is configured with 3 parameters:
- Max number of files to cache (basically a limit to how much JAVA heap memory should be used)
- Minimum allowed free space in the file-system in percent (so we do not overflow the file-system with temporary files)
- Max number of concurrent requests expected
Then when a file is requested, the FileCache is called with an object-name and a reference to a file-producer. If the file exists in the cache and is still available in the file-system, a reference to it is returned. Otherwise the file-producer is called to produce the file directly into the temporary file-system, the reference is cached and returned.
From the test-setup:
@GetMapping(path = "{number}", produces = MediaType.IMAGE_PNG_VALUE)
public void getPng(@PathVariable("number") int number) {
final var produced = fileCache.produceAndCache(
"numbers-%d.png".formatted(number),
(objectName, tempFile) -> producePng(number, tempFile)
);
responses.streamFile(MediaType.IMAGE_PNG_VALUE, produced);
}
The object-name is similar to a filename. It must uniquely identify the file or resource requested. It is used as key in the internal registries for serializing access to the file and the producer.
SimplestCache
The FileCache internally uses a SimplestCache to maintain the in-memory cache of file-references and serializing access to the producer.
The SimplestCache is basically just a wrapper around a LinkedHashMap with a semaphore protecting concurrent access to the Map.
It also supports a “clean predicate” returning true if oldest element (LRU) is to be evicted even if the cache is not full and “cleaner” method that is invoked when the oldest element is evicted.
Test setup
In the github repository there is also a “test-setup” which is a simple spring-boot application serving PNGs containing requested numbers to mimic the PNG generating behavior of the bankopladerne backend.
Enjoy 🙂