Cache the file share path list instead of re-reading it per request - #419
Open
krokicki wants to merge 1 commit into
Open
Cache the file share path list instead of re-reading it per request#419krokicki wants to merge 1 commit into
krokicki wants to merge 1 commit into
Conversation
Every proxied data request resolves its share through get_file_share_paths (once per chunk a viewer pulls), and every file action dispatched to a worker serializes the whole list into the request. Both were paying a full query plus a rebuild of every FileSharePath model, on the hottest paths in the app. Nothing in Fileglancer writes the file_share_paths table -- it is populated externally, and last_refresh has no writer -- so there is no event to invalidate on. A 60s TTL is the honest option: bounded staleness, no invalidation protocol. Two caches, both keyed by database URL because a process can talk to more than one database and an unkeyed cache would serve one database's shares for another's: the model list in database.py, and the JSON-serialized rows in worker_pool, since dumping a few hundred models costs more than the query does once the query is cached. Measured per call (sqlite, median of 5 batches), 100 shares: full list 1050us -> 2.6us by name (proxy) 184us -> 7.2us worker rows 1487us -> 1.7us At 500 shares the full list goes 5291us -> 3.8us. With 1ms of injected per-query latency, modelling a non-local database, by-name goes 1253us -> 7.2us. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
Follow-up to #403, which noted that the file share path list is still fetched and serialized on every file action. It turns out the worker dispatch path isn't even the hottest caller.
Where the cost actually was
server.py:428_resolve_proxy_info->get_file_share_pathworker_pool.py:81_fetch_fsp_rowsserver.py:830GET /api/file-share-pathsapps/command.py:393,database.py:724Each of those was doing a full
SELECTplus a rebuild of everyFileSharePathmodel, and the worker path then dumped all of them to JSON. So the fix belongs indatabase.get_file_share_paths, not at the call site #403 happened to touch — caching there would have left the per-chunk path untouched.Also worth knowing:
get_fsp_names_to_mount_pathsandfind_fsp_by_pathare both dead (zero callers), so they're not in the list above. The first is already flagged innotes/ponytail-audit.md.Why a TTL and not invalidation
file_share_pathsis populated out-of-band by a separate process, not by anything running in the server — the only reference toFileSharePathDBin this repo is the read query atdatabase.py:415. So no request handler is ever in a position to know the list changed, and there is no in-process event to invalidate on.That leaves polling something cheaper than the full list (the
last_refreshrow exists for roughly this purpose) versus plain bounded staleness. The arithmetic says don't bother: the cache converts a per-request cost into a per-TTL cost, so at 60s the rebuild is a 0.008% duty cycle — one 5ms rebuild per minute per process, against thousands of requests a second on the proxy path during a viewer session. Precise invalidation would add code to eliminate a query that no longer happens in any meaningful quantity.The TTL is also not the binding constraint on how fast a new share appears: the out-of-band updater has its own schedule, and it is the larger term. Tightening the TTL is a one-constant change if that ever stops being true.
Two caches, both keyed by database URL:
database._fsp_cache— theFileSharePathmodel list, so every caller above benefitsworker_pool._fsp_row_cache— the JSON-serialized rows, because once the query is cached themodel_dumpper row is what dominates the worker path, and it lets_fetch_fsp_rowsskip creating a session at all on a hitThe keying matters: a process can talk to more than one database (the test suite builds a fresh one per module), and an unkeyed cache serves one database's shares for another's. There's a regression test for exactly that.
Benchmark
Method: N shares in a sqlite DB, median per-call time over 5 batches of 500 calls, warmed first.
db+1msinjects 1ms per query via abefore_cursor_executelistener to model a database that isn't on localhost. Every scenario asserts the returned list is the right length and contents first, so a cache that returned the wrong thing couldn't look fast. Script is not committed; it's a throwaway.100 shares:
500 shares:
Two honest caveats on those numbers. sqlite on the same filesystem is the best case for the old code, so the local figures understate what a non-local database sees — that's what the
db+1msrow is for. And the by-name lookup now scans the cached list in Python where it used to push aWHEREinto SQL, so its win shrinks as the share count grows: 26x at 100 shares, 7x at 500, and it would only lose to the query somewhere north of ~3500 shares. A name-keyed dict would fix that if we ever get there; not worth it now.Behaviour change
An operator adding a share waits up to 60s for it to appear, and
GET /api/file-share-pathscan be up to 60s stale.tests/conftest.pyclears both caches between tests so nothing races the TTL.Validation
pixi run -e test test-backend— 770 passed (768 on main, plus the two new tests)test_file_share_path_cache_is_keyed_by_database, and removing the cache fails onlytest_file_share_paths_are_served_from_cache@StephanPreibisch @JaneliaSciComp/fileglancer