diff --git a/Makefile b/Makefile index f6fa6e583..42f2f34b2 100644 --- a/Makefile +++ b/Makefile @@ -305,6 +305,16 @@ unittest: prepare $(CC) -g -Wall -O0 -std=c99 -I. -Ibase -Iprotocol -o bin/ping unittest/ping_test.c protocol/icmp.c base/hsocket.c base/htime.c -DPRINT_DEBUG $(CC) -g -Wall -O0 -std=c99 -I. -Ibase -Iprotocol -o bin/ftp unittest/ftp_test.c protocol/ftp.c base/hsocket.c base/htime.c $(CC) -g -Wall -O0 -std=c99 -I. -Ibase -Iprotocol -Iutil -o bin/sendmail unittest/sendmail_test.c protocol/smtp.c base/hsocket.c base/htime.c util/base64.c +ifeq ($(WITH_HTTP), yes) +ifeq ($(WITH_HTTP_SERVER), yes) + $(MAKE) libhv + $(CXX) -g -Wall -O0 -std=c++11 -I. -Ibase -Ievent -Icpputil -Ihttp -Ihttp/server -o bin/file_cache_test unittest/file_cache_test.cpp -Llib -lhv -pthread +else + $(RM) bin/file_cache_test +endif +else + $(RM) bin/file_cache_test +endif ifeq ($(WITH_EVPP), yes) ifeq ($(WITH_REDIS), yes) $(MAKE) libhv diff --git a/http/server/FileCache.cpp b/http/server/FileCache.cpp index 26b76e150..8934bc780 100644 --- a/http/server/FileCache.cpp +++ b/http/server/FileCache.cpp @@ -14,27 +14,41 @@ #define ETAG_FMT "\"%zx-%zx\"" -FileCache::FileCache(size_t capacity) : hv::LRUCache(capacity) { - stat_interval = 10; // s - expired_time = 60; // s +FileCache::FileCache(size_t capacity) + : hv::LRUCache(capacity) { + stat_interval = 10; // s + expired_time = 60; // s + max_header_length = FILE_CACHE_DEFAULT_HEADER_LENGTH; + max_file_size = FILE_CACHE_DEFAULT_MAX_FILE_SIZE; } file_cache_ptr FileCache::Open(const char* filepath, OpenParam* param) { + // Serialize cache inspection and loading per path. Different files still + // load concurrently, while same-path cold misses reuse one published entry. + BeginPathAccess(filepath); + defer(EndPathAccess(filepath);) + file_cache_ptr fc = Get(filepath); #ifdef OS_WIN std::wstring wfilepath; #endif bool modified = false; if (fc) { + std::lock_guard lock(fc->mutex); time_t now = time(NULL); if (now - fc->stat_time > stat_interval) { fc->stat_time = now; fc->stat_cnt++; #ifdef OS_WIN wfilepath = hv::utf8_to_wchar(filepath); - now = fc->st.st_mtime; - _wstat(wfilepath.c_str(), (struct _stat*)&fc->st); - modified = now != fc->st.st_mtime; + struct _stat latest_st; + if (_wstat(wfilepath.c_str(), &latest_st) != 0) { + modified = true; + } else { + modified = fc->st.st_mtime != latest_st.st_mtime || + fc->st.st_size != latest_st.st_size || + fc->st.st_mode != latest_st.st_mode; + } #else modified = fc->is_modified(); #endif @@ -46,91 +60,112 @@ file_cache_ptr FileCache::Open(const char* filepath, OpenParam* param) { } } if (fc == NULL || modified || param->need_read) { + auto fail_open = [this, filepath, param, &fc](int error) -> file_cache_ptr { + param->error = error; + if (fc) { + remove(filepath); + } + return NULL; + }; struct stat st; int flags = O_RDONLY; #ifdef O_BINARY flags |= O_BINARY; #endif int fd = -1; + bool is_dir = false; #ifdef OS_WIN - if(wfilepath.empty()) wfilepath = hv::utf8_to_wchar(filepath); - if(_wstat(wfilepath.c_str(), (struct _stat*)&st) != 0) { - param->error = ERR_OPEN_FILE; - return NULL; + if (wfilepath.empty()) wfilepath = hv::utf8_to_wchar(filepath); + if (_wstat(wfilepath.c_str(), (struct _stat*)&st) != 0) { + return fail_open(ERR_OPEN_FILE); } - if(S_ISREG(st.st_mode)) { + if (S_ISREG(st.st_mode)) { fd = _wopen(wfilepath.c_str(), flags); - }else if (S_ISDIR(st.st_mode)) { - // NOTE: open(dir) return -1 on windows - fd = 0; + } else if (S_ISDIR(st.st_mode)) { + is_dir = true; } #else - if(stat(filepath, &st) != 0) { - param->error = ERR_OPEN_FILE; - return NULL; + if (::stat(filepath, &st) != 0) { + return fail_open(ERR_OPEN_FILE); } fd = open(filepath, flags); #endif - if (fd < 0) { - param->error = ERR_OPEN_FILE; - return NULL; + if (fd < 0 && !is_dir) { + return fail_open(ERR_OPEN_FILE); } - defer(if (fd > 0) { close(fd); }) - if (fc == NULL) { - if (S_ISREG(st.st_mode) || - (S_ISDIR(st.st_mode) && - filepath[strlen(filepath)-1] == '/')) { - fc = std::make_shared(); - fc->filepath = filepath; - fc->st = st; - time(&fc->open_time); - fc->stat_time = fc->open_time; - fc->stat_cnt = 1; - put(filepath, fc); - } - else { - param->error = ERR_MISMATCH; - return NULL; - } + defer(if (fd >= 0) { close(fd); }) + size_t filepath_len = strlen(filepath); + if (!S_ISREG(st.st_mode) && + !(S_ISDIR(st.st_mode) && filepath_len > 0 && filepath[filepath_len - 1] == '/')) { + return fail_open(ERR_MISMATCH); } - if (S_ISREG(fc->st.st_mode)) { - param->filesize = fc->st.st_size; + + // Build a replacement off-cache. Published entries stay immutable so + // in-flight responses never observe a realloc or partially refreshed metadata. + file_cache_ptr refreshed = std::make_shared(); + refreshed->filepath = filepath; + refreshed->st = st; + refreshed->header_reserve = max_header_length; + time(&refreshed->open_time); + refreshed->stat_time = refreshed->open_time; + refreshed->stat_cnt = 1; + + if (S_ISREG(st.st_mode)) { + param->filesize = st.st_size; // FILE + int max_read = param->max_read > 0 ? param->max_read : max_file_size; + if (param->need_read && st.st_size > max_read) { + return fail_open(ERR_OVER_LIMIT); + } if (param->need_read) { - if (fc->st.st_size > param->max_read) { - param->error = ERR_OVER_LIMIT; - return NULL; - } - fc->resize_buf(fc->st.st_size); - int nread = read(fd, fc->filebuf.base, fc->filebuf.len); - if (nread != fc->filebuf.len) { - hloge("Failed to read file: %s", filepath); - param->error = ERR_READ_FILE; - return NULL; + refreshed->resize_buf(st.st_size, max_header_length); + // Loop to handle partial reads (EINTR, etc.). The entry is not + // published yet, so no cache mutex is needed around blocking IO. + char* dst = refreshed->filebuf.base; + size_t remaining = refreshed->filebuf.len; + while (remaining > 0) { + int nread = read(fd, dst, remaining); + if (nread < 0) { + if (errno == EINTR) { + continue; + } + hloge("Failed to read file: %s", filepath); + return fail_open(ERR_READ_FILE); + } + if (nread == 0) { + hloge("Unexpected EOF reading file: %s", filepath); + return fail_open(ERR_READ_FILE); + } + dst += nread; + remaining -= nread; } } const char* suffix = strrchr(filepath, '.'); if (suffix) { - http_content_type content_type = http_content_type_enum_by_suffix(suffix+1); + http_content_type content_type = http_content_type_enum_by_suffix(suffix + 1); if (content_type == TEXT_HTML) { - fc->content_type = "text/html; charset=utf-8"; + refreshed->content_type = "text/html; charset=utf-8"; } else if (content_type == TEXT_PLAIN) { - fc->content_type = "text/plain; charset=utf-8"; + refreshed->content_type = "text/plain; charset=utf-8"; } else { - fc->content_type = http_content_type_str_by_suffix(suffix+1); + refreshed->content_type = http_content_type_str_by_suffix(suffix + 1); } } - } - else if (S_ISDIR(fc->st.st_mode)) { + } else if (S_ISDIR(st.st_mode)) { // DIR std::string page; make_index_of_page(filepath, page, param->path); - fc->resize_buf(page.size()); - memcpy(fc->filebuf.base, page.c_str(), page.size()); - fc->content_type = "text/html; charset=utf-8"; + refreshed->resize_buf(page.size(), max_header_length); + memcpy(refreshed->filebuf.base, page.c_str(), page.size()); + refreshed->content_type = "text/html; charset=utf-8"; } - gmtime_fmt(fc->st.st_mtime, fc->last_modified); - snprintf(fc->etag, sizeof(fc->etag), ETAG_FMT, (size_t)fc->st.st_mtime, (size_t)fc->st.st_size); + gmtime_fmt(refreshed->st.st_mtime, refreshed->last_modified); + snprintf(refreshed->etag, sizeof(refreshed->etag), ETAG_FMT, + (size_t)refreshed->st.st_mtime, (size_t)refreshed->st.st_size); + + // Cache the fully initialized entry (acquires LRUCache mutex only) + fc = refreshed; + put(filepath, fc); } return fc; } @@ -151,9 +186,31 @@ file_cache_ptr FileCache::Get(const char* filepath) { return NULL; } +void FileCache::BeginPathAccess(const std::string& filepath) { + std::unique_lock lock(loading_mutex_); + loading_cv_.wait(lock, [this, &filepath]() { + return loading_files_.find(filepath) == loading_files_.end(); + }); + loading_files_.insert(filepath); +} + +void FileCache::EndPathAccess(const std::string& filepath) { + { + std::lock_guard lock(loading_mutex_); + loading_files_.erase(filepath); + } + loading_cv_.notify_all(); +} + void FileCache::RemoveExpiredFileCache() { time_t now = time(NULL); remove_if([this, now](const std::string& filepath, const file_cache_ptr& fc) { + // Use try_to_lock to avoid lock-order inversion with Open(). + // If the entry is busy, skip it — it will be checked next cycle. + std::unique_lock lock(fc->mutex, std::try_to_lock); + if (!lock.owns_lock()) { + return false; + } return (now - fc->stat_time > expired_time); }); } diff --git a/http/server/FileCache.h b/http/server/FileCache.h index 363c41d88..c9b936c79 100644 --- a/http/server/FileCache.h +++ b/http/server/FileCache.h @@ -1,92 +1,162 @@ #ifndef HV_FILE_CACHE_H_ #define HV_FILE_CACHE_H_ +/* + * FileCache — Enhanced File Cache for libhv HTTP server + * + */ + #include -#include #include #include +#include +#include +#include "hexport.h" #include "hbuf.h" #include "hstring.h" #include "LRUCache.h" -#define HTTP_HEADER_MAX_LENGTH 1024 // 1K -#define FILE_CACHE_MAX_NUM 100 -#define FILE_CACHE_MAX_SIZE (1 << 22) // 4M +// Default values — may be overridden at runtime via FileCache setters +#define FILE_CACHE_DEFAULT_HEADER_LENGTH 4096 // 4K +#define FILE_CACHE_DEFAULT_MAX_NUM 100 +#define FILE_CACHE_DEFAULT_MAX_FILE_SIZE (1 << 22) // 4M typedef struct file_cache_s { + mutable std::mutex mutex; // protects all mutable state below std::string filepath; struct stat st; time_t open_time; time_t stat_time; uint32_t stat_cnt; - HBuf buf; // http_header + file_content - hbuf_t filebuf; - hbuf_t httpbuf; + HBuf buf; // header_reserve + file_content + hbuf_t filebuf; // points into buf: file content region + hbuf_t httpbuf; // points into buf: header + file content after prepend char last_modified[64]; char etag[64]; std::string content_type; + int header_reserve; // reserved bytes before file content + int header_used; // actual bytes used by prepend_header + file_cache_s() { stat_cnt = 0; + header_reserve = FILE_CACHE_DEFAULT_HEADER_LENGTH; + header_used = 0; + memset(last_modified, 0, sizeof(last_modified)); + memset(etag, 0, sizeof(etag)); } - bool is_modified() { - time_t mtime = st.st_mtime; - stat(filepath.c_str(), &st); - return mtime != st.st_mtime; + // NOTE: caller must hold mutex. + // Keep published cache metadata immutable so in-flight responses can safely + // retain a shared_ptr while a refreshed entry is prepared. + // On Windows, Open() uses _wstat() directly instead of calling this. + bool is_modified() const { + struct stat latest_st; + if (::stat(filepath.c_str(), &latest_st) != 0) { + return true; + } + return st.st_mtime != latest_st.st_mtime || + st.st_size != latest_st.st_size || + st.st_mode != latest_st.st_mode; } + // NOTE: caller must hold mutex bool is_complete() { - if(S_ISDIR(st.st_mode)) return filebuf.len > 0; - return filebuf.len == st.st_size; + if (S_ISDIR(st.st_mode)) return filebuf.len > 0; + return filebuf.len == (size_t)st.st_size; } - void resize_buf(int filesize) { - buf.resize(HTTP_HEADER_MAX_LENGTH + filesize); - filebuf.base = buf.base + HTTP_HEADER_MAX_LENGTH; + // NOTE: caller must hold mutex — invalidates filebuf/httpbuf pointers + void resize_buf(size_t filesize, int reserved) { + if (reserved < 0) reserved = 0; + header_reserve = reserved; + buf.resize((size_t)reserved + filesize); + filebuf.base = buf.base + reserved; filebuf.len = filesize; + // Invalidate httpbuf since buffer may have been reallocated + httpbuf.base = NULL; + httpbuf.len = 0; + header_used = 0; } - void prepend_header(const char* header, int len) { - if (len > HTTP_HEADER_MAX_LENGTH) return; + void resize_buf(size_t filesize) { + resize_buf(filesize, header_reserve); + } + + // Caller must hold mutex. HttpHandler keeps the lock until hio_write has + // either sent or copied the returned buffer. + bool prepend_header(const char* header, int len) { + if (len <= 0 || len > header_reserve) { + // Safe fallback: point httpbuf at filebuf so callers always get valid data + httpbuf = filebuf; + header_used = 0; + return false; + } httpbuf.base = filebuf.base - len; - httpbuf.len = len + filebuf.len; + httpbuf.len = (size_t)len + filebuf.len; memcpy(httpbuf.base, header, len); + header_used = len; + return true; } + + // --- thread-safe accessors --- + int get_header_reserve() const { std::lock_guard lock(mutex); return header_reserve; } + int get_header_used() const { std::lock_guard lock(mutex); return header_used; } + int get_header_remaining() const { std::lock_guard lock(mutex); return header_reserve - header_used; } + bool header_fits(int len) const { std::lock_guard lock(mutex); return len > 0 && len <= header_reserve; } } file_cache_t; -typedef std::shared_ptr file_cache_ptr; +typedef std::shared_ptr file_cache_ptr; -class FileCache : public hv::LRUCache { +class HV_EXPORT FileCache : public hv::LRUCache { public: - int stat_interval; - int expired_time; + int stat_interval; // seconds between stat() checks + int expired_time; // seconds before cache entry expires + int max_header_length; // reserved header bytes per entry + int max_file_size; // max cached file size (larger = large-file path) - FileCache(size_t capacity = FILE_CACHE_MAX_NUM); + explicit FileCache(size_t capacity = FILE_CACHE_DEFAULT_MAX_NUM); struct OpenParam { - bool need_read; - int max_read; - const char* path; - size_t filesize; - int error; + bool need_read; + int max_read; // per-request override; <= 0 uses FileCache::max_file_size + const char* path; // URL path (for directory listing) + size_t filesize; // [out] actual file size + int error; // [out] error code if Open returns NULL OpenParam() { need_read = true; - max_read = FILE_CACHE_MAX_SIZE; + max_read = 0; path = "/"; filesize = 0; error = 0; } }; + file_cache_ptr Open(const char* filepath, OpenParam* param); bool Exists(const char* filepath) const; bool Close(const char* filepath); void RemoveExpiredFileCache(); + int GetMaxHeaderLength() const { return max_header_length; } + int GetMaxFileSize() const { return max_file_size; } + int GetStatInterval() const { return stat_interval; } + int GetExpiredTime() const { return expired_time; } + + void SetMaxHeaderLength(int len) { max_header_length = len < 0 ? 0 : len; } + void SetMaxFileSize(int size) { max_file_size = size < 1 ? 1 : size; } + protected: file_cache_ptr Get(const char* filepath); + +private: + void BeginPathAccess(const std::string& filepath); + void EndPathAccess(const std::string& filepath); + + std::mutex loading_mutex_; + std::condition_variable loading_cv_; + std::set loading_files_; }; #endif // HV_FILE_CACHE_H_ diff --git a/http/server/HttpHandler.cpp b/http/server/HttpHandler.cpp index 477a8469c..21d352c61 100644 --- a/http/server/HttpHandler.cpp +++ b/http/server/HttpHandler.cpp @@ -403,15 +403,13 @@ void HttpHandler::handleRequestHeaders() { auto iter = pReq->headers.find("Proxy-Connection"); if (iter != pReq->headers.end()) { const char* keepalive_value = iter->second.c_str(); - if (stricmp(keepalive_value, "keep-alive") == 0) { + if (stricmp(keepalive_value, "keep-alive") == 0 || + stricmp(keepalive_value, "upgrade") == 0) { keepalive = true; } else if (stricmp(keepalive_value, "close") == 0) { keepalive = false; } - else if (stricmp(keepalive_value, "upgrade") == 0) { - keepalive = true; - } } } else { @@ -577,10 +575,8 @@ int HttpHandler::defaultStaticHandler() { int status_code = HTTP_STATUS_OK; // Range: - bool has_range = false; long from, to = 0; if (req->GetRange(from, to)) { - has_range = true; if (openFile(filepath.c_str()) != 0) { return HTTP_STATUS_NOT_FOUND; } @@ -620,7 +616,7 @@ int HttpHandler::defaultStaticHandler() { // FileCache FileCache::OpenParam param; param.max_read = service->max_file_cache_size; - param.need_read = !(req->method == HTTP_HEAD || has_range); + param.need_read = req->method != HTTP_HEAD; param.path = req_path; if (files) { fc = files->Open(filepath.c_str(), ¶m); @@ -810,11 +806,15 @@ int HttpHandler::GetSendData(char** data, size_t* len) { // FileCache // NOTE: no copy filebuf, more efficient header = pResp->Dump(true, false); - fc->prepend_header(header.c_str(), header.size()); - *data = fc->httpbuf.base; - *len = fc->httpbuf.len; - state = SEND_DONE; - return *len; + if (fc->prepend_header(header.c_str(), header.size())) { + *data = fc->httpbuf.base; + *len = fc->httpbuf.len; + state = SEND_DONE; + return *len; + } + // Header too large for reserved space: send header first, then continue with file body. + state = SEND_BODY; + goto return_header; } // API service content_length = pResp->ContentLength(); @@ -851,8 +851,8 @@ int HttpHandler::GetSendData(char** data, size_t* len) { } case SEND_DONE: { - // NOTE: remove file cache if > FILE_CACHE_MAX_SIZE - if (fc && fc->filebuf.len > FILE_CACHE_MAX_SIZE) { + // NOTE: remove file cache if > max_file_size + if (fc && fc->filebuf.len > files->GetMaxFileSize()) { files->Close(fc->filepath.c_str()); } fc = NULL; @@ -874,6 +874,16 @@ int HttpHandler::SendHttpResponse(bool submit) { if (!io || !parser) return -1; char* data = NULL; size_t len = 0, total_len = 0; + // GetSendData may remove both the LRU and handler references while the + // entry mutex is locked. Keep the entry alive until after the lock is + // destroyed and has released that mutex. + file_cache_ptr fc_keepalive = fc; + // FileCache is shared by all worker loops. Keep its mutex held until each + // returned buffer has been synchronously sent or copied into hio's queue. + std::unique_lock file_cache_lock; + if (fc_keepalive) { + file_cache_lock = std::unique_lock(fc_keepalive->mutex); + } if (submit) parser->SubmitResponse(resp.get()); while (GetSendData(&data, &len)) { // printf("GetSendData %d\n", (int)len); diff --git a/http/server/HttpServer.cpp b/http/server/HttpServer.cpp index aa296fe8b..6471de503 100644 --- a/http/server/HttpServer.cpp +++ b/http/server/HttpServer.cpp @@ -136,6 +136,7 @@ static void loop_thread(void* userdata) { FileCache* filecache = &privdata->filecache; filecache->stat_interval = service->file_cache_stat_interval; filecache->expired_time = service->file_cache_expired_time; + filecache->SetMaxFileSize(service->max_file_cache_size); if (filecache->expired_time > 0) { // NOTE: add timer to remove expired file cache htimer_t* timer = htimer_add(hloop, [](htimer_t* timer) { diff --git a/scripts/unittest.sh b/scripts/unittest.sh index dec6d3c87..5c5f55147 100755 --- a/scripts/unittest.sh +++ b/scripts/unittest.sh @@ -32,6 +32,9 @@ bin/socketpair_test # bin/objectpool_test bin/sizeof_test bin/http_router_test +if [ -x bin/file_cache_test ]; then + bin/file_cache_test +fi for redis_test in redis_async_client_test redis_client_test redis_batch_test redis_subscriber_test; do if [ -x bin/${redis_test} ]; then bin/${redis_test} diff --git a/unittest/CMakeLists.txt b/unittest/CMakeLists.txt index db466c0fb..b045d3924 100644 --- a/unittest/CMakeLists.txt +++ b/unittest/CMakeLists.txt @@ -90,6 +90,13 @@ target_include_directories(sendmail PRIVATE .. ../base ../protocol ../util) add_executable(http_router_test http_router_test.cpp) target_include_directories(http_router_test PRIVATE ../http/server) +if(WITH_HTTP AND WITH_HTTP_SERVER) +add_executable(file_cache_test file_cache_test.cpp) +target_include_directories(file_cache_test PRIVATE .. ../base ../cpputil ../event ../http ../http/server) +target_link_libraries(file_cache_test ${HV_LIBRARIES}) +set(HTTP_UNITTEST_TARGETS file_cache_test) +endif() + # ------redis------ if(WITH_EVPP AND WITH_REDIS) add_executable(redis_protocol_test redis_protocol_test.cpp ../redis/RedisMessage.cpp) @@ -138,5 +145,6 @@ add_custom_target(unittest DEPENDS ftp sendmail http_router_test + ${HTTP_UNITTEST_TARGETS} ${REDIS_UNITTEST_TARGETS} ) diff --git a/unittest/file_cache_test.cpp b/unittest/file_cache_test.cpp new file mode 100644 index 000000000..cd9284501 --- /dev/null +++ b/unittest/file_cache_test.cpp @@ -0,0 +1,178 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include "FileCache.h" +#include "herr.h" +#include "hthread.h" + +#define CHECK(condition) do { \ + if (!(condition)) { \ + std::fprintf(stderr, "CHECK failed at line %d: %s\n", __LINE__, #condition); \ + return 1; \ + } \ +} while (0) + +static bool write_file(const std::string& filepath, const std::string& content) { + std::ofstream stream(filepath.c_str(), std::ios::binary | std::ios::trunc); + stream.write(content.data(), content.size()); + return stream.good(); +} + +int main() { + const std::string filepath = "libhv_file_cache_test_" + std::to_string(hv_getpid()) + ".txt"; + + CHECK(write_file(filepath, "old")); + + FileCache cache; + cache.stat_interval = -1; // force a stat check on every cache hit + + FileCache::OpenParam first_param; + first_param.max_read = 1024; + file_cache_ptr first = cache.Open(filepath.c_str(), &first_param); + CHECK(first != NULL); + CHECK(first->filebuf.len == 3); + CHECK(std::memcmp(first->filebuf.base, "old", 3) == 0); + + CHECK(write_file(filepath, "new-content")); + + FileCache::OpenParam second_param; + second_param.max_read = 1024; + file_cache_ptr second = cache.Open(filepath.c_str(), &second_param); + CHECK(second != NULL); + CHECK(second != first); + CHECK(second->filebuf.len == 11); + CHECK(std::memcmp(second->filebuf.base, "new-content", 11) == 0); + + // A refreshed entry must not mutate buffers retained by in-flight responses. + CHECK(first->filebuf.len == 3); + CHECK(std::memcmp(first->filebuf.base, "old", 3) == 0); + + CHECK(std::remove(filepath.c_str()) == 0); + + FileCache::OpenParam missing_param; + file_cache_ptr missing = cache.Open(filepath.c_str(), &missing_param); + CHECK(missing == NULL); + CHECK(missing_param.error != 0); + + // A failed refresh must invalidate the old entry instead of serving it + // until the next stat interval. + cache.stat_interval = 60; + FileCache::OpenParam repeated_missing_param; + file_cache_ptr repeated_missing = cache.Open(filepath.c_str(), &repeated_missing_param); + CHECK(repeated_missing == NULL); + CHECK(repeated_missing_param.error != 0); + + // The default OpenParam limit follows the owning cache instance. + const std::string limit_filepath = filepath + ".limit"; + CHECK(write_file(limit_filepath, "12345")); + FileCache limited_cache; + limited_cache.SetMaxFileSize(4); + FileCache::OpenParam limited_param; + CHECK(limited_cache.Open(limit_filepath.c_str(), &limited_param) == NULL); + CHECK(limited_param.error == ERR_OVER_LIMIT); + FileCache::OpenParam override_param; + override_param.max_read = 5; + file_cache_ptr overridden = limited_cache.Open(limit_filepath.c_str(), &override_param); + CHECK(overridden != NULL); + CHECK(overridden->filebuf.len == 5); + CHECK(limited_cache.Close(limit_filepath.c_str())); + limited_cache.SetMaxFileSize(5); + FileCache::OpenParam allowed_param; + file_cache_ptr allowed = limited_cache.Open(limit_filepath.c_str(), &allowed_param); + CHECK(allowed != NULL); + CHECK(allowed->filebuf.len == 5); + { + std::lock_guard lock(allowed->mutex); + CHECK(!allowed->prepend_header("oversized", allowed->header_reserve + 1)); + CHECK(allowed->httpbuf.base == allowed->filebuf.base); + CHECK(allowed->httpbuf.len == allowed->filebuf.len); + CHECK(allowed->header_used == 0); + } + CHECK(std::remove(limit_filepath.c_str()) == 0); + + // Concurrent cold misses for one path must reuse the same published entry. + const std::string concurrent_filepath = filepath + ".concurrent"; + const std::string concurrent_content(static_cast(2) * 1024 * 1024, 'x'); + CHECK(write_file(concurrent_filepath, concurrent_content)); + FileCache concurrent_cache; + concurrent_cache.SetMaxFileSize(4 * 1024 * 1024); + const size_t thread_count = 8; + std::vector results(thread_count); + std::vector threads; + std::mutex start_mutex; + std::condition_variable start_cv; + size_t ready = 0; + bool start = false; + for (size_t i = 0; i < thread_count; ++i) { + threads.push_back(std::thread([&, i]() { + { + std::unique_lock lock(start_mutex); + ++ready; + start_cv.notify_all(); + start_cv.wait(lock, [&]() { return start; }); + } + FileCache::OpenParam param; + results[i] = concurrent_cache.Open(concurrent_filepath.c_str(), ¶m); + })); + } + { + std::unique_lock lock(start_mutex); + start_cv.wait(lock, [&]() { return ready == thread_count; }); + start = true; + } + start_cv.notify_all(); + for (size_t i = 0; i < threads.size(); ++i) { + threads[i].join(); + } + CHECK(results[0] != NULL); + for (size_t i = 1; i < results.size(); ++i) { + CHECK(results[i] == results[0]); + } + + // A concurrent refresh also publishes one replacement and preserves the + // old buffer held by in-flight responses. + concurrent_cache.stat_interval = -1; + CHECK(write_file(concurrent_filepath, "refreshed")); + std::vector refreshed_results(thread_count); + threads.clear(); + ready = 0; + start = false; + for (size_t i = 0; i < thread_count; ++i) { + threads.push_back(std::thread([&, i]() { + { + std::unique_lock lock(start_mutex); + ++ready; + start_cv.notify_all(); + start_cv.wait(lock, [&]() { return start; }); + } + FileCache::OpenParam param; + refreshed_results[i] = concurrent_cache.Open(concurrent_filepath.c_str(), ¶m); + })); + } + { + std::unique_lock lock(start_mutex); + start_cv.wait(lock, [&]() { return ready == thread_count; }); + start = true; + } + start_cv.notify_all(); + for (size_t i = 0; i < threads.size(); ++i) { + threads[i].join(); + } + CHECK(refreshed_results[0] != NULL); + CHECK(refreshed_results[0] != results[0]); + CHECK(refreshed_results[0]->filebuf.len == 9); + CHECK(std::memcmp(refreshed_results[0]->filebuf.base, "refreshed", 9) == 0); + for (size_t i = 1; i < refreshed_results.size(); ++i) { + CHECK(refreshed_results[i] == refreshed_results[0]); + } + CHECK(results[0]->filebuf.len == concurrent_content.size()); + CHECK(std::memcmp(results[0]->filebuf.base, concurrent_content.data(), concurrent_content.size()) == 0); + CHECK(std::remove(concurrent_filepath.c_str()) == 0); + return 0; +}