aboutsummaryrefslogtreecommitdiff
path: root/zenserver/projectstore.cpp
diff options
context:
space:
mode:
authorDan Engelbrecht <[email protected]>2022-10-17 10:48:32 +0200
committerGitHub <[email protected]>2022-10-17 01:48:32 -0700
commit2493c28b434374c00fa06a928fae8a698a846cb9 (patch)
tree9c366fd7adc96ff2ca4ff43a6ecf9e593ab368f6 /zenserver/projectstore.cpp
parentAdd "Accept" field in RPC request to gracefully handle requests from older in... (diff)
downloadzen-2493c28b434374c00fa06a928fae8a698a846cb9.tar.xz
zen-2493c28b434374c00fa06a928fae8a698a846cb9.zip
fix concurrency issues in projectstore and enable GC (#181)
* Fix concurreny issues when deleting projects/oplogs * remove rocksdb test code * project store unit tests * safer deletion of oplogs/projects * reference count ProjectStore::Project to handle lifetime during GC * Don't open all project oplogs unless we need them * Don't scrub expired projects * Don't gather references from expired projects * added logging details for GC * release lock as soon as folder is moved * more tests for project store * changelog
Diffstat (limited to 'zenserver/projectstore.cpp')
-rw-r--r--zenserver/projectstore.cpp831
1 files changed, 594 insertions, 237 deletions
diff --git a/zenserver/projectstore.cpp b/zenserver/projectstore.cpp
index ab407f868..d953651af 100644
--- a/zenserver/projectstore.cpp
+++ b/zenserver/projectstore.cpp
@@ -21,31 +21,50 @@
#include "config.h"
-#if ZEN_PLATFORM_WINDOWS
-# include <zencore/windows.h>
-#endif
-
-#define USE_ROCKSDB 0
-
ZEN_THIRD_PARTY_INCLUDES_START
-
-#if USE_ROCKSDB
-# pragma comment(lib, "Rpcrt4.lib") // RocksDB made me do this
-# include <rocksdb/db.h>
-#endif
-
#include <xxh3.h>
-#include <asio.hpp>
ZEN_THIRD_PARTY_INCLUDES_END
-#include <latch>
-#include <string>
+#if ZEN_WITH_TESTS
+#endif // ZEN_WITH_TESTS
namespace zen {
-#if USE_ROCKSDB
-namespace rocksdb = ROCKSDB_NAMESPACE;
-#endif
+namespace {
+ bool PrepareDirectoryDelete(const std::filesystem::path& Dir, std::filesystem::path& OutDeleteDir)
+ {
+ int DropIndex = 0;
+ do
+ {
+ if (!std::filesystem::exists(Dir))
+ {
+ return true;
+ }
+
+ std::string DroppedName = fmt::format("[dropped]{}({})", Dir.filename().string(), DropIndex);
+ std::filesystem::path DroppedBucketPath = Dir.parent_path() / DroppedName;
+ if (std::filesystem::exists(DroppedBucketPath))
+ {
+ DropIndex++;
+ continue;
+ }
+
+ std::error_code Ec;
+ std::filesystem::rename(Dir, DroppedBucketPath, Ec);
+ if (!Ec)
+ {
+ OutDeleteDir = DroppedBucketPath;
+ return true;
+ }
+ if (Ec && !std::filesystem::exists(DroppedBucketPath))
+ {
+ // We can't move our folder, probably because it is busy, bail..
+ return false;
+ }
+ zen::Sleep(100);
+ } while (true);
+ }
+} // namespace
//////////////////////////////////////////////////////////////////////////
@@ -79,24 +98,6 @@ struct ProjectStore::OplogStorage : public RefCounted
{
ZEN_INFO("closing oplog storage at {}", m_OplogStoragePath);
Flush();
-
-#if USE_ROCKSDB
- if (m_RocksDb)
- {
- // Column families must be torn down before database is closed
- for (const auto& Handle : m_RocksDbColumnHandles)
- {
- m_RocksDb->DestroyColumnFamilyHandle(Handle);
- }
-
- rocksdb::Status Status = m_RocksDb->Close();
-
- if (!Status.ok())
- {
- ZEN_WARN("db close error reported for '{}' : '{}'", m_OplogStoragePath, Status.getState());
- }
- }
-#endif
}
[[nodiscard]] bool Exists() { return Exists(m_OplogStoragePath); }
@@ -126,51 +127,6 @@ struct ProjectStore::OplogStorage : public RefCounted
ZEN_ASSERT(IsPow2(m_OpsAlign));
ZEN_ASSERT(!(m_NextOpsOffset & (m_OpsAlign - 1)));
-
-#if USE_ROCKSDB
- {
- std::string RocksdbPath = PathToUtf8(m_OplogStoragePath / "ops.rdb");
-
- ZEN_DEBUG("opening rocksdb db at '{}'", RocksdbPath);
-
- rocksdb::DB* Db;
- rocksdb::DBOptions Options;
- Options.create_if_missing = true;
-
- std::vector<std::string> ExistingColumnFamilies;
- rocksdb::Status Status = rocksdb::DB::ListColumnFamilies(Options, RocksdbPath, &ExistingColumnFamilies);
-
- std::vector<rocksdb::ColumnFamilyDescriptor> ColumnDescriptors;
-
- if (Status.IsPathNotFound())
- {
- ColumnDescriptors.emplace_back(rocksdb::ColumnFamilyDescriptor{rocksdb::kDefaultColumnFamilyName, {}});
- }
- else if (Status.ok())
- {
- for (const std::string& Column : ExistingColumnFamilies)
- {
- rocksdb::ColumnFamilyDescriptor ColumnFamily;
- ColumnFamily.name = Column;
- ColumnDescriptors.push_back(ColumnFamily);
- }
- }
- else
- {
- throw std::runtime_error(
- fmt::format("column family iteration failed for '{}': '{}'", RocksdbPath, Status.getState()).c_str());
- }
-
- Status = rocksdb::DB::Open(Options, RocksdbPath, ColumnDescriptors, &m_RocksDbColumnHandles, &Db);
-
- if (!Status.ok())
- {
- throw std::runtime_error(fmt::format("database open failed for '{}': '{}'", RocksdbPath, Status.getState()).c_str());
- }
-
- m_RocksDb.reset(Db);
- }
-#endif
}
void ReplayLog(std::function<void(CbObject, const OplogEntry&)>&& Handler)
@@ -301,11 +257,6 @@ private:
std::atomic<uint64_t> m_NextOpsOffset{0};
uint64_t m_OpsAlign = 32;
std::atomic<uint32_t> m_MaxLsn{0};
-
-#if USE_ROCKSDB
- std::unique_ptr<rocksdb::DB> m_RocksDb;
- std::vector<rocksdb::ColumnFamilyHandle*> m_RocksDbColumnHandles;
-#endif
};
//////////////////////////////////////////////////////////////////////////
@@ -329,12 +280,16 @@ ProjectStore::Oplog::Oplog(std::string_view Id, Project* Project, CidStore& Stor
ProjectStore::Oplog::~Oplog()
{
- Flush();
+ if (m_Storage)
+ {
+ Flush();
+ }
}
void
ProjectStore::Oplog::Flush()
{
+ ZEN_ASSERT(m_Storage);
m_Storage->Flush();
}
@@ -350,6 +305,7 @@ ProjectStore::Oplog::GatherReferences(GcContext& GcCtx)
RwLock::SharedLockScope _(m_OplogLock);
std::vector<IoHash> Hashes;
+ Hashes.reserve(Max(m_ChunkMap.size(), m_MetaMap.size()));
for (const auto& Kv : m_ChunkMap)
{
@@ -368,6 +324,28 @@ ProjectStore::Oplog::GatherReferences(GcContext& GcCtx)
GcCtx.AddRetainedCids(Hashes);
}
+std::filesystem::path
+ProjectStore::Oplog::PrepareForDelete(bool MoveFolder)
+{
+ RwLock::ExclusiveLockScope _(m_OplogLock);
+ m_ChunkMap.clear();
+ m_MetaMap.clear();
+ m_FileMap.clear();
+ m_OpAddressMap.clear();
+ m_LatestOpMap.clear();
+ m_Storage = {};
+ if (!MoveFolder)
+ {
+ return {};
+ }
+ std::filesystem::path MovedDir;
+ if (PrepareDirectoryDelete(m_BasePath, MovedDir))
+ {
+ return MovedDir;
+ }
+ return {};
+}
+
bool
ProjectStore::Oplog::ExistsAt(std::filesystem::path BasePath)
{
@@ -383,12 +361,12 @@ ProjectStore::Oplog::ReplayLog()
IoBuffer
ProjectStore::Oplog::FindChunk(Oid ChunkId)
{
- RwLock::SharedLockScope _(m_OplogLock);
+ RwLock::SharedLockScope OplogLock(m_OplogLock);
if (auto ChunkIt = m_ChunkMap.find(ChunkId); ChunkIt != m_ChunkMap.end())
{
IoHash ChunkHash = ChunkIt->second;
- _.ReleaseNow();
+ OplogLock.ReleaseNow();
IoBuffer Chunk = m_CidStore.FindChunkByCid(ChunkHash);
Chunk.SetContentType(ZenContentType::kCompressedBinary);
@@ -400,7 +378,7 @@ ProjectStore::Oplog::FindChunk(Oid ChunkId)
{
std::filesystem::path FilePath = m_OuterProject->RootDir / FileIt->second.ServerPath;
- _.ReleaseNow();
+ OplogLock.ReleaseNow();
IoBuffer FileChunk = IoBufferBuilder::MakeFromFile(FilePath);
FileChunk.SetContentType(ZenContentType::kBinary);
@@ -411,7 +389,7 @@ ProjectStore::Oplog::FindChunk(Oid ChunkId)
if (auto MetaIt = m_MetaMap.find(ChunkId); MetaIt != m_MetaMap.end())
{
IoHash ChunkHash = MetaIt->second;
- _.ReleaseNow();
+ OplogLock.ReleaseNow();
IoBuffer Chunk = m_CidStore.FindChunkByCid(ChunkHash);
Chunk.SetContentType(ZenContentType::kCompressedBinary);
@@ -487,10 +465,12 @@ ProjectStore::Oplog::GetOpByIndex(int Index)
}
bool
-ProjectStore::Oplog::AddFileMapping(Oid FileId, IoHash Hash, std::string_view ServerPath, std::string_view ClientPath)
+ProjectStore::Oplog::AddFileMapping(const RwLock::ExclusiveLockScope&,
+ Oid FileId,
+ IoHash Hash,
+ std::string_view ServerPath,
+ std::string_view ClientPath)
{
- // NOTE: Caller must hold an exclusive lock on m_OplogLock
-
if (ServerPath.empty() || ClientPath.empty())
{
return false;
@@ -511,18 +491,14 @@ ProjectStore::Oplog::AddFileMapping(Oid FileId, IoHash Hash, std::string_view Se
}
void
-ProjectStore::Oplog::AddChunkMapping(Oid ChunkId, IoHash Hash)
+ProjectStore::Oplog::AddChunkMapping(const RwLock::ExclusiveLockScope&, Oid ChunkId, IoHash Hash)
{
- // NOTE: Caller must hold an exclusive lock on m_OplogLock
-
m_ChunkMap.insert_or_assign(ChunkId, Hash);
}
void
-ProjectStore::Oplog::AddMetaMapping(Oid ChunkId, IoHash Hash)
+ProjectStore::Oplog::AddMetaMapping(const RwLock::ExclusiveLockScope&, Oid ChunkId, IoHash Hash)
{
- // NOTE: Caller must hold an exclusive lock on m_OplogLock
-
m_MetaMap.insert_or_assign(ChunkId, Hash);
}
@@ -536,7 +512,7 @@ ProjectStore::Oplog::RegisterOplogEntry(CbObject Core, const OplogEntry& OpEntry
// For now we're assuming the update is all in-memory so we can hold an exclusive lock without causing
// too many problems. Longer term we'll probably want to ensure we can do concurrent updates however
- RwLock::ExclusiveLockScope _(m_OplogLock);
+ RwLock::ExclusiveLockScope OplogLock(m_OplogLock);
using namespace std::literals;
@@ -548,7 +524,7 @@ ProjectStore::Oplog::RegisterOplogEntry(CbObject Core, const OplogEntry& OpEntry
Oid PackageId = PkgObj["id"sv].AsObjectId();
IoHash PackageHash = PkgObj["data"sv].AsBinaryAttachment();
- AddChunkMapping(PackageId, PackageHash);
+ AddChunkMapping(OplogLock, PackageId, PackageHash);
ZEN_DEBUG("package data {} -> {}", PackageId, PackageHash);
}
@@ -560,7 +536,7 @@ ProjectStore::Oplog::RegisterOplogEntry(CbObject Core, const OplogEntry& OpEntry
Oid BulkDataId = BulkObj["id"sv].AsObjectId();
IoHash BulkDataHash = BulkObj["data"sv].AsBinaryAttachment();
- AddChunkMapping(BulkDataId, BulkDataHash);
+ AddChunkMapping(OplogLock, BulkDataId, BulkDataHash);
ZEN_DEBUG("bulkdata {} -> {}", BulkDataId, BulkDataHash);
}
@@ -568,7 +544,8 @@ ProjectStore::Oplog::RegisterOplogEntry(CbObject Core, const OplogEntry& OpEntry
if (Core["files"sv])
{
Stopwatch Timer;
- int32_t FileCount = 0;
+ int32_t FileCount = 0;
+ int32_t ChunkCount = 0;
for (CbFieldView& Entry : Core["files"sv])
{
@@ -578,7 +555,7 @@ ProjectStore::Oplog::RegisterOplogEntry(CbObject Core, const OplogEntry& OpEntry
std::string_view ServerPath = FileObj["serverpath"sv].AsString();
std::string_view ClientPath = FileObj["clientpath"sv].AsString();
- if (AddFileMapping(FileId, FileDataHash, ServerPath, ClientPath))
+ if (AddFileMapping(OplogLock, FileId, FileDataHash, ServerPath, ClientPath))
{
++FileCount;
}
@@ -588,7 +565,11 @@ ProjectStore::Oplog::RegisterOplogEntry(CbObject Core, const OplogEntry& OpEntry
}
}
- ZEN_DEBUG("added {} file(s) in {}", FileCount, NiceTimeSpanMs(Timer.GetElapsedTimeMs()));
+ ZEN_INFO("added {} file(s), {} as files and {} as chunks in {}",
+ FileCount + ChunkCount,
+ FileCount,
+ ChunkCount,
+ NiceTimeSpanMs(Timer.GetElapsedTimeMs()));
}
for (CbFieldView& Entry : Core["meta"sv])
@@ -598,7 +579,7 @@ ProjectStore::Oplog::RegisterOplogEntry(CbObject Core, const OplogEntry& OpEntry
auto NameString = MetaObj["name"sv].AsString();
IoHash MetaDataHash = MetaObj["data"sv].AsBinaryAttachment();
- AddMetaMapping(MetaId, MetaDataHash);
+ AddMetaMapping(OplogLock, MetaId, MetaDataHash);
ZEN_DEBUG("meta data ({}) {} -> {}", NameString, MetaId, MetaDataHash);
}
@@ -614,6 +595,8 @@ ProjectStore::Oplog::AppendNewOplogEntry(CbPackage OpPackage)
{
ZEN_TRACE_CPU("ProjectStore::Oplog::AppendNewOplogEntry");
+ ZEN_ASSERT(m_Storage);
+
using namespace std::literals;
const CbObject& Core = OpPackage.GetObject();
@@ -750,9 +733,11 @@ ProjectStore::Project::NewOplog(std::string_view OplogId)
try
{
- Oplog& Log = m_Oplogs.try_emplace(std::string{OplogId}, OplogId, this, m_CidStore, OplogBasePath).first->second;
+ Oplog* Log =
+ m_Oplogs.try_emplace(std::string{OplogId}, std::make_unique<ProjectStore::Oplog>(OplogId, this, m_CidStore, OplogBasePath))
+ .first->second.get();
- return &Log;
+ return Log;
}
catch (std::exception&)
{
@@ -776,7 +761,7 @@ ProjectStore::Project::OpenOplog(std::string_view OplogId)
if (OplogIt != m_Oplogs.end())
{
- return &OplogIt->second;
+ return OplogIt->second.get();
}
}
@@ -790,11 +775,13 @@ ProjectStore::Project::OpenOplog(std::string_view OplogId)
try
{
- Oplog& Log = m_Oplogs.try_emplace(std::string{OplogId}, OplogId, this, m_CidStore, OplogBasePath).first->second;
+ Oplog* Log =
+ m_Oplogs.try_emplace(std::string{OplogId}, std::make_unique<ProjectStore::Oplog>(OplogId, this, m_CidStore, OplogBasePath))
+ .first->second.get();
- Log.ReplayLog();
+ Log->ReplayLog();
- return &Log;
+ return Log;
}
catch (std::exception& ex)
{
@@ -810,6 +797,7 @@ ProjectStore::Project::OpenOplog(std::string_view OplogId)
void
ProjectStore::Project::DeleteOplog(std::string_view OplogId)
{
+ std::filesystem::path DeletePath;
{
RwLock::ExclusiveLockScope _(m_ProjectLock);
@@ -817,83 +805,114 @@ ProjectStore::Project::DeleteOplog(std::string_view OplogId)
if (OplogIt != m_Oplogs.end())
{
+ std::unique_ptr<Oplog>& Oplog = OplogIt->second;
+ DeletePath = Oplog->PrepareForDelete(true);
+ m_DeletedOplogs.emplace_back(std::move(Oplog));
m_Oplogs.erase(OplogIt);
}
}
- // Actually erase
-
- std::filesystem::path OplogBasePath = BasePathForOplog(OplogId);
-
- OplogStorage::Delete(OplogBasePath);
+ // Erase content on disk
+ if (!DeletePath.empty())
+ {
+ OplogStorage::Delete(DeletePath);
+ }
}
-void
-ProjectStore::Project::DiscoverOplogs()
+std::vector<std::string>
+ProjectStore::Project::ScanForOplogs() const
{
DirectoryContent DirContent;
GetDirectoryContent(m_OplogStoragePath, DirectoryContent::IncludeDirsFlag, DirContent);
-
+ std::vector<std::string> Oplogs;
+ Oplogs.reserve(DirContent.Directories.size());
for (const std::filesystem::path& DirPath : DirContent.Directories)
{
- OpenOplog(PathToUtf8(DirPath.filename()));
+ Oplogs.push_back(DirPath.filename().string());
}
+ return Oplogs;
}
void
ProjectStore::Project::IterateOplogs(std::function<void(const Oplog&)>&& Fn) const
{
- // TODO: should also iterate over oplogs which are present on disk but not yet loaded
-
RwLock::SharedLockScope _(m_ProjectLock);
for (auto& Kv : m_Oplogs)
{
- Fn(Kv.second);
+ Fn(*Kv.second);
}
}
void
ProjectStore::Project::IterateOplogs(std::function<void(Oplog&)>&& Fn)
{
- // TODO: should also iterate over oplogs which are present on disk but not yet loaded
-
RwLock::SharedLockScope _(m_ProjectLock);
for (auto& Kv : m_Oplogs)
{
- Fn(Kv.second);
+ Fn(*Kv.second);
}
}
void
ProjectStore::Project::Flush()
{
- // TODO
+ // We only need to flush oplogs that we have already loaded
+ IterateOplogs([&](Oplog& Ops) { Ops.Flush(); });
}
void
ProjectStore::Project::Scrub(ScrubContext& Ctx)
{
+ // Scrubbing needs to check all existing oplogs
+ std::vector<std::string> OpLogs = ScanForOplogs();
+ for (const std::string& OpLogId : OpLogs)
+ {
+ OpenOplog(OpLogId);
+ }
IterateOplogs([&](const Oplog& Ops) { Ops.Scrub(Ctx); });
}
void
ProjectStore::Project::GatherReferences(GcContext& GcCtx)
{
- if (IsExpired())
+ ZEN_TRACE_CPU("ProjectStore::Project::GatherReferences");
+
+ Stopwatch Timer;
+ const auto Guard = MakeGuard(
+ [&] { ZEN_INFO("gathered references from project store project {} in {}", Identifier, NiceTimeSpanMs(Timer.GetElapsedTimeMs())); });
+
+ // GatherReferences needs to check all existing oplogs
+ std::vector<std::string> OpLogs = ScanForOplogs();
+ for (const std::string& OpLogId : OpLogs)
{
- ZEN_DEBUG("Skipping reference gathering for '{}', project file '{}' no longer exist", Identifier, ProjectFilePath);
- return;
+ OpenOplog(OpLogId);
}
IterateOplogs([&](Oplog& Ops) { Ops.GatherReferences(GcCtx); });
}
-void
-ProjectStore::Project::Delete()
+bool
+ProjectStore::Project::PrepareForDelete(std::filesystem::path& OutDeletePath)
{
RwLock::ExclusiveLockScope _(m_ProjectLock);
- DeleteDirectories(m_OplogStoragePath);
+
+ for (auto& It : m_Oplogs)
+ {
+ // We don't care about the moved folder
+ It.second->PrepareForDelete(false);
+ m_DeletedOplogs.emplace_back(std::move(It.second));
+ }
+
+ m_Oplogs.clear();
+
+ bool Success = PrepareDirectoryDelete(m_OplogStoragePath, OutDeletePath);
+ if (!Success)
+ {
+ return false;
+ }
+ m_OplogStoragePath.clear();
+ return true;
}
bool
@@ -944,12 +963,7 @@ ProjectStore::DiscoverProjects()
for (const std::filesystem::path& DirPath : DirContent.Directories)
{
std::string DirName = PathToUtf8(DirPath.filename());
- Project* Project = OpenProject(DirName);
-
- if (Project)
- {
- Project->DiscoverOplogs();
- }
+ OpenProject(DirName);
}
}
@@ -960,74 +974,166 @@ ProjectStore::IterateProjects(std::function<void(Project& Prj)>&& Fn)
for (auto& Kv : m_Projects)
{
- Fn(Kv.second);
+ Fn(*Kv.second.Get());
}
}
void
ProjectStore::Flush()
{
- RwLock::SharedLockScope _(m_ProjectsLock);
+ std::vector<Ref<Project>> Projects;
+ {
+ RwLock::SharedLockScope _(m_ProjectsLock);
+ Projects.reserve(m_Projects.size());
- for (auto& Kv : m_Projects)
+ for (auto& Kv : m_Projects)
+ {
+ Projects.push_back(Kv.second);
+ }
+ }
+ for (const Ref<Project>& Project : Projects)
{
- Kv.second.Flush();
+ Project->Flush();
}
}
void
ProjectStore::Scrub(ScrubContext& Ctx)
{
- RwLock::SharedLockScope _(m_ProjectsLock);
+ DiscoverProjects();
- for (auto& Kv : m_Projects)
+ std::vector<Ref<Project>> Projects;
{
- Kv.second.Scrub(Ctx);
+ RwLock::SharedLockScope _(m_ProjectsLock);
+ Projects.reserve(m_Projects.size());
+
+ for (auto& Kv : m_Projects)
+ {
+ if (Kv.second->IsExpired())
+ {
+ continue;
+ }
+ Projects.push_back(Kv.second);
+ }
+ }
+ for (const Ref<Project>& Project : Projects)
+ {
+ Project->Scrub(Ctx);
}
}
void
ProjectStore::GatherReferences(GcContext& GcCtx)
{
+ ZEN_TRACE_CPU("ProjectStore::GatherReferences");
+
+ size_t ProjectCount = 0;
+ size_t ExpiredProjectCount = 0;
Stopwatch Timer;
- const auto Guard =
- MakeGuard([&] { ZEN_INFO("project store gathered all references in {}", NiceTimeSpanMs(Timer.GetElapsedTimeMs())); });
+ const auto Guard = MakeGuard([&] {
+ ZEN_INFO("gathered references from '{}' in {}, found {} active projects and {} expired projects",
+ m_ProjectBasePath.string(),
+ NiceTimeSpanMs(Timer.GetElapsedTimeMs()),
+ ProjectCount,
+ ExpiredProjectCount);
+ });
DiscoverProjects();
- RwLock::SharedLockScope _(m_ProjectsLock);
+ std::vector<Ref<Project>> Projects;
+ {
+ RwLock::SharedLockScope _(m_ProjectsLock);
+ Projects.reserve(m_Projects.size());
- for (auto& Kv : m_Projects)
+ for (auto& Kv : m_Projects)
+ {
+ if (Kv.second->IsExpired())
+ {
+ ExpiredProjectCount++;
+ continue;
+ }
+ Projects.push_back(Kv.second);
+ }
+ }
+ ProjectCount = Projects.size();
+ for (const Ref<Project>& Project : Projects)
{
- Kv.second.GatherReferences(GcCtx);
+ Project->GatherReferences(GcCtx);
}
}
void
ProjectStore::CollectGarbage(GcContext& GcCtx)
{
- std::vector<std::string> ExpiredProjects;
+ ZEN_TRACE_CPU("ProjectStore::CollectGarbage");
+
+ size_t ProjectCount = 0;
+ size_t ExpiredProjectCount = 0;
+
+ Stopwatch Timer;
+ const auto Guard = MakeGuard([&] {
+ ZEN_INFO("garbage collect from '{}' DONE after {}, found {} active projects and {} expired projects",
+ m_ProjectBasePath.string(),
+ NiceTimeSpanMs(Timer.GetElapsedTimeMs()),
+ ProjectCount,
+ ExpiredProjectCount);
+ });
+ std::vector<Ref<Project>> ExpiredProjects;
{
RwLock::SharedLockScope _(m_ProjectsLock);
for (auto& Kv : m_Projects)
{
- if (Kv.second.IsExpired())
+ if (Kv.second->IsExpired())
{
- ExpiredProjects.push_back(Kv.first);
+ ExpiredProjects.push_back(Kv.second);
+ ExpiredProjectCount++;
+ continue;
}
+ ProjectCount++;
}
}
+ if (ExpiredProjects.empty())
+ {
+ ZEN_INFO("garbage collect SKIPPED, for '{}', no expired projects found", m_ProjectBasePath.string());
+ return;
+ }
+
if (!GcCtx.IsDeletionMode())
{
+ ZEN_INFO("garbage collect DISABLED, for '{}' ", m_ProjectBasePath.string());
return;
}
- for (const std::string& ProjectId : ExpiredProjects)
+ for (const Ref<Project>& Project : ExpiredProjects)
{
- ZEN_INFO("ProjectStore::CollectGarbage would delete '{}'. Disabled for now due to concurrency issues in ProjectStore", ProjectId);
- // DeleteProject(ProjectId, true);
+ std::filesystem::path PathToRemove;
+ std::string ProjectId;
+ {
+ RwLock::ExclusiveLockScope _(m_ProjectsLock);
+ if (!Project->IsExpired())
+ {
+ ZEN_INFO("ProjectStore::CollectGarbage skipped garbage collect of project '{}'. Project no longer expired.", ProjectId);
+ continue;
+ }
+ bool Success = Project->PrepareForDelete(PathToRemove);
+ if (!Success)
+ {
+ ZEN_INFO("ProjectStore::CollectGarbage skipped garbage collect of project '{}'. Project folder is locked.", ProjectId);
+ continue;
+ }
+ m_Projects.erase(Project->Identifier);
+ ProjectId = Project->Identifier;
+ }
+
+ ZEN_INFO("ProjectStore::CollectGarbage garbage collected project '{}'. Removing storage on disk", ProjectId);
+ if (PathToRemove.empty())
+ {
+ continue;
+ }
+
+ DeleteDirectories(PathToRemove);
}
}
@@ -1037,7 +1143,7 @@ ProjectStore::StorageSize() const
return {0, 0};
}
-ProjectStore::Project*
+Ref<ProjectStore::Project>
ProjectStore::OpenProject(std::string_view ProjectId)
{
{
@@ -1047,28 +1153,31 @@ ProjectStore::OpenProject(std::string_view ProjectId)
if (ProjIt != m_Projects.end())
{
- return &(ProjIt->second);
+ return ProjIt->second;
}
}
RwLock::ExclusiveLockScope _(m_ProjectsLock);
- std::filesystem::path ProjectBasePath = BasePathForProject(ProjectId);
+ std::filesystem::path BasePath = BasePathForProject(ProjectId);
- if (Project::Exists(ProjectBasePath))
+ if (Project::Exists(BasePath))
{
try
{
- ZEN_INFO("opening project {} @ {}", ProjectId, ProjectBasePath);
-
- ProjectStore::Project& Prj = m_Projects.try_emplace(std::string{ProjectId}, this, m_CidStore, ProjectBasePath).first->second;
- Prj.Identifier = ProjectId;
- Prj.Read();
- return &Prj;
+ ZEN_INFO("opening project {} @ {}", ProjectId, BasePath);
+
+ Ref<Project>& Prj =
+ m_Projects
+ .try_emplace(std::string{ProjectId}, Ref<ProjectStore::Project>(new ProjectStore::Project(this, m_CidStore, BasePath)))
+ .first->second;
+ Prj->Identifier = ProjectId;
+ Prj->Read();
+ return Prj;
}
catch (std::exception& e)
{
- ZEN_WARN("failed to open {} @ {} ({})", ProjectId, ProjectBasePath, e.what());
+ ZEN_WARN("failed to open {} @ {} ({})", ProjectId, BasePath, e.what());
m_Projects.erase(std::string{ProjectId});
}
}
@@ -1076,7 +1185,7 @@ ProjectStore::OpenProject(std::string_view ProjectId)
return nullptr;
}
-ProjectStore::Project*
+Ref<ProjectStore::Project>
ProjectStore::NewProject(std::filesystem::path BasePath,
std::string_view ProjectId,
std::string_view RootDir,
@@ -1086,39 +1195,48 @@ ProjectStore::NewProject(std::filesystem::path BasePath,
{
RwLock::ExclusiveLockScope _(m_ProjectsLock);
- ProjectStore::Project& Prj = m_Projects.try_emplace(std::string{ProjectId}, this, m_CidStore, BasePath).first->second;
- Prj.Identifier = ProjectId;
- Prj.RootDir = RootDir;
- Prj.EngineRootDir = EngineRootDir;
- Prj.ProjectRootDir = ProjectRootDir;
- Prj.ProjectFilePath = ProjectFilePath;
- Prj.Write();
-
- return &Prj;
+ Ref<Project>& Prj =
+ m_Projects.try_emplace(std::string{ProjectId}, Ref<ProjectStore::Project>(new ProjectStore::Project(this, m_CidStore, BasePath)))
+ .first->second;
+ Prj->Identifier = ProjectId;
+ Prj->RootDir = RootDir;
+ Prj->EngineRootDir = EngineRootDir;
+ Prj->ProjectRootDir = ProjectRootDir;
+ Prj->ProjectFilePath = ProjectFilePath;
+ Prj->Write();
+
+ return Prj;
}
-void
-ProjectStore::DeleteProject(std::string_view ProjectId, bool OnlyDeleteIfExpired)
+bool
+ProjectStore::DeleteProject(std::string_view ProjectId)
{
ZEN_INFO("deleting project {}", ProjectId);
- RwLock::SharedLockScope _(m_ProjectsLock);
+ RwLock::ExclusiveLockScope ProjectsLock(m_ProjectsLock);
auto ProjIt = m_Projects.find(std::string{ProjectId});
if (ProjIt == m_Projects.end())
{
- return;
+ return true;
}
- if (OnlyDeleteIfExpired && !ProjIt->second.IsExpired())
+
+ std::filesystem::path DeletePath;
+ bool Success = ProjIt->second->PrepareForDelete(DeletePath);
+
+ if (!Success)
{
- return;
+ return false;
}
- // _.ReleaseNow(); TODO: We can't release here since any changes to m_Projects map will invalidate our ProjIt pointer
- ProjIt->second.Delete();
+ m_Projects.erase(ProjIt);
+ ProjectsLock.ReleaseNow();
- m_Projects.erase(
- ProjIt); // TODO: We need to keep the project pointer alive, somebody else may use it after fetching it from m_Projects
+ if (!DeletePath.empty())
+ {
+ DeleteDirectories(DeletePath);
+ }
+ return true;
}
bool
@@ -1127,17 +1245,6 @@ ProjectStore::Exists(std::string_view ProjectId)
return Project::Exists(BasePathForProject(ProjectId));
}
-ProjectStore::Oplog*
-ProjectStore::OpenProjectOplog(std::string_view ProjectId, std::string_view OplogId)
-{
- if (Project* ProjectIt = OpenProject(ProjectId))
- {
- return ProjectIt->OpenOplog(OplogId);
- }
-
- return nullptr;
-}
-
//////////////////////////////////////////////////////////////////////////
HttpProjectService::HttpProjectService(CidStore& Store, ProjectStore* Projects)
@@ -1186,9 +1293,15 @@ HttpProjectService::HttpProjectService(CidStore& Store, ProjectStore* Projects)
const auto& ProjectId = Req.GetCapture(1);
const auto& OplogId = Req.GetCapture(2);
- ProjectStore::Oplog* FoundLog = m_ProjectStore->OpenProjectOplog(ProjectId, OplogId);
+ Ref<ProjectStore::Project> Project = m_ProjectStore->OpenProject(ProjectId);
+ if (!Project)
+ {
+ return HttpReq.WriteResponse(HttpResponseCode::NotFound);
+ }
- if (FoundLog == nullptr)
+ ProjectStore::Oplog* FoundLog = Project->OpenOplog(OplogId);
+
+ if (!FoundLog)
{
return HttpReq.WriteResponse(HttpResponseCode::NotFound);
}
@@ -1314,9 +1427,15 @@ HttpProjectService::HttpProjectService(CidStore& Store, ProjectStore* Projects)
const auto& ProjectId = Req.GetCapture(1);
const auto& OplogId = Req.GetCapture(2);
- ProjectStore::Oplog* FoundLog = m_ProjectStore->OpenProjectOplog(ProjectId, OplogId);
+ Ref<ProjectStore::Project> Project = m_ProjectStore->OpenProject(ProjectId);
+ if (!Project)
+ {
+ return HttpReq.WriteResponse(HttpResponseCode::NotFound);
+ }
+
+ ProjectStore::Oplog* FoundLog = Project->OpenOplog(OplogId);
- if (FoundLog == nullptr)
+ if (!FoundLog)
{
return HttpReq.WriteResponse(HttpResponseCode::NotFound);
}
@@ -1356,9 +1475,15 @@ HttpProjectService::HttpProjectService(CidStore& Store, ProjectStore* Projects)
const auto& OplogId = Req.GetCapture(2);
const auto& ChunkId = Req.GetCapture(3);
- ProjectStore::Oplog* FoundLog = m_ProjectStore->OpenProjectOplog(ProjectId, OplogId);
+ Ref<ProjectStore::Project> Project = m_ProjectStore->OpenProject(ProjectId);
+ if (!Project)
+ {
+ return HttpReq.WriteResponse(HttpResponseCode::NotFound);
+ }
- if (FoundLog == nullptr)
+ ProjectStore::Oplog* FoundLog = Project->OpenOplog(OplogId);
+
+ if (!FoundLog)
{
return HttpReq.WriteResponse(HttpResponseCode::NotFound);
}
@@ -1435,9 +1560,16 @@ HttpProjectService::HttpProjectService(CidStore& Store, ProjectStore* Projects)
AcceptType = HttpContentType::kBinary;
}
- ProjectStore::Oplog* FoundLog = m_ProjectStore->OpenProjectOplog(ProjectId, OplogId);
+ Ref<ProjectStore::Project> Project = m_ProjectStore->OpenProject(ProjectId);
+ if (!Project)
+ {
+ m_Log.warn("chunk - '{}/{}/{}' FAILED, missing project", ProjectId, OplogId, ChunkId);
+ return HttpReq.WriteResponse(HttpResponseCode::NotFound);
+ }
+
+ ProjectStore::Oplog* FoundLog = Project->OpenOplog(OplogId);
- if (FoundLog == nullptr)
+ if (!FoundLog)
{
m_Log.warn("chunk - '{}/{}/{}' FAILED, missing oplog", ProjectId, OplogId, ChunkId);
return HttpReq.WriteResponse(HttpResponseCode::NotFound);
@@ -1548,9 +1680,15 @@ HttpProjectService::HttpProjectService(CidStore& Store, ProjectStore* Projects)
const auto& ProjectId = Req.GetCapture(1);
const auto& OplogId = Req.GetCapture(2);
- ProjectStore::Oplog* FoundLog = m_ProjectStore->OpenProjectOplog(ProjectId, OplogId);
+ Ref<ProjectStore::Project> Project = m_ProjectStore->OpenProject(ProjectId);
+ if (!Project)
+ {
+ return HttpReq.WriteResponse(HttpResponseCode::NotFound);
+ }
+
+ ProjectStore::Oplog* FoundLog = Project->OpenOplog(OplogId);
- if (FoundLog == nullptr)
+ if (!FoundLog)
{
return HttpReq.WriteResponse(HttpResponseCode::NotFound);
}
@@ -1611,9 +1749,15 @@ HttpProjectService::HttpProjectService(CidStore& Store, ProjectStore* Projects)
IsUsingSalt = true;
}
- ProjectStore::Oplog* FoundLog = m_ProjectStore->OpenProjectOplog(ProjectId, OplogId);
+ Ref<ProjectStore::Project> Project = m_ProjectStore->OpenProject(ProjectId);
+ if (!Project)
+ {
+ return HttpReq.WriteResponse(HttpResponseCode::NotFound);
+ }
+
+ ProjectStore::Oplog* FoundLog = Project->OpenOplog(OplogId);
- if (FoundLog == nullptr)
+ if (!FoundLog)
{
return HttpReq.WriteResponse(HttpResponseCode::NotFound);
}
@@ -1713,9 +1857,15 @@ HttpProjectService::HttpProjectService(CidStore& Store, ProjectStore* Projects)
const std::string& OplogId = Req.GetCapture(2);
const std::string& OpIdString = Req.GetCapture(3);
- ProjectStore::Oplog* FoundLog = m_ProjectStore->OpenProjectOplog(ProjectId, OplogId);
+ Ref<ProjectStore::Project> Project = m_ProjectStore->OpenProject(ProjectId);
+ if (!Project)
+ {
+ return HttpReq.WriteResponse(HttpResponseCode::NotFound);
+ }
+
+ ProjectStore::Oplog* FoundLog = Project->OpenOplog(OplogId);
- if (FoundLog == nullptr)
+ if (!FoundLog)
{
return HttpReq.WriteResponse(HttpResponseCode::NotFound);
}
@@ -1797,22 +1947,20 @@ HttpProjectService::HttpProjectService(CidStore& Store, ProjectStore* Projects)
const auto& ProjectId = Req.GetCapture(1);
const auto& OplogId = Req.GetCapture(2);
- ProjectStore::Project* ProjectIt = m_ProjectStore->OpenProject(ProjectId);
+ Ref<ProjectStore::Project> Project = m_ProjectStore->OpenProject(ProjectId);
- if (!ProjectIt)
+ if (!Project)
{
return Req.ServerRequest().WriteResponse(HttpResponseCode::NotFound,
HttpContentType::kText,
fmt::format("project {} not found", ProjectId));
}
- ProjectStore::Project& Prj = *ProjectIt;
-
switch (Req.ServerRequest().RequestVerb())
{
case HttpVerb::kGet:
{
- ProjectStore::Oplog* OplogIt = Prj.OpenOplog(OplogId);
+ ProjectStore::Oplog* OplogIt = Project->OpenOplog(OplogId);
if (!OplogIt)
{
@@ -1824,7 +1972,7 @@ HttpProjectService::HttpProjectService(CidStore& Store, ProjectStore* Projects)
ProjectStore::Oplog& Log = *OplogIt;
CbObjectWriter Cb;
- Cb << "id"sv << Log.OplogId() << "project"sv << Prj.Identifier << "tempdir"sv << Log.TempPath().c_str();
+ Cb << "id"sv << Log.OplogId() << "project"sv << Project->Identifier << "tempdir"sv << Log.TempPath().c_str();
Req.ServerRequest().WriteResponse(HttpResponseCode::OK, Cb.Save());
}
@@ -1832,11 +1980,11 @@ HttpProjectService::HttpProjectService(CidStore& Store, ProjectStore* Projects)
case HttpVerb::kPost:
{
- ProjectStore::Oplog* OplogIt = Prj.OpenOplog(OplogId);
+ ProjectStore::Oplog* OplogIt = Project->OpenOplog(OplogId);
if (!OplogIt)
{
- if (!Prj.NewOplog(OplogId))
+ if (!Project->NewOplog(OplogId))
{
// TODO: indicate why the operation failed!
return Req.ServerRequest().WriteResponse(HttpResponseCode::InternalServerError);
@@ -1858,7 +2006,7 @@ HttpProjectService::HttpProjectService(CidStore& Store, ProjectStore* Projects)
{
ZEN_INFO("deleting oplog '{}/{}'", ProjectId, OplogId);
- ProjectIt->DeleteOplog(OplogId);
+ Project->DeleteOplog(OplogId);
return Req.ServerRequest().WriteResponse(HttpResponseCode::OK);
}
@@ -1878,9 +2026,15 @@ HttpProjectService::HttpProjectService(CidStore& Store, ProjectStore* Projects)
const auto& ProjectId = Req.GetCapture(1);
const auto& OplogId = Req.GetCapture(2);
- ProjectStore::Oplog* FoundLog = m_ProjectStore->OpenProjectOplog(ProjectId, OplogId);
+ Ref<ProjectStore::Project> Project = m_ProjectStore->OpenProject(ProjectId);
+ if (!Project)
+ {
+ return HttpReq.WriteResponse(HttpResponseCode::NotFound);
+ }
- if (FoundLog == nullptr)
+ ProjectStore::Oplog* FoundLog = Project->OpenOplog(OplogId);
+
+ if (!FoundLog)
{
return HttpReq.WriteResponse(HttpResponseCode::NotFound);
}
@@ -1954,26 +2108,27 @@ HttpProjectService::HttpProjectService(CidStore& Store, ProjectStore* Projects)
case HttpVerb::kGet:
{
- ProjectStore::Project* ProjectIt = m_ProjectStore->OpenProject(ProjectId);
+ Ref<ProjectStore::Project> Project = m_ProjectStore->OpenProject(ProjectId);
- if (!ProjectIt)
+ if (!Project)
{
return Req.ServerRequest().WriteResponse(HttpResponseCode::NotFound,
HttpContentType::kText,
fmt::format("project {} not found", ProjectId));
}
- const ProjectStore::Project& Prj = *ProjectIt;
+ std::vector<std::string> OpLogs = Project->ScanForOplogs();
CbObjectWriter Response;
- Response << "id"sv << Prj.Identifier << "root"sv << PathToUtf8(Prj.RootDir);
+ Response << "id"sv << Project->Identifier << "root"sv << PathToUtf8(Project->RootDir);
Response.BeginArray("oplogs"sv);
- Prj.IterateOplogs([&](const ProjectStore::Oplog& I) {
+ for (const std::string& OplogId : OpLogs)
+ {
Response.BeginObject();
- Response << "id"sv << I.OplogId();
+ Response << "id"sv << OplogId;
Response.EndObject();
- });
+ }
Response.EndArray(); // oplogs
Req.ServerRequest().WriteResponse(HttpResponseCode::OK, Response.Save());
@@ -1982,16 +2137,21 @@ HttpProjectService::HttpProjectService(CidStore& Store, ProjectStore* Projects)
case HttpVerb::kDelete:
{
- ProjectStore::Project* ProjectIt = m_ProjectStore->OpenProject(ProjectId);
+ Ref<ProjectStore::Project> Project = m_ProjectStore->OpenProject(ProjectId);
- if (!ProjectIt)
+ if (!Project)
{
return Req.ServerRequest().WriteResponse(HttpResponseCode::NotFound,
HttpContentType::kText,
fmt::format("project {} not found", ProjectId));
}
- m_ProjectStore->DeleteProject(ProjectId, false);
+ if (!m_ProjectStore->DeleteProject(ProjectId))
+ {
+ return Req.ServerRequest().WriteResponse(HttpResponseCode::Locked,
+ HttpContentType::kText,
+ fmt::format("project {} is in use", ProjectId));
+ }
return Req.ServerRequest().WriteResponse(HttpResponseCode::NoContent);
}
@@ -2027,11 +2187,208 @@ HttpProjectService::HandleRequest(HttpServerRequest& Request)
#if ZEN_WITH_TESTS
-TEST_CASE("prj.store")
+namespace testutils {
+ using namespace std::literals;
+
+ CbPackage CreateOplogPackage(const Oid& Id, const std::span<const CompressedBuffer>& Attachments)
+ {
+ CbPackage Package;
+ CbObjectWriter Object;
+ Object << "key"sv << Id;
+ if (!Attachments.empty())
+ {
+ Object.BeginArray("bulkdata");
+ for (const CompressedBuffer& Attachment : Attachments)
+ {
+ Object.BeginObject();
+ Object << "id" << Oid::NewOid();
+ Object << "type"
+ << "Standard";
+ Object << "data" << CbAttachment(Attachment);
+ Object.EndObject();
+
+ Package.AddAttachment(CbAttachment(Attachment));
+ }
+ Object.EndArray();
+ }
+ Package.SetObject(Object.Save());
+ return Package;
+ };
+
+ std::vector<CompressedBuffer> CreateAttachments(const std::span<const size_t>& Sizes)
+ {
+ std::vector<CompressedBuffer> Result;
+ Result.reserve(Sizes.size());
+ for (size_t Size : Sizes)
+ {
+ std::vector<uint8_t> Data;
+ Data.resize(Size);
+ for (size_t Idx = 0; Idx < Size; ++Idx)
+ {
+ Data[Idx] = Idx % 255;
+ }
+
+ Result.emplace_back(zen::CompressedBuffer::Compress(SharedBuffer::MakeView(Data.data(), Data.size())));
+ }
+ return Result;
+ }
+
+} // namespace testutils
+
+TEST_CASE("project.store.create")
+{
+ using namespace std::literals;
+
+ ScopedTemporaryDirectory TempDir;
+
+ GcManager Gc;
+ CidStore CidStore(Gc);
+ CidStoreConfiguration CidConfig = {.RootDirectory = TempDir.Path() / "cas", .TinyValueThreshold = 1024, .HugeValueThreshold = 4096};
+ CidStore.Initialize(CidConfig);
+
+ std::string_view ProjectName("proj1"sv);
+ std::filesystem::path BasePath = TempDir.Path() / "projectstore";
+ ProjectStore ProjectStore(CidStore, BasePath, Gc);
+ std::filesystem::path RootDir = TempDir.Path() / "root";
+ std::filesystem::path EngineRootDir = TempDir.Path() / "engine";
+ std::filesystem::path ProjectRootDir = TempDir.Path() / "game";
+ std::filesystem::path ProjectFilePath = TempDir.Path() / "game" / "game.uproject";
+
+ Ref<ProjectStore::Project> Project(ProjectStore.NewProject(BasePath / ProjectName,
+ ProjectName,
+ RootDir.string(),
+ EngineRootDir.string(),
+ ProjectRootDir.string(),
+ ProjectFilePath.string()));
+ CHECK(ProjectStore.DeleteProject(ProjectName));
+ CHECK(!Project->Exists(BasePath));
+}
+
+TEST_CASE("project.store.lifetimes")
{
using namespace std::literals;
ScopedTemporaryDirectory TempDir;
+
+ GcManager Gc;
+ CidStore CidStore(Gc);
+ CidStoreConfiguration CidConfig = {.RootDirectory = TempDir.Path() / "cas", .TinyValueThreshold = 1024, .HugeValueThreshold = 4096};
+ CidStore.Initialize(CidConfig);
+
+ std::filesystem::path BasePath = TempDir.Path() / "projectstore";
+ ProjectStore ProjectStore(CidStore, BasePath, Gc);
+ std::filesystem::path RootDir = TempDir.Path() / "root";
+ std::filesystem::path EngineRootDir = TempDir.Path() / "engine";
+ std::filesystem::path ProjectRootDir = TempDir.Path() / "game";
+ std::filesystem::path ProjectFilePath = TempDir.Path() / "game" / "game.uproject";
+
+ Ref<ProjectStore::Project> Project(ProjectStore.NewProject(BasePath / "proj1"sv,
+ "proj1"sv,
+ RootDir.string(),
+ EngineRootDir.string(),
+ ProjectRootDir.string(),
+ ProjectFilePath.string()));
+ ProjectStore::Oplog* Oplog = Project->NewOplog("oplog1");
+ CHECK(Oplog != nullptr);
+
+ std::filesystem::path DeletePath;
+ CHECK(Project->PrepareForDelete(DeletePath));
+ CHECK(!DeletePath.empty());
+ CHECK(Project->OpenOplog("oplog1") == nullptr);
+ // Oplog is now invalid, but pointer can still be accessed since we store old oplog pointers
+ CHECK(Oplog->OplogCount() == 0);
+ // Project is still valid since we have a Ref to it
+ CHECK(Project->Identifier == "proj1"sv);
+}
+
+TEST_CASE("project.store.gc")
+{
+ using namespace std::literals;
+ using namespace testutils;
+
+ ScopedTemporaryDirectory TempDir;
+
+ GcManager Gc;
+ CidStore CidStore(Gc);
+ CidStoreConfiguration CidConfig = {.RootDirectory = TempDir.Path() / "cas", .TinyValueThreshold = 1024, .HugeValueThreshold = 4096};
+ CidStore.Initialize(CidConfig);
+
+ std::filesystem::path BasePath = TempDir.Path() / "projectstore";
+ ProjectStore ProjectStore(CidStore, BasePath, Gc);
+ std::filesystem::path RootDir = TempDir.Path() / "root";
+ std::filesystem::path EngineRootDir = TempDir.Path() / "engine";
+
+ std::filesystem::path Project1RootDir = TempDir.Path() / "game1";
+ std::filesystem::path Project1FilePath = TempDir.Path() / "game1" / "game.uproject";
+ {
+ CreateDirectories(Project1FilePath.parent_path());
+ BasicFile ProjectFile;
+ ProjectFile.Open(Project1FilePath, BasicFile::Mode::kTruncate);
+ }
+
+ std::filesystem::path Project2RootDir = TempDir.Path() / "game2";
+ std::filesystem::path Project2FilePath = TempDir.Path() / "game2" / "game.uproject";
+ {
+ CreateDirectories(Project2FilePath.parent_path());
+ BasicFile ProjectFile;
+ ProjectFile.Open(Project2FilePath, BasicFile::Mode::kTruncate);
+ }
+
+ {
+ Ref<ProjectStore::Project> Project1(ProjectStore.NewProject(BasePath / "proj1"sv,
+ "proj1"sv,
+ RootDir.string(),
+ EngineRootDir.string(),
+ Project1RootDir.string(),
+ Project1FilePath.string()));
+ ProjectStore::Oplog* Oplog = Project1->NewOplog("oplog1");
+ CHECK(Oplog != nullptr);
+
+ Oplog->AppendNewOplogEntry(CreateOplogPackage(Oid::NewOid(), {}));
+ Oplog->AppendNewOplogEntry(CreateOplogPackage(Oid::NewOid(), CreateAttachments(std::initializer_list<size_t>{77})));
+ Oplog->AppendNewOplogEntry(CreateOplogPackage(Oid::NewOid(), CreateAttachments(std::initializer_list<size_t>{7123, 583, 690, 99})));
+ Oplog->AppendNewOplogEntry(CreateOplogPackage(Oid::NewOid(), CreateAttachments(std::initializer_list<size_t>{55, 122})));
+ }
+
+ {
+ Ref<ProjectStore::Project> Project2(ProjectStore.NewProject(BasePath / "proj2"sv,
+ "proj2"sv,
+ RootDir.string(),
+ EngineRootDir.string(),
+ Project2RootDir.string(),
+ Project2FilePath.string()));
+ ProjectStore::Oplog* Oplog = Project2->NewOplog("oplog1");
+ CHECK(Oplog != nullptr);
+
+ Oplog->AppendNewOplogEntry(CreateOplogPackage(Oid::NewOid(), {}));
+ Oplog->AppendNewOplogEntry(CreateOplogPackage(Oid::NewOid(), CreateAttachments(std::initializer_list<size_t>{177})));
+ Oplog->AppendNewOplogEntry(CreateOplogPackage(Oid::NewOid(), CreateAttachments(std::initializer_list<size_t>{9123, 383, 590, 96})));
+ Oplog->AppendNewOplogEntry(CreateOplogPackage(Oid::NewOid(), CreateAttachments(std::initializer_list<size_t>{535, 221})));
+ }
+
+ {
+ GcContext GcCtx;
+ 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);
+
+ {
+ GcContext GcCtx;
+ 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));
+ }
}
#endif