mirror of
https://github.com/LBPUnion/ProjectLighthouse.git
synced 2025-07-19 11:41:30 +00:00
Refactor RepeatingTaskHandler (#796)
* Refactor RepeatingTaskHandler into an ASP.NET service * Add unit tests for RepeatingTaskService * Make repeating task unit tests work independent of time * Fix weird behavior when task is canceled
This commit is contained in:
parent
9ac8a166d4
commit
a0d021f1e2
7 changed files with 213 additions and 69 deletions
|
@ -1,4 +1,5 @@
|
||||||
using System.Net;
|
using System.Net;
|
||||||
|
using LBPUnion.ProjectLighthouse.Administration.Maintenance;
|
||||||
using LBPUnion.ProjectLighthouse.Configuration;
|
using LBPUnion.ProjectLighthouse.Configuration;
|
||||||
using LBPUnion.ProjectLighthouse.Database;
|
using LBPUnion.ProjectLighthouse.Database;
|
||||||
using LBPUnion.ProjectLighthouse.Logging;
|
using LBPUnion.ProjectLighthouse.Logging;
|
||||||
|
@ -6,6 +7,7 @@ using LBPUnion.ProjectLighthouse.Mail;
|
||||||
using LBPUnion.ProjectLighthouse.Middlewares;
|
using LBPUnion.ProjectLighthouse.Middlewares;
|
||||||
using LBPUnion.ProjectLighthouse.Serialization;
|
using LBPUnion.ProjectLighthouse.Serialization;
|
||||||
using LBPUnion.ProjectLighthouse.Servers.GameServer.Middlewares;
|
using LBPUnion.ProjectLighthouse.Servers.GameServer.Middlewares;
|
||||||
|
using LBPUnion.ProjectLighthouse.Services;
|
||||||
using LBPUnion.ProjectLighthouse.Types.Logging;
|
using LBPUnion.ProjectLighthouse.Types.Logging;
|
||||||
using LBPUnion.ProjectLighthouse.Types.Mail;
|
using LBPUnion.ProjectLighthouse.Types.Mail;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
@ -63,6 +65,8 @@ public class GameServerStartup
|
||||||
: new NullMailService();
|
: new NullMailService();
|
||||||
services.AddSingleton(mailService);
|
services.AddSingleton(mailService);
|
||||||
|
|
||||||
|
services.AddHostedService(provider => new RepeatingTaskService(provider, MaintenanceHelper.RepeatingTasks));
|
||||||
|
|
||||||
services.Configure<ForwardedHeadersOptions>
|
services.Configure<ForwardedHeadersOptions>
|
||||||
(
|
(
|
||||||
options =>
|
options =>
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
|
using LBPUnion.ProjectLighthouse.Administration.Maintenance;
|
||||||
using LBPUnion.ProjectLighthouse.Configuration;
|
using LBPUnion.ProjectLighthouse.Configuration;
|
||||||
using LBPUnion.ProjectLighthouse.Configuration.ConfigurationCategories;
|
using LBPUnion.ProjectLighthouse.Configuration.ConfigurationCategories;
|
||||||
using LBPUnion.ProjectLighthouse.Database;
|
using LBPUnion.ProjectLighthouse.Database;
|
||||||
|
@ -8,6 +9,7 @@ using LBPUnion.ProjectLighthouse.Mail;
|
||||||
using LBPUnion.ProjectLighthouse.Middlewares;
|
using LBPUnion.ProjectLighthouse.Middlewares;
|
||||||
using LBPUnion.ProjectLighthouse.Servers.Website.Captcha;
|
using LBPUnion.ProjectLighthouse.Servers.Website.Captcha;
|
||||||
using LBPUnion.ProjectLighthouse.Servers.Website.Middlewares;
|
using LBPUnion.ProjectLighthouse.Servers.Website.Middlewares;
|
||||||
|
using LBPUnion.ProjectLighthouse.Services;
|
||||||
using LBPUnion.ProjectLighthouse.Types.Mail;
|
using LBPUnion.ProjectLighthouse.Types.Mail;
|
||||||
using Microsoft.AspNetCore.HttpOverrides;
|
using Microsoft.AspNetCore.HttpOverrides;
|
||||||
using Microsoft.AspNetCore.Localization;
|
using Microsoft.AspNetCore.Localization;
|
||||||
|
@ -59,6 +61,8 @@ public class WebsiteStartup
|
||||||
: new NullMailService();
|
: new NullMailService();
|
||||||
services.AddSingleton(mailService);
|
services.AddSingleton(mailService);
|
||||||
|
|
||||||
|
services.AddHostedService(provider => new RepeatingTaskService(provider, MaintenanceHelper.RepeatingTasks));
|
||||||
|
|
||||||
services.AddHttpClient<ICaptchaService, CaptchaService>("CaptchaAPI",
|
services.AddHttpClient<ICaptchaService, CaptchaService>("CaptchaAPI",
|
||||||
client =>
|
client =>
|
||||||
{
|
{
|
||||||
|
|
120
ProjectLighthouse.Tests/Unit/RepeatingTaskTests.cs
Normal file
120
ProjectLighthouse.Tests/Unit/RepeatingTaskTests.cs
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using LBPUnion.ProjectLighthouse.Database;
|
||||||
|
using LBPUnion.ProjectLighthouse.Services;
|
||||||
|
using LBPUnion.ProjectLighthouse.Types.Maintenance;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Moq;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace LBPUnion.ProjectLighthouse.Tests.Unit;
|
||||||
|
|
||||||
|
[Trait("Category", "Unit")]
|
||||||
|
public class RepeatingTaskTests
|
||||||
|
{
|
||||||
|
private class TestTask : IRepeatingTask
|
||||||
|
{
|
||||||
|
public string Name { get; init; } = "";
|
||||||
|
public TimeSpan RepeatInterval => TimeSpan.FromSeconds(5);
|
||||||
|
public DateTime LastRan { get; set; }
|
||||||
|
public Task Run(DatabaseContext database) => Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetNextTask_ShouldReturnNull_WhenTaskListEmpty()
|
||||||
|
{
|
||||||
|
List<IRepeatingTask> tasks = new();
|
||||||
|
IServiceProvider provider = new DefaultServiceProviderFactory().CreateServiceProvider(new ServiceCollection());
|
||||||
|
RepeatingTaskService service = new(provider, tasks);
|
||||||
|
|
||||||
|
bool gotTask = service.TryGetNextTask(out IRepeatingTask? outTask);
|
||||||
|
Assert.False(gotTask);
|
||||||
|
Assert.Null(outTask);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetNextTask_ShouldReturnTask_WhenTaskListContainsOne()
|
||||||
|
{
|
||||||
|
List<IRepeatingTask> tasks = new()
|
||||||
|
{
|
||||||
|
new TestTask(),
|
||||||
|
};
|
||||||
|
IServiceProvider provider = new DefaultServiceProviderFactory().CreateServiceProvider(new ServiceCollection());
|
||||||
|
RepeatingTaskService service = new(provider, tasks);
|
||||||
|
|
||||||
|
bool gotTask = service.TryGetNextTask(out IRepeatingTask? outTask);
|
||||||
|
Assert.True(gotTask);
|
||||||
|
Assert.NotNull(outTask);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetNextTask_ShouldReturnShortestTask_WhenTaskListContainsMultiple()
|
||||||
|
{
|
||||||
|
List<IRepeatingTask> tasks = new()
|
||||||
|
{
|
||||||
|
new TestTask
|
||||||
|
{
|
||||||
|
Name = "Task 1",
|
||||||
|
LastRan = DateTime.UtcNow,
|
||||||
|
},
|
||||||
|
new TestTask
|
||||||
|
{
|
||||||
|
Name = "Task 2",
|
||||||
|
LastRan = DateTime.UtcNow.AddMinutes(1),
|
||||||
|
},
|
||||||
|
new TestTask
|
||||||
|
{
|
||||||
|
Name = "Task 3",
|
||||||
|
LastRan = DateTime.UtcNow.AddMinutes(-1),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
IServiceProvider provider = new DefaultServiceProviderFactory().CreateServiceProvider(new ServiceCollection());
|
||||||
|
RepeatingTaskService service = new(provider, tasks);
|
||||||
|
|
||||||
|
bool gotTask = service.TryGetNextTask(out IRepeatingTask? outTask);
|
||||||
|
Assert.True(gotTask);
|
||||||
|
Assert.NotNull(outTask);
|
||||||
|
Assert.Equal("Task 3", outTask.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task BackgroundService_ShouldExecuteTask_AndUpdateTime()
|
||||||
|
{
|
||||||
|
Mock<IRepeatingTask> taskMock = new();
|
||||||
|
taskMock.Setup(t => t.Run(It.IsAny<DatabaseContext>())).Returns(Task.CompletedTask);
|
||||||
|
taskMock.SetupGet(t => t.RepeatInterval).Returns(TimeSpan.FromSeconds(10));
|
||||||
|
TaskCompletionSource taskCompletion = new();
|
||||||
|
DateTime lastRan = default;
|
||||||
|
taskMock.SetupSet(t => t.LastRan = It.IsAny<DateTime>())
|
||||||
|
.Callback<DateTime>(time =>
|
||||||
|
{
|
||||||
|
lastRan = time;
|
||||||
|
taskCompletion.TrySetResult();
|
||||||
|
});
|
||||||
|
taskMock.SetupGet(t => t.LastRan).Returns(() => lastRan);
|
||||||
|
List<IRepeatingTask> tasks = new()
|
||||||
|
{
|
||||||
|
taskMock.Object,
|
||||||
|
};
|
||||||
|
ServiceCollection serviceCollection = new();
|
||||||
|
serviceCollection.AddScoped(_ => new Mock<DatabaseContext>().Object);
|
||||||
|
IServiceProvider provider = new DefaultServiceProviderFactory().CreateServiceProvider(serviceCollection);
|
||||||
|
|
||||||
|
RepeatingTaskService service = new(provider, tasks);
|
||||||
|
|
||||||
|
CancellationTokenSource stoppingToken = new();
|
||||||
|
|
||||||
|
await service.StartAsync(stoppingToken.Token);
|
||||||
|
|
||||||
|
await taskCompletion.Task;
|
||||||
|
stoppingToken.Cancel();
|
||||||
|
|
||||||
|
Assert.NotNull(service.ExecuteTask);
|
||||||
|
await service.ExecuteTask;
|
||||||
|
|
||||||
|
taskMock.Verify(x => x.Run(It.IsAny<DatabaseContext>()), Times.Once);
|
||||||
|
taskMock.VerifySet(x => x.LastRan = It.IsAny<DateTime>(), Times.Once());
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,65 +0,0 @@
|
||||||
#nullable enable
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using LBPUnion.ProjectLighthouse.Administration.Maintenance;
|
|
||||||
using LBPUnion.ProjectLighthouse.Database;
|
|
||||||
using LBPUnion.ProjectLighthouse.Logging;
|
|
||||||
using LBPUnion.ProjectLighthouse.Types.Logging;
|
|
||||||
using LBPUnion.ProjectLighthouse.Types.Maintenance;
|
|
||||||
|
|
||||||
namespace LBPUnion.ProjectLighthouse.Administration;
|
|
||||||
|
|
||||||
public static class RepeatingTaskHandler
|
|
||||||
{
|
|
||||||
private static bool initialized = false;
|
|
||||||
|
|
||||||
public static void Initialize()
|
|
||||||
{
|
|
||||||
if (initialized) throw new InvalidOperationException("RepeatingTaskHandler was initialized twice");
|
|
||||||
|
|
||||||
initialized = true;
|
|
||||||
Task.Factory.StartNew(taskLoop);
|
|
||||||
}
|
|
||||||
|
|
||||||
[SuppressMessage("ReSharper", "FunctionNeverReturns")]
|
|
||||||
private static async Task taskLoop()
|
|
||||||
{
|
|
||||||
Queue<IRepeatingTask> taskQueue = new();
|
|
||||||
foreach (IRepeatingTask task in MaintenanceHelper.RepeatingTasks) taskQueue.Enqueue(task);
|
|
||||||
|
|
||||||
DatabaseContext database = DatabaseContext.CreateNewInstance();
|
|
||||||
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (!taskQueue.TryDequeue(out IRepeatingTask? task))
|
|
||||||
{
|
|
||||||
Thread.Sleep(100);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
Debug.Assert(task != null);
|
|
||||||
|
|
||||||
if ((task.LastRan + task.RepeatInterval) <= DateTime.Now)
|
|
||||||
{
|
|
||||||
await task.Run(database);
|
|
||||||
task.LastRan = DateTime.Now;
|
|
||||||
|
|
||||||
Logger.Debug($"Ran task \"{task.Name}\"", LogArea.Maintenance);
|
|
||||||
}
|
|
||||||
|
|
||||||
taskQueue.Enqueue(task);
|
|
||||||
Thread.Sleep(500); // Doesn't need to be that precise.
|
|
||||||
}
|
|
||||||
catch(Exception e)
|
|
||||||
{
|
|
||||||
Logger.Warn($"Error occured while processing repeating tasks: \n{e}", LogArea.Maintenance);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -64,6 +64,10 @@ public partial class DatabaseContext : DbContext
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
// Used for mocking DbContext
|
||||||
|
protected internal DatabaseContext()
|
||||||
|
{ }
|
||||||
|
|
||||||
public DatabaseContext(DbContextOptions<DatabaseContext> options) : base(options)
|
public DatabaseContext(DbContextOptions<DatabaseContext> options) : base(options)
|
||||||
{ }
|
{ }
|
||||||
|
|
||||||
|
|
81
ProjectLighthouse/Services/RepeatingTaskService.cs
Normal file
81
ProjectLighthouse/Services/RepeatingTaskService.cs
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
#nullable enable
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using LBPUnion.ProjectLighthouse.Database;
|
||||||
|
using LBPUnion.ProjectLighthouse.Logging;
|
||||||
|
using LBPUnion.ProjectLighthouse.Types.Logging;
|
||||||
|
using LBPUnion.ProjectLighthouse.Types.Maintenance;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
|
||||||
|
namespace LBPUnion.ProjectLighthouse.Services;
|
||||||
|
|
||||||
|
public class RepeatingTaskService : BackgroundService
|
||||||
|
{
|
||||||
|
private readonly IServiceProvider provider;
|
||||||
|
private readonly List<IRepeatingTask> taskList;
|
||||||
|
|
||||||
|
public RepeatingTaskService(IServiceProvider provider, List<IRepeatingTask> tasks)
|
||||||
|
{
|
||||||
|
this.provider = provider;
|
||||||
|
this.taskList = tasks;
|
||||||
|
|
||||||
|
Logger.Info("Initializing repeating tasks service", LogArea.Startup);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryGetNextTask(out IRepeatingTask? outTask)
|
||||||
|
{
|
||||||
|
TimeSpan smallestSpan = TimeSpan.MaxValue;
|
||||||
|
IRepeatingTask? smallestTask = null;
|
||||||
|
foreach (IRepeatingTask task in this.taskList)
|
||||||
|
{
|
||||||
|
TimeSpan smallestTimeRemaining = task.RepeatInterval.Subtract(DateTime.UtcNow.Subtract(task.LastRan));
|
||||||
|
if (smallestTimeRemaining >= smallestSpan) continue;
|
||||||
|
|
||||||
|
smallestSpan = smallestTimeRemaining;
|
||||||
|
smallestTask = task;
|
||||||
|
}
|
||||||
|
outTask = smallestTask;
|
||||||
|
|
||||||
|
return outTask != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||||
|
{
|
||||||
|
await Task.Yield();
|
||||||
|
while (!stoppingToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
if (!this.TryGetNextTask(out IRepeatingTask? task) || task == null)
|
||||||
|
{
|
||||||
|
// If we fail to fetch the next task then something has gone wrong and the service should halt
|
||||||
|
Logger.Debug("Failed to fetch next smallest task", LogArea.Maintenance);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
TimeSpan timeElapsedSinceRun = DateTime.UtcNow.Subtract(task.LastRan);
|
||||||
|
|
||||||
|
// If the task's repeat interval hasn't elapsed
|
||||||
|
if (timeElapsedSinceRun < task.RepeatInterval)
|
||||||
|
{
|
||||||
|
TimeSpan timeToWait = task.RepeatInterval.Subtract(timeElapsedSinceRun);
|
||||||
|
Logger.Debug($"Waiting {timeToWait} for {task.Name}", LogArea.Maintenance);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Task.Delay(timeToWait, stoppingToken);
|
||||||
|
}
|
||||||
|
catch (TaskCanceledException)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
using IServiceScope scope = this.provider.CreateScope();
|
||||||
|
DatabaseContext database = scope.ServiceProvider.GetRequiredService<DatabaseContext>();
|
||||||
|
await task.Run(database);
|
||||||
|
Logger.Debug($"Successfully ran task \"{task.Name}\"", LogArea.Maintenance);
|
||||||
|
task.LastRan = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,7 +5,6 @@ using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using LBPUnion.ProjectLighthouse.Administration;
|
|
||||||
using LBPUnion.ProjectLighthouse.Administration.Maintenance;
|
using LBPUnion.ProjectLighthouse.Administration.Maintenance;
|
||||||
using LBPUnion.ProjectLighthouse.Configuration;
|
using LBPUnion.ProjectLighthouse.Configuration;
|
||||||
using LBPUnion.ProjectLighthouse.Database;
|
using LBPUnion.ProjectLighthouse.Database;
|
||||||
|
@ -93,9 +92,6 @@ public static class StartupTasks
|
||||||
Logger.Info("Initializing Redis...", LogArea.Startup);
|
Logger.Info("Initializing Redis...", LogArea.Startup);
|
||||||
RedisDatabase.Initialize().Wait();
|
RedisDatabase.Initialize().Wait();
|
||||||
|
|
||||||
Logger.Info("Initializing repeating tasks...", LogArea.Startup);
|
|
||||||
RepeatingTaskHandler.Initialize();
|
|
||||||
|
|
||||||
// Create admin user if no users exist
|
// Create admin user if no users exist
|
||||||
if (serverType == ServerType.Website && database.Users.CountAsync().Result == 0)
|
if (serverType == ServerType.Website && database.Users.CountAsync().Result == 0)
|
||||||
{
|
{
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue