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();
}
}
}