-
Notifications
You must be signed in to change notification settings - Fork 18
feat(mount): Implement negative cache #724
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
Merged
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
823ca39
refactor(mount): Refactor lookup for readability
ZaRdEr15 59c44b7
feat(mount): Add negative cache implementation
ZaRdEr15 74b88f1
test(mount): Add negative cache unit test
ZaRdEr15 0840051
feat(mount): Add configurable negative cache options
ZaRdEr15 986342b
feat(mount): Utilize negative cache in sauna client lookup
ZaRdEr15 6a9fdee
test: Add negative cache system test
ZaRdEr15 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,223 @@ | ||
| /* | ||
|
|
||
| Copyright 2026 Leil Storage OÜ | ||
|
|
||
| This file is part of SaunaFS. | ||
|
|
||
| SaunaFS is free software: you can redistribute it and/or modify | ||
| it under the terms of the GNU General Public License as published by | ||
| the Free Software Foundation, version 3. | ||
|
|
||
| SaunaFS is distributed in the hope that it will be useful, | ||
| but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| GNU General Public License for more details. | ||
|
|
||
| You should have received a copy of the GNU General Public License | ||
| along with SaunaFS. If not, see <http://www.gnu.org/licenses/>. | ||
| */ | ||
|
|
||
| #pragma once | ||
|
|
||
| #include <atomic> | ||
| #include <cstdint> | ||
| #include <list> | ||
| #include <mutex> | ||
| #include <unordered_map> | ||
|
|
||
| #include "common/hashfn.h" | ||
| #include "common/shared_mutex.h" | ||
| #include "common/type_defs.h" | ||
| #include "common/time_utils.h" | ||
|
|
||
| inline std::atomic<uint32_t> gNegativeCacheTimeoutMs; | ||
| inline std::atomic<uint32_t> gNegativeCacheMaxSize; | ||
|
|
||
| class NegativeCache { | ||
ZaRdEr15 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| public: | ||
| NegativeCache() = default; | ||
|
|
||
| // To prevent accidental copies of the lock/map/list | ||
| NegativeCache(const NegativeCache&) = delete; | ||
| NegativeCache& operator=(const NegativeCache&) = delete; | ||
| NegativeCache(NegativeCache&&) = delete; | ||
| NegativeCache& operator=(NegativeCache&&) = delete; | ||
|
|
||
| // Atomic access | ||
| static uint32_t getGlobalMaxSize() noexcept { | ||
| return gNegativeCacheMaxSize.load(); | ||
| } | ||
|
|
||
| // Atomic access | ||
| static void setGlobalMaxSize(uint32_t maxSize) noexcept { | ||
| gNegativeCacheMaxSize.store(maxSize); | ||
| } | ||
|
|
||
| // Locked | ||
| // Reserve enough buckets without the map needing to rehash | ||
| void setMaxSize(uint32_t maxSize) { | ||
| std::unique_lock<shared_mutex> lock(rwLock_); | ||
| maxSize_ = maxSize; | ||
| negativeCache_.reserve(maxSize_); | ||
| } | ||
|
|
||
| // Atomic access | ||
| static uint32_t getGlobalTimeoutMs() noexcept { | ||
| return gNegativeCacheTimeoutMs.load(); | ||
| } | ||
|
|
||
| // Atomic access | ||
| static void setGlobalTimeoutMs(uint32_t timeoutMs) noexcept { | ||
| gNegativeCacheTimeoutMs.store(timeoutMs); | ||
| } | ||
|
|
||
| // Locked | ||
| void setTimeoutMs(uint32_t timeoutMs) { | ||
| std::unique_lock<shared_mutex> lock(rwLock_); | ||
| timeoutMs_ = timeoutMs; | ||
| } | ||
|
|
||
| // Locked | ||
| size_t size() const { | ||
| shared_lock<shared_mutex> lock(rwLock_); | ||
| return negativeCache_.size(); | ||
| } | ||
|
|
||
| // Locked | ||
| bool isMaxSizeAndTimeoutMsSet() const { | ||
| shared_lock<shared_mutex> lock(rwLock_); | ||
| return (maxSize_ > 0) && (timeoutMs_ > 0); | ||
| } | ||
|
|
||
| // Locked | ||
| // Works only if max size and timeout are set | ||
| bool lookup(inode_t parent, std::string_view name) const { | ||
| if (!isMaxSizeAndTimeoutMsSet()) { return false; } | ||
|
|
||
| shared_lock<shared_mutex> lock(rwLock_); | ||
| auto it = negativeCache_.find(InodeNameLookupPair{ parent, name }); | ||
| if (it != negativeCache_.end()) { | ||
| NegativeCacheEntry entry = it->second; | ||
| if (static_cast<uint64_t>(timer_.elapsed_ms()) < entry.timeoutMs) { | ||
| return true; | ||
| } // else ignore expired | ||
| } | ||
| return false; | ||
| } | ||
|
|
||
| // Locked, LRU eviction policy, entries refreshed | ||
| // Works only if max size and timeout are set | ||
| void add(inode_t parent, std::string_view name) { | ||
| if (!isMaxSizeAndTimeoutMsSet()) { return; } | ||
|
|
||
| std::unique_lock<shared_mutex> lock(rwLock_); | ||
|
|
||
| // sfsnegativecachetimeout and sfsnegativecachesize | ||
| // are not changed in Tweaks frequently, so it is okay | ||
| // for consistency | ||
| bool clearCache = false; | ||
| uint32_t globalTimeoutMs = getGlobalTimeoutMs(); | ||
| if (timeoutMs_ != globalTimeoutMs) { | ||
| timeoutMs_ = globalTimeoutMs; | ||
| clearCache = true; | ||
| } | ||
| uint32_t globalMaxSize = getGlobalMaxSize(); | ||
| if (maxSize_ != globalMaxSize) { | ||
| maxSize_ = globalMaxSize; | ||
| negativeCache_.reserve(maxSize_); // doesn't shrink | ||
| clearCache = true; | ||
| } | ||
| if (clearCache) { | ||
| negativeCache_.clear(); | ||
| timeoutList_.clear(); | ||
| } | ||
|
|
||
| InodeNamePairPtr inodeNamePtr; | ||
|
|
||
| // Update existing | ||
| auto it = negativeCache_.find(InodeNameLookupPair{ parent, name }); | ||
| if (it != negativeCache_.end()) { | ||
| inodeNamePtr = it->first; | ||
| timeoutList_.erase(it->second.listIt); | ||
| } else { | ||
| if (negativeCache_.size() >= maxSize_) { | ||
| // Remove oldest | ||
| negativeCache_.erase(timeoutList_.front()); | ||
| timeoutList_.pop_front(); | ||
| } | ||
| inodeNamePtr = std::make_shared<const InodeNamePair>(parent, name); | ||
| } | ||
| timeoutList_.push_back(inodeNamePtr); | ||
|
|
||
| // Update timeout if the same parent/name | ||
| negativeCache_.insert_or_assign(inodeNamePtr, NegativeCacheEntry{ | ||
| .timeoutMs = static_cast<uint64_t>(timer_.elapsed_ms()) + timeoutMs_, | ||
| .listIt = std::prev(timeoutList_.end()) | ||
| }); | ||
| } | ||
|
|
||
| private: | ||
| using InodeNamePair = std::pair<inode_t, std::string>; | ||
| using InodeNamePairPtr = std::shared_ptr<const InodeNamePair>; | ||
| using InodeNameLookupPair = std::pair<inode_t, std::string_view>; | ||
|
|
||
| struct NegativeCacheEntry { | ||
| uint64_t timeoutMs; | ||
| std::list<InodeNamePairPtr>::iterator listIt; | ||
| }; | ||
|
|
||
| struct InodeNameHash { | ||
| using is_transparent = void; // zero allocation for string | ||
|
|
||
| static uint64_t hashCompute(inode_t parent, std::string_view name) noexcept { | ||
| uint64_t seed = 0; | ||
| hashCombine(seed, parent, ByteArray(name.data(), name.size())); | ||
| return seed; | ||
| } | ||
|
|
||
| // Storage hash calculation | ||
| size_t operator()(const InodeNamePairPtr& obj) const noexcept { | ||
| return hashCompute(obj->first, obj->second); | ||
| } | ||
|
|
||
| // Lookup hash calculation | ||
| size_t operator()(const InodeNameLookupPair& obj) const noexcept { | ||
| return hashCompute(obj.first, obj.second); | ||
| } | ||
| }; | ||
|
|
||
| struct InodeNameEqual { | ||
| using is_transparent = void; // zero allocation for string | ||
|
|
||
| // Compare two stored pairs | ||
| bool operator()(const InodeNamePairPtr& lsp, | ||
| const InodeNamePairPtr& rsp) const noexcept { | ||
| return lsp->first == rsp->first && lsp->second == rsp->second; | ||
| } | ||
|
|
||
| // Compare stored pair against lookup pair | ||
| bool operator()(const InodeNamePairPtr& lsp, | ||
| const InodeNameLookupPair& rsp) const noexcept { | ||
| return lsp->first == rsp.first && lsp->second == rsp.second; | ||
| } | ||
|
|
||
| bool operator()(const InodeNameLookupPair& lsp, | ||
| const InodeNamePairPtr& rsp) const noexcept { | ||
| return lsp.first == rsp->first && lsp.second == rsp->second; | ||
| } | ||
| }; | ||
|
|
||
| mutable shared_mutex rwLock_; | ||
| std::unordered_map< | ||
| InodeNamePairPtr, | ||
| NegativeCacheEntry, | ||
| InodeNameHash, | ||
| InodeNameEqual | ||
| > negativeCache_; | ||
| Timer timer_; | ||
| size_t maxSize_; | ||
| uint32_t timeoutMs_; | ||
| std::list<InodeNamePairPtr> timeoutList_; | ||
| }; | ||
|
|
||
| inline NegativeCache gNegativeCache; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,102 @@ | ||
| /* | ||
|
|
||
| Copyright 2026 Leil Storage OÜ | ||
|
|
||
| This file is part of SaunaFS. | ||
|
|
||
| SaunaFS is free software: you can redistribute it and/or modify | ||
| it under the terms of the GNU General Public License as published by | ||
| the Free Software Foundation, version 3. | ||
|
|
||
| SaunaFS is distributed in the hope that it will be useful, | ||
| but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| GNU General Public License for more details. | ||
|
|
||
| You should have received a copy of the GNU General Public License | ||
| along with SaunaFS. If not, see <http://www.gnu.org/licenses/>. | ||
| */ | ||
|
|
||
| #include "mount/negative_cache.h" | ||
|
|
||
| #include <ranges> | ||
|
|
||
| #include <gtest/gtest.h> | ||
|
|
||
| class NegativeCacheTest : public testing::Test { | ||
ZaRdEr15 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| protected: | ||
| void SetUp() override { | ||
| NegativeCache::setGlobalMaxSize(kMaxSize); | ||
| negativeCache.setMaxSize(NegativeCache::getGlobalMaxSize()); | ||
| NegativeCache::setGlobalTimeoutMs(k1000ms); | ||
| negativeCache.setTimeoutMs(NegativeCache::getGlobalTimeoutMs()); | ||
| } | ||
|
|
||
| static constexpr uint32_t kMaxSize = 10; | ||
| static constexpr uint32_t k1000ms = 1000; | ||
| static constexpr inode_t kParent = 0; | ||
| static constexpr std::string_view kName = "some_file"; | ||
|
|
||
| NegativeCache negativeCache; | ||
| }; | ||
|
|
||
| TEST_F(NegativeCacheTest, InitiallyEmpty) { | ||
| EXPECT_EQ(negativeCache.size(), 0U); | ||
| } | ||
|
|
||
| TEST_F(NegativeCacheTest, SingleInsertion) { | ||
| negativeCache.add(kParent, kName); | ||
| EXPECT_EQ(negativeCache.size(), 1U); | ||
| } | ||
|
|
||
| TEST_F(NegativeCacheTest, RemoveOldestWithLRUUpdate) { | ||
| NegativeCache::setGlobalMaxSize(2); | ||
| negativeCache.setMaxSize(NegativeCache::getGlobalMaxSize()); | ||
| EXPECT_EQ(negativeCache.size(), 0U); | ||
| negativeCache.add(1, "A"); | ||
| negativeCache.add(1, "B"); | ||
| EXPECT_EQ(negativeCache.size(), 2U); | ||
| EXPECT_TRUE(negativeCache.lookup(1, "A")); | ||
| EXPECT_TRUE(negativeCache.lookup(1, "B")); | ||
|
|
||
| negativeCache.add(1, "A"); | ||
| EXPECT_EQ(negativeCache.size(), 2U); | ||
| EXPECT_TRUE(negativeCache.lookup(1, "A")); | ||
| EXPECT_TRUE(negativeCache.lookup(1, "B")); | ||
|
|
||
| negativeCache.add(1, "C"); | ||
| EXPECT_EQ(negativeCache.size(), 2U); | ||
| EXPECT_TRUE(negativeCache.lookup(1, "A")); | ||
| EXPECT_FALSE(negativeCache.lookup(1, "B")); | ||
| EXPECT_TRUE(negativeCache.lookup(1, "C")); | ||
| } | ||
|
|
||
| TEST_F(NegativeCacheTest, LookupSuccess) { | ||
| negativeCache.add(kParent, kName); | ||
| EXPECT_TRUE(negativeCache.lookup(kParent, kName)); | ||
| } | ||
|
|
||
| TEST_F(NegativeCacheTest, LookupTimeoutOrNotFound) { | ||
| NegativeCache::setGlobalTimeoutMs(0); | ||
| negativeCache.setTimeoutMs(NegativeCache::getGlobalTimeoutMs()); | ||
| negativeCache.add(kParent, kName); | ||
| EXPECT_FALSE(negativeCache.lookup(kParent, kName)); | ||
| EXPECT_FALSE(negativeCache.lookup(1, "not-added-to-ncache")); | ||
| } | ||
|
|
||
| TEST_F(NegativeCacheTest, TimeoutOrMaxSizeChangeClearAll) { | ||
| for (auto parent : std::views::iota(1U, kMaxSize)) { | ||
| negativeCache.add(parent, kName); | ||
| } | ||
| EXPECT_EQ(negativeCache.size(), 9U); | ||
| NegativeCache::setGlobalTimeoutMs(1001); | ||
| negativeCache.add(kParent, kName); | ||
| EXPECT_EQ(negativeCache.size(), 1U); | ||
| for (auto parent : std::views::iota(1U, kMaxSize)) { | ||
| negativeCache.add(parent, kName); | ||
| } | ||
| EXPECT_EQ(negativeCache.size(), 10U); | ||
| NegativeCache::setGlobalMaxSize(11); | ||
| negativeCache.add(kParent, kName); | ||
| EXPECT_EQ(negativeCache.size(), 1U); | ||
| } | ||
ZaRdEr15 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.