3
3
using System . Threading ;
4
4
using System . Threading . Tasks ;
5
5
using Hangfire ;
6
+ using Hangfire . Console ;
7
+ using Hangfire . Server ;
8
+ using Humanizer ;
6
9
using Microsoft . Extensions . Hosting ;
7
10
using Microsoft . Extensions . Options ;
8
11
using StardewModdingAPI . Toolkit ;
9
12
using StardewModdingAPI . Toolkit . Framework . Clients . CurseForgeExport ;
10
- using StardewModdingAPI . Toolkit . Framework . Clients . CurseForgeExport . ResponseModels ;
11
13
using StardewModdingAPI . Toolkit . Framework . Clients . NexusExport ;
12
- using StardewModdingAPI . Toolkit . Framework . Clients . NexusExport . ResponseModels ;
13
14
using StardewModdingAPI . Toolkit . Framework . Clients . Wiki ;
15
+ using StardewModdingAPI . Web . Framework . Caching ;
14
16
using StardewModdingAPI . Web . Framework . Caching . CurseForgeExport ;
15
17
using StardewModdingAPI . Web . Framework . Caching . Mods ;
16
18
using StardewModdingAPI . Web . Framework . Caching . NexusExport ;
@@ -108,19 +110,19 @@ public Task StartAsync(CancellationToken cancellationToken)
108
110
bool enableNexusExport = BackgroundService . NexusExportApiClient is not DisabledNexusExportApiClient ;
109
111
110
112
// set startup tasks
111
- BackgroundJob . Enqueue ( ( ) => BackgroundService . UpdateWikiAsync ( ) ) ;
113
+ BackgroundJob . Enqueue ( ( ) => BackgroundService . UpdateWikiAsync ( null ) ) ;
112
114
if ( enableCurseForgeExport )
113
- BackgroundJob . Enqueue ( ( ) => BackgroundService . UpdateCurseForgeExportAsync ( ) ) ;
115
+ BackgroundJob . Enqueue ( ( ) => BackgroundService . UpdateCurseForgeExportAsync ( null ) ) ;
114
116
if ( enableNexusExport )
115
- BackgroundJob . Enqueue ( ( ) => BackgroundService . UpdateNexusExportAsync ( ) ) ;
117
+ BackgroundJob . Enqueue ( ( ) => BackgroundService . UpdateNexusExportAsync ( null ) ) ;
116
118
BackgroundJob . Enqueue ( ( ) => BackgroundService . RemoveStaleModsAsync ( ) ) ;
117
119
118
120
// set recurring tasks
119
- RecurringJob . AddOrUpdate ( "update wiki data" , ( ) => BackgroundService . UpdateWikiAsync ( ) , "*/10 * * * *" ) ; // every 10 minutes
121
+ RecurringJob . AddOrUpdate ( "update wiki data" , ( ) => BackgroundService . UpdateWikiAsync ( null ) , "*/10 * * * *" ) ; // every 10 minutes
120
122
if ( enableCurseForgeExport )
121
- RecurringJob . AddOrUpdate ( "update CurseForge export" , ( ) => BackgroundService . UpdateCurseForgeExportAsync ( ) , "*/10 * * * *" ) ;
123
+ RecurringJob . AddOrUpdate ( "update CurseForge export" , ( ) => BackgroundService . UpdateCurseForgeExportAsync ( null ) , "*/10 * * * *" ) ;
122
124
if ( enableNexusExport )
123
- RecurringJob . AddOrUpdate ( "update Nexus export" , ( ) => BackgroundService . UpdateNexusExportAsync ( ) , "*/10 * * * *" ) ;
125
+ RecurringJob . AddOrUpdate ( "update Nexus export" , ( ) => BackgroundService . UpdateNexusExportAsync ( null ) , "*/10 * * * *" ) ;
124
126
RecurringJob . AddOrUpdate ( "remove stale mods" , ( ) => BackgroundService . RemoveStaleModsAsync ( ) , "2/10 * * * *" ) ; // offset by 2 minutes so it runs after updates (e.g. 00:02, 00:12, etc)
125
127
126
128
BackgroundService . IsStarted = true ;
@@ -150,54 +152,48 @@ public void Dispose()
150
152
** Tasks
151
153
****/
152
154
/// <summary>Update the cached wiki metadata.</summary>
153
- [ AutomaticRetry ( Attempts = 3 , DelaysInSeconds = new [ ] { 30 , 60 , 120 } ) ]
154
- public static async Task UpdateWikiAsync ( )
155
+ /// <param name="context">Information about the context in which the job is performed. This is injected automatically by Hangfire.</param>
156
+ [ AutomaticRetry ( Attempts = 3 , DelaysInSeconds = [ 30 , 60 , 120 ] ) ]
157
+ public static async Task UpdateWikiAsync ( PerformContext ? context )
155
158
{
156
159
if ( ! BackgroundService . IsStarted )
157
160
throw new InvalidOperationException ( $ "Must call { nameof ( BackgroundService . StartAsync ) } before scheduling tasks.") ;
158
161
162
+ context . WriteLine ( "Fetching data from wiki..." ) ;
159
163
WikiModList wikiCompatList = await new ModToolkit ( ) . GetWikiCompatibilityListAsync ( ) ;
164
+
165
+ context . WriteLine ( "Saving data..." ) ;
160
166
BackgroundService . WikiCache . SaveWikiData ( wikiCompatList . StableVersion , wikiCompatList . BetaVersion , wikiCompatList . Mods ) ;
167
+
168
+ context . WriteLine ( "Done!" ) ;
161
169
}
162
170
163
171
/// <summary>Update the cached CurseForge mod dump.</summary>
164
- [ AutomaticRetry ( Attempts = 3 , DelaysInSeconds = new [ ] { 30 , 60 , 120 } ) ]
165
- public static async Task UpdateCurseForgeExportAsync ( )
172
+ /// <param name="context">Information about the context in which the job is performed. This is injected automatically by Hangfire.</param>
173
+ [ AutomaticRetry ( Attempts = 3 , DelaysInSeconds = [ 30 , 60 , 120 ] ) ]
174
+ public static async Task UpdateCurseForgeExportAsync ( PerformContext ? context )
166
175
{
167
- if ( ! BackgroundService . IsStarted )
168
- throw new InvalidOperationException ( $ "Must call { nameof ( BackgroundService . StartAsync ) } before scheduling tasks.") ;
169
-
170
- var cache = BackgroundService . CurseForgeExportCache ;
171
- var client = BackgroundService . CurseForgeExportApiClient ;
172
-
173
- if ( await cache . CanRefreshFromAsync ( client , BackgroundService . ExportStaleAge ) )
174
- {
175
- CurseForgeFullExport data = await client . FetchExportAsync ( ) ;
176
- cache . SetData ( data ) ;
177
- }
178
-
179
- if ( cache . IsStale ( BackgroundService . ExportStaleAge ) )
180
- cache . SetData ( null ) ; // if the export is too old, fetch fresh mod data from the API instead
176
+ await UpdateExportAsync (
177
+ context ,
178
+ BackgroundService . CurseForgeExportCache ! ,
179
+ BackgroundService . CurseForgeExportApiClient ! ,
180
+ client => client . FetchLastModifiedDateAsync ( ) ,
181
+ async ( cache , client ) => cache . SetData ( await client . FetchExportAsync ( ) )
182
+ ) ;
181
183
}
182
184
183
185
/// <summary>Update the cached Nexus mod dump.</summary>
184
- [ AutomaticRetry ( Attempts = 3 , DelaysInSeconds = new [ ] { 30 , 60 , 120 } ) ]
185
- public static async Task UpdateNexusExportAsync ( )
186
+ /// <param name="context">Information about the context in which the job is performed. This is injected automatically by Hangfire.</param>
187
+ [ AutomaticRetry ( Attempts = 3 , DelaysInSeconds = [ 30 , 60 , 120 ] ) ]
188
+ public static async Task UpdateNexusExportAsync ( PerformContext ? context )
186
189
{
187
- if ( ! BackgroundService . IsStarted )
188
- throw new InvalidOperationException ( $ "Must call { nameof ( BackgroundService . StartAsync ) } before scheduling tasks.") ;
189
-
190
- var cache = BackgroundService . NexusExportCache ;
191
- var client = BackgroundService . NexusExportApiClient ;
192
-
193
- if ( await cache . CanRefreshFromAsync ( client , BackgroundService . ExportStaleAge ) )
194
- {
195
- NexusFullExport data = await client . FetchExportAsync ( ) ;
196
- cache . SetData ( data ) ;
197
- }
198
-
199
- if ( cache . IsStale ( BackgroundService . ExportStaleAge ) )
200
- cache . SetData ( null ) ; // if the export is too old, fetch fresh mod data from the site/API instead
190
+ await UpdateExportAsync (
191
+ context ,
192
+ BackgroundService . NexusExportCache ! ,
193
+ BackgroundService . NexusExportApiClient ! ,
194
+ client => client . FetchLastModifiedDateAsync ( ) ,
195
+ async ( cache , client ) => cache . SetData ( await client . FetchExportAsync ( ) )
196
+ ) ;
201
197
}
202
198
203
199
/// <summary>Remove mods which haven't been requested in over 48 hours.</summary>
@@ -209,10 +205,6 @@ public static Task RemoveStaleModsAsync()
209
205
// remove mods in mod cache
210
206
BackgroundService . ModCache . RemoveStaleMods ( TimeSpan . FromHours ( 48 ) ) ;
211
207
212
- // remove stale export cache
213
- if ( BackgroundService . NexusExportCache . IsStale ( BackgroundService . ExportStaleAge ) )
214
- BackgroundService . NexusExportCache . SetData ( null ) ;
215
-
216
208
return Task . CompletedTask ;
217
209
}
218
210
@@ -229,5 +221,74 @@ private void TryInit()
229
221
230
222
BackgroundService . JobServer = new BackgroundJobServer ( ) ;
231
223
}
224
+
225
+ /// <summary>Update the cached mods export for a site.</summary>
226
+ /// <typeparam name="TCacheRepository">The export cache repository type.</typeparam>
227
+ /// <typeparam name="TExportApiClient">The export API client.</typeparam>
228
+ /// <param name="context">Information about the context in which the job is performed. This is injected automatically by Hangfire.</param>
229
+ /// <param name="cache">The export cache to update.</param>
230
+ /// <param name="client">The export API with which to fetch data from the remote API.</param>
231
+ /// <param name="fetchLastModifiedDateAsync">Fetch the date when the export on the server was last modified.</param>
232
+ /// <param name="fetchDataAsync">Fetch the latest export file from the Nexus Mods export API.</param>
233
+ /// <exception cref="InvalidOperationException">The <see cref="StartAsync"/> method wasn't called before running this task.</exception>
234
+ private static async Task UpdateExportAsync < TCacheRepository , TExportApiClient > ( PerformContext ? context , TCacheRepository cache , TExportApiClient client , Func < TExportApiClient , Task < DateTimeOffset > > fetchLastModifiedDateAsync , Func < TCacheRepository , TExportApiClient , Task > fetchDataAsync )
235
+ where TCacheRepository : IExportCacheRepository
236
+ {
237
+ if ( ! BackgroundService . IsStarted )
238
+ throw new InvalidOperationException ( $ "Must call { nameof ( BackgroundService . StartAsync ) } before scheduling tasks.") ;
239
+
240
+ // refresh data
241
+ context . WriteLine ( "Checking if we can refresh the data..." ) ;
242
+ if ( BackgroundService . CanRefreshFromExportApi ( await fetchLastModifiedDateAsync ( client ) , cache , out string ? failReason ) )
243
+ {
244
+ context . WriteLine ( "Fetching data..." ) ;
245
+ await fetchDataAsync ( cache , client ) ;
246
+ context . WriteLine ( $ "Cache updated. The data was last modified { BackgroundService . FormatDateModified ( cache . GetLastModified ( ) ) } .") ;
247
+ }
248
+ else
249
+ context . WriteLine ( $ "Skipped data fetch: { failReason } .") ;
250
+
251
+ // clear if stale
252
+ if ( cache . IsStale ( BackgroundService . ExportStaleAge ) )
253
+ {
254
+ context . WriteLine ( "The cached data is stale, clearing cache..." ) ;
255
+ cache . Clear ( ) ;
256
+ }
257
+
258
+ context . WriteLine ( "Done!" ) ;
259
+ }
260
+
261
+ /// <summary>Get whether newer non-stale data can be fetched from the server.</summary>
262
+ /// <param name="serverModified">The last-modified data from the remote API.</param>
263
+ /// <param name="repository">The repository to update.</param>
264
+ /// <param name="failReason">The reason to log if we can't fetch data.</param>
265
+ private static bool CanRefreshFromExportApi ( DateTimeOffset serverModified , IExportCacheRepository repository , [ NotNullWhen ( false ) ] out string ? failReason )
266
+ {
267
+ if ( repository . IsStale ( serverModified , BackgroundService . ExportStaleAge ) )
268
+ {
269
+ failReason = $ "server was last modified { BackgroundService . FormatDateModified ( serverModified ) } , which exceeds the { BackgroundService . ExportStaleAge } -minute-stale limit";
270
+ return false ;
271
+ }
272
+
273
+ if ( repository . IsLoaded ( ) )
274
+ {
275
+ DateTimeOffset localModified = repository . GetLastModified ( ) ;
276
+ if ( localModified >= serverModified )
277
+ {
278
+ failReason = $ "server was last modified { BackgroundService . FormatDateModified ( serverModified ) } , which { ( serverModified == localModified ? "matches our cached data" : $ "is older than our cached { BackgroundService . FormatDateModified ( localModified ) } ") } ";
279
+ return false ;
280
+ }
281
+ }
282
+
283
+ failReason = null ;
284
+ return true ;
285
+ }
286
+
287
+ /// <summary>Format a 'date modified' value for the task logs.</summary>
288
+ /// <param name="date">The date to log.</param>
289
+ private static string FormatDateModified ( DateTimeOffset date )
290
+ {
291
+ return $ "{ date : O} (age: { ( DateTimeOffset . UtcNow - date ) . Humanize ( ) } )";
292
+ }
232
293
}
233
294
}
0 commit comments