perf(db): reads no longer wait behind writes; pages answer from cache
A series page took about a second to show its seasons on a phone, every visit, although they were cached. Three things stacked up: - One SQLite connection behind one mutex served the whole app, so every read queued behind every write. The database now has one owner: a writer thread for writes and a pool of read-only WAL connections for reads. synchronous = NORMAL and a busy timeout on every connection. - The listing query built the set of every available item in the database before filtering to the parent (~80 ms on a desktop for a 100k-item cache), then fetched user data one row at a time. It now checks availability per row, uses the hierarchy indexes (1.5 ms on the same benchmark) and batches the user-data lookup. - A cache read that missed the 100 ms fast path was set aside until the server answered. It is now raced against the server; whichever answers first with content wins. On the Fairphone, Frasier's season and episode lists now come from cache in 34-133 ms (was 600-1030 ms waiting on the server). Fixes found on the way, each with a test that failed first: - sync_queue_mutation could return another mutation's row id: the id came from a second trip to the shared connection. insert() reads it in the same job. - save_to_cache switched foreign keys off on the shared connection across its awaits, so concurrent writes ran unchecked. The toggle now lives inside one writer job, and a page is one transaction instead of one commit per row. Also: thumbnail LRU touches no longer block the lookup; unused tokio-rusqlite dropped. Design and invariants in docs/architecture/08-database-design.md (Connection ownership, Listing query shape) and 03-data-flow.md.
This commit is contained in:
@@ -28,12 +28,17 @@ sequenceDiagram
|
||||
Server->>Conn: mark_unreachable() (debounced)
|
||||
end
|
||||
|
||||
alt Cache returns with content
|
||||
alt Cache answers first with content (inside 100ms, or later but before the server)
|
||||
Cache-->>Hybrid: Result with items
|
||||
Hybrid-->>Rust: Return cache result
|
||||
else Cache timeout or empty
|
||||
Server-->>Hybrid: Fresh result (later)
|
||||
Hybrid->>Cache: save_to_cache() in background
|
||||
else Server answers first, or cache is empty
|
||||
Server-->>Hybrid: Fresh result
|
||||
Hybrid-->>Rust: Return server result
|
||||
else Server fails
|
||||
Cache-->>Hybrid: Whatever the cache has (waited for)
|
||||
Hybrid-->>Rust: Return cache result, else the server error
|
||||
end
|
||||
|
||||
Rust-->>Client: SearchResult
|
||||
@@ -42,11 +47,21 @@ sequenceDiagram
|
||||
```
|
||||
|
||||
**Key Points:**
|
||||
- Cache queries have 100ms timeout for responsiveness
|
||||
- Server queries always run for fresh data
|
||||
- Cache wins if it has meaningful content
|
||||
- Automatic fallback to server if cache is empty/stale
|
||||
- Background cache updates (planned)
|
||||
- Both legs start together. A cache answer with content inside 100 ms
|
||||
(`CACHE_FAST_PATH`) returns at once.
|
||||
- **The deadline does not decide the race.** A cache read still running at
|
||||
100 ms is raced against the server (`HybridRepository::race_slow_cache`), and
|
||||
whichever answers first *with content* wins. It used to be that a read past
|
||||
the deadline was only consulted if the server failed, so a page whose cache
|
||||
read took 150 ms always paid the full server round trip — about a second on a
|
||||
phone, on every visit.
|
||||
- An empty or failed cache answer is not a win; the server decides. A failed
|
||||
server falls back to whatever the cache said, waiting for it if necessary.
|
||||
- On a cache win the server's page is still cached in the background when it
|
||||
arrives, so per-user state (positions, favourites) keeps up.
|
||||
- A `get_items` leg that takes 250 ms or more is logged at INFO with its row
|
||||
count, so a slow page can be attributed to the cache or the server from a
|
||||
device log alone.
|
||||
- **Connectivity side-effect**: each server request feeds the `ConnectivityMonitor`, which is the source of truth for the offline/online banner (see [07-connectivity.md](07-connectivity.md)). A server-answered error (401/404/5xx) still counts as *reachable* — only network failures, sustained past a debounce window, flip the app to offline.
|
||||
|
||||
### Listing order is decided in Rust
|
||||
|
||||
Reference in New Issue
Block a user