Haio 0.2.X
Homebrew Asset IO/CDN
Loading...
Searching...
No Matches
haio_cache.hpp
Go to the documentation of this file.
1#pragma once
2
3#include "haio.hpp"
4
5#include <boost/asio/any_io_executor.hpp>
6#include <boost/asio/awaitable.hpp>
7
8#include <chrono>
9#include <filesystem>
10#include <functional>
11#include <memory>
12#include <optional>
13#include <span>
14#include <string>
15
16namespace Haio::Cdn {
17
18/**
19 * what the cache holds is a finished response, not an image: the work being saved is
20 * the fetch and the whole conversion behind it, so replaying an entry costs a copy.
21 */
22struct CacheEntry {
23 std::vector<uint8_t> data;
24 std::string contentType;
25 std::string filename;
26};
27
29 std::chrono::seconds ttl{0}; // zero leaves the cache off
30 size_t maxUsage = 64u << 20; // bytes the store may hold, whichever store it is
31 std::string url; // empty keeps entries in this process
32};
33
34/**
35 * a backend is chosen at runtime from the scheme of the url, which is why this is a
36 * virtual interface rather than a concept the way the codecs do it: the answer is in
37 * a config file, not in the build.
38 */
40public:
41 virtual ~CacheStore() = default;
42 /**
43 * a hit renews the entry's ttl, so what is being asked for keeps living and what
44 * nobody wants ages out. the window is therefore since the last read, not since
45 * the write: a popular creative is never rebuilt on a clock.
46 */
47 virtual boost::asio::awaitable<std::optional<CacheEntry>> get(const std::string& key) = 0;
48 virtual boost::asio::awaitable<void> put(const std::string& key, const CacheEntry& entry) = 0;
49};
50
51/**
52 * the md5 of the bucket, the path and the query, in the order they were written.
53 *
54 * the query is not sorted, and must not be: it spells out a pipeline, so crop before
55 * resize is a different picture from resize before crop. sorting would collide two
56 * different requests onto one entry and serve the wrong image, which is a worse fault
57 * than keeping two entries for one answer. the method is left out so a HEAD finds
58 * what a GET stored.
59 */
60std::string cacheKey(std::string_view bucket, std::string_view path, std::string_view query);
61
62/** the framing the file and redis backends share; the memory one keeps the struct */
63std::vector<uint8_t> encodeEntry(const CacheEntry& entry);
64std::optional<CacheEntry> decodeEntry(std::span<const uint8_t> raw);
65
66std::unique_ptr<CacheStore> makeMemoryStore(size_t maxUsage, std::chrono::seconds ttl);
67std::unique_ptr<CacheStore> makeFileStore(std::filesystem::path root, std::chrono::seconds ttl, size_t maxUsage);
68std::unique_ptr<CacheStore> makeRedisStore(std::string url, std::chrono::seconds ttl, size_t maxUsage, boost::asio::any_io_executor executor);
69
70/**
71 * the store, plus the part that keeps one key from being produced many times at once.
72 *
73 * the two are independent on purpose: deduplication needs no store and stays on even
74 * when no cache is configured, because a burst on one key sending one upstream
75 * request per caller is an amplifier whether or not the answer is kept afterwards.
76 */
77class Cache {
78public:
79 using Producer = std::function<boost::asio::awaitable<Result<CacheEntry>>()>;
80
81 Cache() = default;
82 /**
83 * maxEntriesByIp is spelled in [security] rather than in [cache], because it is a
84 * limit on a caller and not on the store. it is passed rather than copied into
85 * CacheConfig so that the number lives in exactly one place.
86 */
87 Cache(CacheConfig config, size_t maxEntriesByIp, boost::asio::any_io_executor executor);
88
89 /** true when entries are kept; deduplication happens either way */
90 bool enabled() const;
91
92 /**
93 * returns the stored entry, or runs the producer once and stores what it made.
94 *
95 * client is the address the request came from, and it is only used to decide
96 * whether the result may be stored. reading is always free: the limit is there to
97 * stop one caller from filling the cache with keys nobody else will ask for, and
98 * evicting everyone else's entries on the way.
99 */
100 boost::asio::awaitable<Result<CacheEntry>> fetch(std::string key, std::string client, Producer produce);
101
102private:
103 struct Impl;
104 std::shared_ptr<Impl> impl_;
105};
106
107}
virtual ~CacheStore()=default
virtual boost::asio::awaitable< std::optional< CacheEntry > > get(const std::string &key)=0
virtual boost::asio::awaitable< void > put(const std::string &key, const CacheEntry &entry)=0
boost::asio::awaitable< Result< CacheEntry > > fetch(std::string key, std::string client, Producer produce)
Definition cache.cpp:59
std::function< boost::asio::awaitable< Result< CacheEntry > >()> Producer
bool enabled() const
Definition cache.cpp:55
std::optional< CacheEntry > decodeEntry(std::span< const uint8_t > raw)
Definition key.cpp:58
std::vector< uint8_t > data
std::string contentType
std::vector< uint8_t > encodeEntry(const CacheEntry &entry)
Definition key.cpp:45
std::unique_ptr< CacheStore > makeMemoryStore(size_t maxUsage, std::chrono::seconds ttl)
Definition memory.cpp:87
std::unique_ptr< CacheStore > makeRedisStore(std::string url, std::chrono::seconds ttl, size_t maxUsage, boost::asio::any_io_executor executor)
std::string cacheKey(std::string_view bucket, std::string_view path, std::string_view query)
Definition key.cpp:34
std::unique_ptr< CacheStore > makeFileStore(std::filesystem::path root, std::chrono::seconds ttl, size_t maxUsage)
Definition file.cpp:171
std::chrono::seconds ttl