rest server: serialize access to the shared backend - #220
Merged
ThomasWaldmann merged 1 commit intoSep 6, 2026
Merged
Conversation
BorgStoreRESTServer is a ThreadingHTTPServer: it handles each connection in its own thread, and every handler operates on the one backend instance shared by all of them (self.server.backend). Each handler does `with self.server.backend:`, i.e. open() on enter and close() on exit, plus one backend operation. With no serialization, concurrent requests to the same server raced: - on the backend's `opened` flag: a second request entering `with backend:` while another still had it open hit `BackendMustNotBeOpen` (HTTP 500), and a close() from one request could tear down the backend under another; - with a quota, on the in-memory usage counter (`_quota_use += delta` is a non-atomic read-modify-write and the limit check reads a value another thread is changing), so usage could drift and the limit be overshot. This only affected the standalone TCP server (e.g. behind nginx) with concurrent connections to the same repository. The stdio-over-ssh server handles requests serially (one process per ssh connection), so it was never affected. Fix: a single per-server lock (self.backend_lock) held around every backend access in the handler - the `with self.server.backend:` operation blocks as well as the create/destroy calls that do not open the backend. This serializes all backend use, mirroring the client-side Store lock. The stdio server gets the lock too (uncontended there, but keeps the handler code uniform). The regression test starts the threaded server with a backend whose store() is slowed (to widen the open()..close() window like real I/O latency) and fires several concurrent requests: without the lock they fail with BackendMustNotBeOpen and the quota drifts; with it all succeed and the quota usage is exact. Co-Authored-By: Claude Opus 4.8 <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.
The standalone REST server (
BorgStoreRESTServer) is aThreadingHTTPServer: it handles each connection in its own thread, and every handler operates on the one backend instance shared by all of them (self.server.backend), with no serialization. Each handler doeswith self.server.backend:(open on enter, close on exit) plus one backend operation.So concurrent requests to the same server raced:
openedflag: a second request enteringwith backend:while another still had it open hitBackendMustNotBeOpen(HTTP 500), and aclose()from one request could tear the backend down under another;_quota_use += deltais a non-atomic read-modify-write and the limit check reads a value another thread is mutating — so usage could drift and the limit be overshot.This only affected the standalone TCP server (e.g. behind nginx) with concurrent connections to the same repository. The stdio-over-ssh server handles requests serially (one process per ssh connection), so it was never affected.
Fix: a single per-server lock (
self.backend_lock) held around every backend access in the handler — thewith self.server.backend:operation blocks as well as thecreate/destroycalls that do not open the backend. This serializes all backend use, mirroring the client-sideStorelock (#206). The stdio server gets the lock too (uncontended there, but keeps the handler code uniform).The lock covers only the backend operation, not the request/response body transfer, so uploads and downloads on other connections still proceed in parallel; only the backend op itself is serialized. A non-threaded server was considered but would serialize the network I/O too, and with HTTP/1.1 keep-alive a single idle connection would block the whole server.
The regression test starts the threaded server with a backend whose
store()is slowed (to widen the open()..close() window like real I/O latency) and fires several concurrent requests: without the lock they fail withBackendMustNotBeOpenand the quota drifts; with it all succeed and the quota usage is exact. It fails onmain(3/3 runs) and passes with this change (3/3).🤖 Generated with Claude Code