more complex route

This commit is contained in:
Anton Martynenko
2025-11-01 17:24:59 +01:00
parent 11395ec913
commit f8798cd3d3
6 changed files with 431 additions and 77 deletions
@@ -52,13 +52,31 @@ public class GoogleMapsDownloaderV2
{
var str = JsonConvert.SerializeObject(new { mapType = "satellite" });
var response = await httpClient.PostAsync(url, new StringContent(str));
if (!response.IsSuccessStatusCode)
{
var errorBody = await response.Content.ReadAsStringAsync();
_logger.LogError("Failed to get session token. Status: {StatusCode}, Response: {Response}",
response.StatusCode, errorBody);
}
response.EnsureSuccessStatusCode();
var sessionResponse = await response.Content.ReadFromJsonAsync<SessionResponse>();
return sessionResponse?.Session;
}
catch (TaskCanceledException ex)
{
_logger.LogError(ex, "Session token request cancelled or timed out");
throw;
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "HTTP request failed while getting session token. StatusCode: {StatusCode}", ex.StatusCode);
throw;
}
catch (Exception e)
{
_logger.LogError(e, "Failed to get session token");
_logger.LogError(e, "Unexpected error getting session token");
throw;
}
}
@@ -92,6 +110,14 @@ public class GoogleMapsDownloaderV2
httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(USER_AGENT);
var response = await httpClient.GetAsync(url, token);
if (!response.IsSuccessStatusCode)
{
var errorBody = await response.Content.ReadAsStringAsync(token);
_logger.LogError("Single tile download failed. Tile: ({X}, {Y}), Status: {StatusCode}, Response: {Response}",
tileX, tileY, response.StatusCode, errorBody);
}
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsByteArrayAsync(token);
@@ -127,6 +153,7 @@ public class GoogleMapsDownloaderV2
{
int attempt = 0;
int delay = BASE_RETRY_DELAY_SECONDS;
Exception? lastException = null;
while (attempt < maxRetries)
{
@@ -134,34 +161,68 @@ public class GoogleMapsDownloaderV2
{
return await action();
}
catch (TaskCanceledException ex)
{
_logger.LogError(ex, "Request was cancelled (timeout or explicit cancellation). Attempt {Attempt}/{Max}", attempt + 1, maxRetries);
throw;
}
catch (OperationCanceledException ex)
{
_logger.LogError(ex, "Operation was cancelled. Attempt {Attempt}/{Max}", attempt + 1, maxRetries);
throw;
}
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.TooManyRequests || ex.StatusCode == (HttpStatusCode)429)
{
attempt++;
lastException = ex;
if (attempt >= maxRetries)
{
_logger.LogError("Rate limit exceeded after {Attempts} attempts", maxRetries);
throw new RateLimitException($"Rate limit exceeded after {maxRetries} attempts");
_logger.LogError(ex, "Rate limit (429) exceeded after {Attempts} attempts. This indicates Google Maps API throttling.", maxRetries);
throw new RateLimitException($"Rate limit exceeded after {maxRetries} attempts. Google Maps API is throttling requests.");
}
delay = Math.Min(delay * 2, MAX_RETRY_DELAY_SECONDS);
_logger.LogWarning("Rate limited. Waiting {Delay}s before retry {Attempt}/{Max}", delay, attempt, maxRetries);
_logger.LogWarning("Rate limited (429 Too Many Requests). Waiting {Delay}s before retry {Attempt}/{Max}", delay, attempt, maxRetries);
await Task.Delay(TimeSpan.FromSeconds(delay), cancellationToken);
}
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.Forbidden)
{
_logger.LogError(ex, "Access forbidden (403). Check API key validity and permissions.");
throw;
}
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.Unauthorized)
{
_logger.LogError(ex, "Unauthorized (401). API key is invalid or missing.");
throw;
}
catch (HttpRequestException ex) when (ex.StatusCode >= HttpStatusCode.InternalServerError)
{
attempt++;
lastException = ex;
if (attempt >= maxRetries)
{
_logger.LogError(ex, "Server error after {Attempts} attempts", maxRetries);
_logger.LogError(ex, "Server error ({StatusCode}) after {Attempts} attempts", ex.StatusCode, maxRetries);
throw;
}
delay = Math.Min(delay * 2, MAX_RETRY_DELAY_SECONDS);
_logger.LogWarning("Server error. Waiting {Delay}s before retry {Attempt}/{Max}", delay, attempt, maxRetries);
_logger.LogWarning("Server error ({StatusCode}). Waiting {Delay}s before retry {Attempt}/{Max}", ex.StatusCode, delay, attempt, maxRetries);
await Task.Delay(TimeSpan.FromSeconds(delay), cancellationToken);
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "HTTP request failed with status {StatusCode}. Message: {Message}", ex.StatusCode, ex.Message);
throw;
}
}
if (lastException != null)
{
throw new InvalidOperationException($"Retry logic exhausted after {maxRetries} attempts", lastException);
}
throw new InvalidOperationException("Retry logic failed unexpectedly");
}
@@ -229,6 +290,14 @@ public class GoogleMapsDownloaderV2
httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(USER_AGENT);
var response = await httpClient.GetAsync(url, token);
if (!response.IsSuccessStatusCode)
{
var errorBody = await response.Content.ReadAsStringAsync(token);
_logger.LogError("Tile download failed. Tile: ({X}, {Y}), Status: {StatusCode}, URL: {Url}, Response: {Response}",
x, y, response.StatusCode, url, errorBody);
}
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsByteArrayAsync(token);
@@ -242,14 +311,31 @@ public class GoogleMapsDownloaderV2
downloadedTiles.Add(new DownloadedTileInfoV2(
x, y, zoomLevel, tileCenter.Lat, tileCenter.Lon, filePath, tileSizeMeters));
}
catch (TaskCanceledException ex)
{
_logger.LogError(ex, "Tile download cancelled for ({X}, {Y}). This may be due to HttpClient timeout or explicit cancellation.", x, y);
throw;
}
catch (OperationCanceledException ex)
{
_logger.LogError(ex, "Tile download operation cancelled for ({X}, {Y})", x, y);
throw;
}
catch (RateLimitException ex)
{
_logger.LogError(ex, "Rate limit exceeded for tile ({X}, {Y})", x, y);
_logger.LogError(ex, "Rate limit exceeded for tile ({X}, {Y}). Google Maps API is throttling requests. Consider reducing concurrent requests or adding delays.", x, y);
throw;
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "HTTP request failed for tile ({X}, {Y}). StatusCode: {StatusCode}, Message: {Message}",
x, y, ex.StatusCode, ex.Message);
throw;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to download tile ({X}, {Y})", x, y);
_logger.LogError(ex, "Unexpected error downloading tile ({X}, {Y}). Type: {ExceptionType}, Message: {Message}",
x, y, ex.GetType().Name, ex.Message);
throw;
}
}
+33 -7
View File
@@ -164,28 +164,54 @@ public class RegionService : IRegionService
var duration = (DateTime.UtcNow - startTime).TotalSeconds;
_logger.LogInformation("Region {RegionId} processing completed in {Duration:F2}s", id, duration);
}
catch (OperationCanceledException) when (timeoutCts.IsCancellationRequested)
catch (TaskCanceledException ex) when (timeoutCts.IsCancellationRequested)
{
errorMessage = "Processing timed out after 5 minutes. Unable to download tiles within the time limit.";
_logger.LogError("Region {RegionId} processing timed out after 5 minutes", id);
_logger.LogError(ex, "Region {RegionId} processing timed out after 5 minutes", id);
await HandleProcessingFailureAsync(id, region, startTime, tiles, tilesDownloaded, tilesReused, errorMessage);
}
catch (TaskCanceledException ex) when (cancellationToken.IsCancellationRequested)
{
errorMessage = "Processing was cancelled externally (likely application shutdown).";
_logger.LogError(ex, "Region {RegionId} processing was cancelled externally", id);
await HandleProcessingFailureAsync(id, region, startTime, tiles, tilesDownloaded, tilesReused, errorMessage);
}
catch (TaskCanceledException ex)
{
errorMessage = $"Request cancelled or timed out: {ex.Message}. This may indicate HttpClient timeout or network issues.";
_logger.LogError(ex, "Region {RegionId} processing was cancelled (TaskCanceledException)", id);
await HandleProcessingFailureAsync(id, region, startTime, tiles, tilesDownloaded, tilesReused, errorMessage);
}
catch (OperationCanceledException ex) when (timeoutCts.IsCancellationRequested)
{
errorMessage = "Processing timed out after 5 minutes. Unable to download tiles within the time limit.";
_logger.LogError(ex, "Region {RegionId} processing timed out after 5 minutes", id);
await HandleProcessingFailureAsync(id, region, startTime, tiles, tilesDownloaded, tilesReused, errorMessage);
}
catch (OperationCanceledException ex)
{
errorMessage = $"Operation cancelled: {ex.Message}";
_logger.LogError(ex, "Region {RegionId} processing was cancelled", id);
await HandleProcessingFailureAsync(id, region, startTime, tiles, tilesDownloaded, tilesReused, errorMessage);
}
catch (RateLimitException ex)
{
errorMessage = $"Rate limit exceeded: {ex.Message}. Google Maps API rate limit was reached and retries were exhausted.";
_logger.LogError(ex, "Rate limit exceeded for region {RegionId}", id);
_logger.LogError(ex, "Rate limit exceeded for region {RegionId}. Google is throttling requests. Consider reducing request rate.", id);
await HandleProcessingFailureAsync(id, region, startTime, tiles, tilesDownloaded, tilesReused, errorMessage);
}
catch (HttpRequestException ex)
{
errorMessage = $"Network error: {ex.Message}. Failed to download tiles from Google Maps.";
_logger.LogError(ex, "Network error processing region {RegionId}", id);
errorMessage = $"Network error (HTTP {ex.StatusCode}): {ex.Message}. Failed to download tiles from Google Maps.";
_logger.LogError(ex, "Network error processing region {RegionId}. StatusCode: {StatusCode}, Message: {Message}",
id, ex.StatusCode, ex.Message);
await HandleProcessingFailureAsync(id, region, startTime, tiles, tilesDownloaded, tilesReused, errorMessage);
}
catch (Exception ex)
{
errorMessage = $"Unexpected error: {ex.Message}";
_logger.LogError(ex, "Failed to process region {RegionId}: {Message}", id, ex.Message);
errorMessage = $"Unexpected error ({ex.GetType().Name}): {ex.Message}";
_logger.LogError(ex, "Failed to process region {RegionId}. Type: {ExceptionType}, Message: {Message}",
id, ex.GetType().Name, ex.Message);
await HandleProcessingFailureAsync(id, region, startTime, tiles, tilesDownloaded, tilesReused, errorMessage);
}
}
@@ -1,3 +1,4 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@@ -14,18 +15,21 @@ public class RouteProcessingService : BackgroundService
{
private readonly IRouteRepository _routeRepository;
private readonly IRegionRepository _regionRepository;
private readonly IServiceProvider _serviceProvider;
private readonly StorageConfig _storageConfig;
private readonly ILogger<RouteProcessingService> _logger;
private readonly TimeSpan _checkInterval = TimeSpan.FromSeconds(10);
private readonly TimeSpan _checkInterval = TimeSpan.FromSeconds(5);
public RouteProcessingService(
IRouteRepository routeRepository,
IRegionRepository regionRepository,
IServiceProvider serviceProvider,
IOptions<StorageConfig> storageConfig,
ILogger<RouteProcessingService> logger)
{
_routeRepository = routeRepository;
_regionRepository = regionRepository;
_serviceProvider = serviceProvider;
_storageConfig = storageConfig.Value;
_logger = logger;
}
@@ -62,7 +66,7 @@ public class RouteProcessingService : BackgroundService
try
{
await ProcessRouteIfReadyAsync(route.Id, cancellationToken);
await ProcessRouteSequentiallyAsync(route.Id, cancellationToken);
}
catch (Exception ex)
{
@@ -77,7 +81,7 @@ public class RouteProcessingService : BackgroundService
return routes.Select(r => (r.Id, r.RequestMaps)).ToList();
}
private async Task ProcessRouteIfReadyAsync(Guid routeId, CancellationToken cancellationToken)
private async Task ProcessRouteSequentiallyAsync(Guid routeId, CancellationToken cancellationToken)
{
var route = await _routeRepository.GetByIdAsync(routeId);
if (route == null || !route.RequestMaps || route.MapsReady)
@@ -85,49 +89,84 @@ public class RouteProcessingService : BackgroundService
return;
}
var regionIds = await _routeRepository.GetRegionIdsByRouteAsync(routeId);
if (!regionIds.Any())
var routePointsList = (await _routeRepository.GetRoutePointsAsync(routeId)).ToList();
var regionIdsList = (await _routeRepository.GetRegionIdsByRouteAsync(routeId)).ToList();
if (regionIdsList.Count == 0)
{
_logger.LogWarning("Route {RouteId} has no regions linked", routeId);
using var scope = _serviceProvider.CreateScope();
var regionService = scope.ServiceProvider.GetRequiredService<Common.Interfaces.IRegionService>();
_logger.LogInformation("Route {RouteId}: Starting sequential region processing for {PointCount} points",
routeId, routePointsList.Count);
var firstPoint = routePointsList.First();
var regionId = Guid.NewGuid();
await regionService.RequestRegionAsync(
regionId,
firstPoint.Latitude,
firstPoint.Longitude,
route.RegionSizeMeters,
route.ZoomLevel,
stitchTiles: false);
await _routeRepository.LinkRouteToRegionAsync(routeId, regionId);
_logger.LogInformation("Route {RouteId}: Queued first region {RegionId} (1/{TotalPoints})",
routeId, regionId, routePointsList.Count);
return;
}
var allCompleted = true;
var anyFailed = false;
foreach (var regionId in regionIds)
{
var region = await _regionRepository.GetByIdAsync(regionId);
if (region == null)
{
_logger.LogWarning("Region {RegionId} not found for route {RouteId}", regionId, routeId);
continue;
}
if (region.Status == "failed")
{
anyFailed = true;
}
else if (region.Status != "completed")
{
allCompleted = false;
}
}
if (!allCompleted)
var lastRegion = await _regionRepository.GetByIdAsync(regionIdsList.Last());
if (lastRegion == null)
{
return;
}
if (anyFailed)
if (lastRegion.Status == "queued" || lastRegion.Status == "processing")
{
_logger.LogWarning("Route {RouteId} has failed regions, skipping processing", routeId);
return;
}
_logger.LogInformation("All regions completed for route {RouteId}, starting final processing", routeId);
await GenerateRouteMapsAsync(routeId, route, regionIds, cancellationToken);
if (lastRegion.Status == "failed")
{
_logger.LogError("Route {RouteId}: Region {RegionId} failed. Stopping route processing.",
routeId, lastRegion.Id);
route.MapsReady = false;
route.UpdatedAt = DateTime.UtcNow;
await _routeRepository.UpdateRouteAsync(route);
return;
}
if (regionIdsList.Count < routePointsList.Count)
{
using var scope = _serviceProvider.CreateScope();
var regionService = scope.ServiceProvider.GetRequiredService<Common.Interfaces.IRegionService>();
var nextPoint = routePointsList[regionIdsList.Count];
var regionId = Guid.NewGuid();
await regionService.RequestRegionAsync(
regionId,
nextPoint.Latitude,
nextPoint.Longitude,
route.RegionSizeMeters,
route.ZoomLevel,
stitchTiles: false);
await _routeRepository.LinkRouteToRegionAsync(routeId, regionId);
_logger.LogInformation("Route {RouteId}: Queued next region {RegionId} ({CurrentRegion}/{TotalPoints})",
routeId, regionId, regionIdsList.Count + 1, routePointsList.Count);
return;
}
_logger.LogInformation("Route {RouteId}: All {RegionCount} regions completed, generating final maps",
routeId, regionIdsList.Count);
await GenerateRouteMapsAsync(routeId, route, regionIdsList, cancellationToken);
}
private async Task GenerateRouteMapsAsync(
@@ -353,8 +392,8 @@ public class RouteProcessingService : BackgroundService
if (tileImage.Width != tileSizePixels || tileImage.Height != tileSizePixels)
{
_logger.LogWarning("Tile {FilePath} has wrong size {Width}x{Height}, expected {Expected}x{Expected}",
tile.FilePath, tileImage.Width, tileImage.Height, tileSizePixels);
_logger.LogWarning("Tile {FilePath} has wrong size {Width}x{Height}, expected {ExpectedWidth}x{ExpectedHeight}",
tile.FilePath, tileImage.Width, tileImage.Height, tileSizePixels, tileSizePixels);
}
stitchedImage.Mutate(ctx => ctx.DrawImage(tileImage, new Point(destX, destY), 1f));
+3 -21
View File
@@ -65,7 +65,7 @@ public class RouteService : IRouteService
totalDistance += distanceFromPrevious.Value;
}
var pointType = isStart ? "start" : (isEnd ? "end" : "waypoint");
var pointType = isStart ? "start" : (isEnd ? "end" : "action");
allPoints.Add(new RoutePointDto
{
@@ -147,26 +147,8 @@ public class RouteService : IRouteService
if (request.RequestMaps)
{
_logger.LogInformation("Route {RouteId}: Requesting regions for all {Count} points",
request.Id, allPoints.Count);
foreach (var point in allPoints)
{
var regionId = Guid.NewGuid();
await _regionService.RequestRegionAsync(
regionId,
point.Latitude,
point.Longitude,
request.RegionSizeMeters,
request.ZoomLevel,
stitchTiles: false);
await _routeRepository.LinkRouteToRegionAsync(request.Id, regionId);
}
_logger.LogInformation("Route {RouteId}: {Count} regions requested",
request.Id, allPoints.Count);
_logger.LogInformation("Route {RouteId}: Maps requested. Regions will be processed sequentially by background service.",
request.Id);
}
_logger.LogInformation("Route {RouteId} created successfully", request.Id);