Add General:DeploymentDirectoryCopyTasksPerCore config option

This commit is contained in:
Dominion
2023-04-23 02:34:11 -04:00
parent 8e179c52da
commit d771da71b9
11 changed files with 207 additions and 18 deletions
@@ -139,6 +139,11 @@ namespace Tgstation.Server.Host.Components
/// </summary>
readonly IRemoteDeploymentManagerFactory remoteDeploymentManagerFactory;
/// <summary>
/// The <see cref="GeneralConfiguration"/> for the <see cref="InstanceFactory"/>.
/// </summary>
readonly GeneralConfiguration generalConfiguration;
/// <summary>
/// The <see cref="SessionConfiguration"/> for the <see cref="InstanceFactory"/>.
/// </summary>
@@ -177,6 +182,7 @@ namespace Tgstation.Server.Host.Components
/// <param name="fileTransferService">The value of <see cref="fileTransferService"/>.</param>
/// <param name="gitRemoteFeaturesFactory">The value of <see cref="gitRemoteFeaturesFactory"/>.</param>
/// <param name="remoteDeploymentManagerFactory">The value of <see cref="remoteDeploymentManagerFactory"/>.</param>
/// <param name="generalConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the value of <see cref="generalConfiguration"/>.</param>
/// <param name="sessionConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the value of <see cref="sessionConfiguration"/>.</param>
public InstanceFactory(
IIOManager ioManager,
@@ -201,6 +207,7 @@ namespace Tgstation.Server.Host.Components
IFileTransferTicketProvider fileTransferService,
IGitRemoteFeaturesFactory gitRemoteFeaturesFactory,
IRemoteDeploymentManagerFactory remoteDeploymentManagerFactory,
IOptions<GeneralConfiguration> generalConfigurationOptions,
IOptions<SessionConfiguration> sessionConfigurationOptions)
{
this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
@@ -225,6 +232,7 @@ namespace Tgstation.Server.Host.Components
this.fileTransferService = fileTransferService ?? throw new ArgumentNullException(nameof(fileTransferService));
this.gitRemoteFeaturesFactory = gitRemoteFeaturesFactory ?? throw new ArgumentNullException(nameof(gitRemoteFeaturesFactory));
this.remoteDeploymentManagerFactory = remoteDeploymentManagerFactory ?? throw new ArgumentNullException(nameof(remoteDeploymentManagerFactory));
generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
sessionConfiguration = sessionConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(sessionConfigurationOptions));
}
#pragma warning restore CA1502
@@ -266,7 +274,8 @@ namespace Tgstation.Server.Host.Components
postWriteHandler,
platformIdentifier,
fileTransferService,
loggerFactory.CreateLogger<StaticFiles.Configuration>());
loggerFactory.CreateLogger<StaticFiles.Configuration>(),
generalConfiguration);
var eventConsumer = new EventConsumer(configuration);
var repoManager = new RepositoryManager(
repositoryFactory,
@@ -276,7 +285,8 @@ namespace Tgstation.Server.Host.Components
postWriteHandler,
gitRemoteFeaturesFactory,
loggerFactory.CreateLogger<Repository.Repository>(),
loggerFactory.CreateLogger<RepositoryManager>());
loggerFactory.CreateLogger<RepositoryManager>(),
generalConfiguration);
try
{
var byond = new ByondManager(byondIOManager, byondInstaller, eventConsumer, loggerFactory.CreateLogger<ByondManager>());
@@ -12,6 +12,7 @@ using Microsoft.Extensions.Logging;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Api.Models.Internal;
using Tgstation.Server.Host.Components.Events;
using Tgstation.Server.Host.Configuration;
using Tgstation.Server.Host.IO;
using Tgstation.Server.Host.Jobs;
@@ -106,6 +107,11 @@ namespace Tgstation.Server.Host.Components.Repository
/// </summary>
readonly ILogger<Repository> logger;
/// <summary>
/// The <see cref="GeneralConfiguration"/> for the <see cref="Repository"/>.
/// </summary>
readonly GeneralConfiguration generalConfiguration;
/// <summary>
/// <see cref="Action"/> to be taken when <see cref="Dispose"/> is called.
/// </summary>
@@ -127,6 +133,7 @@ namespace Tgstation.Server.Host.Components.Repository
/// <param name="postWriteHandler">The value of <see cref="postWriteHandler"/>.</param>
/// <param name="gitRemoteFeaturesFactory">The <see cref="IGitRemoteFeaturesFactory"/> to provide the value of <see cref="gitRemoteFeatures"/>.</param>
/// <param name="logger">The value of <see cref="logger"/>.</param>
/// <param name="generalConfiguration">The value of <see cref="generalConfiguration"/>.</param>
/// <param name="onDispose">The value if <see cref="onDispose"/>.</param>
public Repository(
LibGit2Sharp.IRepository libGitRepo,
@@ -137,6 +144,7 @@ namespace Tgstation.Server.Host.Components.Repository
IPostWriteHandler postWriteHandler,
IGitRemoteFeaturesFactory gitRemoteFeaturesFactory,
ILogger<Repository> logger,
GeneralConfiguration generalConfiguration,
Action onDispose)
{
this.libGitRepo = libGitRepo ?? throw new ArgumentNullException(nameof(libGitRepo));
@@ -149,6 +157,7 @@ namespace Tgstation.Server.Host.Components.Repository
throw new ArgumentNullException(nameof(gitRemoteFeaturesFactory));
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
this.generalConfiguration = generalConfiguration ?? throw new ArgumentNullException(nameof(generalConfiguration));
this.onDispose = onDispose ?? throw new ArgumentNullException(nameof(onDispose));
gitRemoteFeatures = gitRemoteFeaturesFactory.CreateGitRemoteFeatures(this);
@@ -524,8 +533,6 @@ namespace Tgstation.Server.Host.Components.Repository
throw new ArgumentNullException(nameof(path));
logger.LogTrace("Copying to {0}...", path);
await ioMananger.CopyDirectory(
ioMananger.ResolvePath(),
path,
new List<string> { ".git" },
(src, dest) =>
{
@@ -534,6 +541,9 @@ namespace Tgstation.Server.Host.Components.Repository
return Task.CompletedTask;
},
ioMananger.ResolvePath(),
path,
generalConfiguration.GetCopyDirectoryTaskThrottle(),
cancellationToken);
}
@@ -7,6 +7,7 @@ using Microsoft.Extensions.Logging;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Host.Components.Events;
using Tgstation.Server.Host.Configuration;
using Tgstation.Server.Host.Core;
using Tgstation.Server.Host.IO;
using Tgstation.Server.Host.Jobs;
@@ -62,6 +63,11 @@ namespace Tgstation.Server.Host.Components.Repository
/// </summary>
readonly ILogger<RepositoryManager> logger;
/// <summary>
/// The <see cref="GeneralConfiguration"/> for the <see cref="RepositoryManager"/>.
/// </summary>
readonly GeneralConfiguration generalConfiguration;
/// <summary>
/// Used for controlling single access to the <see cref="IRepository"/>.
/// </summary>
@@ -78,6 +84,7 @@ namespace Tgstation.Server.Host.Components.Repository
/// <param name="gitRemoteFeaturesFactory">The value of <see cref="gitRemoteFeaturesFactory"/>.</param>
/// <param name="repositoryLogger">The value of <see cref="repositoryLogger"/>.</param>
/// <param name="logger">The value of <see cref="logger"/>.</param>
/// <param name="generalConfiguration">The value of <see cref="generalConfiguration"/>.</param>
public RepositoryManager(
ILibGit2RepositoryFactory repositoryFactory,
ILibGit2Commands commands,
@@ -86,7 +93,8 @@ namespace Tgstation.Server.Host.Components.Repository
IPostWriteHandler postWriteHandler,
IGitRemoteFeaturesFactory gitRemoteFeaturesFactory,
ILogger<Repository> repositoryLogger,
ILogger<RepositoryManager> logger)
ILogger<RepositoryManager> logger,
GeneralConfiguration generalConfiguration)
{
this.repositoryFactory = repositoryFactory ?? throw new ArgumentNullException(nameof(repositoryFactory));
this.commands = commands ?? throw new ArgumentNullException(nameof(commands));
@@ -96,6 +104,7 @@ namespace Tgstation.Server.Host.Components.Repository
this.gitRemoteFeaturesFactory = gitRemoteFeaturesFactory ?? throw new ArgumentNullException(nameof(gitRemoteFeaturesFactory));
this.repositoryLogger = repositoryLogger ?? throw new ArgumentNullException(nameof(repositoryLogger));
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
this.generalConfiguration = generalConfiguration ?? throw new ArgumentNullException(nameof(generalConfiguration));
semaphore = new SemaphoreSlim(1);
}
@@ -217,6 +226,7 @@ namespace Tgstation.Server.Host.Components.Repository
postWriteHandler,
gitRemoteFeaturesFactory,
repositoryLogger,
generalConfiguration,
() =>
{
logger.LogTrace("Releasing semaphore due to Repository disposal...");
@@ -13,6 +13,7 @@ using Microsoft.Extensions.Logging;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Api.Models.Response;
using Tgstation.Server.Host.Components.Events;
using Tgstation.Server.Host.Configuration;
using Tgstation.Server.Host.Core;
using Tgstation.Server.Host.IO;
using Tgstation.Server.Host.Jobs;
@@ -111,6 +112,11 @@ namespace Tgstation.Server.Host.Components.StaticFiles
/// </summary>
readonly ILogger<Configuration> logger;
/// <summary>
/// The <see cref="GeneralConfiguration"/> for <see cref="Configuration"/>.
/// </summary>
readonly GeneralConfiguration generalConfiguration;
/// <summary>
/// The <see cref="SemaphoreSlim"/> for <see cref="Configuration"/>. Also used as a <see langword="lock"/> <see cref="object"/>.
/// </summary>
@@ -137,6 +143,7 @@ namespace Tgstation.Server.Host.Components.StaticFiles
/// <param name="platformIdentifier">The value of <see cref="platformIdentifier"/>.</param>
/// <param name="fileTransferService">The value of <see cref="fileTransferService"/>.</param>
/// <param name="logger">The value of <see cref="logger"/>.</param>
/// <param name="generalConfiguration">The value of <see cref="generalConfiguration"/>.</param>
public Configuration(
IIOManager ioManager,
ISynchronousIOManager synchronousIOManager,
@@ -145,7 +152,8 @@ namespace Tgstation.Server.Host.Components.StaticFiles
IPostWriteHandler postWriteHandler,
IPlatformIdentifier platformIdentifier,
IFileTransferTicketProvider fileTransferService,
ILogger<Configuration> logger)
ILogger<Configuration> logger,
GeneralConfiguration generalConfiguration)
{
this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
this.synchronousIOManager = synchronousIOManager ?? throw new ArgumentNullException(nameof(synchronousIOManager));
@@ -155,6 +163,7 @@ namespace Tgstation.Server.Host.Components.StaticFiles
this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier));
this.fileTransferService = fileTransferService ?? throw new ArgumentNullException(nameof(fileTransferService));
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
this.generalConfiguration = generalConfiguration ?? throw new ArgumentNullException(nameof(generalConfiguration));
semaphore = new SemaphoreSlim(1);
disposeCts = new CancellationTokenSource();
@@ -180,7 +189,13 @@ namespace Tgstation.Server.Host.Components.StaticFiles
var dmeExistsTask = ioManager.FileExists(ioManager.ConcatPath(CodeModificationsSubdirectory, dmeFile), cancellationToken);
var headFileExistsTask = ioManager.FileExists(ioManager.ConcatPath(CodeModificationsSubdirectory, CodeModificationsHeadFile), cancellationToken);
var tailFileExistsTask = ioManager.FileExists(ioManager.ConcatPath(CodeModificationsSubdirectory, CodeModificationsTailFile), cancellationToken);
var copyTask = ioManager.CopyDirectory(CodeModificationsSubdirectory, destination, null, null, cancellationToken);
var copyTask = ioManager.CopyDirectory(
null,
null,
CodeModificationsSubdirectory,
destination,
generalConfiguration.GetCopyDirectoryTaskThrottle(),
cancellationToken);
await Task.WhenAll(dmeExistsTask, headFileExistsTask, tailFileExistsTask, copyTask);
@@ -116,6 +116,11 @@ namespace Tgstation.Server.Host.Configuration
/// </summary>
public bool SkipAddingByondFirewallException { get; set; }
/// <summary>
/// A limit on the amount of tasks used for asynchronous I/O when copying directories during the deployment process as a multiplier to the machine's <see cref="Environment.ProcessorCount"/>. Too few can significantly increase deployment times, too many can make TGS unresponsive and slowdown other I/O operations on the machine.
/// </summary>
public uint? DeploymentDirectoryCopyTasksPerCore { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="GeneralConfiguration"/> class.
/// </summary>
@@ -147,6 +152,26 @@ namespace Tgstation.Server.Host.Configuration
CurrentConfigVersion);
else
logger.LogWarning("Your `ConfigVersion` is out-of-date. Please follow migration instructions from the TGS release notes.");
if (DeploymentDirectoryCopyTasksPerCore == 0)
throw new InvalidOperationException(
$"{nameof(DeploymentDirectoryCopyTasksPerCore)} must be at least 1!");
else if (GetCopyDirectoryTaskThrottle() < 1)
throw new InvalidOperationException(
$"{nameof(DeploymentDirectoryCopyTasksPerCore)} is too large for the CPU core count of {Environment.ProcessorCount} and overflows a 32-bit signed integer. Please lower the value!");
}
/// <summary>
/// Gets the total number of tasks that may run simultaneously during an asynchronous directory copy operation.
/// </summary>
/// <returns>The total number of tasks that may run simultaneously during an asynchronous directory copy operation.</returns>
public int? GetCopyDirectoryTaskThrottle()
{
if (!DeploymentDirectoryCopyTasksPerCore.HasValue)
return null;
var taskThrottle = (uint)Environment.ProcessorCount * DeploymentDirectoryCopyTasksPerCore.Value;
return (int)taskThrottle;
}
}
}
@@ -62,10 +62,11 @@ namespace Tgstation.Server.Host.IO
/// <inheritdoc />
public async Task CopyDirectory(
string src,
string dest,
IEnumerable<string> ignore,
Func<string, string, Task> postCopyCallback,
string src,
string dest,
int? taskThrottle,
CancellationToken cancellationToken)
{
if (src == null)
@@ -73,10 +74,13 @@ namespace Tgstation.Server.Host.IO
if (dest == null)
throw new ArgumentNullException(nameof(src));
if (taskThrottle.HasValue && taskThrottle < 1)
throw new ArgumentOutOfRangeException(nameof(taskThrottle), taskThrottle, "taskThrottle must be at least 1!");
src = ResolvePath(src);
dest = ResolvePath(dest);
using var semaphore = new SemaphoreSlim(100 * Environment.ProcessorCount);
using var semaphore = taskThrottle.HasValue ? new SemaphoreSlim(taskThrottle.Value) : null;
await Task.WhenAll(CopyDirectoryImpl(src, dest, ignore, postCopyCallback, semaphore, cancellationToken));
}
@@ -327,7 +331,7 @@ namespace Tgstation.Server.Host.IO
/// <param name="dest">The destination directory path.</param>
/// <param name="ignore">Files and folders to ignore at the root level.</param>
/// <param name="postCopyCallback">The optional callback called for each source/dest file pair post copy.</param>
/// <param name="semaphore"><see cref="SemaphoreSlim"/> used to limit degree of parallelism.</param>
/// <param name="semaphore">Optional <see cref="SemaphoreSlim"/> used to limit degree of parallelism.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="IEnumerable{T}"/> of <see cref="Task"/>s representing the running operations. The first <see cref="Task"/> returned is always the necessary call to <see cref="CreateDirectory(string, CancellationToken)"/>.</returns>
IEnumerable<Task> CopyDirectoryImpl(
@@ -377,7 +381,9 @@ namespace Tgstation.Server.Host.IO
async Task CopyThisFile()
{
await subdirCreationTask;
using var lockContext = await SemaphoreSlimContext.Lock(semaphore, cancellationToken);
using var lockContext = semaphore != null
? await SemaphoreSlimContext.Lock(semaphore, cancellationToken)
: null;
await CopyFile(sourceFile, destFile, cancellationToken);
if (postCopyCallback != null)
await postCopyCallback(sourceFile, destFile);
+6 -4
View File
@@ -48,17 +48,19 @@ namespace Tgstation.Server.Host.IO
/// <summary>
/// Copies a directory from <paramref name="src"/> to <paramref name="dest"/>.
/// </summary>
/// <param name="src">The source directory path.</param>
/// <param name="dest">The destination directory path.</param>
/// <param name="ignore">Files and folders to ignore at the root level.</param>
/// <param name="postCopyCallback">The optional callback called for each source/dest file pair post copy.</param>
/// <param name="src">The source directory path.</param>
/// <param name="dest">The destination directory path.</param>
/// <param name="taskThrottle">The optional maximum number of simultaneous tasks allowed to execute.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
Task CopyDirectory(
string src,
string dest,
IEnumerable<string> ignore,
Func<string, string, Task> postCopyCallback,
string src,
string dest,
int? taskThrottle,
CancellationToken cancellationToken);
/// <summary>
@@ -12,6 +12,7 @@ General:
ValidInstancePaths:
HostApiDocumentation: false
SkipAddingByondFirewallException: false
DeploymentDirectoryCopyTasksPerCore: 100
Session:
HighPriorityLiveDreamDaemon: false
LowPriorityDeploymentProcesses: true
@@ -1,5 +1,8 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Remora.Discord.API.Objects;
using System;
using System.IO;
using System.Threading.Tasks;
@@ -71,5 +74,108 @@ namespace Tgstation.Server.Host.IO.Tests
throw;
}
}
[TestMethod]
public async Task TestCopyDirectoryThrows()
{
int? throttle = null;
var tempPath1 = Guid.NewGuid().ToString();
var tempPath2 = Guid.NewGuid().ToString();
await Assert.ThrowsExceptionAsync<ArgumentNullException>(() => ioManager.CopyDirectory(
null,
null,
null,
tempPath2,
throttle,
default));
await Assert.ThrowsExceptionAsync<ArgumentNullException>(() => ioManager.CopyDirectory(
null,
null,
tempPath1,
null,
throttle,
default));
await Assert.ThrowsExceptionAsync<ArgumentNullException>(() => ioManager.CopyDirectory(
null,
null,
null,
null,
throttle,
default));
await Assert.ThrowsExceptionAsync<ArgumentOutOfRangeException>(() => ioManager.CopyDirectory(
null,
null,
tempPath1,
tempPath2,
-1,
default));
}
[TestMethod]
public async Task TestCopyDirectoryOneTask()
{
await TestCopyDirectory(1);
}
[TestMethod]
public async Task TestCopyDirectoryMaxTasks()
{
await TestCopyDirectory(Int32.MaxValue);
}
[TestMethod]
public async Task TestCopyDirectoryUnlimitedTasks()
{
await TestCopyDirectory(null);
}
async Task TestCopyDirectory(int? throttle)
{
var tempPath = Path.GetTempFileName();
File.Delete(tempPath);
Directory.CreateDirectory(tempPath);
try
{
var tempPath2 = Path.GetTempFileName();
File.Delete(tempPath2);
await File.WriteAllTextAsync(Path.Combine(tempPath, "file.txt"), "asdf");
var subDir = Path.Combine(tempPath, "subdir");
Directory.CreateDirectory(subDir);
await File.WriteAllTextAsync(Path.Combine(subDir, "file2.txt"), "fdsa");
try
{
await ioManager.CopyDirectory(
null,
null,
tempPath,
tempPath2,
throttle,
default);
Assert.IsTrue(Directory.Exists(tempPath2));
var newFilePath = Path.Combine(tempPath2, "file.txt");
Assert.IsTrue(File.Exists(newFilePath));
var newFileText = await File.ReadAllTextAsync(newFilePath);
Assert.AreEqual("asdf", newFileText);
var newDirPath = Path.Combine(tempPath2, "subdir");
Assert.IsTrue(Directory.Exists(newDirPath));
var newFile2Path = Path.Combine(newDirPath, "file2.txt");
Assert.IsTrue(File.Exists(newFile2Path));
var newFile2Text = await File.ReadAllTextAsync(newFile2Path);
Assert.AreEqual("fdsa", newFile2Text);
}
finally
{
Directory.Delete(tempPath2, true);
}
}
finally
{
Directory.Delete(tempPath, true);
}
}
}
}
@@ -106,15 +106,17 @@ namespace Tgstation.Server.Tests.Live.Instance
var ioManager = new DefaultIOManager();
return Task.WhenAll(
ioManager.CopyDirectory(
Enumerable.Empty<string>(),
null,
"../../../../DMAPI",
ioManager.ConcatPath(instance.Path, "Repository", "tests", "DMAPI"),
Enumerable.Empty<string>(),
null,
cancellationToken),
ioManager.CopyDirectory(
Enumerable.Empty<string>(),
null,
"../../../../../src/DMAPI",
ioManager.ConcatPath(instance.Path, "Repository", "src", "DMAPI"),
Enumerable.Empty<string>(),
null,
cancellationToken)
);
@@ -7,6 +7,7 @@ using Moq;
using Tgstation.Server.Host.Components.Events;
using Tgstation.Server.Host.Components.Repository;
using Tgstation.Server.Host.Configuration;
using Tgstation.Server.Host.IO;
using Tgstation.Server.Host.Jobs;
using Tgstation.Server.Tests.Live;
@@ -31,6 +32,7 @@ namespace Tgstation.Server.Tests
Mock.Of<IPostWriteHandler>(),
Mock.Of<IGitRemoteFeaturesFactory>(),
Mock.Of<ILogger<Repository>>(),
new GeneralConfiguration(),
() => { });
const string StartSha = "af4da8beb9f9b374b04a3cc4d65acca662e8cc1a";