Haio 0.2.X
Homebrew Asset IO/CDN
Loading...
Searching...
No Matches
file.cpp
Go to the documentation of this file.
1#include <haio_cache.hpp>
2
3#include <boost/asio/use_awaitable.hpp>
4
5#include <algorithm>
6#include <fstream>
7#include <iterator>
8#include <system_error>
9#include <vector>
10
11namespace asio = boost::asio;
12
13namespace {
14
15using FileClock = std::filesystem::file_time_type::clock;
16
17/**
18 * one file per key, named by the md5, so nothing in a request ever reaches the file
19 * system as a path. writes land on a neighbouring ".tmp" and are renamed into place,
20 * because a reader must never see half an entry.
21 *
22 * the modification time is the expiry clock, and a hit touches it: an entry that
23 * keeps being asked for keeps living, and one nobody wants ages out. that also means
24 * the sweep can order by mtime and be evicting the least recently *used*.
25 */
26class FileStore final : public Haio::Cdn::CacheStore {
27public:
28 FileStore(std::filesystem::path root, std::chrono::seconds ttl, size_t maxUsage)
29 : root_(std::move(root)), ttl_(ttl), maxUsage_(maxUsage) {
30 std::error_code ec;
31 std::filesystem::create_directories(root_, ec);
32 held_ = measure();
33 }
34
35 asio::awaitable<std::optional<Haio::Cdn::CacheEntry>> get(const std::string& key) override {
36 const auto path = root_ / key;
37
38 std::error_code ec;
39 const auto written = std::filesystem::last_write_time(path, ec);
40 if (ec) co_return std::nullopt;
41
42 if (FileClock::now() - written >= ttl_) {
43 forget(path);
44 co_return std::nullopt;
45 }
46
47 std::ifstream in(path, std::ios::binary);
48 if (!in) co_return std::nullopt;
49
50 const std::vector<uint8_t> raw((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>());
51 auto entry = Haio::Cdn::decodeEntry(raw);
52 if (!entry) {
53 // a file that cannot be read back is worse than no file at all
54 forget(path);
55 co_return std::nullopt;
56 }
57
58 // renew, so the ttl measures time since the last read rather than the write
59 std::filesystem::last_write_time(path, FileClock::now(), ec);
60 co_return entry;
61 }
62
63 asio::awaitable<void> put(const std::string& key, const Haio::Cdn::CacheEntry& entry) override {
64 const auto raw = Haio::Cdn::encodeEntry(entry);
65 if (raw.size() > maxUsage_) co_return; // one entry that cannot fit never evicts the rest
66
67 const auto path = root_ / key;
68 const auto pending = root_ / (key + ".tmp");
69
70 std::error_code ec;
71 const auto replacing = std::filesystem::file_size(path, ec);
72 const auto previous = ec ? size_t{0} : static_cast<size_t>(replacing);
73
74 {
75 std::ofstream out(pending, std::ios::binary | std::ios::trunc);
76 if (!out) co_return;
77 out.write(reinterpret_cast<const char*>(raw.data()), static_cast<std::streamsize>(raw.size()));
78 if (!out) {
79 out.close();
80 std::filesystem::remove(pending, ec);
81 co_return;
82 }
83 }
84
85 std::filesystem::rename(pending, path, ec);
86 if (ec) {
87 std::filesystem::remove(pending, ec);
88 co_return;
89 }
90
91 held_ = held_ - previous + raw.size();
92 if (held_ > maxUsage_) sweep();
93 co_return;
94 }
95
96private:
97 /** the directory is the state, so its size is read from it rather than remembered */
98 size_t measure() const {
99 size_t total = 0;
100 std::error_code ec;
101 for (const auto& item : std::filesystem::directory_iterator(root_, ec)) {
102 std::error_code each;
103 if (const auto size = item.file_size(each); !each) total += static_cast<size_t>(size);
104 }
105 return total;
106 }
107
108 void forget(const std::filesystem::path& path) {
109 std::error_code ec;
110 const auto size = std::filesystem::file_size(path, ec);
111 if (!ec && !std::filesystem::remove(path, ec)) return;
112 if (!ec) held_ -= std::min(held_, static_cast<size_t>(size));
113 }
114
115 /**
116 * expired entries go first, and then the oldest until the budget is met. without
117 * this the directory only ever grows: an entry written once and never read again
118 * is never asked for, so nothing would ever notice it had expired.
119 */
120 void sweep() {
121 struct Aged {
122 std::filesystem::path file; // not "path": that reads like directory_entry::path()
123 std::filesystem::file_time_type written;
124 size_t size;
125 };
126
127 std::vector<Aged> kept;
128 std::error_code ec;
129 const auto now = FileClock::now();
130
131 for (const auto& item : std::filesystem::directory_iterator(root_, ec)) {
132 std::error_code each;
133 const auto written = item.last_write_time(each);
134 if (each) continue;
135 const auto size = item.file_size(each);
136 if (each) continue;
137
138 // a .tmp left behind by a crash has nobody coming back for it
139 if (item.path().extension() == ".tmp" || now - written >= ttl_) {
140 std::error_code drop;
141 std::filesystem::remove(item.path(), drop);
142 continue;
143 }
144 kept.push_back(Aged{item.path(), written, static_cast<size_t>(size)});
145 }
146
147 size_t total = 0;
148 for (const auto& item : kept) total += item.size;
149
150 if (total > maxUsage_) {
151 std::ranges::sort(kept, {}, &Aged::written); // least recently used first
152 for (const auto& item : kept) {
153 if (total <= maxUsage_) break;
154 std::error_code drop;
155 if (std::filesystem::remove(item.file, drop)) total -= item.size;
156 }
157 }
158 held_ = total;
159 }
160
161 std::filesystem::path root_;
162 std::chrono::seconds ttl_;
163 size_t maxUsage_;
164 size_t held_ = 0;
165};
166
167}
168
169namespace Haio::Cdn {
170
171std::unique_ptr<CacheStore> makeFileStore(std::filesystem::path root, std::chrono::seconds ttl, size_t maxUsage) {
172 return std::make_unique<FileStore>(std::move(root), ttl, maxUsage);
173}
174
175}
std::optional< CacheEntry > decodeEntry(std::span< const uint8_t > raw)
Definition key.cpp:58
std::vector< uint8_t > encodeEntry(const CacheEntry &entry)
Definition key.cpp:45
std::unique_ptr< CacheStore > makeFileStore(std::filesystem::path root, std::chrono::seconds ttl, size_t maxUsage)
Definition file.cpp:171