From 7f3cc2ec646b8f915b8987d88b212eea789049ba Mon Sep 17 00:00:00 2001 From: GZTime Date: Thu, 19 Dec 2024 22:10:53 +0800 Subject: [PATCH] refactor(cronjob): use attribute to get expressions and add job --- src/GZCTF/Program.cs | 1 + .../Services/CronJob/CronJobAttribute.cs | 22 ++++++ .../Services/{ => CronJob}/CronJobService.cs | 79 ++++--------------- src/GZCTF/Services/CronJob/DefaultCronJobs.cs | 56 +++++++++++++ 4 files changed, 93 insertions(+), 65 deletions(-) create mode 100644 src/GZCTF/Services/CronJob/CronJobAttribute.cs rename src/GZCTF/Services/{ => CronJob}/CronJobService.cs (66%) create mode 100644 src/GZCTF/Services/CronJob/DefaultCronJobs.cs diff --git a/src/GZCTF/Program.cs b/src/GZCTF/Program.cs index 7a6462ae7..9f2e9cdbb 100644 --- a/src/GZCTF/Program.cs +++ b/src/GZCTF/Program.cs @@ -33,6 +33,7 @@ using GZCTF.Services.Cache; using GZCTF.Services.Config; using GZCTF.Services.Container; +using GZCTF.Services.CronJob; using GZCTF.Services.Mail; using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.HttpOverrides; diff --git a/src/GZCTF/Services/CronJob/CronJobAttribute.cs b/src/GZCTF/Services/CronJob/CronJobAttribute.cs new file mode 100644 index 000000000..51044d969 --- /dev/null +++ b/src/GZCTF/Services/CronJob/CronJobAttribute.cs @@ -0,0 +1,22 @@ +using System.Reflection; +using Cronos; + +namespace GZCTF.Services.CronJob; + +[AttributeUsage(AttributeTargets.Method)] +public class CronJobAttribute(string expression) : Attribute +{ + public CronExpression Expression { get; } = CronExpression.Parse(expression); +} + +public class CronJobNotFoundException(string message) : Exception(message); + +public static class CronJobExtensions +{ + public static (string, CronJobEntry) ToEntry(this CronJob job) + { + var method = job.Method; + var attr = method.GetCustomAttribute() ?? throw new CronJobNotFoundException(method.Name); + return (method.Name, new CronJobEntry(job, attr.Expression)); + } +} diff --git a/src/GZCTF/Services/CronJobService.cs b/src/GZCTF/Services/CronJob/CronJobService.cs similarity index 66% rename from src/GZCTF/Services/CronJobService.cs rename to src/GZCTF/Services/CronJob/CronJobService.cs index d8566d97e..bc0f19ccf 100644 --- a/src/GZCTF/Services/CronJobService.cs +++ b/src/GZCTF/Services/CronJob/CronJobService.cs @@ -1,25 +1,13 @@ -using System.Threading.Channels; +using System.Reflection; using Cronos; -using GZCTF.Repositories; -using GZCTF.Repositories.Interface; using GZCTF.Services.Cache; using Microsoft.Extensions.Caching.Distributed; -namespace GZCTF.Services; +namespace GZCTF.Services.CronJob; public delegate Task CronJob(AsyncServiceScope scope, ILogger logger); -public record CronJobEntry(CronJob Job, CronExpression Expression) -{ - /// - /// Create a cron job entry - /// - /// - /// - /// - public static CronJobEntry Create(CronJob job, string expression) => - new(job, CronExpression.Parse(expression)); -} +public record CronJobEntry(CronJob Job, CronExpression Expression); public class CronJobService(IDistributedCache cache, IServiceScopeFactory provider, ILogger logger) : IHostedService, IDisposable @@ -37,11 +25,12 @@ public void Dispose() /// /// Add a job to the cron job service /// - public bool AddJob(string job, CronJobEntry task) + public bool AddJob(CronJob job) { lock (_jobs) { - if (!_jobs.TryAdd(job, task)) + (string name, CronJobEntry entry) = job.ToEntry(); + if (!_jobs.TryAdd(name, entry)) return false; } @@ -65,13 +54,15 @@ public bool RemoveJob(string job) void LaunchCronJob() { - // container checker, every 3min - AddJob(nameof(CronJobs.ContainerChecker), - CronJobEntry.Create(CronJobs.ContainerChecker, "* * * * *")); + var methods = typeof(DefaultCronJobs).GetMethods(BindingFlags.Static | BindingFlags.Public); + foreach (var method in methods) + { + var attr = method.GetCustomAttribute(); + if (attr is null) + continue; - // bootstrap cache, every 10min - AddJob(nameof(CronJobs.BootstrapCache), - CronJobEntry.Create(CronJobs.BootstrapCache, "*/10 * * * *")); + AddJob(method.CreateDelegate()); + } _timer = new Timer(_ => Task.Run(Execute), null, TimeSpan.FromSeconds(60 - DateTime.UtcNow.Second), TimeSpan.FromMinutes(1)); @@ -196,45 +187,3 @@ async Task Execute() await Task.WhenAll(handles); } } - -public static class CronJobs -{ - public static async Task ContainerChecker(AsyncServiceScope scope, ILogger logger) - { - var containerRepo = scope.ServiceProvider.GetRequiredService(); - - foreach (Models.Data.Container container in await containerRepo.GetDyingContainers()) - { - await containerRepo.DestroyContainer(container); - logger.SystemLog( - Program.StaticLocalizer[nameof(Resources.Program.CronJob_RemoveExpiredContainer), - container.ContainerId], - TaskStatus.Success, LogLevel.Debug); - } - } - - public static async Task BootstrapCache(AsyncServiceScope scope, ILogger logger) - { - var gameRepo = scope.ServiceProvider.GetRequiredService(); - var upcoming = await gameRepo.GetUpcomingGames(); - - if (upcoming.Length <= 0) - return; - - var channelWriter = scope.ServiceProvider.GetRequiredService>(); - var cache = scope.ServiceProvider.GetRequiredService(); - - foreach (var game in upcoming) - { - var key = CacheKey.ScoreBoard(game); - var value = await cache.GetAsync(key); - if (value is not null) - continue; - - await channelWriter.WriteAsync(ScoreboardCacheHandler.MakeCacheRequest(game)); - logger.SystemLog(Program.StaticLocalizer[nameof(Resources.Program.CronJob_BootstrapRankingCache), key], - TaskStatus.Success, - LogLevel.Debug); - } - } -} diff --git a/src/GZCTF/Services/CronJob/DefaultCronJobs.cs b/src/GZCTF/Services/CronJob/DefaultCronJobs.cs new file mode 100644 index 000000000..4544272e3 --- /dev/null +++ b/src/GZCTF/Services/CronJob/DefaultCronJobs.cs @@ -0,0 +1,56 @@ +using System.Threading.Channels; +using GZCTF.Repositories; +using GZCTF.Repositories.Interface; +using GZCTF.Services.Cache; +using Microsoft.Extensions.Caching.Distributed; +// ReSharper disable UnusedMember.Global + +namespace GZCTF.Services.CronJob; + +public static class DefaultCronJobs +{ + [CronJob("*/3 * * * *")] + public static async Task ContainerChecker(AsyncServiceScope scope, ILogger logger) + { + logger.SystemLog($"Executing {nameof(ContainerChecker)}", TaskStatus.Pending, LogLevel.Debug); + + var containerRepo = scope.ServiceProvider.GetRequiredService(); + + foreach (Models.Data.Container container in await containerRepo.GetDyingContainers()) + { + await containerRepo.DestroyContainer(container); + logger.SystemLog( + Program.StaticLocalizer[nameof(Resources.Program.CronJob_RemoveExpiredContainer), + container.ContainerId], + TaskStatus.Success, LogLevel.Debug); + } + } + + [CronJob("*/10 * * * *")] + public static async Task BootstrapCache(AsyncServiceScope scope, ILogger logger) + { + logger.SystemLog($"Executing {nameof(BootstrapCache)}", TaskStatus.Pending, LogLevel.Debug); + + var gameRepo = scope.ServiceProvider.GetRequiredService(); + var upcoming = await gameRepo.GetUpcomingGames(); + + if (upcoming.Length <= 0) + return; + + var channelWriter = scope.ServiceProvider.GetRequiredService>(); + var cache = scope.ServiceProvider.GetRequiredService(); + + foreach (var game in upcoming) + { + var key = CacheKey.ScoreBoard(game); + var value = await cache.GetAsync(key); + if (value is not null) + continue; + + await channelWriter.WriteAsync(ScoreboardCacheHandler.MakeCacheRequest(game)); + logger.SystemLog(Program.StaticLocalizer[nameof(Resources.Program.CronJob_BootstrapRankingCache), key], + TaskStatus.Success, + LogLevel.Debug); + } + } +}