Fix Spotify 429 rate limiting and startup performance issues

- Fix: Use correct HttpClient (_webApiClient) for GraphQL library playlists endpoint
  - Was using _httpClient which pointed to wrong base URL causing 429 errors
- Add: Retry logic with Retry-After header support for 429 responses
- Add: Minimum 500ms delay between library playlist pages to prevent rate limiting
- Add: 5-second timeout per endpoint benchmark ping to prevent slow endpoints from blocking startup
- Add: Documentation for timeout requirements in EndpointBenchmarkService
- Fix: ARM64 compatibility for spotify-lyrics service via platform emulation in docker-compose
This commit is contained in:
2026-02-09 16:09:38 -05:00
parent f135db3f60
commit 0dca6b792d
4 changed files with 40 additions and 7 deletions

View File

@@ -20,6 +20,9 @@ public class EndpointBenchmarkService
/// <summary>
/// Benchmarks a list of endpoints by making test requests.
/// Returns endpoints sorted by average response time (fastest first).
///
/// IMPORTANT: The testFunc should implement its own timeout to prevent slow endpoints
/// from blocking startup. Recommended: 5-10 second timeout per ping.
/// </summary>
public async Task<List<string>> BenchmarkEndpointsAsync(
List<string> endpoints,

View File

@@ -349,6 +349,17 @@ public class SpotifyApiClient : IDisposable
var response = await _webApiClient.SendAsync(request, cancellationToken);
// Handle 429 rate limiting with exponential backoff
if (response.StatusCode == System.Net.HttpStatusCode.TooManyRequests)
{
var retryAfter = response.Headers.RetryAfter?.Delta ?? TimeSpan.FromSeconds(5);
_logger.LogWarning("Spotify rate limit hit (429) when fetching playlist {PlaylistId}. Waiting {Seconds}s before retry...", playlistId, retryAfter.TotalSeconds);
await Task.Delay(retryAfter, cancellationToken);
// Retry the request
response = await _webApiClient.SendAsync(request, cancellationToken);
}
if (!response.IsSuccessStatusCode)
{
_logger.LogError("Failed to fetch playlist via GraphQL: {StatusCode}", response.StatusCode);
@@ -801,7 +812,19 @@ public class SpotifyApiClient : IDisposable
};
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
var response = await _httpClient.SendAsync(request, cancellationToken);
var response = await _webApiClient.SendAsync(request, cancellationToken);
// Handle 429 rate limiting with exponential backoff
if (response.StatusCode == System.Net.HttpStatusCode.TooManyRequests)
{
var retryAfter = response.Headers.RetryAfter?.Delta ?? TimeSpan.FromSeconds(5);
_logger.LogWarning("Spotify rate limit hit (429) when fetching library playlists. Waiting {Seconds}s before retry...", retryAfter.TotalSeconds);
await Task.Delay(retryAfter, cancellationToken);
// Retry the request
response = await _httpClient.SendAsync(request, cancellationToken);
}
if (!response.IsSuccessStatusCode)
{
_logger.LogWarning("GraphQL user playlists request failed: {StatusCode}", response.StatusCode);
@@ -851,11 +874,11 @@ public class SpotifyApiClient : IDisposable
if (itemCount < limit) break;
offset += limit;
// GraphQL is less rate-limited, but still add a small delay
if (_settings.RateLimitDelayMs > 0)
{
await Task.Delay(_settings.RateLimitDelayMs, cancellationToken);
}
// Add delay between pages to avoid rate limiting
// Library fetching can be aggressive, so use a longer delay
var delayMs = Math.Max(_settings.RateLimitDelayMs, 500); // Minimum 500ms between pages
_logger.LogDebug("Waiting {DelayMs}ms before fetching next page of library playlists...", delayMs);
await Task.Delay(delayMs, cancellationToken);
}
_logger.LogInformation("Found {Count} playlists matching '{SearchName}' via GraphQL", playlists.Count, searchName);

View File

@@ -73,7 +73,11 @@ public class SquidWTFStartupValidator : BaseStartupValidator
{
try
{
var response = await _httpClient.GetAsync(endpoint, ct);
// 5 second timeout per ping - mark slow endpoints as failed
using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
timeoutCts.CancelAfter(TimeSpan.FromSeconds(5));
var response = await _httpClient.GetAsync(endpoint, timeoutCts.Token);
return response.IsSuccessStatusCode;
}
catch

View File

@@ -17,8 +17,11 @@ services:
networks:
- allstarr-network
# Spotify Lyrics API sidecar service
# Note: This image only supports AMD64. On ARM64 systems, Docker will use emulation.
spotify-lyrics:
image: akashrchandran/spotify-lyrics-api:latest
platform: linux/amd64
container_name: allstarr-spotify-lyrics
restart: unless-stopped
ports: