Skip to content

rest server: serialize access to the shared backend - #220

Merged
ThomasWaldmann merged 1 commit into
borgbackup:mainfrom
ThomasWaldmann:rest-server-serialize-backend
Sep 6, 2026
Merged

rest server: serialize access to the shared backend#220
ThomasWaldmann merged 1 commit into
borgbackup:mainfrom
ThomasWaldmann:rest-server-serialize-backend

Conversation

@ThomasWaldmann

Copy link
Copy Markdown
Member

The standalone REST server (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), with no serialization. Each handler does with self.server.backend: (open on enter, close on exit) plus one backend operation.

So 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 the backend down 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 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 — 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 (#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 with BackendMustNotBeOpen and the quota drifts; with it all succeed and the quota usage is exact. It fails on main (3/3 runs) and passes with this change (3/3).

🤖 Generated with Claude Code

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>
@ThomasWaldmann
ThomasWaldmann merged commit 02f3fd9 into borgbackup:main Sep 6, 2026
9 checks passed
@ThomasWaldmann
ThomasWaldmann deleted the rest-server-serialize-backend branch September 6, 2026 22:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant