Mesh single-file helpers: delete the second reader, the older catalog layout and the lock-free replace
Deletes CultMesh's ReadSingleFileSnapshot, HasOlderCatalogLayout, ReadLegacySchemaCatalogEntry, WriteFileAtomically and the File.Exists gate, ReadStore's entry-reader delegate, and the three tests of the older catalog layout. This commit does not build: the Mesh read and write sites still name the deleted reader; the next commit routes them through the CultCache single-file store. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
245b0619b4
commit
2e12e9eebb
4 changed files with 4 additions and 193 deletions
|
|
@ -185,13 +185,7 @@ public static class CultDocumentMessagePackSerialization
|
|||
/// <see cref="CultStoreUnreadableException"/>, so a reader never takes part of a file for the whole and never skips a slot
|
||||
/// it does not understand. The rewriting paths use this same reader as their verdict on whether a file may be replaced.
|
||||
/// </summary>
|
||||
public static CultPersistedStoreSnapshot DeserializeSnapshot(byte[] payload) => ReadStore(payload, ReadSchemaCatalogEntry);
|
||||
|
||||
internal delegate CultSchemaCatalogEntry CatalogEntryReader(ref MessagePackReader reader);
|
||||
|
||||
// The one store reader. A caller with an older catalog layout supplies its own entry reader; the framing, the slot count
|
||||
// and the records are judged here, once.
|
||||
internal static CultPersistedStoreSnapshot ReadStore(byte[] payload, CatalogEntryReader readCatalogEntry)
|
||||
public static CultPersistedStoreSnapshot DeserializeSnapshot(byte[] payload)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
|
@ -215,7 +209,7 @@ public static class CultDocumentMessagePackSerialization
|
|||
snapshot.SchemaCatalog = new CultSchemaCatalogEntry[catalogCount];
|
||||
for (var index = 0; index < catalogCount; index++)
|
||||
{
|
||||
snapshot.SchemaCatalog[index] = readCatalogEntry(ref reader);
|
||||
snapshot.SchemaCatalog[index] = ReadSchemaCatalogEntry(ref reader);
|
||||
}
|
||||
|
||||
var recordCount = reader.ReadArrayHeader();
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
using System;
|
||||
using System.Buffers;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using GameCult.Caching;
|
||||
using GameCult.Caching.MessagePack;
|
||||
using MessagePack;
|
||||
|
||||
namespace GameCult.Mesh
|
||||
{
|
||||
|
|
@ -241,9 +239,7 @@ namespace GameCult.Mesh
|
|||
throw new ArgumentException("Catalog entry must include a schema id.", nameof(catalogEntry));
|
||||
|
||||
payload ??= Array.Empty<byte>();
|
||||
// The file being replaced is read first, by the reader every read of it uses: one this runtime cannot read refuses the
|
||||
// write instead of being overwritten.
|
||||
var existing = File.Exists(path) ? ReadSingleFileSnapshot(path) : null;
|
||||
CultPersistedStoreSnapshot? existing = null;
|
||||
|
||||
// A typed write sees the document it replaces the file with, so its ids decide. A raw payload is opaque: the writer
|
||||
// cannot see whether it holds ids, so a file already marked stays marked.
|
||||
|
|
@ -263,96 +259,6 @@ namespace GameCult.Mesh
|
|||
}
|
||||
};
|
||||
|
||||
var directory = Path.GetDirectoryName(path);
|
||||
if (!string.IsNullOrWhiteSpace(directory))
|
||||
Directory.CreateDirectory(directory);
|
||||
|
||||
WriteFileAtomically(path, CultDocumentMessagePackSerialization.SerializeSnapshot(snapshot));
|
||||
}
|
||||
|
||||
// Every refusal is a CultStoreUnreadableException naming the file. A store whose catalog entries are in the older layout is
|
||||
// read by the same store reader with the older entry reader, so it is judged by the same framing, slot count and records.
|
||||
// Only a store that is in the older layout goes there; any other refusal stands as the current reader gave it.
|
||||
private static CultPersistedStoreSnapshot ReadSingleFileSnapshot(string path)
|
||||
{
|
||||
var bytes = File.ReadAllBytes(path);
|
||||
CultPersistedStoreSnapshot snapshot;
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
snapshot = CultDocumentMessagePackSerialization.DeserializeSnapshot(bytes);
|
||||
}
|
||||
catch (CultStoreUnreadableException) when (HasOlderCatalogLayout(bytes))
|
||||
{
|
||||
snapshot = CultDocumentMessagePackSerialization.ReadStore(bytes, ReadLegacySchemaCatalogEntry);
|
||||
}
|
||||
|
||||
CultDocumentMessagePackSerialization.RequireSingleFileFormat(snapshot);
|
||||
}
|
||||
catch (CultStoreUnreadableException ex) when (ex.Path == null)
|
||||
{
|
||||
throw new CultStoreUnreadableException(ex.Message, path, ex.InnerException);
|
||||
}
|
||||
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
// The older layout puts the content hash, a string, at catalog entry slot 5, where the current layout has the array of
|
||||
// compatible schema ids.
|
||||
private static bool HasOlderCatalogLayout(byte[] bytes)
|
||||
{
|
||||
try
|
||||
{
|
||||
var reader = new MessagePackReader(bytes);
|
||||
if (reader.ReadArrayHeader() != 3)
|
||||
return false;
|
||||
reader.Skip();
|
||||
var entries = reader.ReadArrayHeader();
|
||||
for (var entry = 0; entry < entries; entry++)
|
||||
{
|
||||
var fields = reader.ReadArrayHeader();
|
||||
for (var field = 0; field < fields; field++)
|
||||
{
|
||||
if (field == 5 && reader.NextMessagePackType == MessagePackType.String)
|
||||
return true;
|
||||
reader.Skip();
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex) when (ex is EndOfStreamException or MessagePackSerializationException or InvalidOperationException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static CultSchemaCatalogEntry ReadLegacySchemaCatalogEntry(ref MessagePackReader reader)
|
||||
{
|
||||
var fieldCount = reader.ReadArrayHeader();
|
||||
var entry = new CultSchemaCatalogEntry();
|
||||
if (fieldCount > 0) entry.SchemaId = reader.ReadString() ?? string.Empty;
|
||||
if (fieldCount > 1) entry.SchemaName = reader.ReadString() ?? string.Empty;
|
||||
if (fieldCount > 2) entry.SchemaVersion = reader.ReadString() ?? string.Empty;
|
||||
if (fieldCount > 3) entry.CanonicalSchemaJson = reader.ReadString() ?? string.Empty;
|
||||
if (fieldCount > 4) reader.Skip();
|
||||
if (fieldCount > 5) entry.ContentHash = reader.ReadString() ?? string.Empty;
|
||||
if (fieldCount > 6)
|
||||
{
|
||||
var memberCount = reader.ReadArrayHeader();
|
||||
for (var index = 0; index < memberCount; index++)
|
||||
reader.Skip();
|
||||
}
|
||||
|
||||
for (var index = 7; index < fieldCount; index++)
|
||||
reader.Skip();
|
||||
|
||||
entry.CompatibleSchemaIds = string.IsNullOrWhiteSpace(entry.SchemaId)
|
||||
? Array.Empty<string>()
|
||||
: new[] { entry.SchemaId };
|
||||
entry.Members = Array.Empty<CultSchemaMemberCatalogEntry>();
|
||||
return entry;
|
||||
}
|
||||
|
||||
private static bool PublishesSchema(CultSchemaCatalogEntry[] catalog, string schemaId)
|
||||
|
|
@ -361,14 +267,5 @@ namespace GameCult.Mesh
|
|||
string.Equals(entry.SchemaId, schemaId, StringComparison.Ordinal) ||
|
||||
entry.CompatibleSchemaIds.Any(candidate => string.Equals(candidate, schemaId, StringComparison.Ordinal)));
|
||||
}
|
||||
|
||||
private static void WriteFileAtomically(string path, byte[] bytes)
|
||||
{
|
||||
var tempPath = path + ".tmp";
|
||||
File.WriteAllBytes(tempPath, bytes);
|
||||
if (File.Exists(path))
|
||||
File.Delete(path);
|
||||
File.Move(tempPath, path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2585,34 +2585,6 @@ public sealed class CultMeshStreamingTests
|
|||
cache.Get<MeshPublicationNoteDocument>(key)!.Text.Should().Be("published-directly");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SingleFileDocumentHelpers_ReadLegacySingleDocumentSnapshots()
|
||||
{
|
||||
using var directory = new TemporaryDirectory();
|
||||
var filePath = Path.Combine(directory.Path, "legacy-single-document.ccmp");
|
||||
var key = new CultRecordKey("mesh-note:legacy-single-document");
|
||||
var descriptor = CultDocumentRegistry.Shared.GetRequired<MeshPublicationNoteDocument>();
|
||||
var payload = CultDocumentMessagePackSerialization.SerializeUntyped(new MeshPublicationNoteDocument
|
||||
{
|
||||
Schema = "tests.mesh_publication_note.v1",
|
||||
Text = "legacy-published",
|
||||
Revision = 4
|
||||
}, typeof(MeshPublicationNoteDocument));
|
||||
|
||||
File.WriteAllBytes(filePath, WriteLegacySingleDocumentSnapshot(
|
||||
key.Value,
|
||||
descriptor.SchemaId,
|
||||
descriptor.SchemaName,
|
||||
descriptor.SchemaVersion,
|
||||
"2026-06-27T12:00:00.0000000Z",
|
||||
payload));
|
||||
|
||||
var document = CultMesh.ReadSingleFileDocument<MeshPublicationNoteDocument>(filePath, key);
|
||||
|
||||
document.Text.Should().Be("legacy-published");
|
||||
document.Revision.Should().Be(4);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task DocumentHandle_ReadsRemotePeerSnapshotsAsTypedDocuments()
|
||||
{
|
||||
|
|
@ -3853,39 +3825,6 @@ public sealed class CultMeshStreamingTests
|
|||
ring.Stats().UnavoidableCopyCount.Should().Be(1);
|
||||
}
|
||||
|
||||
private static byte[] WriteLegacySingleDocumentSnapshot(
|
||||
string key,
|
||||
string schemaId,
|
||||
string schemaName,
|
||||
string schemaVersion,
|
||||
string storedAt,
|
||||
byte[] payload)
|
||||
{
|
||||
var buffer = new ArrayBufferWriter<byte>();
|
||||
var writer = new MessagePackWriter(buffer);
|
||||
|
||||
writer.WriteArrayHeader(3);
|
||||
writer.Write("cultcache.store.v1");
|
||||
writer.WriteArrayHeader(1);
|
||||
writer.WriteArrayHeader(7);
|
||||
writer.Write(schemaId);
|
||||
writer.Write(schemaName);
|
||||
writer.Write(schemaVersion);
|
||||
writer.Write("Legacy Aetheria single document");
|
||||
writer.Write(storedAt);
|
||||
writer.Write("legacy-hash");
|
||||
writer.WriteArrayHeader(0);
|
||||
writer.WriteArrayHeader(1);
|
||||
writer.WriteArrayHeader(4);
|
||||
writer.Write(key);
|
||||
writer.Write(schemaId);
|
||||
writer.Write(storedAt);
|
||||
writer.Write(payload);
|
||||
writer.Flush();
|
||||
|
||||
return buffer.WrittenSpan.ToArray();
|
||||
}
|
||||
|
||||
private static CultMeshStreamCatalog CatalogWithByteStream()
|
||||
{
|
||||
var catalog = CultMesh.CreateStreamCatalog();
|
||||
|
|
|
|||
|
|
@ -38,26 +38,7 @@ public sealed class CultMeshVariantRefusalTests
|
|||
message.Should().Contain("item:anvil").And.Contain(ItemSchemaId);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ReadSingleFileDocument_RefusesExtraRecordSlotInTheLegacyCatalogLayoutToo()
|
||||
{
|
||||
var message = Refusal("legacy-catalog-extra-slot.msgpack").Message;
|
||||
message.Should().Contain("item:anvil").And.Contain(ItemSchemaId);
|
||||
}
|
||||
|
||||
// The older catalog layout is read by the same store reader, so it is held to one array too: the same store with bytes after it
|
||||
// is refused, and only the bytes make the difference.
|
||||
[Test]
|
||||
public void ReadSingleFileDocument_ReadsTheLegacyCatalogLayoutButNotWithBytesAfterIt()
|
||||
{
|
||||
CultMesh.ReadSingleFileDocumentPayload(VectorPath("legacy-catalog-plain.msgpack"), new CultRecordKey("item:bellows"), ItemSchemaId)
|
||||
.Should().NotBeEmpty();
|
||||
|
||||
Refusal("legacy-catalog-trailing.msgpack").Message.Should().Contain("after its MessagePack array");
|
||||
}
|
||||
|
||||
// A store in the current layout with one catalog slot malformed is refused as CultCache refuses it. It is not the older layout,
|
||||
// so it is not read with the older reader, which would take the malformed canonical schema slot for a slot to skip.
|
||||
// A store in the current layout with one catalog slot malformed is refused as CultCache refuses it.
|
||||
[TestCase("current-catalog-bad-canonical.msgpack")]
|
||||
[TestCase("current-catalog-bad-members.msgpack")]
|
||||
[TestCase("current-catalog-bad-compat.msgpack")]
|
||||
|
|
|
|||
Loading…
Reference in a new issue