Skip to content

Fix cold-cache tile loading: 503 + client-side retry for budget exhaustion#208

Merged
stef-k merged 2 commits intomainfrom
feature/cold-cache-tile-loading-206
Mar 26, 2026
Merged

Fix cold-cache tile loading: 503 + client-side retry for budget exhaustion#208
stef-k merged 2 commits intomainfrom
feature/cold-cache-tile-loading-206

Conversation

@stef-k
Copy link
Copy Markdown
Owner

@stef-k stef-k commented Mar 26, 2026

Summary

  • Fix cold-cache tile loading: budget-exhausted tiles now return HTTP 503 + Retry-After instead of 404, so the client can retry instead of showing permanent gray areas
  • Add RetryTileLayer (fetch-based Leaflet TileLayer subclass) that detects 503, reads Retry-After, and retries with exponential backoff -- tiles progressively stream in
  • Centralize tile layer creation via createTileLayer() factory, replacing duplicated boilerplate across 13 JS files
  • Add RequestIdLoggingMiddleware + Serilog FromLogContext enrichment -- every log line now includes RequestId for cross-request correlation
  • Fix ghost metadata: prevent DB rows with Size=0 when tile fetch is aborted by budget exhaustion
  • Extract DbMetadataZoomThreshold constant replacing magic number 9 (12 occurrences)

Closes #206

Test plan

  • dotnet build passes (0 errors)
  • dotnet test passes (1404 tests)
  • Cold-cache test: clear tile cache, load map at zoom 18 -- tiles should progressively fill in over ~10s instead of showing persistent gray areas
  • Verify server logs show 503 (not 404) for budget-exhausted tiles, and each log line includes RequestId in {Properties:j}
  • Verify SELECT * FROM "TileCacheMetadata" WHERE "Size" = 0 returns zero rows after cold-cache load
  • Code review via code-reviewer subagent

…ient-side retry

After clearing the tile cache, map views showed persistent gray areas because
budget-exhausted tile fetches returned 404 (permanent) instead of signaling a
transient failure. Leaflet never retried, so tiles stayed gray until manual refresh.

Changes:
- Extract DbMetadataZoomThreshold constant (replaces magic number 9 in 9 locations)
- Guard metadata insert with tileData != null to prevent ghost DB rows on budget exhaustion
- Add RequestIdLoggingMiddleware + Serilog .Enrich.FromLogContext() for per-request log correlation
- Introduce TileRetrievalResult to distinguish success/not-found/throttled states
- Return HTTP 503 + Retry-After header when outbound budget is exhausted
- Add RetryTileLayer (fetch-based L.TileLayer subclass) that retries on 503 with backoff
- Centralize tile layer creation via createTileLayer() factory, replacing duplicated
  boilerplate across 13 JS files
- Add inline tileerror retry fallback for 2 cshtml views with inline scripts

Closes #206
@stef-k stef-k force-pushed the feature/cold-cache-tile-loading-206 branch from 936ffbf to 75190b8 Compare March 26, 2026 16:32
- Add ±25% jitter to client-side retry backoff to prevent thundering herd
- Clamp Retry-After floor to baseDelay, reject zero/negative values
- Add explicit early return in CacheTileAsync for upstream HTTP failures
- Pass cancellationToken to ReadAsByteArrayAsync and Task.Delay
- Extract BudgetRetryAfterSeconds constant with doc linking to budget config
- Replace tileData?.Length with tileData.Length (guaranteed non-null after guards)
- Guard blob URL creation with signal.aborted check to prevent memory leak
- Add [Collection("OutboundBudget")] to prevent parallel test interference
- Add controller test for 503 + Retry-After on budget exhaustion
@stef-k stef-k merged commit 33be9b4 into main Mar 26, 2026
1 check passed
@stef-k stef-k deleted the feature/cold-cache-tile-loading-206 branch March 26, 2026 17:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cold-cache tile loading returns gray areas instead of progressively filling tiles

1 participant