3#include <boost/asio/as_tuple.hpp>
4#include <boost/asio/consign.hpp>
5#include <boost/asio/detached.hpp>
6#include <boost/asio/use_awaitable.hpp>
7#include <boost/redis/connection.hpp>
12#include <boost/redis/src.hpp>
13#include <boost/url/parse.hpp>
14#include <boost/url/url.hpp>
22namespace asio = boost::asio;
23namespace redis = boost::redis;
24namespace urls = boost::urls;
35class RedisStore final :
public Haio::Cdn::CacheStore {
37 RedisStore(
const std::string& url, std::chrono::seconds ttl,
size_t maxUsage, asio::any_io_executor executor)
38 : ttl_(ttl), maxUsage_(maxUsage), connection_(std::make_shared<redis::connection>(executor)) {
39 const auto parsed = urls::parse_uri(url);
40 if (!parsed)
throw std::runtime_error(
"redis cache url is not a url: " + url);
43 if (!parsed->host().empty()) config.addr.host = std::string(parsed->host());
44 if (parsed->has_port()) config.addr.port = std::string(parsed->port());
45 if (!parsed->userinfo().empty()) {
46 config.username = std::string(parsed->user());
47 config.password = std::string(parsed->password());
50 if (
const auto path = std::string(parsed->path()); path.size() > 1) {
51 config.database_index = std::stoi(path.substr(1));
53 config.use_ssl = parsed->scheme() ==
"rediss";
55 connection_->async_run(config, asio::consign(asio::detached, connection_));
58 ~RedisStore()
override { connection_->cancel(); }
60 asio::awaitable<std::optional<Haio::Cdn::CacheEntry>> get(
const std::string& key)
override {
62 redis::request request;
63 request.push(
"GET", prefixed(key));
64 request.push(
"EXPIRE", prefixed(key), std::to_string(ttl_.count()),
"XX");
65 request.push(
"ZADD", index(), std::to_string(stamp()), key);
67 redis::response<std::optional<std::string>, redis::ignore_t, redis::ignore_t> reply;
68 const auto [error, size] =
co_await connection_->async_exec(request, reply, asio::as_tuple(asio::use_awaitable));
70 warnOnce(error.message());
71 co_return std::nullopt;
74 const auto& value = std::get<0>(reply);
75 if (!value.has_value() || !value->has_value())
co_return std::nullopt;
77 const auto& raw = **value;
81 asio::awaitable<void> put(
const std::string& key,
const Haio::Cdn::CacheEntry& entry)
override {
84 redis::request request;
86 request.push(
"SET", prefixed(key), std::string_view(
reinterpret_cast<const char*
>(raw.data()), raw.size()),
87 "EX", std::to_string(ttl_.count()));
88 request.push(
"ZADD", index(), std::to_string(stamp()), key);
89 request.push(
"HSET", sizes(), key, std::to_string(raw.size()));
90 request.push(
"EXPIRE", index(), std::to_string(ttl_.count() * 2));
91 request.push(
"EXPIRE", sizes(), std::to_string(ttl_.count() * 2));
93 redis::response<redis::ignore_t, redis::ignore_t, redis::ignore_t,
94 redis::ignore_t, redis::ignore_t> reply;
95 const auto [error, size] =
co_await connection_->async_exec(request, reply, asio::as_tuple(asio::use_awaitable));
97 warnOnce(error.message());
101 if (++sinceTrim_ >= trimEvery) {
110 static std::string prefixed(
const std::string& key) {
return "haio:" + key; }
113 static std::string index() {
return "haio:index"; }
115 static long long stamp() {
116 return std::chrono::duration_cast<std::chrono::seconds>(
117 std::chrono::system_clock::now().time_since_epoch()).count();
121 static std::string sizes() {
return "haio:sizes"; }
136 asio::awaitable<void> trim() {
137 redis::request listing;
138 listing.push(
"ZRANGE", index(),
"0",
"-1");
140 redis::response<std::vector<std::string>> listed;
141 if (
const auto [error, size] =
co_await connection_->async_exec(listing, listed, asio::as_tuple(asio::use_awaitable)); error) {
142 warnOnce(error.message());
146 const auto& keys = std::get<0>(listed).value();
147 if (keys.empty())
co_return;
150 redis::request measuring;
151 for (
const auto& key : keys) measuring.push(
"EXISTS", prefixed(key));
153 redis::generic_response present;
154 if (
const auto [error, size] =
co_await connection_->async_exec(measuring, present, asio::as_tuple(asio::use_awaitable)); error) {
155 warnOnce(error.message());
159 redis::request weighing;
160 for (
const auto& key : keys) weighing.push(
"HGET", sizes(), key);
162 redis::generic_response weights;
163 if (
const auto [error, size] =
co_await connection_->async_exec(weighing, weights, asio::as_tuple(asio::use_awaitable)); error) {
164 warnOnce(error.message());
168 std::vector<size_t> cost(keys.size(), 0);
169 std::vector<bool> alive(keys.size(),
false);
170 for (
size_t at = 0; at < keys.size(); at++) {
171 if (at < present.value().size()) alive[at] = present.value()[at].value ==
"1";
172 if (at < weights.value().size()) {
173 const auto& text = weights.value()[at].value;
174 if (!text.empty()) cost[at] =
static_cast<size_t>(std::strtoull(text.c_str(),
nullptr, 10));
179 for (
size_t at = 0; at < keys.size(); at++) {
180 if (alive[at]) held += cost[at];
183 redis::request dropping;
184 bool dropped =
false;
187 for (
size_t at = 0; at < keys.size(); at++) {
188 if (alive[at])
continue;
189 dropping.push(
"ZREM", index(), keys[at]);
190 dropping.push(
"HDEL", sizes(), keys[at]);
195 for (
size_t at = 0; at < keys.size() && held > maxUsage_; at++) {
196 if (!alive[at])
continue;
197 dropping.push(
"DEL", prefixed(keys[at]));
198 dropping.push(
"ZREM", index(), keys[at]);
199 dropping.push(
"HDEL", sizes(), keys[at]);
200 held -= std::min(held, cost[at]);
204 if (!dropped)
co_return;
206 redis::generic_response ignored;
207 if (
const auto [error, size] =
co_await connection_->async_exec(dropping, ignored, asio::as_tuple(asio::use_awaitable)); error) {
208 warnOnce(error.message());
214 static constexpr int trimEvery = 64;
216 void warnOnce(
const std::string& reason) {
217 if (std::exchange(warned_,
true))
return;
218 std::cerr <<
"warning: redis cache unavailable, serving from upstream: " << reason <<
"\n";
221 std::chrono::seconds ttl_;
224 std::shared_ptr<redis::connection> connection_;
225 bool warned_ =
false;
232std::unique_ptr<CacheStore>
makeRedisStore(std::string url, std::chrono::seconds ttl,
size_t maxUsage, asio::any_io_executor executor) {
233 return std::make_unique<RedisStore>(url, ttl, maxUsage, std::move(executor));
std::optional< CacheEntry > decodeEntry(std::span< const uint8_t > raw)
std::vector< uint8_t > encodeEntry(const CacheEntry &entry)
std::unique_ptr< CacheStore > makeRedisStore(std::string url, std::chrono::seconds ttl, size_t maxUsage, boost::asio::any_io_executor executor)