Precommit directory store schema catalog
This commit is contained in:
parent
edceef54d0
commit
fe55bff2e0
2 changed files with 139 additions and 24 deletions
|
|
@ -42,7 +42,7 @@ public sealed class DirectoryMessagePackBackingStore : CacheBackingStore
|
|||
/// <inheritdoc />
|
||||
public override void PullAll()
|
||||
{
|
||||
if (!_manifestFile.Exists && !_recordDirectory.Exists)
|
||||
if (!File.Exists(_manifestFile.FullName) && !_recordDirectory.Exists)
|
||||
{
|
||||
SetLastSchemaMigrationReports(Array.Empty<CultSchemaMigrationReport>());
|
||||
IsDirty = false;
|
||||
|
|
@ -53,7 +53,7 @@ public sealed class DirectoryMessagePackBackingStore : CacheBackingStore
|
|||
var reports = new List<CultSchemaMigrationReport>();
|
||||
foreach (var record in manifest.Records)
|
||||
{
|
||||
var catalog = ResolveCatalogForRecord(record, manifest.SchemaCatalog);
|
||||
var catalog = ResolveLegacyUncataloguedRecordCatalog(record, manifest.SchemaCatalog);
|
||||
reports.Add(Registry.ResolvePersistedSchemaReport(record.SchemaId, catalog));
|
||||
var stored = ToStoredDocument(record, catalog, CultDocumentMessagePackSerialization.DeserializeUntyped);
|
||||
Entries[stored.Key.Value] = stored;
|
||||
|
|
@ -66,7 +66,7 @@ public sealed class DirectoryMessagePackBackingStore : CacheBackingStore
|
|||
foreach (var recordFile in _recordDirectory.EnumerateFiles("*.msgpack").OrderBy(file => file.Name, StringComparer.Ordinal))
|
||||
{
|
||||
var record = CultDocumentMessagePackSerialization.DeserializePersistedRecord(File.ReadAllBytes(recordFile.FullName));
|
||||
var catalog = ResolveCatalogForRecord(record, manifest.SchemaCatalog);
|
||||
var catalog = ResolveLegacyUncataloguedRecordCatalog(record, manifest.SchemaCatalog);
|
||||
reports.Add(Registry.ResolvePersistedSchemaReport(record.SchemaId, catalog));
|
||||
var stored = ToStoredDocument(record, catalog, CultDocumentMessagePackSerialization.DeserializeUntyped);
|
||||
Entries[stored.Key.Value] = stored;
|
||||
|
|
@ -108,14 +108,16 @@ public sealed class DirectoryMessagePackBackingStore : CacheBackingStore
|
|||
Directory.CreateDirectory(_manifestFile.DirectoryName!);
|
||||
Directory.CreateDirectory(_recordDirectory.FullName);
|
||||
|
||||
foreach (var key in _deletedKeys.Keys)
|
||||
{
|
||||
var path = RecordPath(key);
|
||||
if (File.Exists(path))
|
||||
{
|
||||
File.Delete(path);
|
||||
}
|
||||
}
|
||||
var targetCatalog = BuildTargetCatalog();
|
||||
var durableCatalog = ReadManifest().SchemaCatalog;
|
||||
var precommitCatalog = durableCatalog
|
||||
.Concat(targetCatalog)
|
||||
.GroupBy(entry => entry.SchemaId, StringComparer.Ordinal)
|
||||
.Select(group => group.Last())
|
||||
.OrderBy(entry => entry.SchemaName, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
|
||||
WriteManifest(precommitCatalog);
|
||||
|
||||
foreach (var key in _dirtyKeys.Keys.OrderBy(value => value, StringComparer.Ordinal))
|
||||
{
|
||||
|
|
@ -129,27 +131,50 @@ public sealed class DirectoryMessagePackBackingStore : CacheBackingStore
|
|||
WriteFileAtomically(RecordPath(key), CultDocumentMessagePackSerialization.SerializePersistedRecord(record));
|
||||
}
|
||||
|
||||
var manifest = new CultPersistedStoreSnapshot
|
||||
foreach (var key in _deletedKeys.Keys.OrderBy(value => value, StringComparer.Ordinal))
|
||||
{
|
||||
FormatVersion = "cultcache.store.v1.directory",
|
||||
SchemaCatalog = Entries.Values
|
||||
.Select(entry => entry.Descriptor.ToCatalogEntry())
|
||||
.GroupBy(entry => entry.SchemaId, StringComparer.Ordinal)
|
||||
.Select(group => group.First())
|
||||
.OrderBy(entry => entry.SchemaName, StringComparer.Ordinal)
|
||||
.ToArray(),
|
||||
Records = Array.Empty<CultPersistedRecord>()
|
||||
};
|
||||
WriteFileAtomically(_manifestFile.FullName, CultDocumentMessagePackSerialization.SerializeSnapshot(manifest));
|
||||
var path = RecordPath(key);
|
||||
if (File.Exists(path))
|
||||
{
|
||||
File.Delete(path);
|
||||
}
|
||||
}
|
||||
|
||||
if (!CatalogsEqual(precommitCatalog, targetCatalog))
|
||||
{
|
||||
WriteManifest(targetCatalog);
|
||||
}
|
||||
|
||||
_dirtyKeys.Clear();
|
||||
_deletedKeys.Clear();
|
||||
MarkFlushSucceeded();
|
||||
}
|
||||
|
||||
private CultSchemaCatalogEntry[] BuildTargetCatalog() => Entries.Values
|
||||
.Select(entry => entry.Descriptor.ToCatalogEntry())
|
||||
.GroupBy(entry => entry.SchemaId, StringComparer.Ordinal)
|
||||
.Select(group => group.First())
|
||||
.OrderBy(entry => entry.SchemaName, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
|
||||
private static bool CatalogsEqual(CultSchemaCatalogEntry[] left, CultSchemaCatalogEntry[] right) =>
|
||||
CultDocumentMessagePackSerialization.SerializeSchemaCatalog(left)
|
||||
.SequenceEqual(CultDocumentMessagePackSerialization.SerializeSchemaCatalog(right));
|
||||
|
||||
private void WriteManifest(CultSchemaCatalogEntry[] catalog)
|
||||
{
|
||||
var manifest = new CultPersistedStoreSnapshot
|
||||
{
|
||||
FormatVersion = "cultcache.store.v1.directory",
|
||||
SchemaCatalog = catalog,
|
||||
Records = Array.Empty<CultPersistedRecord>()
|
||||
};
|
||||
WriteFileAtomically(_manifestFile.FullName, CultDocumentMessagePackSerialization.SerializeSnapshot(manifest));
|
||||
}
|
||||
|
||||
private CultPersistedStoreSnapshot ReadManifest()
|
||||
{
|
||||
if (_manifestFile.Exists)
|
||||
if (File.Exists(_manifestFile.FullName))
|
||||
{
|
||||
var snapshot = CultDocumentMessagePackSerialization.DeserializeSnapshot(File.ReadAllBytes(_manifestFile.FullName));
|
||||
if (snapshot.SchemaCatalog.Length > 0 || snapshot.Records.Length == 0)
|
||||
|
|
@ -158,7 +183,7 @@ public sealed class DirectoryMessagePackBackingStore : CacheBackingStore
|
|||
}
|
||||
}
|
||||
|
||||
if (_manifestFile.Exists)
|
||||
if (File.Exists(_manifestFile.FullName))
|
||||
{
|
||||
return CultDocumentMessagePackSerialization.DeserializeSnapshot(File.ReadAllBytes(_manifestFile.FullName));
|
||||
}
|
||||
|
|
@ -171,7 +196,8 @@ public sealed class DirectoryMessagePackBackingStore : CacheBackingStore
|
|||
};
|
||||
}
|
||||
|
||||
private IReadOnlyCollection<CultSchemaCatalogEntry> ResolveCatalogForRecord(
|
||||
// Explicit compatibility path for records written before catalog precommit existed.
|
||||
private IReadOnlyCollection<CultSchemaCatalogEntry> ResolveLegacyUncataloguedRecordCatalog(
|
||||
CultPersistedRecord record,
|
||||
IReadOnlyCollection<CultSchemaCatalogEntry> manifestCatalog)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -282,6 +282,95 @@ namespace GameCult.Caching.Tests
|
|||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task DirectoryMessagePackBackingStore_Precommits_Catalog_And_Remains_Dirty_When_Record_Write_Fails()
|
||||
{
|
||||
var filePath = Path.Combine(Path.GetTempPath(), $"cultlib-tests-{Guid.NewGuid():N}.cc");
|
||||
var recordsPath = DirectoryMessagePackBackingStore.DefaultRecordDirectoryPath(filePath);
|
||||
|
||||
try
|
||||
{
|
||||
var store = new DirectoryMessagePackBackingStore(filePath, recordsPath);
|
||||
var cache = new CultCache();
|
||||
cache.AddBackingStore(store);
|
||||
var key = new CultRecordKey("record:catalog-precommit");
|
||||
await cache.UpsertAsync(new NamedTestEntry { Name = "old", Value = "durable" }, new CultRecordHandle<NamedTestEntry>(key));
|
||||
store.PushAll();
|
||||
|
||||
var recordPath = Directory.GetFiles(recordsPath, "*.msgpack").Single();
|
||||
var durableSchema = CultDocumentMessagePackSerialization
|
||||
.DeserializePersistedRecord(File.ReadAllBytes(recordPath))
|
||||
.SchemaId;
|
||||
var alternateDescriptor = CultDocumentRegistry.Shared.GetRequired<AlternateNamedTestEntry>();
|
||||
store.Push(new CultStoredDocument(
|
||||
key,
|
||||
DateTimeOffset.UtcNow.ToString("O"),
|
||||
alternateDescriptor,
|
||||
new AlternateNamedTestEntry { Name = "new" }));
|
||||
|
||||
using (new FileStream(recordPath, FileMode.Open, FileAccess.Read, FileShare.Read))
|
||||
{
|
||||
Assert.That(() => store.PushAll(), Throws.Exception);
|
||||
}
|
||||
|
||||
var precommit = CultDocumentMessagePackSerialization.DeserializeSnapshot(File.ReadAllBytes(filePath));
|
||||
var alternateSchema = alternateDescriptor.SchemaId;
|
||||
Assert.That(precommit.SchemaCatalog.Select(entry => entry.SchemaId), Does.Contain(durableSchema));
|
||||
Assert.That(precommit.SchemaCatalog.Select(entry => entry.SchemaId), Does.Contain(alternateSchema));
|
||||
Assert.That(store.IsDirty, Is.True);
|
||||
Assert.That(CultDocumentMessagePackSerialization.DeserializePersistedRecord(File.ReadAllBytes(recordPath)).SchemaId, Is.EqualTo(durableSchema));
|
||||
|
||||
store.PushAll();
|
||||
|
||||
var compacted = CultDocumentMessagePackSerialization.DeserializeSnapshot(File.ReadAllBytes(filePath));
|
||||
Assert.That(compacted.SchemaCatalog.Select(entry => entry.SchemaId), Is.EqualTo(new[] { alternateSchema }));
|
||||
Assert.That(store.IsDirty, Is.False);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (File.Exists(filePath)) File.Delete(filePath);
|
||||
if (Directory.Exists(recordsPath)) Directory.Delete(recordsPath, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task DirectoryMessagePackBackingStore_Remains_Dirty_When_Deletion_Fails()
|
||||
{
|
||||
var filePath = Path.Combine(Path.GetTempPath(), $"cultlib-tests-{Guid.NewGuid():N}.cc");
|
||||
var recordsPath = DirectoryMessagePackBackingStore.DefaultRecordDirectoryPath(filePath);
|
||||
|
||||
try
|
||||
{
|
||||
var store = new DirectoryMessagePackBackingStore(filePath, recordsPath);
|
||||
var cache = new CultCache();
|
||||
cache.AddBackingStore(store);
|
||||
var handle = await cache.AddAsync(new NamedTestEntry { Name = "delete", Value = "durable" });
|
||||
store.PushAll();
|
||||
var recordPath = Directory.GetFiles(recordsPath, "*.msgpack").Single();
|
||||
Assert.That(cache.Remove(handle.Key), Is.True);
|
||||
|
||||
using (new FileStream(recordPath, FileMode.Open, FileAccess.Read, FileShare.Read))
|
||||
{
|
||||
Assert.That(() => store.PushAll(), Throws.Exception);
|
||||
}
|
||||
|
||||
Assert.That(File.Exists(recordPath), Is.True);
|
||||
Assert.That(store.IsDirty, Is.True);
|
||||
|
||||
store.PushAll();
|
||||
|
||||
Assert.That(File.Exists(recordPath), Is.False);
|
||||
Assert.That(store.IsDirty, Is.False);
|
||||
var compacted = CultDocumentMessagePackSerialization.DeserializeSnapshot(File.ReadAllBytes(filePath));
|
||||
Assert.That(compacted.SchemaCatalog, Is.Empty);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (File.Exists(filePath)) File.Delete(filePath);
|
||||
if (Directory.Exists(recordsPath)) Directory.Delete(recordsPath, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task CultCache_RuntimeType_Upsert_Snapshot_And_Remove_Work_For_Editor_Tooling()
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in a new issue