simplify caching

This commit is contained in:
Doug Hoyte
2023-09-28 01:49:55 -04:00
parent 4de8a5675f
commit c0ea21a359
7 changed files with 31 additions and 135 deletions

View File

@ -34,18 +34,21 @@ struct HTTPResponse : NonCopyable {
std::string_view contentType = "text/html; charset=utf-8"; std::string_view contentType = "text/html; charset=utf-8";
std::string extraHeaders; std::string extraHeaders;
std::string body; std::string body;
bool noCompress = false;
std::string eTag() { std::string getETag() {
unsigned char hash[SHA256_DIGEST_LENGTH]; unsigned char hash[SHA256_DIGEST_LENGTH];
SHA256(reinterpret_cast<unsigned char*>(body.data()), body.size(), hash); SHA256(reinterpret_cast<unsigned char*>(body.data()), body.size(), hash);
return to_hex(std::string_view(reinterpret_cast<char*>(hash), SHA256_DIGEST_LENGTH/2)); return to_hex(std::string_view(reinterpret_cast<char*>(hash), SHA256_DIGEST_LENGTH/2));
} }
std::string encode(bool doCompress) { std::string encode(bool doCompress) {
std::string eTag = getETag();
std::string compressed; std::string compressed;
bool didCompress = false; bool didCompress = false;
if (doCompress) { if (!noCompress && doCompress) {
compressed.resize(body.size()); compressed.resize(body.size());
z_stream zs; z_stream zs;
@ -78,7 +81,9 @@ struct HTTPResponse : NonCopyable {
output += std::to_string(bodySize); output += std::to_string(bodySize);
output += "\r\nContent-Type: "; output += "\r\nContent-Type: ";
output += contentType; output += contentType;
output += "\r\n"; output += "\r\nETag: \"";
output += eTag;
output += "\"\r\n";
if (didCompress) output += "Content-Encoding: gzip\r\nVary: Accept-Encoding\r\n"; if (didCompress) output += "Content-Encoding: gzip\r\nVary: Accept-Encoding\r\n";
output += extraHeaders; output += extraHeaders;
output += "Connection: Keep-Alive\r\n\r\n"; output += "Connection: Keep-Alive\r\n\r\n";

View File

@ -132,7 +132,7 @@ TemplarResult renderCommunityEvents(lmdb::txn &txn, Decompressor &decomp, UserCa
HTTPResponse WebServer::generateReadResponse(lmdb::txn &txn, Decompressor &decomp, const HTTPRequest &req, uint64_t &cacheTime) { HTTPResponse WebServer::generateReadResponse(lmdb::txn &txn, Decompressor &decomp, const HTTPRequest &req) {
HTTPResponse httpResp; HTTPResponse httpResp;
auto startTime = hoytech::curr_time_us(); auto startTime = hoytech::curr_time_us();
@ -157,7 +157,7 @@ HTTPResponse WebServer::generateReadResponse(lmdb::txn &txn, Decompressor &decom
if (u.path.size() == 0 || u.path[0] == "algo") { if (u.path.size() == 0 || u.path[0] == "algo") {
communitySpec = lookupCommunitySpec(txn, decomp, userCache, cfg().web__homepageCommunity); communitySpec = lookupCommunitySpec(txn, decomp, userCache, cfg().web__homepageCommunity);
cacheTime = 30'000'000; httpResp.extraHeaders += "Cache-Control: max-age=600\r\n";
} }
if (u.path.size() == 0) { if (u.path.size() == 0) {
@ -280,6 +280,8 @@ HTTPResponse WebServer::generateReadResponse(lmdb::txn &txn, Decompressor &decom
} else if (u.path[0] == "post") { } else if (u.path[0] == "post") {
body = tmpl::newPost(nullptr); body = tmpl::newPost(nullptr);
} else if (u.path[0] == "static" && u.path.size() >= 2) { } else if (u.path[0] == "static" && u.path.size() >= 2) {
httpResp.extraHeaders += "Cache-Control: max-age=31536000\r\n";
if (u.path[1] == "oddbean.js") { if (u.path[1] == "oddbean.js") {
rawBody = std::string(oddbeanStatic__oddbean_js()); rawBody = std::string(oddbeanStatic__oddbean_js());
contentType = "application/javascript"; contentType = "application/javascript";
@ -293,6 +295,8 @@ HTTPResponse WebServer::generateReadResponse(lmdb::txn &txn, Decompressor &decom
} else if (u.path[0] == "favicon.ico") { } else if (u.path[0] == "favicon.ico") {
rawBody = std::string(oddbeanStatic__favicon_ico()); rawBody = std::string(oddbeanStatic__favicon_ico());
contentType = "image/x-icon"; contentType = "image/x-icon";
httpResp.extraHeaders += "Cache-Control: max-age=2592000\r\n";
httpResp.noCompress = true;
} else if (u.path[0] == "login") { } else if (u.path[0] == "login") {
body = tmpl::login(0); body = tmpl::login(0);
} else if (u.path[0] == "about") { } else if (u.path[0] == "about") {
@ -312,11 +316,17 @@ HTTPResponse WebServer::generateReadResponse(lmdb::txn &txn, Decompressor &decom
const std::optional<CommunitySpec> &communitySpec; const std::optional<CommunitySpec> &communitySpec;
std::string_view title; std::string_view title;
std::string staticFilesPrefix; std::string staticFilesPrefix;
std::string_view staticOddbeanCssHash;
std::string_view staticOddbeanJsHash;
std::string_view staticOddbeanSvgHash;
} ctx = { } ctx = {
*body, *body,
communitySpec, communitySpec,
title, title,
cfg().web__staticFilesPrefix.size() ? cfg().web__staticFilesPrefix : "/static", cfg().web__staticFilesPrefix.size() ? cfg().web__staticFilesPrefix : "/static",
oddbeanStatic__oddbean_css__hash().substr(0, 16),
oddbeanStatic__oddbean_js__hash().substr(0, 16),
oddbeanStatic__oddbean_svg__hash().substr(0, 16),
}; };
responseData = std::move(tmpl::main(ctx).str); responseData = std::move(tmpl::main(ctx).str);
@ -337,107 +347,6 @@ HTTPResponse WebServer::generateReadResponse(lmdb::txn &txn, Decompressor &decom
} }
void WebServer::handleReadRequest(lmdb::txn &txn, Decompressor &decomp, uint64_t lockedThreadId, HTTPRequest &req) {
auto now = hoytech::curr_time_us();
std::string response;
bool cacheItemFound = false, preGenerate = false;
{
CacheItem *item = nullptr;
{
std::lock_guard<std::mutex> guard(cacheLock);
auto it = cache.find(req.url);
if (it != cache.end()) item = it->second.get();
}
bool addedToPending = false;
if (item) {
cacheItemFound = true;
std::lock_guard<std::mutex> guard(item->lock);
if (now < item->expiry) {
response = req.acceptGzip ? item->payloadGzip : item->payload;
if (now > item->softExpiry && !item->generationInProgress) {
preGenerate = true;
item->generationInProgress = true;
LI << "DOING PREGEN";
}
} else {
if (item->generationInProgress) {
item->pendingRequests.emplace_back(std::move(req));
addedToPending = true;
}
item->generationInProgress = true;
}
}
if (addedToPending) {
unlockThread(lockedThreadId);
return;
}
}
if (response.size()) {
if (preGenerate) {
sendHttpResponseAndUnlock(MAX_U64, req, response);
} else {
sendHttpResponseAndUnlock(lockedThreadId, req, response);
return;
}
}
uint64_t cacheTime = 0;
// FIXME: try/catch
auto resp = generateReadResponse(txn, decomp, req, cacheTime);
if (cacheTime == 0 && !cacheItemFound) {
std::string payload = resp.encode(req.acceptGzip);
sendHttpResponseAndUnlock(lockedThreadId, req, payload);
return;
}
std::string payload = resp.encode(false);
std::string payloadGzip = resp.encode(true);
now = hoytech::curr_time_us();
std::vector<HTTPRequest> pendingRequests;
{
CacheItem *item = nullptr;
{
std::lock_guard<std::mutex> guard(cacheLock);
item = cache.emplace(req.url, std::make_unique<CacheItem>()).first->second.get();
}
{
std::lock_guard<std::mutex> guard(item->lock);
item->expiry = now + cacheTime;
item->softExpiry = now + (cacheTime/2);
item->payload = payload;
item->payloadGzip = payloadGzip;
item->generationInProgress = false;
std::swap(item->pendingRequests, pendingRequests);
}
}
for (const auto &r : pendingRequests) {
std::string myPayload = r.acceptGzip ? payloadGzip : payload;
sendHttpResponseAndUnlock(MAX_U64, r, myPayload);
}
if (preGenerate) unlockThread(lockedThreadId);
else sendHttpResponseAndUnlock(lockedThreadId, req, req.acceptGzip ? payloadGzip : payload);
}
void WebServer::runReader(ThreadPool<MsgWebReader>::Thread &thr) { void WebServer::runReader(ThreadPool<MsgWebReader>::Thread &thr) {
Decompressor decomp; Decompressor decomp;
@ -449,7 +358,9 @@ void WebServer::runReader(ThreadPool<MsgWebReader>::Thread &thr) {
for (auto &newMsg : newMsgs) { for (auto &newMsg : newMsgs) {
if (auto msg = std::get_if<MsgWebReader::Request>(&newMsg.msg)) { if (auto msg = std::get_if<MsgWebReader::Request>(&newMsg.msg)) {
try { try {
handleReadRequest(txn, decomp, msg->lockedThreadId, msg->req); HTTPResponse resp = generateReadResponse(txn, decomp, msg->req);
std::string payload = resp.encode(msg->req.acceptGzip);
sendHttpResponseAndUnlock(msg->lockedThreadId, msg->req, payload);
} catch (std::exception &e) { } catch (std::exception &e) {
HTTPResponse res; HTTPResponse res;
res.code = "500 Server Error"; res.code = "500 Server Error";

View File

@ -77,26 +77,6 @@ struct WebServer {
std::unique_ptr<uS::Async> hubTrigger; std::unique_ptr<uS::Async> hubTrigger;
// HTTP response cache
struct CacheItem {
std::mutex lock;
uint64_t expiry;
uint64_t softExpiry;
std::string payload;
std::string payloadGzip;
std::string eTag;
bool generationInProgress = false;
std::vector<HTTPRequest> pendingRequests;
};
std::mutex cacheLock;
flat_hash_map<std::string, std::unique_ptr<CacheItem>> cache;
// Thread Pools // Thread Pools
ThreadPool<MsgHttpsocket> tpHttpsocket; ThreadPool<MsgHttpsocket> tpHttpsocket;
@ -110,7 +90,7 @@ struct WebServer {
void runReader(ThreadPool<MsgWebReader>::Thread &thr); void runReader(ThreadPool<MsgWebReader>::Thread &thr);
void handleReadRequest(lmdb::txn &txn, Decompressor &decomp, uint64_t lockedThreadId, HTTPRequest &req); void handleReadRequest(lmdb::txn &txn, Decompressor &decomp, uint64_t lockedThreadId, HTTPRequest &req);
HTTPResponse generateReadResponse(lmdb::txn &txn, Decompressor &decomp, const HTTPRequest &req, uint64_t &cacheTime); HTTPResponse generateReadResponse(lmdb::txn &txn, Decompressor &decomp, const HTTPRequest &req);
void runWriter(ThreadPool<MsgWebWriter>::Thread &thr); void runWriter(ThreadPool<MsgWebWriter>::Thread &thr);

View File

@ -15,7 +15,7 @@ void WebServer::run() {
// FIXME: cfg().web__numThreads__* // FIXME: cfg().web__numThreads__*
tpReader.init("Reader", 3, [this](auto &thr){ tpReader.init("Reader", 10, [this](auto &thr){
runReader(thr); runReader(thr);
}); });

View File

@ -1,6 +1,6 @@
<div class="info-page"> <div class="info-page">
<p> <p>
Hi, thanks for stopping by! Oddbean is a discussion site built on <a href="https://github.com/nostr-protocol/nostr">the nostr protocol</a>. You may notice that our design is heavily inspired by Hacker News and Reddit. This is on purpose. I want Oddbean to be a comfortable and familiar experience, with fast loading pages and minimal distractions. Hi, thanks for stopping by! Oddbean is a discussion site built on <a href="https://github.com/nostr-protocol/nostr">the nostr protocol</a>. You may notice that the design is heavily inspired by Hacker News and Reddit. This is on purpose. I want Oddbean to be a comfortable and familiar experience, with fast loading pages and minimal distractions.
</p> </p>
<p> <p>
@ -8,7 +8,7 @@
</p> </p>
<p> <p>
<b>No crap.</b> I just want to build a great distraction-free discussion site. No elements bouncing around as the page loads. No cookie pop-ups. In fact, no cookies, period. No "sign-up to our newsletter" modals that appear half way down. Javascript is optional (only needed for posting). <b>No crap.</b> I just want to build a great distraction-free discussion site. No elements bouncing around as the page loads. No cookie pop-ups. In fact, no cookies, period. No "sign-up to our newsletter" modals that appear half way down. No dark patterns to boost engagement numbers. No tracking. Javascript is optional (only needed for posting).
</p> </p>
<p> <p>

View File

@ -12,7 +12,7 @@
(<a href="/e/$(ctx.ev->getNoteId())/raw.json">raw</a>) (<a href="/e/$(ctx.ev->getNoteId())/raw.json">raw</a>)
<> | <a href="/e/$(ctx.ev->getRootNoteId())">root</a> </> ?(ctx.ev->root.size()) <> | <a href="/e/$(ctx.ev->getRootNoteId())">root</a> </> ?(ctx.ev->root.size())
<> | <a href="/e/$(ctx.ev->getNoteId())">show full thread</a> </> ?(ctx.ev->root.empty() && !ctx.isFullThreadLoaded) <> | <a href="/e/$(ctx.ev->getNoteId())">full thread</a> </> ?(ctx.ev->root.empty() && !ctx.isFullThreadLoaded)
<> | <a href="/e/$(ctx.ev->getNoteId())/export.jsonl">export</a> </> ?(ctx.ev->root.empty() && ctx.isFullThreadLoaded) <> | <a href="/e/$(ctx.ev->getNoteId())/export.jsonl">export</a> </> ?(ctx.ev->root.empty() && ctx.isFullThreadLoaded)
<> | <a href="/e/$(ctx.ev->getParentNoteId())">parent</a> </> ?(ctx.ev->parent.size()) <> | <a href="/e/$(ctx.ev->getParentNoteId())">parent</a> </> ?(ctx.ev->parent.size())

View File

@ -2,8 +2,8 @@
<head> <head>
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="$(ctx.staticFilesPrefix)/oddbean.css" rel="stylesheet"> <link href="$(ctx.staticFilesPrefix)/oddbean.css?$(ctx.staticOddbeanCssHash)" rel="stylesheet">
<script defer src="$(ctx.staticFilesPrefix)/oddbean.js"></script> <script defer src="$(ctx.staticFilesPrefix)/oddbean.js?$(ctx.staticOddbeanJsHash)"></script>
<title>$(ctx.title)Oddbean</title> <title>$(ctx.title)Oddbean</title>
</head> </head>
@ -11,7 +11,7 @@
<body> <body>
<div id="ob-header"> <div id="ob-header">
<span class="links"> <span class="links">
<a href="/"><img class="logo" src="$(ctx.staticFilesPrefix)/oddbean.svg"></a> <a href="/"><img class="logo" src="$(ctx.staticFilesPrefix)/oddbean.svg?$(ctx.staticOddbeanSvgHash)"></a>
<a href="/" class="sitename"> <a href="/" class="sitename">
<span class="oddbean-name">Oddbean</span> <span class="oddbean-name">Oddbean</span>