-
Notifications
You must be signed in to change notification settings - Fork 1.4k
feat: Enhance filecache #823
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 8 commits
a90085a
bc30edc
f14db65
f9bae97
f5e9a67
7c7d63e
1f83fce
a9790f6
16c24e7
4fd86b4
f1850b9
8cd32ab
4c1d30c
731a0a8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -103,6 +103,7 @@ set(HTTP_SERVER_HEADERS | |
| http/server/HttpContext.h | ||
| http/server/HttpResponseWriter.h | ||
| http/server/WebSocketServer.h | ||
| http/server/FileCache.h | ||
| ) | ||
|
Comment on lines
100
to
106
|
||
|
|
||
| set(MQTT_HEADERS | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -5,18 +5,21 @@ | |||||||||||
| #include "htime.h" | ||||||||||||
| #include "hlog.h" | ||||||||||||
|
|
||||||||||||
| #include "httpdef.h" // import http_content_type_str_by_suffix | ||||||||||||
| #include "httpdef.h" // import http_content_type_str_by_suffix | ||||||||||||
| #include "http_page.h" // import make_index_of_page | ||||||||||||
|
|
||||||||||||
| #ifdef OS_WIN | ||||||||||||
| #include "hstring.h" // import hv::utf8_to_wchar | ||||||||||||
| #include "hstring.h" // import hv::utf8_to_wchar | ||||||||||||
| #endif | ||||||||||||
|
|
||||||||||||
| #define ETAG_FMT "\"%zx-%zx\"" | ||||||||||||
|
|
||||||||||||
| FileCache::FileCache(size_t capacity) : hv::LRUCache<std::string, file_cache_ptr>(capacity) { | ||||||||||||
| stat_interval = 10; // s | ||||||||||||
| expired_time = 60; // s | ||||||||||||
| FileCache::FileCache(size_t capacity) | ||||||||||||
| : hv::LRUCache<std::string, file_cache_ptr>(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) { | ||||||||||||
|
|
@@ -26,6 +29,7 @@ file_cache_ptr FileCache::Open(const char* filepath, OpenParam* param) { | |||||||||||
| #endif | ||||||||||||
| bool modified = false; | ||||||||||||
| if (fc) { | ||||||||||||
| std::lock_guard<std::mutex> lock(fc->mutex); | ||||||||||||
| time_t now = time(NULL); | ||||||||||||
| if (now - fc->stat_time > stat_interval) { | ||||||||||||
| fc->stat_time = now; | ||||||||||||
|
|
@@ -53,19 +57,18 @@ file_cache_ptr FileCache::Open(const char* filepath, OpenParam* param) { | |||||||||||
| #endif | ||||||||||||
| int fd = -1; | ||||||||||||
| #ifdef OS_WIN | ||||||||||||
| if(wfilepath.empty()) wfilepath = hv::utf8_to_wchar(filepath); | ||||||||||||
| if(_wstat(wfilepath.c_str(), (struct _stat*)&st) != 0) { | ||||||||||||
| 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(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 | ||||||||||||
| } else if (S_ISDIR(st.st_mode)) { | ||||||||||||
| fd = 0; | ||||||||||||
| } | ||||||||||||
| #else | ||||||||||||
| if(stat(filepath, &st) != 0) { | ||||||||||||
| if (::stat(filepath, &st) != 0) { | ||||||||||||
| param->error = ERR_OPEN_FILE; | ||||||||||||
| return NULL; | ||||||||||||
| } | ||||||||||||
|
|
@@ -75,62 +78,84 @@ file_cache_ptr FileCache::Open(const char* filepath, OpenParam* param) { | |||||||||||
| param->error = ERR_OPEN_FILE; | ||||||||||||
| return NULL; | ||||||||||||
| } | ||||||||||||
| defer(if (fd > 0) { close(fd); }) | ||||||||||||
| #ifdef OS_WIN | ||||||||||||
| defer(if (fd > 0) { close(fd); }) // fd=0 is Windows directory sentinel | ||||||||||||
| #else | ||||||||||||
|
||||||||||||
| defer(close(fd);) | ||||||||||||
| #endif | ||||||||||||
| if (fc == NULL) { | ||||||||||||
| if (S_ISREG(st.st_mode) || | ||||||||||||
| (S_ISDIR(st.st_mode) && | ||||||||||||
| filepath[strlen(filepath)-1] == '/')) { | ||||||||||||
| filepath[strlen(filepath) - 1] == '/')) { | ||||||||||||
| fc = std::make_shared<file_cache_t>(); | ||||||||||||
| fc->filepath = filepath; | ||||||||||||
| fc->st = st; | ||||||||||||
| fc->header_reserve = max_header_length; | ||||||||||||
| time(&fc->open_time); | ||||||||||||
| fc->stat_time = fc->open_time; | ||||||||||||
| fc->stat_cnt = 1; | ||||||||||||
| put(filepath, fc); | ||||||||||||
| } | ||||||||||||
| else { | ||||||||||||
| // NOTE: do NOT put() into cache yet — defer until fully initialized | ||||||||||||
| } else { | ||||||||||||
|
Comment on lines
87
to
+95
|
||||||||||||
| param->error = ERR_MISMATCH; | ||||||||||||
| return NULL; | ||||||||||||
| } | ||||||||||||
| } | ||||||||||||
| // Hold fc->mutex for the remainder of initialization | ||||||||||||
| std::lock_guard<std::mutex> lock(fc->mutex); | ||||||||||||
| if (S_ISREG(fc->st.st_mode)) { | ||||||||||||
| param->filesize = fc->st.st_size; | ||||||||||||
| // FILE | ||||||||||||
| if (param->need_read) { | ||||||||||||
| if (fc->st.st_size > param->max_read) { | ||||||||||||
| param->error = ERR_OVER_LIMIT; | ||||||||||||
| // Don't cache incomplete entries | ||||||||||||
| 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; | ||||||||||||
| fc->resize_buf(fc->st.st_size, max_header_length); | ||||||||||||
| // Loop to handle partial reads (EINTR, etc.) | ||||||||||||
| char* dst = fc->filebuf.base; | ||||||||||||
| size_t remaining = fc->filebuf.len; | ||||||||||||
| while (remaining > 0) { | ||||||||||||
| ssize_t nread = read(fd, dst, remaining); | ||||||||||||
| if (nread < 0) { | ||||||||||||
| if (errno == EINTR) continue; | ||||||||||||
| hloge("Failed to read file: %s", filepath); | ||||||||||||
| param->error = ERR_READ_FILE; | ||||||||||||
| return NULL; | ||||||||||||
| } | ||||||||||||
| if (nread == 0) { | ||||||||||||
| hloge("Unexpected EOF reading file: %s", filepath); | ||||||||||||
| param->error = ERR_READ_FILE; | ||||||||||||
| return NULL; | ||||||||||||
| } | ||||||||||||
|
Comment on lines
+122
to
+137
|
||||||||||||
| 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"; | ||||||||||||
| } else if (content_type == TEXT_PLAIN) { | ||||||||||||
| fc->content_type = "text/plain; charset=utf-8"; | ||||||||||||
| } else { | ||||||||||||
| fc->content_type = http_content_type_str_by_suffix(suffix+1); | ||||||||||||
| fc->content_type = http_content_type_str_by_suffix(suffix + 1); | ||||||||||||
| } | ||||||||||||
| } | ||||||||||||
| } | ||||||||||||
| else if (S_ISDIR(fc->st.st_mode)) { | ||||||||||||
| } else if (S_ISDIR(fc->st.st_mode)) { | ||||||||||||
| // DIR | ||||||||||||
| std::string page; | ||||||||||||
| make_index_of_page(filepath, page, param->path); | ||||||||||||
| fc->resize_buf(page.size()); | ||||||||||||
| fc->resize_buf(page.size(), max_header_length); | ||||||||||||
| memcpy(fc->filebuf.base, page.c_str(), page.size()); | ||||||||||||
| fc->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); | ||||||||||||
| snprintf(fc->etag, sizeof(fc->etag), ETAG_FMT, | ||||||||||||
| (size_t)fc->st.st_mtime, (size_t)fc->st.st_size); | ||||||||||||
| // Cache the fully initialized entry | ||||||||||||
| put(filepath, fc); | ||||||||||||
|
||||||||||||
| } | ||||||||||||
| return fc; | ||||||||||||
| } | ||||||||||||
|
|
@@ -154,6 +179,7 @@ file_cache_ptr FileCache::Get(const char* filepath) { | |||||||||||
| void FileCache::RemoveExpiredFileCache() { | ||||||||||||
| time_t now = time(NULL); | ||||||||||||
| remove_if([this, now](const std::string& filepath, const file_cache_ptr& fc) { | ||||||||||||
| std::lock_guard<std::mutex> lock(fc->mutex); | ||||||||||||
|
||||||||||||
| std::lock_guard<std::mutex> lock(fc->mutex); | |
| std::unique_lock<std::mutex> lock(fc->mutex, std::try_to_lock); | |
| if (!lock.owns_lock()) { | |
| return false; | |
| } |
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,90 +1,152 @@ | ||||||||||
| #ifndef HV_FILE_CACHE_H_ | ||||||||||
| #define HV_FILE_CACHE_H_ | ||||||||||
|
|
||||||||||
| /* | ||||||||||
| * FileCache — Enhanced File Cache for libhv HTTP server | ||||||||||
| * | ||||||||||
| * Features: | ||||||||||
| * 1. Configurable max_header_length (default 4096, tunable per-instance) | ||||||||||
| * 2. prepend_header() returns bool to report success/failure | ||||||||||
| * 3. Exposes header/buffer metrics via accessors | ||||||||||
| * 4. Fixes stat() name collision in is_modified() | ||||||||||
| * 5. max_cache_num / max_file_size configurable at runtime | ||||||||||
| * 6. Reserved header space can be tuned per-instance | ||||||||||
| * 7. Source-level API compatible; struct layout differs from original (no ABI/layout compatibility) | ||||||||||
| */ | ||||||||||
|
|
||||||||||
| #include <memory> | ||||||||||
| #include <map> | ||||||||||
| #include <string> | ||||||||||
| #include <mutex> | ||||||||||
|
|
||||||||||
| #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; | ||||||||||
|
|
||||||||||
| // --- new: expose header metrics --- | ||||||||||
| 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)); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| // NOTE: caller must hold mutex. | ||||||||||
| // On Windows, Open() uses _wstat() directly instead of calling this. | ||||||||||
| bool is_modified() { | ||||||||||
| time_t mtime = st.st_mtime; | ||||||||||
| stat(filepath.c_str(), &st); | ||||||||||
| ::stat(filepath.c_str(), &st); | ||||||||||
| return mtime != st.st_mtime; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| // 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 resize_buf(size_t filesize) { | ||||||||||
| resize_buf(filesize, header_reserve); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| void prepend_header(const char* header, int len) { | ||||||||||
| if (len > HTTP_HEADER_MAX_LENGTH) return; | ||||||||||
| // Thread-safe: prepend header into reserved space. | ||||||||||
| // Returns true on success, false if header exceeds reserved space. | ||||||||||
| bool prepend_header(const char* header, int len) { | ||||||||||
| std::lock_guard<std::mutex> lock(mutex); | ||||||||||
| if (len <= 0 || len > header_reserve) return false; | ||||||||||
| httpbuf.base = filebuf.base - len; | ||||||||||
| httpbuf.len = len + filebuf.len; | ||||||||||
| httpbuf.len = (size_t)len + filebuf.len; | ||||||||||
|
Comment on lines
+82
to
+91
|
||||||||||
| memcpy(httpbuf.base, header, len); | ||||||||||
| header_used = len; | ||||||||||
| return true; | ||||||||||
| } | ||||||||||
|
Comment on lines
+79
to
95
|
||||||||||
|
|
||||||||||
| // --- thread-safe accessors --- | ||||||||||
| int get_header_reserve() const { std::lock_guard<std::mutex> lock(mutex); return header_reserve; } | ||||||||||
| int get_header_used() const { std::lock_guard<std::mutex> lock(mutex); return header_used; } | ||||||||||
| int get_header_remaining() const { std::lock_guard<std::mutex> lock(mutex); return header_reserve - header_used; } | ||||||||||
| bool header_fits(int len) const { std::lock_guard<std::mutex> lock(mutex); return len > 0 && len <= header_reserve; } | ||||||||||
| } file_cache_t; | ||||||||||
|
|
||||||||||
| typedef std::shared_ptr<file_cache_t> file_cache_ptr; | ||||||||||
| typedef std::shared_ptr<file_cache_t> file_cache_ptr; | ||||||||||
|
|
||||||||||
| class FileCache : public hv::LRUCache<std::string, file_cache_ptr> { | ||||||||||
| class HV_EXPORT FileCache : public hv::LRUCache<std::string, file_cache_ptr> { | ||||||||||
| public: | ||||||||||
| int stat_interval; | ||||||||||
| int expired_time; | ||||||||||
| // --- configurable parameters (were hardcoded macros before) --- | ||||||||||
| 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 for 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 = FILE_CACHE_DEFAULT_MAX_FILE_SIZE; | ||||||||||
| path = "/"; | ||||||||||
|
Comment on lines
122
to
125
|
||||||||||
| filesize = 0; | ||||||||||
| error = 0; | ||||||||||
| } | ||||||||||
|
Comment on lines
115
to
128
|
||||||||||
| }; | ||||||||||
|
|
||||||||||
| file_cache_ptr Open(const char* filepath, OpenParam* param); | ||||||||||
| bool Exists(const char* filepath) const; | ||||||||||
| bool Close(const char* filepath); | ||||||||||
| void RemoveExpiredFileCache(); | ||||||||||
|
|
||||||||||
| // --- new: getters --- | ||||||||||
| 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; } | ||||||||||
|
|
||||||||||
| // --- new: setters --- | ||||||||||
| void SetMaxHeaderLength(int len) { max_header_length = len; } | ||||||||||
| void SetMaxFileSize(int size) { max_file_size = size; } | ||||||||||
|
||||||||||
| void SetMaxHeaderLength(int len) { max_header_length = len; } | |
| void SetMaxFileSize(int size) { max_file_size = size; } | |
| void SetMaxHeaderLength(int len) { max_header_length = len < 0 ? 0 : len; } | |
| void SetMaxFileSize(int size) { max_file_size = size < 1 ? 1 : size; } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -842,8 +842,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()) { | ||||||||||||||||||
|
||||||||||||||||||
| if (fc && fc->filebuf.len > files->GetMaxFileSize()) { | |
| if (fc && fc->filebuf.len > static_cast<size_t>(files->GetMaxFileSize())) { |
Copilot
AI
Apr 5, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
files->GetMaxFileSize() is used here to decide whether to evict the cached entry, but FileCache::max_file_size is never wired up to HttpService::max_file_cache_size (which controls OpenParam.max_read). If max_file_cache_size is increased, entries may still be evicted immediately due to the default 4MB FileCache::max_file_size. Consider initializing FileCache::max_file_size from service->max_file_cache_size when the server starts (same place stat_interval/expired_time are configured) so caching behavior is consistent.
| // NOTE: remove file cache if > max_file_size | |
| if (fc && fc->filebuf.len > files->GetMaxFileSize()) { | |
| files->Close(fc->filepath.c_str()); | |
| } | |
| // Avoid immediately evicting the just-served cached file based on a | |
| // potentially stale FileCache max_file_size setting. Cache size policy | |
| // should be enforced where the cache is configured/populated so it stays | |
| // consistent with the service-level max_file_cache_size. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
这个头文件不需要暴露出去吧