diff options
| author | Dan Engelbrecht <[email protected]> | 2023-05-08 10:53:06 +0200 |
|---|---|---|
| committer | GitHub <[email protected]> | 2023-05-08 10:53:06 +0200 |
| commit | 4d63d67beba80811543ad5e4cb5f6c61d5f6b720 (patch) | |
| tree | 82bf70f030d4e21a873e9064820f8e9886e31b73 /src | |
| parent | changelog (diff) | |
| download | zen-4d63d67beba80811543ad5e4cb5f6c61d5f6b720.tar.xz zen-4d63d67beba80811543ad5e4cb5f6c61d5f6b720.zip | |
project store gc lifetime (#257)
* keep track of last access time for project store projects and oplogs
* check size on disk for project store total size
* read/write access times to disk
* changelog
* change some std::filesystem::path -> const std::filesystem::path&
Diffstat (limited to 'src')
| -rw-r--r-- | src/zenserver/projectstore/projectstore.cpp | 388 | ||||
| -rw-r--r-- | src/zenserver/projectstore/projectstore.h | 39 |
2 files changed, 343 insertions, 84 deletions
diff --git a/src/zenserver/projectstore/projectstore.cpp b/src/zenserver/projectstore/projectstore.cpp index 847a79a1d..80e3f27f1 100644 --- a/src/zenserver/projectstore/projectstore.cpp +++ b/src/zenserver/projectstore/projectstore.cpp @@ -435,18 +435,23 @@ struct ProjectStore::OplogStorage : public RefCounted Flush(); } - [[nodiscard]] bool Exists() { return Exists(m_OplogStoragePath); } - [[nodiscard]] static bool Exists(std::filesystem::path BasePath) + [[nodiscard]] bool Exists() const { return Exists(m_OplogStoragePath); } + [[nodiscard]] static bool Exists(const std::filesystem::path& BasePath) { return std::filesystem::exists(BasePath / "ops.zlog") && std::filesystem::exists(BasePath / "ops.zops"); } - static bool Delete(std::filesystem::path BasePath) { return DeleteDirectories(BasePath); } + static bool Delete(const std::filesystem::path& BasePath) { return DeleteDirectories(BasePath); } - uint64_t OpBlobsSize() const + uint64_t OpBlobsSize() const { return OpBlobsSize(m_OplogStoragePath); } + static uint64_t OpBlobsSize(const std::filesystem::path& BasePath) { - RwLock::SharedLockScope _(m_RwLock); - return m_NextOpsOffset; + using namespace std::literals; + if (Exists(BasePath)) + { + return std::filesystem::file_size(BasePath / "ops.zlog"sv) + std::filesystem::file_size(BasePath / "ops.zops"sv); + } + return 0; } void Open(bool IsCreate) @@ -668,24 +673,24 @@ ProjectStore::Oplog::GatherReferences(GcContext& GcCtx) } uint64_t -ProjectStore::Oplog::TotalSize() const +ProjectStore::Oplog::TotalSize(const std::filesystem::path& BasePath) { - RwLock::SharedLockScope _(m_OplogLock); - if (m_Storage) + using namespace std::literals; + + uint64_t Size = OplogStorage::OpBlobsSize(BasePath); + std::filesystem::path StateFilePath = BasePath / "oplog.zcb"sv; + if (std::filesystem::exists(StateFilePath)) { - return m_Storage->OpBlobsSize(); + Size += std::filesystem::file_size(StateFilePath); } - return 0; + + return Size; } -bool -ProjectStore::Oplog::IsExpired() const +uint64_t +ProjectStore::Oplog::TotalSize() const { - if (m_MarkerPath.empty()) - { - return false; - } - return !std::filesystem::exists(m_MarkerPath); + return TotalSize(m_BasePath); } std::filesystem::path @@ -711,7 +716,7 @@ ProjectStore::Oplog::PrepareForDelete(bool MoveFolder) } bool -ProjectStore::Oplog::ExistsAt(std::filesystem::path BasePath) +ProjectStore::Oplog::ExistsAt(const std::filesystem::path& BasePath) { using namespace std::literals; @@ -1209,15 +1214,21 @@ ProjectStore::Project::Project(ProjectStore* PrjStore, CidStore& Store, std::fil : m_ProjectStore(PrjStore) , m_CidStore(Store) , m_OplogStoragePath(BasePath) +, m_LastAccessTimes({std::make_pair(std::string(), GcClock::TickCount())}) { } ProjectStore::Project::~Project() { + // Only write access times if we have not been explicitly deleted + if (!m_OplogStoragePath.empty()) + { + WriteAccessTimes(); + } } bool -ProjectStore::Project::Exists(std::filesystem::path BasePath) +ProjectStore::Project::Exists(const std::filesystem::path& BasePath) { return std::filesystem::exists(BasePath / "Project.zcb"); } @@ -1251,6 +1262,8 @@ ProjectStore::Project::Read() { ZEN_ERROR("validation error {} hit for '{}'", int(ValidationError), ProjectStateFilePath); } + + ReadAccessTimes(); } void @@ -1281,6 +1294,84 @@ ProjectStore::Project::Write() Blob.Flush(); } +void +ProjectStore::Project::ReadAccessTimes() +{ + using namespace std::literals; + + RwLock::SharedLockScope _(m_ProjectLock); + + std::filesystem::path ProjectAccessTimesFilePath = m_OplogStoragePath / "AccessTimes.zcb"sv; + if (!std::filesystem::exists(ProjectAccessTimesFilePath)) + { + return; + } + + ZEN_INFO("reading access times for project '{}' from {}", Identifier, ProjectAccessTimesFilePath); + + BasicFile Blob; + Blob.Open(ProjectAccessTimesFilePath, BasicFile::Mode::kRead); + + IoBuffer Obj = Blob.ReadAll(); + CbValidateError ValidationError = ValidateCompactBinary(MemoryView(Obj.Data(), Obj.Size()), CbValidateMode::All); + + if (ValidationError == CbValidateError::None) + { + CbObject Reader = LoadCompactBinaryObject(Obj); + CbArrayView LastAccessTimes = Reader["lastaccess"sv].AsArrayView(); + + for (CbFieldView& Entry : LastAccessTimes) + { + CbObjectView AccessTime = Entry.AsObjectView(); + std::string_view Id = AccessTime["id"sv].AsString(); + GcClock::Tick AccessTick = AccessTime["tick"sv].AsUInt64(); + m_LastAccessTimes.insert_or_assign(std::string(Id), AccessTick); + } + } + else + { + ZEN_ERROR("validation error {} hit for '{}'", int(ValidationError), ProjectAccessTimesFilePath); + } +} + +void +ProjectStore::Project::WriteAccessTimes() +{ + using namespace std::literals; + + RwLock::ExclusiveLockScope _(m_ProjectLock); + + BinaryWriter Mem; + + CbObjectWriter Writer; + Writer.BeginArray("lastaccess"); + { + for (const auto& It : m_LastAccessTimes) + { + Writer.BeginObject(); + { + Writer << "id"sv << It.first; + Writer << "tick"sv << gsl::narrow<uint64_t>(It.second); + } + Writer.EndObject(); + } + } + Writer.EndArray(); + + Writer.Save(Mem); + + CreateDirectories(m_OplogStoragePath); + + std::filesystem::path ProjectAccessTimesFilePath = m_OplogStoragePath / "AccessTimes.zcb"sv; + + ZEN_INFO("persisting access times for project '{}' to {}", Identifier, ProjectAccessTimesFilePath); + + BasicFile Blob; + Blob.Open(ProjectAccessTimesFilePath, BasicFile::Mode::kTruncate); + Blob.Write(Mem.Data(), Mem.Size(), 0); + Blob.Flush(); +} + spdlog::logger& ProjectStore::Project::Log() { @@ -1382,6 +1473,7 @@ ProjectStore::Project::DeleteOplog(std::string_view OplogId) m_DeletedOplogs.emplace_back(std::move(Oplog)); m_Oplogs.erase(OplogIt); } + m_LastAccessTimes.erase(std::string(OplogId)); } // Erase content on disk @@ -1432,6 +1524,7 @@ ProjectStore::Project::Flush() { // We only need to flush oplogs that we have already loaded IterateOplogs([&](Oplog& Ops) { Ops.Flush(); }); + WriteAccessTimes(); } void @@ -1444,7 +1537,7 @@ ProjectStore::Project::Scrub(ScrubContext& Ctx) OpenOplog(OpLogId); } IterateOplogs([&](const Oplog& Ops) { - if (!Ops.IsExpired()) + if (!IsExpired(GcClock::TimePoint::max(), Ops)) { Ops.Scrub(Ctx); } @@ -1467,8 +1560,21 @@ ProjectStore::Project::GatherReferences(GcContext& GcCtx) { OpenOplog(OpLogId); } + + { + // Make sure any oplog at least have a last access time so they eventually will be GC:d if not touched + RwLock::ExclusiveLockScope _(m_ProjectLock); + for (const std::string& OpId : OpLogs) + { + if (auto It = m_LastAccessTimes.find(OpId); It == m_LastAccessTimes.end()) + { + m_LastAccessTimes[OpId] = GcClock::TickCount(); + } + } + } + IterateOplogs([&](Oplog& Ops) { - if (!Ops.IsExpired()) + if (!IsExpired(GcCtx.ExpireTime(), Ops)) { Ops.GatherReferences(GcCtx); } @@ -1480,10 +1586,11 @@ ProjectStore::Project::TotalSize() const { uint64_t Result = 0; { - RwLock::SharedLockScope _(m_ProjectLock); - for (const auto& It : m_Oplogs) + std::vector<std::string> OpLogs = ScanForOplogs(); + for (const std::string& OpLogId : OpLogs) { - Result += It.second->TotalSize(); + std::filesystem::path OplogBasePath = m_OplogStoragePath / OpLogId; + Result += Oplog::TotalSize(OplogBasePath); } } return Result; @@ -1513,15 +1620,58 @@ ProjectStore::Project::PrepareForDelete(std::filesystem::path& OutDeletePath) } bool -ProjectStore::Project::IsExpired() const +ProjectStore::Project::IsExpired(const std::string& EntryName, + const std::filesystem::path& MarkerPath, + const GcClock::TimePoint ExpireTime) const { - if (ProjectFilePath.empty()) + if (!MarkerPath.empty()) { - return false; + if (std::filesystem::exists(MarkerPath)) + { + return false; + } } - return !std::filesystem::exists(ProjectFilePath); + + const GcClock::Tick ExpireTicks = ExpireTime.time_since_epoch().count(); + + RwLock::SharedLockScope _(m_ProjectLock); + if (auto It = m_LastAccessTimes.find(EntryName); It != m_LastAccessTimes.end()) + { + if (It->second <= ExpireTicks) + { + return true; + } + } + return false; +} + +bool +ProjectStore::Project::IsExpired(const GcClock::TimePoint ExpireTime) const +{ + return IsExpired(std::string(), ProjectFilePath, ExpireTime); } +bool +ProjectStore::Project::IsExpired(const GcClock::TimePoint ExpireTime, const ProjectStore::Oplog& Oplog) const +{ + return IsExpired(Oplog.OplogId(), Oplog.MarkerPath(), ExpireTime); +} + +void +ProjectStore::Project::TouchProject() const +{ + RwLock::ExclusiveLockScope _(m_ProjectLock); + m_LastAccessTimes.insert_or_assign(std::string(), GcClock::TickCount()); +}; + +void +ProjectStore::Project::TouchOplog(std::string_view Oplog) const +{ + ZEN_ASSERT(!Oplog.empty()); + RwLock::ExclusiveLockScope _(m_ProjectLock); + m_LastAccessTimes.insert_or_assign(std::string(Oplog), GcClock::TickCount()); +}; + ////////////////////////////////////////////////////////////////////////// ProjectStore::ProjectStore(CidStore& Store, std::filesystem::path BasePath, GcManager& Gc) @@ -1606,7 +1756,7 @@ ProjectStore::Scrub(ScrubContext& Ctx) for (auto& Kv : m_Projects) { - if (Kv.second->IsExpired()) + if (Kv.second->IsExpired(GcClock::TimePoint::max())) { continue; } @@ -1644,7 +1794,7 @@ ProjectStore::GatherReferences(GcContext& GcCtx) for (auto& Kv : m_Projects) { - if (Kv.second->IsExpired()) + if (Kv.second->IsExpired(GcCtx.ExpireTime())) { ExpiredProjectCount++; continue; @@ -1682,7 +1832,7 @@ ProjectStore::CollectGarbage(GcContext& GcCtx) RwLock::SharedLockScope _(m_ProjectsLock); for (auto& Kv : m_Projects) { - if (Kv.second->IsExpired()) + if (Kv.second->IsExpired(GcCtx.ExpireTime())) { ExpiredProjects.push_back(Kv.second); ExpiredProjectCount++; @@ -1704,8 +1854,8 @@ ProjectStore::CollectGarbage(GcContext& GcCtx) std::vector<std::string> ExpiredOplogs; { RwLock::ExclusiveLockScope _(m_ProjectsLock); - Project->IterateOplogs([&ExpiredOplogs](ProjectStore::Oplog& Oplog) { - if (Oplog.IsExpired()) + Project->IterateOplogs([&GcCtx, &Project, &ExpiredOplogs](ProjectStore::Oplog& Oplog) { + if (Project->IsExpired(GcCtx.ExpireTime(), Oplog)) { ExpiredOplogs.push_back(Oplog.OplogId()); } @@ -1718,6 +1868,7 @@ ProjectStore::CollectGarbage(GcContext& GcCtx) Project->Identifier); Project->DeleteOplog(OplogId); } + Project->Flush(); } if (ExpiredProjects.empty()) @@ -1732,7 +1883,7 @@ ProjectStore::CollectGarbage(GcContext& GcCtx) std::string ProjectId; { RwLock::ExclusiveLockScope _(m_ProjectsLock); - if (!Project->IsExpired()) + if (!Project->IsExpired(GcCtx.ExpireTime())) { ZEN_DEBUG("ProjectStore::CollectGarbage skipped garbage collect of project '{}'. Project no longer expired.", ProjectId); continue; @@ -1760,13 +1911,29 @@ ProjectStore::CollectGarbage(GcContext& GcCtx) GcStorageSize ProjectStore::StorageSize() const { + using namespace std::literals; + GcStorageSize Result; { - RwLock::SharedLockScope _(m_ProjectsLock); - for (auto& Kv : m_Projects) + if (std::filesystem::exists(m_ProjectBasePath)) { - const Ref<Project>& Project = Kv.second; - Result.DiskSize += Project->TotalSize(); + DirectoryContent ProjectsFolderContent; + GetDirectoryContent(m_ProjectBasePath, DirectoryContent::IncludeDirsFlag, ProjectsFolderContent); + + for (const std::filesystem::path& ProjectBasePath : ProjectsFolderContent.Directories) + { + std::filesystem::path ProjectStateFilePath = ProjectBasePath / "Project.zcb"sv; + if (std::filesystem::exists(ProjectStateFilePath)) + { + Result.DiskSize += std::filesystem::file_size(ProjectStateFilePath); + DirectoryContent DirContent; + GetDirectoryContent(ProjectBasePath, DirectoryContent::IncludeDirsFlag, DirContent); + for (const std::filesystem::path& OplogBasePath : DirContent.Directories) + { + Result.DiskSize += Oplog::TotalSize(OplogBasePath); + } + } + } } } return Result; @@ -1815,12 +1982,12 @@ ProjectStore::OpenProject(std::string_view ProjectId) } Ref<ProjectStore::Project> -ProjectStore::NewProject(std::filesystem::path BasePath, - std::string_view ProjectId, - std::string_view RootDir, - std::string_view EngineRootDir, - std::string_view ProjectRootDir, - std::string_view ProjectFilePath) +ProjectStore::NewProject(const std::filesystem::path& BasePath, + std::string_view ProjectId, + std::string_view RootDir, + std::string_view EngineRootDir, + std::string_view ProjectRootDir, + std::string_view ProjectFilePath) { RwLock::ExclusiveLockScope _(m_ProjectsLock); @@ -1907,13 +2074,14 @@ ProjectStore::GetProjectFiles(const std::string_view ProjectId, const std::strin { return {HttpResponseCode::NotFound, fmt::format("Project files request for unknown project '{}'", ProjectId)}; } + Project->TouchProject(); ProjectStore::Oplog* FoundLog = Project->OpenOplog(OplogId); - if (!FoundLog) { return {HttpResponseCode::NotFound, fmt::format("Project files for unknown oplog '{}/{}'", ProjectId, OplogId)}; } + Project->TouchOplog(OplogId); CbObjectWriter Response; Response.BeginArray("files"sv); @@ -1947,13 +2115,15 @@ ProjectStore::GetChunkInfo(const std::string_view ProjectId, { return {HttpResponseCode::NotFound, fmt::format("Chunk info request for unknown project '{}'", ProjectId)}; } + Project->TouchProject(); ProjectStore::Oplog* FoundLog = Project->OpenOplog(OplogId); - if (!FoundLog) { return {HttpResponseCode::NotFound, fmt::format("Chunk info request for unknown oplog '{}/{}'", ProjectId, OplogId)}; } + Project->TouchOplog(OplogId); + if (ChunkId.size() != 2 * sizeof(Oid::OidBits)) { return {HttpResponseCode::BadRequest, @@ -2000,13 +2170,14 @@ ProjectStore::GetChunkRange(const std::string_view ProjectId, { return {HttpResponseCode::NotFound, fmt::format("Chunk request for unknown project '{}'", ProjectId)}; } + Project->TouchProject(); ProjectStore::Oplog* FoundLog = Project->OpenOplog(OplogId); - if (!FoundLog) { return {HttpResponseCode::NotFound, fmt::format("Chunk request for unknown oplog '{}/{}'", ProjectId, OplogId)}; } + Project->TouchOplog(OplogId); if (ChunkId.size() != 2 * sizeof(Oid::OidBits)) { @@ -2090,13 +2261,14 @@ ProjectStore::GetChunk(const std::string_view ProjectId, { return {HttpResponseCode::NotFound, fmt::format("Chunk request for unknown project '{}'", ProjectId)}; } + Project->TouchProject(); ProjectStore::Oplog* FoundLog = Project->OpenOplog(OplogId); - if (!FoundLog) { return {HttpResponseCode::NotFound, fmt::format("Chunk request for unknown oplog '{}/{}'", ProjectId, OplogId)}; } + Project->TouchOplog(OplogId); if (Cid.length() != IoHash::StringLength) { @@ -2136,13 +2308,14 @@ ProjectStore::PutChunk(const std::string_view ProjectId, { return {HttpResponseCode::NotFound, fmt::format("Chunk put request for unknown project '{}'", ProjectId)}; } + Project->TouchProject(); ProjectStore::Oplog* FoundLog = Project->OpenOplog(OplogId); - if (!FoundLog) { return {HttpResponseCode::NotFound, fmt::format("Chunk put request for unknown oplog '{}/{}'", ProjectId, OplogId)}; } + Project->TouchOplog(OplogId); if (Cid.length() != IoHash::StringLength) { @@ -2175,13 +2348,14 @@ ProjectStore::WriteOplog(const std::string_view ProjectId, const std::string_vie { return {HttpResponseCode::NotFound, fmt::format("Write oplog request for unknown project '{}'", ProjectId)}; } + Project->TouchProject(); ProjectStore::Oplog* Oplog = Project->OpenOplog(OplogId); - if (!Oplog) { return {HttpResponseCode::NotFound, fmt::format("Write oplog request for unknown oplog '{}/{}'", ProjectId, OplogId)}; } + Project->TouchOplog(OplogId); CbObject ContainerObject = LoadCompactBinaryObject(Payload); if (!ContainerObject) @@ -2243,13 +2417,14 @@ ProjectStore::ReadOplog(const std::string_view ProjectId, { return {HttpResponseCode::NotFound, fmt::format("Read oplog request for unknown project '{}'", ProjectId)}; } + Project->TouchProject(); ProjectStore::Oplog* Oplog = Project->OpenOplog(OplogId); - if (!Oplog) { return {HttpResponseCode::NotFound, fmt::format("Read oplog request for unknown oplog '{}/{}'", ProjectId, OplogId)}; } + Project->TouchOplog(OplogId); size_t MaxBlockSize = 128u * 1024u * 1024u; if (auto Param = Params.GetValue("maxblocksize"); Param.empty() == false) @@ -2292,13 +2467,14 @@ ProjectStore::WriteBlock(const std::string_view ProjectId, const std::string_vie { return {HttpResponseCode::NotFound, fmt::format("Write block request for unknown project '{}'", ProjectId)}; } + Project->TouchProject(); ProjectStore::Oplog* Oplog = Project->OpenOplog(OplogId); - if (!Oplog) { return {HttpResponseCode::NotFound, fmt::format("Write block request for unknown oplog '{}/{}'", ProjectId, OplogId)}; } + Project->TouchOplog(OplogId); if (!IterateBlock(std::move(Payload), [this](CompressedBuffer&& Chunk, const IoHash& AttachmentRawHash) { IoBuffer Compressed = Chunk.GetCompressed().Flatten().AsIoBuffer(); @@ -2369,15 +2545,16 @@ ProjectStore::Rpc(HttpServerRequest& HttpReq, HttpContentType::kText, fmt::format("Rpc oplog request for unknown project '{}'", ProjectId)); } + Project->TouchProject(); ProjectStore::Oplog* Oplog = Project->OpenOplog(OplogId); - if (!Oplog) { return HttpReq.WriteResponse(HttpResponseCode::NotFound, HttpContentType::kText, fmt::format("Rpc oplog request for unknown oplog '{}/{}'", ProjectId, OplogId)); } + Project->TouchOplog(OplogId); std::string_view Method = Cb["method"sv].AsString(); @@ -2540,13 +2717,14 @@ HttpProjectService::HttpProjectService(CidStore& Store, ProjectStore* Projects, { return HttpReq.WriteResponse(HttpResponseCode::NotFound); } + Project->TouchProject(); ProjectStore::Oplog* FoundLog = Project->OpenOplog(OplogId); - if (!FoundLog) { return HttpReq.WriteResponse(HttpResponseCode::NotFound); } + Project->TouchOplog(OplogId); // Parse Request @@ -2879,13 +3057,14 @@ HttpProjectService::HttpProjectService(CidStore& Store, ProjectStore* Projects, { return HttpReq.WriteResponse(HttpResponseCode::NotFound); } + Project->TouchProject(); ProjectStore::Oplog* FoundLog = Project->OpenOplog(OplogId); - if (!FoundLog) { return HttpReq.WriteResponse(HttpResponseCode::NotFound); } + Project->TouchOplog(OplogId); // This operation takes a list of referenced hashes and decides which // chunks are not present on this server. This list is then returned in @@ -2948,13 +3127,14 @@ HttpProjectService::HttpProjectService(CidStore& Store, ProjectStore* Projects, { return HttpReq.WriteResponse(HttpResponseCode::NotFound); } + Project->TouchProject(); ProjectStore::Oplog* FoundLog = Project->OpenOplog(OplogId); - if (!FoundLog) { return HttpReq.WriteResponse(HttpResponseCode::NotFound); } + Project->TouchOplog(OplogId); ProjectStore::Oplog& Oplog = *FoundLog; @@ -3056,13 +3236,14 @@ HttpProjectService::HttpProjectService(CidStore& Store, ProjectStore* Projects, { return HttpReq.WriteResponse(HttpResponseCode::NotFound); } + Project->TouchProject(); ProjectStore::Oplog* FoundLog = Project->OpenOplog(OplogId); - if (!FoundLog) { return HttpReq.WriteResponse(HttpResponseCode::NotFound); } + Project->TouchOplog(OplogId); ProjectStore::Oplog& Oplog = *FoundLog; @@ -3149,26 +3330,27 @@ HttpProjectService::HttpProjectService(CidStore& Store, ProjectStore* Projects, HttpContentType::kText, fmt::format("project {} not found", ProjectId)); } + Project->TouchProject(); switch (Req.ServerRequest().RequestVerb()) { case HttpVerb::kGet: { ProjectStore::Oplog* OplogIt = Project->OpenOplog(OplogId); - if (!OplogIt) { return Req.ServerRequest().WriteResponse(HttpResponseCode::NotFound, HttpContentType::kText, fmt::format("oplog {} not found in project {}", OplogId, ProjectId)); } + Project->TouchOplog(OplogId); ProjectStore::Oplog& Log = *OplogIt; CbObjectWriter Cb; Cb << "id"sv << Log.OplogId() << "project"sv << Project->Identifier << "tempdir"sv << Log.TempPath().c_str() << "markerpath"sv << Log.MarkerPath().c_str() << "totalsize"sv << Log.TotalSize() << "opcount" - << Log.OplogCount() << "expired"sv << Log.IsExpired(); + << Log.OplogCount() << "expired"sv << Project->IsExpired(GcClock::TimePoint::max(), Log); Req.ServerRequest().WriteResponse(HttpResponseCode::OK, Cb.Save()); } @@ -3183,7 +3365,6 @@ HttpProjectService::HttpProjectService(CidStore& Store, ProjectStore* Projects, } ProjectStore::Oplog* OplogIt = Project->OpenOplog(OplogId); - if (!OplogIt) { if (!Project->NewOplog(OplogId, OplogMarkerPath)) @@ -3191,6 +3372,7 @@ HttpProjectService::HttpProjectService(CidStore& Store, ProjectStore* Projects, // TODO: indicate why the operation failed! return Req.ServerRequest().WriteResponse(HttpResponseCode::InternalServerError); } + Project->TouchOplog(OplogId); ZEN_INFO("established oplog '{}/{}', gc marker file at '{}'", ProjectId, OplogId, OplogMarkerPath); @@ -3233,13 +3415,14 @@ HttpProjectService::HttpProjectService(CidStore& Store, ProjectStore* Projects, { return HttpReq.WriteResponse(HttpResponseCode::NotFound); } + Project->TouchProject(); ProjectStore::Oplog* FoundLog = Project->OpenOplog(OplogId); - if (!FoundLog) { return HttpReq.WriteResponse(HttpResponseCode::NotFound); } + Project->TouchOplog(OplogId); CbObjectWriter Response; @@ -3311,13 +3494,13 @@ HttpProjectService::HttpProjectService(CidStore& Store, ProjectStore* Projects, case HttpVerb::kGet: { Ref<ProjectStore::Project> Project = m_ProjectStore->OpenProject(ProjectId); - if (!Project) { return Req.ServerRequest().WriteResponse(HttpResponseCode::NotFound, HttpContentType::kText, fmt::format("project {} not found", ProjectId)); } + Project->TouchProject(); std::vector<std::string> OpLogs = Project->ScanForOplogs(); @@ -3344,7 +3527,6 @@ HttpProjectService::HttpProjectService(CidStore& Store, ProjectStore* Projects, case HttpVerb::kDelete: { Ref<ProjectStore::Project> Project = m_ProjectStore->OpenProject(ProjectId); - if (!Project) { return Req.ServerRequest().WriteResponse(HttpResponseCode::NotFound, @@ -3510,6 +3692,7 @@ HttpProjectService::HttpProjectService(CidStore& Store, ProjectStore* Projects, return HttpReq.WriteResponse(HttpResponseCode::NotFound); } ProjectStore::Project& Project = *FoundProject.Get(); + if (CSV) { ExtendableStringBuilder<4096> CSVWriter; @@ -3556,8 +3739,8 @@ HttpProjectService::HttpProjectService(CidStore& Store, ProjectStore* Projects, { return HttpReq.WriteResponse(HttpResponseCode::NotFound); } - ProjectStore::Oplog* FoundLog = FoundProject->OpenOplog(OplogId); + ProjectStore::Oplog* FoundLog = FoundProject->OpenOplog(OplogId); if (!FoundLog) { return HttpReq.WriteResponse(HttpResponseCode::NotFound); @@ -3608,8 +3791,8 @@ HttpProjectService::HttpProjectService(CidStore& Store, ProjectStore* Projects, { return HttpReq.WriteResponse(HttpResponseCode::NotFound); } - ProjectStore::Oplog* FoundLog = FoundProject->OpenOplog(OplogId); + ProjectStore::Oplog* FoundLog = FoundProject->OpenOplog(OplogId); if (!FoundLog) { return HttpReq.WriteResponse(HttpResponseCode::NotFound); @@ -3885,6 +4068,12 @@ TEST_CASE("project.store.gc") BasicFile ProjectFile; ProjectFile.Open(Project1FilePath, BasicFile::Mode::kTruncate); } + std::filesystem::path Project1OplogPath = TempDir.Path() / "game1" / "saves" / "cooked" / ".projectstore"; + { + CreateDirectories(Project1OplogPath.parent_path()); + BasicFile OplogFile; + OplogFile.Open(Project1OplogPath, BasicFile::Mode::kTruncate); + } std::filesystem::path Project2RootDir = TempDir.Path() / "game2"; std::filesystem::path Project2FilePath = TempDir.Path() / "game2" / "game.uproject"; @@ -3893,6 +4082,12 @@ TEST_CASE("project.store.gc") BasicFile ProjectFile; ProjectFile.Open(Project2FilePath, BasicFile::Mode::kTruncate); } + std::filesystem::path Project2OplogPath = TempDir.Path() / "game1" / "saves" / "cooked" / ".projectstore"; + { + CreateDirectories(Project2OplogPath.parent_path()); + BasicFile OplogFile; + OplogFile.Open(Project2OplogPath, BasicFile::Mode::kTruncate); + } { Ref<ProjectStore::Project> Project1(ProjectStore.NewProject(BasePath / "proj1"sv, @@ -3901,7 +4096,7 @@ TEST_CASE("project.store.gc") EngineRootDir.string(), Project1RootDir.string(), Project1FilePath.string())); - ProjectStore::Oplog* Oplog = Project1->NewOplog("oplog1", {}); + ProjectStore::Oplog* Oplog = Project1->NewOplog("oplog1", Project1OplogPath); CHECK(Oplog != nullptr); Oplog->AppendNewOplogEntry(CreateOplogPackage(Oid::NewOid(), {})); @@ -3917,7 +4112,7 @@ TEST_CASE("project.store.gc") EngineRootDir.string(), Project2RootDir.string(), Project2FilePath.string())); - ProjectStore::Oplog* Oplog = Project2->NewOplog("oplog1", {}); + ProjectStore::Oplog* Oplog = Project2->NewOplog("oplog2", Project2OplogPath); CHECK(Oplog != nullptr); Oplog->AppendNewOplogEntry(CreateOplogPackage(Oid::NewOid(), {})); @@ -3937,6 +4132,17 @@ TEST_CASE("project.store.gc") CHECK(ProjectStore.OpenProject("proj2"sv)); } + { + GcContext GcCtx(GcClock::Now() + std::chrono::hours(24)); + ProjectStore.GatherReferences(GcCtx); + size_t RefCount = 0; + GcCtx.IterateCids([&RefCount](const IoHash&) { RefCount++; }); + CHECK(RefCount == 14); + ProjectStore.CollectGarbage(GcCtx); + CHECK(ProjectStore.OpenProject("proj1"sv)); + CHECK(ProjectStore.OpenProject("proj2"sv)); + } + std::filesystem::remove(Project1FilePath); { @@ -3944,11 +4150,57 @@ TEST_CASE("project.store.gc") ProjectStore.GatherReferences(GcCtx); size_t RefCount = 0; GcCtx.IterateCids([&RefCount](const IoHash&) { RefCount++; }); + CHECK(RefCount == 14); + ProjectStore.CollectGarbage(GcCtx); + CHECK(ProjectStore.OpenProject("proj1"sv)); + CHECK(ProjectStore.OpenProject("proj2"sv)); + } + + { + GcContext GcCtx(GcClock::Now() + std::chrono::hours(24)); + ProjectStore.GatherReferences(GcCtx); + size_t RefCount = 0; + GcCtx.IterateCids([&RefCount](const IoHash&) { RefCount++; }); CHECK(RefCount == 7); ProjectStore.CollectGarbage(GcCtx); CHECK(!ProjectStore.OpenProject("proj1"sv)); CHECK(ProjectStore.OpenProject("proj2"sv)); } + + std::filesystem::remove(Project2OplogPath); + { + GcContext GcCtx(GcClock::Now() - std::chrono::hours(24)); + ProjectStore.GatherReferences(GcCtx); + size_t RefCount = 0; + GcCtx.IterateCids([&RefCount](const IoHash&) { RefCount++; }); + CHECK(RefCount == 7); + ProjectStore.CollectGarbage(GcCtx); + CHECK(!ProjectStore.OpenProject("proj1"sv)); + CHECK(ProjectStore.OpenProject("proj2"sv)); + } + + { + GcContext GcCtx(GcClock::Now() + std::chrono::hours(24)); + ProjectStore.GatherReferences(GcCtx); + size_t RefCount = 0; + GcCtx.IterateCids([&RefCount](const IoHash&) { RefCount++; }); + CHECK(RefCount == 0); + ProjectStore.CollectGarbage(GcCtx); + CHECK(!ProjectStore.OpenProject("proj1"sv)); + CHECK(ProjectStore.OpenProject("proj2"sv)); + } + + std::filesystem::remove(Project2FilePath); + { + GcContext GcCtx(GcClock::Now() + std::chrono::hours(24)); + ProjectStore.GatherReferences(GcCtx); + size_t RefCount = 0; + GcCtx.IterateCids([&RefCount](const IoHash&) { RefCount++; }); + CHECK(RefCount == 0); + ProjectStore.CollectGarbage(GcCtx); + CHECK(!ProjectStore.OpenProject("proj1"sv)); + CHECK(!ProjectStore.OpenProject("proj2"sv)); + } } TEST_CASE("project.store.partial.read") diff --git a/src/zenserver/projectstore/projectstore.h b/src/zenserver/projectstore/projectstore.h index be3ee7075..7b0c96205 100644 --- a/src/zenserver/projectstore/projectstore.h +++ b/src/zenserver/projectstore/projectstore.h @@ -74,7 +74,7 @@ public: const std::filesystem::path& MarkerPath); ~Oplog(); - [[nodiscard]] static bool ExistsAt(std::filesystem::path BasePath); + [[nodiscard]] static bool ExistsAt(const std::filesystem::path& BasePath); void Read(); void Write(); @@ -113,6 +113,7 @@ public: void Flush(); void Scrub(ScrubContext& Ctx) const; void GatherReferences(GcContext& GcCtx); + static uint64_t TotalSize(const std::filesystem::path& BasePath); uint64_t TotalSize() const; std::size_t OplogCount() const @@ -121,7 +122,6 @@ public: return m_LatestOpMap.size(); } - bool IsExpired() const; std::filesystem::path PrepareForDelete(bool MoveFolder); private: @@ -208,14 +208,17 @@ public: void IterateOplogs(std::function<void(const Oplog&)>&& Fn) const; void IterateOplogs(std::function<void(Oplog&)>&& Fn); std::vector<std::string> ScanForOplogs() const; - bool IsExpired() const; + bool IsExpired(const GcClock::TimePoint ExpireTime) const; + bool IsExpired(const GcClock::TimePoint ExpireTime, const ProjectStore::Oplog& Oplog) const; + void TouchProject() const; + void TouchOplog(std::string_view Oplog) const; Project(ProjectStore* PrjStore, CidStore& Store, std::filesystem::path BasePath); virtual ~Project(); void Read(); void Write(); - [[nodiscard]] static bool Exists(std::filesystem::path BasePath); + [[nodiscard]] static bool Exists(const std::filesystem::path& BasePath); void Flush(); void Scrub(ScrubContext& Ctx); spdlog::logger& Log(); @@ -224,25 +227,29 @@ public: bool PrepareForDelete(std::filesystem::path& OutDeletePath); private: - ProjectStore* m_ProjectStore; - CidStore& m_CidStore; - mutable RwLock m_ProjectLock; - std::map<std::string, std::unique_ptr<Oplog>> m_Oplogs; - std::vector<std::unique_ptr<Oplog>> m_DeletedOplogs; - std::filesystem::path m_OplogStoragePath; + ProjectStore* m_ProjectStore; + CidStore& m_CidStore; + mutable RwLock m_ProjectLock; + std::map<std::string, std::unique_ptr<Oplog>> m_Oplogs; + std::vector<std::unique_ptr<Oplog>> m_DeletedOplogs; + std::filesystem::path m_OplogStoragePath; + mutable tsl::robin_map<std::string, GcClock::Tick> m_LastAccessTimes; std::filesystem::path BasePathForOplog(std::string_view OplogId); + bool IsExpired(const std::string& EntryName, const std::filesystem::path& MarkerPath, const GcClock::TimePoint ExpireTime) const; + void WriteAccessTimes(); + void ReadAccessTimes(); }; // Oplog* OpenProjectOplog(std::string_view ProjectId, std::string_view OplogId); Ref<Project> OpenProject(std::string_view ProjectId); - Ref<Project> NewProject(std::filesystem::path BasePath, - std::string_view ProjectId, - std::string_view RootDir, - std::string_view EngineRootDir, - std::string_view ProjectRootDir, - std::string_view ProjectFilePath); + Ref<Project> NewProject(const std::filesystem::path& BasePath, + std::string_view ProjectId, + std::string_view RootDir, + std::string_view EngineRootDir, + std::string_view ProjectRootDir, + std::string_view ProjectFilePath); bool DeleteProject(std::string_view ProjectId); bool Exists(std::string_view ProjectId); void Flush(); |