feat(bake): publish validated pak artifacts atomically
This commit is contained in:
parent
90b378cc70
commit
999201cca7
9 changed files with 362 additions and 14 deletions
39
src/AcDream.Bake/BakeArtifactValidator.cs
Normal file
39
src/AcDream.Bake/BakeArtifactValidator.cs
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
using AcDream.Content.Pak;
|
||||
|
||||
namespace AcDream.Bake;
|
||||
|
||||
/// <summary>Strict pre-publication validation for one completed bake artifact.</summary>
|
||||
public static class BakeArtifactValidator
|
||||
{
|
||||
public static void Validate(
|
||||
string path,
|
||||
in PakHeader expectedHeader,
|
||||
int expectedTocCount)
|
||||
{
|
||||
using var reader = new PakReader(path);
|
||||
var actual = reader.Header;
|
||||
|
||||
if (actual.FormatVersion != PakFormat.CurrentFormatVersion)
|
||||
throw new InvalidDataException(
|
||||
$"bake format version {actual.FormatVersion} does not match " +
|
||||
$"{PakFormat.CurrentFormatVersion}");
|
||||
if (actual.BakeToolVersion != PakFormat.CurrentBakeToolVersion)
|
||||
throw new InvalidDataException(
|
||||
$"bake tool version {actual.BakeToolVersion} does not match " +
|
||||
$"{PakFormat.CurrentBakeToolVersion}");
|
||||
if (actual.PortalIteration != expectedHeader.PortalIteration ||
|
||||
actual.CellIteration != expectedHeader.CellIteration ||
|
||||
actual.HighResIteration != expectedHeader.HighResIteration ||
|
||||
actual.LanguageIteration != expectedHeader.LanguageIteration)
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
"bake DAT iterations do not match the source collection");
|
||||
}
|
||||
if (actual.TocCount != checked((uint)expectedTocCount))
|
||||
throw new InvalidDataException(
|
||||
$"bake TOC count {actual.TocCount} does not match expected " +
|
||||
$"{expectedTocCount}");
|
||||
|
||||
reader.ValidateComplete();
|
||||
}
|
||||
}
|
||||
61
src/AcDream.Bake/BakeOutputTransaction.cs
Normal file
61
src/AcDream.Bake/BakeOutputTransaction.cs
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
using AcDream.Content.Pak;
|
||||
|
||||
namespace AcDream.Bake;
|
||||
|
||||
/// <summary>
|
||||
/// Publishes a bake as one same-volume filesystem transaction. Work and
|
||||
/// validation happen against a unique adjacent temporary file; failure or
|
||||
/// cancellation leaves an existing destination untouched.
|
||||
/// </summary>
|
||||
public static class BakeOutputTransaction
|
||||
{
|
||||
public static TResult WriteValidateAndPublish<TResult>(
|
||||
string destinationPath,
|
||||
Func<string, TResult> writeTemporary,
|
||||
Action<string, TResult> validateTemporary,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(destinationPath);
|
||||
ArgumentNullException.ThrowIfNull(writeTemporary);
|
||||
ArgumentNullException.ThrowIfNull(validateTemporary);
|
||||
|
||||
string fullDestination = Path.GetFullPath(destinationPath);
|
||||
string? directory = Path.GetDirectoryName(fullDestination);
|
||||
if (string.IsNullOrEmpty(directory))
|
||||
throw new InvalidOperationException("destination has no parent directory");
|
||||
Directory.CreateDirectory(directory);
|
||||
|
||||
string temporaryPath = Path.Combine(
|
||||
directory,
|
||||
$".{Path.GetFileName(fullDestination)}.{Guid.NewGuid():N}.tmp");
|
||||
|
||||
try
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
TResult result = writeTemporary(temporaryPath);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
validateTemporary(temporaryPath, result);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (File.Exists(fullDestination))
|
||||
File.Replace(temporaryPath, fullDestination, null, ignoreMetadataErrors: true);
|
||||
else
|
||||
File.Move(temporaryPath, fullDestination);
|
||||
|
||||
return result;
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(temporaryPath))
|
||||
File.Delete(temporaryPath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Preserve the original exception. An adjacent .tmp is
|
||||
// recognizable and never mistaken for a published pak.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -24,12 +24,14 @@ public sealed record BakeOptions
|
|||
/// <summary>Compact result used by the full-scale gate and deterministic tests.</summary>
|
||||
public sealed record BakeReport
|
||||
{
|
||||
public required PakHeader Header { get; init; }
|
||||
public required int GfxObjKeys { get; init; }
|
||||
public required int SetupKeys { get; init; }
|
||||
public required int EnvCellKeys { get; init; }
|
||||
public required int UniqueEnvCellGeometries { get; init; }
|
||||
public required int EnvCellAliases { get; init; }
|
||||
public required int SideStagedKeys { get; init; }
|
||||
public int SideStagedDuplicateKeys { get; init; }
|
||||
public required int PhysicalBlobs { get; init; }
|
||||
public required int TotalKeys { get; init; }
|
||||
public required int Failures { get; init; }
|
||||
|
|
@ -66,12 +68,29 @@ public static class BakeRunner
|
|||
if (options.Threads <= 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(options), "thread count must be positive");
|
||||
|
||||
options.CancellationToken.ThrowIfCancellationRequested();
|
||||
var report = BakeOutputTransaction.WriteValidateAndPublish(
|
||||
options.OutPath,
|
||||
temporaryPath => RunCore(
|
||||
options with { OutPath = temporaryPath },
|
||||
options.OutPath),
|
||||
(temporaryPath, result) => BakeArtifactValidator.Validate(
|
||||
temporaryPath,
|
||||
result.Header,
|
||||
result.TotalKeys),
|
||||
options.CancellationToken);
|
||||
|
||||
PrintSummary(report, options.OutPath);
|
||||
return report;
|
||||
}
|
||||
|
||||
private static BakeReport RunCore(BakeOptions options, string publishedOutputPath)
|
||||
{
|
||||
var cancellationToken = options.CancellationToken;
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
Console.WriteLine("acdream-bake");
|
||||
Console.WriteLine($"dat dir: {options.DatDir}");
|
||||
Console.WriteLine($"out: {options.OutPath}");
|
||||
Console.WriteLine($"out: {publishedOutputPath}");
|
||||
Console.WriteLine($"threads: {options.Threads}");
|
||||
Console.WriteLine();
|
||||
|
||||
|
|
@ -147,7 +166,7 @@ public static class BakeRunner
|
|||
CellIteration = (uint)dats.Cell.Iteration!.CurrentIteration,
|
||||
HighResIteration = (uint)dats.HighRes.Iteration!.CurrentIteration,
|
||||
LanguageIteration = (uint)dats.Local.Iteration!.CurrentIteration,
|
||||
BakeToolVersion = 1,
|
||||
BakeToolVersion = PakFormat.CurrentBakeToolVersion,
|
||||
};
|
||||
|
||||
var writtenKeys = new HashSet<ulong>();
|
||||
|
|
@ -339,6 +358,7 @@ public static class BakeRunner
|
|||
|
||||
var report = new BakeReport
|
||||
{
|
||||
Header = header,
|
||||
GfxObjKeys = gfxObjWritten,
|
||||
SetupKeys = setupWritten,
|
||||
EnvCellKeys = envCellKeysWritten,
|
||||
|
|
@ -354,7 +374,7 @@ public static class BakeRunner
|
|||
PeakWorkingSetBytes = Process.GetCurrentProcess().PeakWorkingSet64,
|
||||
};
|
||||
|
||||
PrintSummary(report, sideStagedDuped, options.OutPath);
|
||||
report = report with { SideStagedDuplicateKeys = sideStagedDuped };
|
||||
PrintFailures(failures);
|
||||
return report;
|
||||
}
|
||||
|
|
@ -392,7 +412,7 @@ public static class BakeRunner
|
|||
lastProgressReport.Restart();
|
||||
}
|
||||
|
||||
private static void PrintSummary(BakeReport report, int sideStagedDuped, string outputPath)
|
||||
private static void PrintSummary(BakeReport report, string outputPath)
|
||||
{
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("=== bake summary ===");
|
||||
|
|
@ -404,7 +424,7 @@ public static class BakeRunner
|
|||
Console.WriteLine($" EnvCell dedup: {report.EnvCellDedupRatio:F1}x");
|
||||
Console.WriteLine(
|
||||
$" side-staged keys: {report.SideStagedKeys:N0} " +
|
||||
$"({sideStagedDuped:N0} duplicate keys)");
|
||||
$"({report.SideStagedDuplicateKeys:N0} duplicate keys)");
|
||||
Console.WriteLine($" physical blobs: {report.PhysicalBlobs:N0}");
|
||||
Console.WriteLine($" total keys: {report.TotalKeys:N0}");
|
||||
Console.WriteLine($" failures: {report.Failures:N0}");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue