Conversation
…olling EfRemotePathMappingRepository injected a scoped ListenArrDbContext directly and shared it across all reads in a DI scope. During download-client polling, DownloadClientGateway fans out (Task.WhenAll over every queue item) and each item calls RemotePathMappingService.TranslatePathAsync several times, so many GetByClientIdAsync reads run concurrently on that one context (and its single SQLite connection). EF Core contexts/SQLite connections are not thread-safe: a concurrent read tears down another's active statement, the pooled connection is poisoned, and every later query -- including library search -- fails until the process is restarted. Convert the repository to the per-operation IDbContextFactory pattern already used across the codebase (e.g. EfDownloadProcessingJobRepository), so each read/write gets its own short-lived context and connection. No behavior change; the download fan-out is now safe as designed. The existing suite never caught this because every persistence/concurrency test uses ;Pooling=False, while production leaves pooling on. Adds a concurrent-read regression test to RemotePathMappingServiceTests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Your diagnosis matches something I filed in August from the other end of the same call path, and I #871 goes at the call site. Yours makes the repository safe when they do happen. Different guarantees, and I do not think With only #964, the tearing stops but a poll still issues one query per queue item. Query volume was With only #871, the gateway stops fanning out, but the repository is still one scoped context behind So I read them as complementary. I would rather hear whether you agree than assume it. One practical thing. We both append a new test at the end of the class in There is also an ordering question I would rather not answer by myself. If #964 lands first and I have not run your branch against mine. The conflict above is read off the two diffs, not observed Disclosure: drafted with Claude Code at my direction; I read the cited code at the stated commit and |
|
Agreed on all counts — complementary, not competing, and your framing is exactly how I'd put it. #964 makes the repository safe for any concurrent caller (a primitive-level guarantee that doesn't depend on how the call site behaves). #871 removes the fan-out and resolves per batch so those concurrent reads mostly stop happening, and — from the file list — caches them too. Neither subsumes the other: with only #964 the tearing stops but the per-item query volume you were chasing remains; with only #871 the gateway stops fanning out, but the repository is still one scoped context behind the next concurrent caller that reaches it. Worth noting: I actually considered adding service-level caching to #964 and deliberately left it out to keep this a minimal repository-safety change. I didn't know #871 existed at the time, but it's turned out for the best — the caching/batch layer clearly belongs in your PR, and if I'd added it here we'd have collided on exactly that. On ordering: I think #964 reads naturally as landing first, precisely because it's the caller-independent guarantee — once the repository is safe per-operation, #871 becomes a clean query-volume optimization rather than the thing standing between the app and a crash, which is the more honest framing for it anyway. But I hold that loosely: if #871 goes first, #964 still stands as defense-in-depth for any other concurrent caller, and I'm glad to rebase either way. Since you raised the ordering fairly, I'd rather the two of us (or the maintainer) agree on it than have me pick the order that flatters my own PR. On the test collision in Disclosure: drafted with Claude Code at my direction; I reviewed it before posting. |
|
Thanks for not taking the ordering that suited you. #964 first. I agree with your reasoning, and I think it holds regardless of whose PR it is, because yours is the only one of the two that is safe for a caller nobody has written yet. Your read of the file list is right, by the way: #871 does cache as well as batch. So I will take the rewording. Once #964 is in, #871 stops being the thing standing between the app and a crash, and its description should talk about query volume and cache lifetime instead. I would rather fix that myself than leave a reviewer reading a PR that still argues for a crash fix which has already landed. On the test collision, I can upgrade what I said last time. Previously I had read it off the two diffs. I have now run it, with
Since #964 goes first, #871 is the one that rebases, so I will take that resolution too. You offered, but it is merge work that only exists because of an order I have just agreed to, and it is a couple of minutes on my side. Disclosure: drafted with Claude Code at my direction; I read the cited code at the stated commit and reviewed this before posting. |
Fixes #963.
EfRemotePathMappingRepositoryinjected a scopedListenArrDbContextdirectly and shared it across all reads in a scope. During download polling,DownloadClientGatewayfans outTask.WhenAllover every queue item, each callingRemotePathMappingService.TranslatePathAsyncseveral times, so manyGetByClientIdAsyncreads run concurrently on that one context / SQLite connection. That tears down an active statement mid-query, poisons the pooled connection, and every later query — including library search — fails until restart.Change
Convert the repository to the per-operation
IDbContextFactory<ListenArrDbContext>pattern already used across the codebase (e.g.EfDownloadProcessingJobRepository). Each read/write gets its own short-lived context/connection, so the existing download fan-out is safe as designed. No behavior change.Why the suite missed it
Every persistence/concurrency test uses
;Pooling=False; production leaves pooling on. Added a concurrent-read regression test toRemotePathMappingServiceTests(64 parallelGetPathMappingByClientAsyncfor one client), which fails against a shared context and passes with the factory.Tests
RemotePathMappingsuite (21) +DownloadClientGateway/DownloadQueue(49) green; full build clean.