PosixWatchdog doesn't need hard links

- Added ISymlinkFactory.SymlinkedDirectoriesAreFiles because they are files on Linux.
- Removed crazy-ass hard link junk from PosixWatchdog.
- Added symlink unit tests.
This commit is contained in:
Jordan
2023-06-16 18:23:29 -04:00
parent efd0936a67
commit f1f0a350d6
7 changed files with 127 additions and 81 deletions
@@ -80,9 +80,11 @@ namespace Tgstation.Server.Host.Components.Deployment
if (Interlocked.Exchange(ref swapped, 1) != 0)
throw new InvalidOperationException("Already swapped!");
// Note this comment from TGS3:
// These next two lines should be atomic but this is the best we can do
await ioManager.DeleteDirectory(LiveGameDirectory, cancellationToken);
if (symlinkFactory.SymlinkedDirectoriesAreDeletedAsFiles)
await ioManager.DeleteFile(LiveGameDirectory, cancellationToken);
else
await ioManager.DeleteDirectory(LiveGameDirectory, cancellationToken);
await symlinkFactory.CreateSymbolicLink(
ioManager.ResolvePath(baseProvider.Directory),
ioManager.ResolvePath(LiveGameDirectory),
@@ -1,5 +1,4 @@
using System;
using System.Threading;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
@@ -22,11 +21,6 @@ namespace Tgstation.Server.Host.Components.Watchdog
/// </summary>
sealed class PosixWatchdog : WindowsWatchdog
{
/// <summary>
/// If the swappable game directory is currently a rename of the compile job.
/// </summary>
IDmbProvider hardLinkedDmb;
/// <summary>
/// Initializes a new instance of the <see cref="PosixWatchdog"/> class.
/// </summary>
@@ -84,77 +78,10 @@ namespace Tgstation.Server.Host.Components.Watchdog
}
/// <inheritdoc />
protected override Task ApplyInitialDmb(CancellationToken cancellationToken) => Task.CompletedTask; // not necessary to hold initial .dmb on Linux because of based inode deletes
/// <inheritdoc />
protected override async Task InitialLink(CancellationToken cancellationToken)
protected override Task ApplyInitialDmb(CancellationToken cancellationToken)
{
// The logic to check for an active live directory is in SwappableDmbProvider, so we just do it again here for safety
Logger.LogTrace("Hard linking compile job...");
// Symlinks are counted as a file on linux??
if (await GameIOManager.DirectoryExists(ActiveSwappable.Directory, cancellationToken))
await GameIOManager.DeleteDirectory(ActiveSwappable.Directory, cancellationToken);
else
await GameIOManager.DeleteFile(ActiveSwappable.Directory, cancellationToken);
// Instead of symlinking to begin with we actually rename the directory
await GameIOManager.MoveDirectory(
ActiveSwappable.CompileJob.DirectoryName.ToString(),
ActiveSwappable.Directory,
cancellationToken);
hardLinkedDmb = ActiveSwappable;
}
/// <inheritdoc />
protected override async Task InitController(Task chatTask, ReattachInformation reattachInfo, CancellationToken cancellationToken)
{
var suspended = false;
try
{
await base.InitController(chatTask, reattachInfo, cancellationToken);
}
finally
{
// Then we move it back and apply the symlink
if (hardLinkedDmb != null)
{
try
{
Logger.LogTrace("Unhardlinking compile job...");
Server?.Suspend();
suspended = true;
var hardLink = hardLinkedDmb.Directory;
var originalPosition = hardLinkedDmb.CompileJob.DirectoryName.ToString();
await GameIOManager.MoveDirectory(
hardLink,
originalPosition,
default);
}
catch (Exception ex)
{
Logger.LogError(
ex,
"Failed to un-hard link compile job #{compileJobId} ({compileJobDirectory})",
hardLinkedDmb.CompileJob.Id,
hardLinkedDmb.CompileJob.DirectoryName);
}
hardLinkedDmb = null;
}
}
if (reattachInfo != null)
{
Logger.LogTrace("Skipping symlink due to reattach");
return;
}
Logger.LogTrace("Symlinking compile job...");
await ActiveSwappable.MakeActive(cancellationToken);
if (suspended)
Server.Resume();
// not necessary to hold initial .dmb on Linux because of based inode deletes
return Task.CompletedTask;
}
}
}
@@ -318,7 +318,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
protected virtual Task InitialLink(CancellationToken cancellationToken)
Task InitialLink(CancellationToken cancellationToken)
{
Logger.LogTrace("Symlinking compile job...");
return ActiveSwappable.MakeActive(cancellationToken);
@@ -8,6 +8,12 @@ namespace Tgstation.Server.Host.IO
/// </summary>
interface ISymlinkFactory
{
/// <summary>
/// If directory symlinks must be deleted as files would in the current environment.
/// </summary>
/// <remarks>This is because Linux symlinked directories must be deleted with <see cref="global::System.IO.File.Delete(string)"/>.</remarks>
bool SymlinkedDirectoriesAreDeletedAsFiles { get; }
/// <summary>
/// Create a symbolic link.
/// </summary>
@@ -12,6 +12,9 @@ namespace Tgstation.Server.Host.IO
/// </summary>
sealed class PosixSymlinkFactory : ISymlinkFactory
{
/// <inheritdoc />
public bool SymlinkedDirectoriesAreDeletedAsFiles => true;
/// <inheritdoc />
public Task CreateSymbolicLink(string targetPath, string linkPath, CancellationToken cancellationToken) => Task.Factory.StartNew(
() =>
@@ -13,6 +13,9 @@ namespace Tgstation.Server.Host.IO
/// </summary>
sealed class WindowsSymlinkFactory : ISymlinkFactory
{
/// <inheritdoc />
public bool SymlinkedDirectoriesAreDeletedAsFiles => false;
/// <inheritdoc />
public Task CreateSymbolicLink(string targetPath, string linkPath, CancellationToken cancellationToken) => Task.Factory.StartNew(
() =>
@@ -0,0 +1,105 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Tgstation.Server.Host.IO;
namespace Tgstation.Server.Host.System.Tests
{
[TestClass]
public sealed class TestSymlinkFactory
{
readonly ISymlinkFactory factory = new PlatformIdentifier().IsWindows
? new WindowsSymlinkFactory()
: new PosixSymlinkFactory();
[TestMethod]
public async Task TestSymlinks()
{
var cancellationToken = CancellationToken.None;
var cwd = Path.GetTempFileName();
File.Delete(cwd);
Directory.CreateDirectory(cwd);
try
{
var realDir = Path.Combine(cwd, "RealDir");
var symDir = Path.Combine(cwd, "SymDir");
var realFile = Path.Combine(cwd, "RealFile.txt");
var symFile = Path.Combine(realDir, "RealFile.txt");
var subRealFile = Path.Combine(realDir, "test.txt");
var subSymFile = Path.Combine(symDir, "test.txt");
try
{
await factory.CreateSymbolicLink(subRealFile, subSymFile, cancellationToken);
Assert.Fail("Expected Exception!");
}
catch
{
}
Directory.CreateDirectory(realDir);
Directory.CreateDirectory(symDir);
await File.WriteAllBytesAsync(realFile, Array.Empty<byte>(), cancellationToken);
await File.WriteAllBytesAsync(symFile, Array.Empty<byte>(), cancellationToken);
try
{
await factory.CreateSymbolicLink(realFile, symFile, cancellationToken);
Assert.Fail("Expected Exception!");
}
catch
{
}
Directory.Delete(symDir);
File.Delete(symFile);
await factory.CreateSymbolicLink(realFile, symFile, cancellationToken);
Assert.IsTrue(File.Exists(symFile));
Assert.IsFalse(Directory.Exists(symFile));
await File.WriteAllTextAsync(realFile, "test", cancellationToken);
var symFileContents = await File.ReadAllTextAsync(symFile, cancellationToken);
Assert.AreEqual("test", symFileContents);
File.Delete(symFile);
File.Delete(realFile);
try
{
await factory.CreateSymbolicLink(realDir, symDir, cancellationToken);
Assert.IsFalse(File.Exists(symDir));
Assert.IsTrue(Directory.Exists(symDir));
await File.WriteAllTextAsync(subRealFile, "test", cancellationToken);
Assert.IsTrue(File.Exists(subSymFile));
File.Delete(subSymFile);
Assert.IsFalse(File.Exists(subRealFile));
}
finally
{
if (factory.SymlinkedDirectoriesAreDeletedAsFiles)
File.Delete(symDir);
else
Directory.Delete(symDir);
}
if (factory.SymlinkedDirectoriesAreDeletedAsFiles)
Assert.IsFalse(File.Exists(symDir));
else
Assert.IsFalse(Directory.Exists(symDir));
}
finally
{
Directory.Delete(cwd, true);
}
}
}
}