Rate-limit retries for unloaded map tiles - #114
Merged
pfeiferj merged 2 commits intoAug 9, 2026
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Offline.Loadedas the tile-load result instead of using a non-empty way list as a proxy.Motivation
The main loop currently reloads the offline tile whenever
len(state.Data.Ways()) == 0. That condition does not distinguish a failed load from a valid tile containing no roads.As a result, both states are retried on every successful GPS update:
Offline.Loadedalready owns that distinction. A successfully decoded empty tile hasLoaded=true; a missing tile or packed-unmarshal failure hasLoaded=false.Retry policy
The retry state remains local to the main loop and uses elapsed time between attempts:
There is no retry limit or increasing backoff. A one-second fixed interval preserves the nominal retry cadence of the 1 Hz internal GPS source while bounding retries from the 10 Hz external source and the 20 Hz main-loop ceiling.
When an unloaded tile is waiting for its next retry, the loop does not run current-way, next-way, or curvature processing against that unloaded data.
Attempt reduction
The deterministic policy oracle counted attempts over ten seconds, including the initial attempt at
t=0:The 10 Hz and 1 Hz rates come from the matching FrogPilot service table. Actual attempts remain gated by successful GPS updates.
Loader cost context
An external Linux probe called the real missing-tile loader 20 times from a fresh directory:
68813e0597597b8This change owns the attempt-frequency correction only. The large base allocation is the separate area-grid defect addressed by PR #97; it is not counted as an allocation improvement from this patch. PR #112 makes intentionally omitted empty cells more common, so retaining a bounded missing-tile retry remains useful after that allocation fix.
Validation
External overlay validation exercised the production retry state and tile loader:
deadline - 1 nsdoes not retry; the exact one-second deadline does.t=0throught=1s.Loadedstates throughFindWaysAroundPosition.go test ./...,go vet ./..., andgo build ./...passed on Linux/amd64 with Go 1.25.1.make buildsuccessfully for exact commit4ed9b1a.Compatibility
Audit follow-up (head
84cf709)An independent multi-agent audit of
4ed9b1afound a regression in the bundledif !state.Data.Loaded { continue }, which this head removes.A tile file that exists but fails to decode makes
FindWaysAroundPositionreturnLoaded=falsewith a nil error. On base that nil error let control reachGetCurrentWay, which exhausts its fallbacks and returnsSelectionType: fail, resetting the current way somapdOutpublishes wayId 0 and speedLimit 0. The earlycontinuemade that reset unreachable, soCurrentWay,NextWays,Curvatures, andTargetVelocitiesretained values from the last good tile indefinitely — mapd kept publishing the previous cell's limit withwaySelectionType=possiblewhileSuggestedSpeed()clamped cruise to it. Measured with the real loader: after crossing into a corrupt neighbour, base reportedid=0 selType=failwhile the PR reportedid=1 name="Stale Street" maxspeed=11.176 selType=possible, never reassigned.This is reachable because tile extraction on base writes tar entries straight to the final path with no temp file, no atomic rename, and no
O_TRUNC, so an interrupted download leaves exactly this corrupt-but-present tile. The missing-tile case was never affected — both branchescontinueon the non-nil error — which is what made the divergence easy to miss. Rate limiting never depended on the skip: the gate is the retry condition, so removing it leaves every attempt-frequency claim above intact.The same head also collapses the single-use
mapLoadRetryStatestruct and its two methods into a local timestamp plus the inline condition, cutting the change from +24/-2 to roughly +7/-2. The retry gate was proven logically identical to the replaced version across the full matrix of loaded/inside/elapsed combinations, including the zero-value first-attempt case. FrogAi Build #53 passed for exact head84cf709.Merge order with #106. These two conflict in
main.go, but only in the both-added declaration block near the top, where the union is the only resolution that compiles. The semantically important hunk auto-merges silently about 115 lines below, in a different function, where a maintainer reviewing the conflict would never look. A trial merge confirms the retry bookkeeping lands above #106's early exits, so the backoff survives; the remaining concern is two stacked "map unusable" exits with different bookkeeping. Land #106 first and fold this PR's gate into #106'sstate.MapValid = state.Data.Loaded; if !state.MapValid { state.ClearRoute(); continue }rather than leaving both in place.