using System; using System.Diagnostics.CodeAnalysis; using System.Threading; using System.Threading.Tasks; using Hangfire; using Microsoft.Extensions.Hosting; using StardewModdingAPI.Toolkit; using StardewModdingAPI.Toolkit.Framework.Clients.Wiki; using StardewModdingAPI.Web.Framework.Caching.Mods; using StardewModdingAPI.Web.Framework.Caching.Wiki; namespace StardewModdingAPI.Web { /// A hosted service which runs background data updates. /// Task methods need to be static, since otherwise Hangfire will try to serialize the entire instance. internal class BackgroundService : IHostedService, IDisposable { /********* ** Fields *********/ /// The background task server. private static BackgroundJobServer? JobServer; /// The cache in which to store wiki metadata. private static IWikiCacheRepository? WikiCache; /// The cache in which to store mod data. private static IModCacheRepository? ModCache; /// Whether the service has been started. [MemberNotNullWhen(true, nameof(BackgroundService.JobServer), nameof(BackgroundService.WikiCache), nameof(BackgroundService.ModCache))] private static bool IsStarted { get; set; } /********* ** Public methods *********/ /**** ** Hosted service ****/ /// Construct an instance. /// The cache in which to store wiki metadata. /// The cache in which to store mod data. /// The Hangfire storage implementation. [SuppressMessage("ReSharper", "UnusedParameter.Local", Justification = "The Hangfire reference forces it to initialize first, since it's needed by the background service.")] public BackgroundService(IWikiCacheRepository wikiCache, IModCacheRepository modCache, JobStorage hangfireStorage) { BackgroundService.WikiCache = wikiCache; BackgroundService.ModCache = modCache; } /// Start the service. /// Tracks whether the start process has been aborted. public Task StartAsync(CancellationToken cancellationToken) { this.TryInit(); // set startup tasks BackgroundJob.Enqueue(() => BackgroundService.UpdateWikiAsync()); BackgroundJob.Enqueue(() => BackgroundService.RemoveStaleModsAsync()); // set recurring tasks RecurringJob.AddOrUpdate(() => BackgroundService.UpdateWikiAsync(), "*/10 * * * *"); // every 10 minutes RecurringJob.AddOrUpdate(() => BackgroundService.RemoveStaleModsAsync(), "0 * * * *"); // hourly BackgroundService.IsStarted = true; return Task.CompletedTask; } /// Triggered when the application host is performing a graceful shutdown. /// Tracks whether the shutdown process should no longer be graceful. public async Task StopAsync(CancellationToken cancellationToken) { BackgroundService.IsStarted = false; if (BackgroundService.JobServer != null) await BackgroundService.JobServer.WaitForShutdownAsync(cancellationToken); } /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. public void Dispose() { BackgroundService.IsStarted = false; BackgroundService.JobServer?.Dispose(); } /**** ** Tasks ****/ /// Update the cached wiki metadata. [AutomaticRetry(Attempts = 3, DelaysInSeconds = new[] { 30, 60, 120 })] public static async Task UpdateWikiAsync() { if (!BackgroundService.IsStarted) throw new InvalidOperationException($"Must call {nameof(BackgroundService.StartAsync)} before scheduling tasks."); WikiModList wikiCompatList = await new ModToolkit().GetWikiCompatibilityListAsync(); BackgroundService.WikiCache.SaveWikiData(wikiCompatList.StableVersion, wikiCompatList.BetaVersion, wikiCompatList.Mods); } /// Remove mods which haven't been requested in over 48 hours. public static Task RemoveStaleModsAsync() { if (!BackgroundService.IsStarted) throw new InvalidOperationException($"Must call {nameof(BackgroundService.StartAsync)} before scheduling tasks."); BackgroundService.ModCache.RemoveStaleMods(TimeSpan.FromHours(48)); return Task.CompletedTask; } /********* ** Private method *********/ /// Initialize the background service if it's not already initialized. /// The background service is already initialized. private void TryInit() { if (BackgroundService.JobServer != null) throw new InvalidOperationException("The scheduler service is already started."); BackgroundService.JobServer = new BackgroundJobServer(); } } }