From a3c34d2181a2f742bb9ac6302bfb1705fd59a8ba Mon Sep 17 00:00:00 2001 From: Doug Hoyte Date: Thu, 28 Sep 2023 01:49:55 -0400 Subject: [PATCH] simplify caching --- src/apps/web/HTTP.h | 11 ++- src/apps/web/WebReader.cpp | 119 ++++------------------------ src/apps/web/WebServer.h | 22 +---- src/apps/web/cmd_web.cpp | 2 +- src/apps/web/tmpls/about.tmpl | 4 +- src/apps/web/tmpls/event/event.tmpl | 2 +- src/apps/web/tmpls/main.tmpl | 6 +- 7 files changed, 31 insertions(+), 135 deletions(-) diff --git a/src/apps/web/HTTP.h b/src/apps/web/HTTP.h index cb42dbf..c714cb2 100644 --- a/src/apps/web/HTTP.h +++ b/src/apps/web/HTTP.h @@ -34,18 +34,21 @@ struct HTTPResponse : NonCopyable { std::string_view contentType = "text/html; charset=utf-8"; std::string extraHeaders; std::string body; + bool noCompress = false; - std::string eTag() { + std::string getETag() { unsigned char hash[SHA256_DIGEST_LENGTH]; SHA256(reinterpret_cast(body.data()), body.size(), hash); return to_hex(std::string_view(reinterpret_cast(hash), SHA256_DIGEST_LENGTH/2)); } std::string encode(bool doCompress) { + std::string eTag = getETag(); + std::string compressed; bool didCompress = false; - if (doCompress) { + if (!noCompress && doCompress) { compressed.resize(body.size()); z_stream zs; @@ -78,7 +81,9 @@ struct HTTPResponse : NonCopyable { output += std::to_string(bodySize); output += "\r\nContent-Type: "; 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"; output += extraHeaders; output += "Connection: Keep-Alive\r\n\r\n"; diff --git a/src/apps/web/WebReader.cpp b/src/apps/web/WebReader.cpp index b4b4a1c..02a2179 100644 --- a/src/apps/web/WebReader.cpp +++ b/src/apps/web/WebReader.cpp @@ -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; 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") { 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) { @@ -280,6 +280,8 @@ HTTPResponse WebServer::generateReadResponse(lmdb::txn &txn, Decompressor &decom } else if (u.path[0] == "post") { body = tmpl::newPost(nullptr); } 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") { rawBody = std::string(oddbeanStatic__oddbean_js()); contentType = "application/javascript"; @@ -293,6 +295,8 @@ HTTPResponse WebServer::generateReadResponse(lmdb::txn &txn, Decompressor &decom } else if (u.path[0] == "favicon.ico") { rawBody = std::string(oddbeanStatic__favicon_ico()); contentType = "image/x-icon"; + httpResp.extraHeaders += "Cache-Control: max-age=2592000\r\n"; + httpResp.noCompress = true; } else if (u.path[0] == "login") { body = tmpl::login(0); } else if (u.path[0] == "about") { @@ -312,11 +316,17 @@ HTTPResponse WebServer::generateReadResponse(lmdb::txn &txn, Decompressor &decom const std::optional &communitySpec; std::string_view title; std::string staticFilesPrefix; + std::string_view staticOddbeanCssHash; + std::string_view staticOddbeanJsHash; + std::string_view staticOddbeanSvgHash; } ctx = { *body, communitySpec, title, 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); @@ -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 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 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 pendingRequests; - - { - CacheItem *item = nullptr; - - { - std::lock_guard guard(cacheLock); - item = cache.emplace(req.url, std::make_unique()).first->second.get(); - } - - { - std::lock_guard 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::Thread &thr) { Decompressor decomp; @@ -449,7 +358,9 @@ void WebServer::runReader(ThreadPool::Thread &thr) { for (auto &newMsg : newMsgs) { if (auto msg = std::get_if(&newMsg.msg)) { 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) { HTTPResponse res; res.code = "500 Server Error"; diff --git a/src/apps/web/WebServer.h b/src/apps/web/WebServer.h index 65b575e..04485ff 100644 --- a/src/apps/web/WebServer.h +++ b/src/apps/web/WebServer.h @@ -77,26 +77,6 @@ struct WebServer { std::unique_ptr 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 pendingRequests; - }; - - std::mutex cacheLock; - flat_hash_map> cache; - - // Thread Pools ThreadPool tpHttpsocket; @@ -110,7 +90,7 @@ struct WebServer { void runReader(ThreadPool::Thread &thr); 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::Thread &thr); diff --git a/src/apps/web/cmd_web.cpp b/src/apps/web/cmd_web.cpp index 3aab146..aa7f74b 100644 --- a/src/apps/web/cmd_web.cpp +++ b/src/apps/web/cmd_web.cpp @@ -15,7 +15,7 @@ void WebServer::run() { // FIXME: cfg().web__numThreads__* - tpReader.init("Reader", 3, [this](auto &thr){ + tpReader.init("Reader", 10, [this](auto &thr){ runReader(thr); }); diff --git a/src/apps/web/tmpls/about.tmpl b/src/apps/web/tmpls/about.tmpl index 36a6742..aab159d 100644 --- a/src/apps/web/tmpls/about.tmpl +++ b/src/apps/web/tmpls/about.tmpl @@ -1,6 +1,6 @@

- Hi, thanks for stopping by! Oddbean is a discussion site built on the nostr protocol. 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 the nostr protocol. 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.

@@ -8,7 +8,7 @@

- No crap. 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). + No crap. 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).

diff --git a/src/apps/web/tmpls/event/event.tmpl b/src/apps/web/tmpls/event/event.tmpl index 15b0ebf..ec569bb 100644 --- a/src/apps/web/tmpls/event/event.tmpl +++ b/src/apps/web/tmpls/event/event.tmpl @@ -12,7 +12,7 @@ (raw) <> | root ?(ctx.ev->root.size()) - <> | show full thread ?(ctx.ev->root.empty() && !ctx.isFullThreadLoaded) + <> | full thread ?(ctx.ev->root.empty() && !ctx.isFullThreadLoaded) <> | export ?(ctx.ev->root.empty() && ctx.isFullThreadLoaded) <> | parent ?(ctx.ev->parent.size()) diff --git a/src/apps/web/tmpls/main.tmpl b/src/apps/web/tmpls/main.tmpl index c5e7079..ff0a155 100644 --- a/src/apps/web/tmpls/main.tmpl +++ b/src/apps/web/tmpls/main.tmpl @@ -2,8 +2,8 @@ - - + + $(ctx.title)Oddbean @@ -11,7 +11,7 @@