fix: handle Nextcloud CalDAV 429 rate limiting with Polly retry
Replaces hand-rolled retry logic with Microsoft.Extensions.Http.Resilience (Polly v8). The named 'nextcloud' HttpClient retries up to 3 times on 429 with exponential backoff (2s, 4s, 8s). Also adds a 300ms delay after every individual PUT/DELETE request to reduce burst rate. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
c4c67e2b73
commit
0520ce3847
3 changed files with 21 additions and 7 deletions
|
|
@ -1,6 +1,9 @@
|
||||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||||
using Microsoft.AspNetCore.Authentication.Google;
|
using Microsoft.AspNetCore.Authentication.Google;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.Extensions.Http.Resilience;
|
||||||
|
using Polly;
|
||||||
|
using System.Net;
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using ShiftScheduler.Services;
|
using ShiftScheduler.Services;
|
||||||
using ShiftScheduler.Shared;
|
using ShiftScheduler.Shared;
|
||||||
|
|
@ -24,6 +27,18 @@ builder.Services.AddSingleton(authorizedEmails);
|
||||||
builder.Services.AddSingleton<IConfigurationService>(new ConfigurationService(appConfiguration));
|
builder.Services.AddSingleton<IConfigurationService>(new ConfigurationService(appConfiguration));
|
||||||
builder.Services.AddMemoryCache();
|
builder.Services.AddMemoryCache();
|
||||||
builder.Services.AddHttpClient<TransportApiService>();
|
builder.Services.AddHttpClient<TransportApiService>();
|
||||||
|
builder.Services.AddHttpClient("nextcloud")
|
||||||
|
.AddResilienceHandler("nextcloud-retry", pipeline =>
|
||||||
|
{
|
||||||
|
pipeline.AddRetry(new HttpRetryStrategyOptions
|
||||||
|
{
|
||||||
|
MaxRetryAttempts = 3,
|
||||||
|
Delay = TimeSpan.FromSeconds(2),
|
||||||
|
BackoffType = DelayBackoffType.Exponential,
|
||||||
|
ShouldHandle = args => ValueTask.FromResult(
|
||||||
|
args.Outcome.Result?.StatusCode == HttpStatusCode.TooManyRequests)
|
||||||
|
});
|
||||||
|
});
|
||||||
builder.Services.AddSingleton<IcsExportService>();
|
builder.Services.AddSingleton<IcsExportService>();
|
||||||
builder.Services.AddSingleton<PdfExportService>();
|
builder.Services.AddSingleton<PdfExportService>();
|
||||||
builder.Services.AddSingleton<ITransportApiService, TransportApiService>();
|
builder.Services.AddSingleton<ITransportApiService, TransportApiService>();
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.Google" Version="9.0.8" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.Google" Version="9.0.8" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="9.0.8" />
|
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="9.0.8" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="9.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -110,7 +110,7 @@ public class NextcloudCalendarService(
|
||||||
reportRequest.Headers.Add("Depth", "1");
|
reportRequest.Headers.Add("Depth", "1");
|
||||||
reportRequest.Content = new StringContent(body, Encoding.UTF8, "application/xml");
|
reportRequest.Content = new StringContent(body, Encoding.UTF8, "application/xml");
|
||||||
|
|
||||||
var client = httpClientFactory.CreateClient();
|
var client = httpClientFactory.CreateClient("nextcloud");
|
||||||
var reportResponse = await client.SendAsync(reportRequest);
|
var reportResponse = await client.SendAsync(reportRequest);
|
||||||
reportResponse.EnsureSuccessStatusCode();
|
reportResponse.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
|
@ -126,8 +126,7 @@ public class NextcloudCalendarService(
|
||||||
deleteResponse.StatusCode != System.Net.HttpStatusCode.NotFound)
|
deleteResponse.StatusCode != System.Net.HttpStatusCode.NotFound)
|
||||||
deleteResponse.EnsureSuccessStatusCode();
|
deleteResponse.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
if (eventUrls.Count > 1)
|
await Task.Delay(300);
|
||||||
await Task.Delay(200);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -155,7 +154,7 @@ public class NextcloudCalendarService(
|
||||||
|
|
||||||
private async Task CreateShiftEventsAsync(string calendarId, List<ShiftWithTransport> shifts)
|
private async Task CreateShiftEventsAsync(string calendarId, List<ShiftWithTransport> shifts)
|
||||||
{
|
{
|
||||||
var client = httpClientFactory.CreateClient();
|
var client = httpClientFactory.CreateClient("nextcloud");
|
||||||
|
|
||||||
foreach (var shiftWithTransport in shifts)
|
foreach (var shiftWithTransport in shifts)
|
||||||
{
|
{
|
||||||
|
|
@ -176,6 +175,7 @@ public class NextcloudCalendarService(
|
||||||
var putRequest = CreateRequest(HttpMethod.Put, $"{calendarId}{Uri.EscapeDataString(uid)}.ics");
|
var putRequest = CreateRequest(HttpMethod.Put, $"{calendarId}{Uri.EscapeDataString(uid)}.ics");
|
||||||
putRequest.Content = new StringContent(icsContent, Encoding.UTF8, "text/calendar");
|
putRequest.Content = new StringContent(icsContent, Encoding.UTF8, "text/calendar");
|
||||||
(await client.SendAsync(putRequest)).EnsureSuccessStatusCode();
|
(await client.SendAsync(putRequest)).EnsureSuccessStatusCode();
|
||||||
|
await Task.Delay(300);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(shift.AfternoonTime))
|
if (!string.IsNullOrEmpty(shift.AfternoonTime))
|
||||||
|
|
@ -189,10 +189,8 @@ public class NextcloudCalendarService(
|
||||||
var putRequest = CreateRequest(HttpMethod.Put, $"{calendarId}{Uri.EscapeDataString(uid)}.ics");
|
var putRequest = CreateRequest(HttpMethod.Put, $"{calendarId}{Uri.EscapeDataString(uid)}.ics");
|
||||||
putRequest.Content = new StringContent(icsContent, Encoding.UTF8, "text/calendar");
|
putRequest.Content = new StringContent(icsContent, Encoding.UTF8, "text/calendar");
|
||||||
(await client.SendAsync(putRequest)).EnsureSuccessStatusCode();
|
(await client.SendAsync(putRequest)).EnsureSuccessStatusCode();
|
||||||
}
|
|
||||||
|
|
||||||
if (shifts.Count > 1)
|
|
||||||
await Task.Delay(300);
|
await Task.Delay(300);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue