using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
namespace Server.Tests;
///
/// A private save pipeline: a chunk source with its own serialization workers, optionally installed as
/// World's workers, optionally a scratch directory and an extra assembly visible to the type loader.
/// Dispose unregisters every tracked persistence, exits the workers and restores every static it swapped.
///
internal sealed class PersistencePipelineScope : IDisposable
{
private readonly List _persistences = [];
private readonly SerializationThreadWorker[] _previousWorkers;
private readonly Assembly[] _previousAssemblies;
private readonly DateTime _previousSaveStart;
private readonly bool _installedWorkers;
private readonly bool _swappedAssemblies;
/// Workers to create, each with an allocated heap.
/// Install the workers as World's, which WriteSnapshot reads segments from.
/// An assembly the loader must resolve types from by hash.
/// Creates a unique scratch directory under the temp path when set.
public PersistencePipelineScope(
int workerCount,
bool installWorkers = false,
Assembly visibleAssembly = null,
string directoryPrefix = null
)
{
_previousSaveStart = World.SaveStartTime;
Source = new SerializationChunkSource();
Workers = new SerializationThreadWorker[workerCount];
for (var i = 0; i < Workers.Length; i++)
{
Workers[i] = new SerializationThreadWorker(i, Source);
Workers[i].AllocateHeap();
}
if (installWorkers)
{
_installedWorkers = true;
_previousWorkers = World._threadWorkers;
World._threadWorkers = Workers;
}
if (visibleAssembly != null)
{
_swappedAssemblies = true;
_previousAssemblies = AssemblyHandler.Assemblies;
AssemblyHandler.Assemblies = [.. _previousAssemblies ?? [], visibleAssembly];
}
if (directoryPrefix != null)
{
Directory = Path.Combine(Path.GetTempPath(), $"{directoryPrefix}-{Guid.NewGuid():N}");
System.IO.Directory.CreateDirectory(Directory);
}
}
public SerializationChunkSource Source { get; }
public SerializationThreadWorker[] Workers { get; }
/// The scratch directory, or null when none was requested.
public string Directory { get; }
/// Tracks a persistence so it leaves the static registry on dispose.
public T Track(T persistence) where T : Persistence
{
_persistences.Add(persistence);
return persistence;
}
public void WakeWorkers()
{
for (var i = 0; i < Workers.Length; i++)
{
Workers[i].Wake();
}
}
public void SleepWorkers()
{
for (var i = 0; i < Workers.Length; i++)
{
Workers[i].Sleep();
}
}
/// Mirrors World.PauseSerializationThreads: publish the partial chunk, then wait for the drain.
public void FlushAndSleepWorkers()
{
Source.Flush();
SleepWorkers();
}
public void Dispose()
{
World.SaveStartTime = _previousSaveStart;
for (var i = 0; i < _persistences.Count; i++)
{
_persistences[i]?.Unregister();
}
for (var i = 0; i < Workers.Length; i++)
{
Workers[i].Exit();
}
if (_installedWorkers)
{
World._threadWorkers = _previousWorkers;
}
if (_swappedAssemblies)
{
AssemblyHandler.Assemblies = _previousAssemblies;
}
if (Directory != null)
{
System.IO.Directory.Delete(Directory, true);
}
}
}