FB-11: serialize FileTileCache mutations and log tile fetch failures
Fixes a real race in FileTileCache where two overlapping put() calls (the persistent background map and a freshly-opened Route Planner map both fetching tiles at once) could interleave at the _saveManifest await point and silently lose a tile from the on-disk manifest. All mutating and reading operations now go through a single serialization queue. Also logs the URL and cause of tile fetch failures in CachedTileProvider before rethrowing, and adds a concurrency regression test. On-device verification was not performed (no adb/emulator access in this environment); see the ticket's Outcome section.
This commit is contained in:
@@ -90,6 +90,45 @@ void main() {
|
||||
expect(await reopened.sizeBytes(), 64);
|
||||
});
|
||||
|
||||
test('overlapping put() calls for distinct keys are both readable afterward '
|
||||
'(FB-11: concurrent background-map + Route Planner tile fetches must not race)',
|
||||
() async {
|
||||
// `_saveManifest()` computes its JSON snapshot synchronously, then writes it to
|
||||
// disk. Two overlapping `put()` calls can interleave so that the call that
|
||||
// captured the *older*, smaller snapshot (fewer entries) is also the one whose
|
||||
// disk write finishes last -- silently overwriting the newer, complete manifest
|
||||
// with a stale one that is missing the other call's tile. On a real device this
|
||||
// depends on incidental I/O timing (which is exactly why it was so hard to catch
|
||||
// and produced a rider-visible blank map only sometimes); this artificial delay
|
||||
// makes that interleaving happen every single time instead of by chance, so the
|
||||
// test is deterministic rather than flaky. It has no effect on production
|
||||
// callers, which never pass it.
|
||||
cache = FileTileCache(
|
||||
directory: tempDir,
|
||||
maxBytes: 1024 * 1024,
|
||||
debugArtificialManifestWriteDelay: (entryCount) =>
|
||||
entryCount < 2 ? const Duration(milliseconds: 50) : Duration.zero,
|
||||
);
|
||||
const a = TileKey(9, 1, 0);
|
||||
const b = TileKey(9, 2, 0);
|
||||
|
||||
// Started without awaiting the first before starting the second, so both calls
|
||||
// are in flight and racing across the same `await` points (`_ensureLoaded`,
|
||||
// `writeAsBytes`, `_saveManifest`) at once.
|
||||
final futureA = cache.put(a, bytesOfSize(1024));
|
||||
final futureB = cache.put(b, bytesOfSize(1024));
|
||||
await Future.wait([futureA, futureB]);
|
||||
|
||||
// Reopen over the same directory: this reads the manifest back from disk, which
|
||||
// is exactly the file the two overlapping writes above raced to overwrite. An
|
||||
// in-memory-only check wouldn't catch this -- the shared `_manifest` map itself
|
||||
// is never corrupted (Dart is single-threaded), only what ends up on disk.
|
||||
final reopened = FileTileCache(directory: tempDir, maxBytes: 1024 * 1024);
|
||||
expect(await reopened.get(a), isNotNull, reason: 'tile a must survive the race');
|
||||
expect(await reopened.get(b), isNotNull, reason: 'tile b must survive the race');
|
||||
expect(await reopened.sizeBytes(), 2048);
|
||||
});
|
||||
|
||||
test('a tile cached under one provider directory is not served from another '
|
||||
'(UI-09)', () async {
|
||||
// `TileKey` carries no provider identity -- (z, x, y) alone can't tell an OSM tan
|
||||
|
||||
Reference in New Issue
Block a user