Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
a90085a
feat: add FileCacheEx - enhanced file cache with configurable header …
Yundi339 Mar 26, 2026
bc30edc
fix: address race conditions and safety issues in FileCacheEx
Yundi339 Mar 26, 2026
f14db65
refactor: align Windows compat with original libhv patterns
Yundi339 Mar 26, 2026
f9bae97
docs: add FileCacheEx documentation (en/cn) and fix comment style
Yundi339 Mar 26, 2026
f5e9a67
fix: add bounds check for negative reserved in resize_buf
Yundi339 Mar 26, 2026
7c7d63e
refactor: replace FileCache with FileCacheEx in HttpHandler and HttpS…
Yundi339 Mar 29, 2026
1f83fce
Merge branch 'ithewei:master' into feature/enhanced-filecache
Yundi339 Apr 5, 2026
a9790f6
Refactor FileCache and remove FileCacheEx
Yundi339 Apr 5, 2026
16c24e7
refactor: enhance FileCache with safe fallback in prepend_header and …
Yundi339 Apr 5, 2026
4fd86b4
refactor: improve FileCache Open method and enhance HttpHandler heade…
Yundi339 Apr 5, 2026
f1850b9
refactor: update FileCache to reset header_used on safe fallback and …
Yundi339 Apr 5, 2026
8cd32ab
refactor: update FileCache to change nread type from ssize_t to int i…
Yundi339 Apr 7, 2026
4c1d30c
rm deprecated comment
ithewei Apr 10, 2026
731a0a8
Header too large for reserved space: send header first, then continue…
ithewei Apr 10, 2026
05248d0
fix: prevent postprocessor/errorHandler from overriding HTTP_STATUS_U…
Copilot Apr 20, 2026
15809f4
fix: thread unsafe call to localtime on linux (#835)
aleksisch May 21, 2026
0763810
Optimize http router using trie (#833) (#836)
ithewei May 21, 2026
19f3e28
chore: add AGENTS.md for Coding Agents (#826)
ithewei May 21, 2026
9c13599
Apply suggestions from ai code review (#837)
ithewei May 23, 2026
33ce9fc
Apply suggestions for evpp dir from ai code review (#838)
ithewei May 23, 2026
51089c6
Apply suggestions for http dir from ai code review (#839)
ithewei May 23, 2026
50a9ede
Merge master into feature/enhanced-filecache
Yundi339 Jul 12, 2026
950bcb2
fix: harden concurrent file cache loading
Yundi339 Jul 12, 2026
9f6af8d
test: align file cache test with make workflow
Yundi339 Jul 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
177 changes: 117 additions & 60 deletions http/server/FileCache.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,27 +14,41 @@

#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) {
// 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);
Comment on lines 25 to 31
#ifdef OS_WIN
std::wstring wfilepath;
#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;
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
Expand All @@ -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<file_cache_t>();
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<file_cache_t>();
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;
}
Expand All @@ -151,9 +186,31 @@ file_cache_ptr FileCache::Get(const char* filepath) {
return NULL;
}

void FileCache::BeginPathAccess(const std::string& filepath) {
std::unique_lock<std::mutex> 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<std::mutex> 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<std::mutex> lock(fc->mutex, std::try_to_lock);
if (!lock.owns_lock()) {
return false;
}
return (now - fc->stat_time > expired_time);
});
}
Loading
Loading