1
- using System . Diagnostics . CodeAnalysis ;
1
+ using System . Collections . Immutable ;
2
2
using System . Net ;
3
3
using System . Net . Http . Headers ;
4
+ using System . Net . Sockets ;
4
5
using DynamicData . Kernel ;
5
6
using JetBrains . Annotations ;
6
7
using Microsoft . Extensions . DependencyInjection ;
7
- using Microsoft . Extensions . Http . Resilience ;
8
8
using Microsoft . Extensions . Logging ;
9
9
using NexusMods . Abstractions . HttpDownloads ;
10
10
using NexusMods . Abstractions . Jobs ;
11
11
using NexusMods . Abstractions . Library . Models ;
12
- using NexusMods . App . BuildInfo ;
13
12
using NexusMods . MnemonicDB . Abstractions ;
14
13
using NexusMods . Paths ;
15
14
using Polly ;
15
+ using Polly . Retry ;
16
16
17
17
namespace NexusMods . Networking . HttpDownloader ;
18
18
@@ -22,9 +22,12 @@ namespace NexusMods.Networking.HttpDownloader;
22
22
[ PublicAPI ]
23
23
public record HttpDownloadJob : IJobDefinitionWithStart < HttpDownloadJob , AbsolutePath > , IHttpDownloadJob
24
24
{
25
- #pragma warning disable EXTEXP0001
26
- private static readonly HttpClient Client = BuildClient ( ) ;
27
- #pragma warning restore EXTEXP0001
25
+ private static readonly ResiliencePipeline < AbsolutePath > ResiliencePipeline = BuildResiliencePipeline ( ) ;
26
+
27
+ /// <summary>
28
+ /// Client.
29
+ /// </summary>
30
+ public required HttpClient Client { get ; init ; }
28
31
29
32
/// <summary>
30
33
/// Logger.
@@ -76,15 +79,29 @@ public static IJobTask<HttpDownloadJob, AbsolutePath> Create(
76
79
DownloadPageUri = downloadPage ,
77
80
Destination = destination ,
78
81
Logger = provider . GetRequiredService < ILogger < HttpDownloadJob > > ( ) ,
82
+ Client = provider . GetRequiredService < HttpClient > ( ) ,
79
83
} ;
80
84
81
85
return monitor . Begin < HttpDownloadJob , AbsolutePath > ( job ) ;
82
86
}
83
87
84
- /// <summary>
85
- /// Execute the job
86
- /// </summary>
88
+ /// <inheritdoc/>
87
89
public async ValueTask < AbsolutePath > StartAsync ( IJobContext < HttpDownloadJob > context )
90
+ {
91
+ var result = await ResiliencePipeline . ExecuteAsync (
92
+ callback : static ( tuple , _ ) =>
93
+ {
94
+ var ( self , context ) = tuple ;
95
+ return self . StartAsyncImpl ( context ) ;
96
+ } ,
97
+ state : ( this , context ) ,
98
+ cancellationToken : context . CancellationToken
99
+ ) ;
100
+
101
+ return result ;
102
+ }
103
+
104
+ private async ValueTask < AbsolutePath > StartAsyncImpl ( IJobContext < HttpDownloadJob > context )
88
105
{
89
106
await context . YieldAsync ( ) ;
90
107
await FetchMetadata ( context ) ;
@@ -168,6 +185,11 @@ public async ValueTask<AbsolutePath> StartAsync(IJobContext<HttpDownloadJob> con
168
185
{
169
186
await response . Content . CopyToAsync ( outputStream , context . CancellationToken ) ;
170
187
}
188
+ catch ( Exception e )
189
+ {
190
+ Logger . LogWarning ( e , "Exception while downloading from `{PageUri}`, downloaded `{DownloadedBytes}` from `{TotalBytes}` bytes" , DownloadPageUri , outputStream . Position , outputStream . Length ) ;
191
+ throw ;
192
+ }
171
193
finally
172
194
{
173
195
TotalBytesDownloaded = Size . FromLong ( outputStream . Position ) ;
@@ -244,36 +266,27 @@ private async ValueTask FetchMetadata(IJobContext context)
244
266
var contentLength = response . Content . Headers . ContentLength ;
245
267
ContentLength = contentLength is not null ? Size . FromLong ( contentLength . Value ) : Optional < Size > . None ;
246
268
}
247
-
248
- [ Experimental ( "EXTEXP0001" ) ]
249
- private static HttpClient BuildClient ( )
250
- {
251
- // TODO: get values from settings, probably make this a singleton
252
269
253
- var pipeline = new ResiliencePipelineBuilder < HttpResponseMessage > ( )
254
- . AddRetry ( new HttpRetryStrategyOptions ( ) )
255
- . Build ( ) ;
256
-
257
- HttpMessageHandler handler = new ResilienceHandler ( pipeline )
258
- {
259
- InnerHandler = new SocketsHttpHandler
270
+ private static ResiliencePipeline < AbsolutePath > BuildResiliencePipeline ( )
271
+ {
272
+ ImmutableArray < Type > networkExceptions =
273
+ [
274
+ typeof ( HttpIOException ) ,
275
+ typeof ( HttpRequestException ) ,
276
+ typeof ( SocketException ) ,
277
+ ] ;
278
+
279
+ var pipeline = new ResiliencePipelineBuilder < AbsolutePath > ( )
280
+ . AddRetry ( new RetryStrategyOptions < AbsolutePath >
260
281
{
261
- ConnectTimeout = TimeSpan . FromSeconds ( 30 ) ,
262
- KeepAlivePingPolicy = HttpKeepAlivePingPolicy . WithActiveRequests ,
263
- KeepAlivePingDelay = TimeSpan . FromSeconds ( 5 ) ,
264
- KeepAlivePingTimeout = TimeSpan . FromSeconds ( 20 ) ,
265
- } ,
266
- } ;
267
-
268
- var client = new HttpClient ( handler )
269
- {
270
- Timeout = TimeSpan . FromSeconds ( 20 ) ,
271
- DefaultRequestVersion = HttpVersion . Version11 ,
272
- DefaultVersionPolicy = HttpVersionPolicy . RequestVersionOrHigher ,
273
- } ;
274
-
275
- client . DefaultRequestHeaders . UserAgent . ParseAdd ( ApplicationConstants . UserAgent ) ;
282
+ ShouldHandle = args => ValueTask . FromResult ( args . Outcome . Exception is not null && networkExceptions . Contains ( args . Outcome . Exception . GetType ( ) ) ) ,
283
+ BackoffType = DelayBackoffType . Exponential ,
284
+ UseJitter = true ,
285
+ MaxRetryAttempts = 3 ,
286
+ Delay = TimeSpan . FromSeconds ( 3 ) ,
287
+ } )
288
+ . Build ( ) ;
276
289
277
- return client ;
290
+ return pipeline ;
278
291
}
279
292
}
0 commit comments