
The default TaskScheduler may also mark tasks as long running after a while and then spawn an extra thread regardless. But this way we make sure to not block the threadpool by spawning the tasks in separate threads instead in the normal pool. Also keep the denychildattach which Run would have added. See: https://devblogs.microsoft.com/pfxteam/task-run-vs-task-factory-startnew/
130 lines
3.4 KiB
C#
130 lines
3.4 KiB
C#
using System.Reflection;
|
|
using Iceshrimp.AssemblyUtils;
|
|
using Iceshrimp.Backend.Core.Helpers;
|
|
|
|
namespace Iceshrimp.Backend.Core.Services;
|
|
|
|
public class CronService(IServiceScopeFactory serviceScopeFactory) : BackgroundService
|
|
{
|
|
protected override Task ExecuteAsync(CancellationToken token)
|
|
{
|
|
var tasks = PluginLoader
|
|
.Assemblies.Prepend(Assembly.GetExecutingAssembly())
|
|
.SelectMany(AssemblyLoader.GetImplementationsOfInterface<ICronTask>)
|
|
.Select(p => Activator.CreateInstance(p) as ICronTask)
|
|
.Where(p => p != null)
|
|
.Cast<ICronTask>();
|
|
|
|
foreach (var task in tasks)
|
|
{
|
|
ICronTrigger trigger = task.Type switch
|
|
{
|
|
CronTaskType.Daily => new DailyTrigger(task.Trigger, token),
|
|
CronTaskType.Interval => new IntervalTrigger(task.Trigger, token),
|
|
_ => throw new ArgumentOutOfRangeException()
|
|
};
|
|
|
|
trigger.OnTrigger += async void () =>
|
|
{
|
|
try
|
|
{
|
|
await using var scope = serviceScopeFactory.CreateAsyncScope();
|
|
await task.InvokeAsync(scope.ServiceProvider);
|
|
}
|
|
catch
|
|
{
|
|
// ignored (errors in the event handler crash the host process)
|
|
}
|
|
};
|
|
}
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
}
|
|
|
|
public interface ICronTask
|
|
{
|
|
public TimeSpan Trigger { get; }
|
|
public CronTaskType Type { get; }
|
|
public Task InvokeAsync(IServiceProvider provider);
|
|
}
|
|
|
|
public enum CronTaskType
|
|
{
|
|
Daily,
|
|
Interval
|
|
}
|
|
|
|
file interface ICronTrigger
|
|
{
|
|
public event Action? OnTrigger;
|
|
}
|
|
|
|
file class DailyTrigger : ICronTrigger, IDisposable
|
|
{
|
|
public DailyTrigger(TimeSpan triggerTime, CancellationToken cancellationToken)
|
|
{
|
|
TriggerTime = triggerTime;
|
|
CancellationToken = cancellationToken;
|
|
|
|
RunningTask = Task.Factory.StartNew(async () =>
|
|
{
|
|
while (!CancellationToken.IsCancellationRequested)
|
|
{
|
|
var nextTrigger = DateTime.Today + TriggerTime - DateTime.Now;
|
|
if (nextTrigger < TimeSpan.Zero)
|
|
nextTrigger = nextTrigger.Add(new TimeSpan(24, 0, 0));
|
|
await Task.Delay(nextTrigger, CancellationToken);
|
|
OnTrigger?.Invoke();
|
|
}
|
|
}, CancellationToken, TaskCreationOptions.DenyChildAttach | TaskCreationOptions.LongRunning, TaskScheduler.Default);
|
|
}
|
|
|
|
private TimeSpan TriggerTime { get; }
|
|
private CancellationToken CancellationToken { get; }
|
|
private Task RunningTask { get; set; }
|
|
|
|
public event Action? OnTrigger;
|
|
|
|
public void Dispose()
|
|
{
|
|
RunningTask.Dispose();
|
|
RunningTask = null!;
|
|
GC.SuppressFinalize(this);
|
|
}
|
|
|
|
~DailyTrigger() => Dispose();
|
|
}
|
|
|
|
file class IntervalTrigger : ICronTrigger, IDisposable
|
|
{
|
|
public IntervalTrigger(TimeSpan triggerInterval, CancellationToken cancellationToken)
|
|
{
|
|
TriggerInterval = triggerInterval;
|
|
CancellationToken = cancellationToken;
|
|
|
|
RunningTask = Task.Factory.StartNew(async () =>
|
|
{
|
|
while (!CancellationToken.IsCancellationRequested)
|
|
{
|
|
await Task.Delay(TriggerInterval, CancellationToken);
|
|
OnTrigger?.Invoke();
|
|
}
|
|
}, CancellationToken, TaskCreationOptions.DenyChildAttach | TaskCreationOptions.LongRunning, TaskScheduler.Default);
|
|
}
|
|
|
|
private TimeSpan TriggerInterval { get; }
|
|
private CancellationToken CancellationToken { get; }
|
|
private Task RunningTask { get; set; }
|
|
|
|
public event Action? OnTrigger;
|
|
|
|
public void Dispose()
|
|
{
|
|
RunningTask.Dispose();
|
|
RunningTask = null!;
|
|
GC.SuppressFinalize(this);
|
|
}
|
|
|
|
~IntervalTrigger() => Dispose();
|
|
}
|