-
-
Notifications
You must be signed in to change notification settings - Fork 30
Expand file tree
/
Copy pathArrowHeadSyncService.cs
More file actions
160 lines (133 loc) · 6.18 KB
/
ArrowHeadSyncService.cs
File metadata and controls
160 lines (133 loc) · 6.18 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
using Helldivers.Core;
using Helldivers.Models.ArrowHead;
using Helldivers.Sync.Configuration;
using Helldivers.Sync.Services;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Prometheus;
using System.Globalization;
namespace Helldivers.Sync.Hosted;
/// <summary>
/// The background synchronization service that pulls information from ArrowHead's API (through <see cref="ArrowHeadApiService" />)
/// and updates the <see cref="StorageFacade" />.
/// </summary>
public sealed partial class ArrowHeadSyncService(
ILogger<ArrowHeadSyncService> logger,
IServiceScopeFactory scopeFactory,
IOptions<HelldiversSyncConfiguration> configuration,
StorageFacade storage
) : BackgroundService
{
/// <summary>
/// Timestamp the store was last updated successfully.
/// </summary>
public DateTime? LastUpdated { get; private set; }
private static readonly Histogram ArrowHeadSyncMetric =
Metrics.CreateHistogram("helldivers_sync_arrowhead", "All ArrowHead synchronizations");
#region Source generated logging
[LoggerMessage(Level = LogLevel.Information, Message = "sync will run every {Interval}")]
private static partial void LogRunAtInterval(ILogger logger, TimeSpan interval);
[LoggerMessage(LogLevel.Debug, Message = "Running sync at {Timestamp}")]
private static partial void LogRunningSyncAt(ILogger logger, DateTime timestamp);
[LoggerMessage(Level = LogLevel.Error, Message = "An exception was thrown when synchronizing from ArrowHead API")]
private static partial void LogSyncThrewAnError(ILogger logger, Exception exception);
[LoggerMessage(LogLevel.Warning, Message = "Failed to download translations for {Language} of {Type}")]
private static partial void LogFailedToLoadTranslation(ILogger logger, Exception exception, string language,
string type);
[LoggerMessage(Level = LogLevel.Information, Message = "ArrowHeadSyncService finished processing, shutting down.")]
private static partial void LogRunOnceCompleted(ILogger logger);
#endregion
/// <inheritdoc cref="BackgroundService.ExecuteAsync(CancellationToken)" />
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
{
var delay = TimeSpan.FromSeconds(configuration.Value.IntervalSeconds);
LogRunAtInterval(logger, delay);
while (cancellationToken.IsCancellationRequested is false)
{
try
{
using var _ = ArrowHeadSyncMetric.NewTimer();
await using var scope = scopeFactory.CreateAsyncScope();
LogRunningSyncAt(logger, DateTime.UtcNow);
await SynchronizeAsync(scope.ServiceProvider, cancellationToken);
}
catch (Exception exception)
{
LogSyncThrewAnError(logger, exception);
if (configuration.Value.RunOnce)
throw;
}
// If we should only run once, we exit the loop (and thus service) after this.
if (configuration.Value.RunOnce)
{
LogRunOnceCompleted(logger);
return;
}
await Task.Delay(delay, cancellationToken);
}
}
private async Task SynchronizeAsync(IServiceProvider services, CancellationToken cancellationToken)
{
var api = services.GetRequiredService<ArrowHeadApiService>();
var (rawWarId, warId) = await api.GetCurrentSeason(cancellationToken);
var season = warId.Id.ToString(CultureInfo.InvariantCulture);
var warInfo = await api.GetWarInfo(season, cancellationToken);
var warSummary = await api.GetSummary(season, cancellationToken);
// For each language, load war status.
var statuses = await DownloadTranslations<WarStatus>(
language => api.GetWarStatus(season, language, cancellationToken),
cancellationToken
);
// For each language, load news feed.
var feeds = await DownloadTranslations<NewsFeedItem>(
async language => await api.LoadFeed(season, language, cancellationToken),
cancellationToken
);
// For each language, load assignments
var assignments = await DownloadTranslations<Assignment>(
async language => await api.LoadAssignments(season, language, cancellationToken),
cancellationToken
);
var spaceStations = await configuration.Value.SpaceStations.ToAsyncEnumerable().ToDictionaryAsync(
static (key, _) => ValueTask.FromResult(key), async (key, stoppingToken) =>
await DownloadTranslations<SpaceStation>(
async language => await api.LoadSpaceStations(season, key, language, stoppingToken),
stoppingToken
), cancellationToken: cancellationToken);
await storage.UpdateStores(
rawWarId,
warInfo,
statuses,
warSummary,
feeds,
assignments,
spaceStations
);
LastUpdated = DateTime.UtcNow;
}
private async Task<Dictionary<string, Memory<byte>>> DownloadTranslations<T>(Func<string, Task<Memory<byte>>> func,
CancellationToken cancellationToken)
{
return await configuration.Value.Languages
.ToAsyncEnumerable()
.Select(async language =>
{
try
{
var result = await func(language);
return new KeyValuePair<string, Memory<byte>?>(language, result);
}
catch (Exception exception)
{
LogFailedToLoadTranslation(logger, exception, language, typeof(T).Name);
return new KeyValuePair<string, Memory<byte>?>(language, null);
}
})
.Select(async (task, stoppingToken) => await task.WaitAsync(stoppingToken))
.Where(pair => pair.Value is not null)
.ToDictionaryAsync(pair => pair.Key, pair => pair.Value.GetValueOrDefault(),
cancellationToken: cancellationToken);
}
}