Skip to content

go: sqle: Fix concurrent-clone deadlock between two remotesapi sql-servers - #11281

Merged
reltuk merged 1 commit into
dolthub:mainfrom
sona-is:fix/concurrent-clone-remotesapi-deadlock
Jul 9, 2026
Merged

go: sqle: Fix concurrent-clone deadlock between two remotesapi sql-servers#11281
reltuk merged 1 commit into
dolthub:mainfrom
sona-is:fix/concurrent-clone-remotesapi-deadlock

Conversation

@mccraigmccraig

Copy link
Copy Markdown
Contributor

DoltDatabaseProvider.CloneDatabaseFromRemote held the provider RWMutex write lock across the entire clone, including the network fetch of all chunks from the remote. When a server serves a --remotesapi-port, that fetch hits a peer's remotesapi whose handler needs a read lock on this same provider (via SessionDatabase). Two sql-servers that CALL DOLT_CLONE from each other's remotesapi at the same time therefore deadlock: each holds its own provider write lock while blocked on the peer, and neither peer can take the read lock needed to answer. DOLT_PULL is immune because it operates on an already-registered database and does not hold the write lock across its fetch. This is the lock the existing // TODO: This holds the database provider lock across the entire duration of the clone ... was warning about.

The fix narrows the lock scope so the network fetch does not run under the provider write lock:

  • Reserve the database name under a brief lock acquisition (a new creatingDatabases set), release the lock for the fetch, then re-acquire it only to register the finished database (registerNewDatabase still requires the lock).
  • The reservation keeps concurrent CREATE DATABASE / dolt_clone / undrop of the same name mutually exclusive, so those races behave as before even though the lock is dropped for the fetch.
  • A shared databaseNameUnavailableLocked helper performs the availability checks for all creation paths and preserves the interrupted-create in-progress marker behaviour (ErrIncompleteDatabaseDir / dbfactory.MarkDatabaseInProgress). The in-memory reservation and the on-disk marker are complementary: the marker guards crash recovery across processes, while creatingDatabases serialises concurrent in-process operations while the lock is dropped.
  • Names in creatingDatabases and deletingDatabases are normalised with formatDbMapKeyName (Dolt database names are case-insensitive), matching how p.databases is keyed. Unlike deletingDatabases, creatingDatabases deliberately does not gate database enumeration: an in-progress clone is simply invisible until it registers, so enumeration / remotesapi reads never block on it.

Tests:

  • integration-tests/bats/sql-server-remotesrv.bats: a regression test that starts two --remotesapi-port servers cloning from each other concurrently. It hangs (clones time out) on the buggy build and passes on the fixed build; it also fails if a clone errors for any non-deadlock reason.
  • go/libraries/doltcore/sqle/database_provider_test.go: TestCreatingDatabaseReservation covering the reservation semantics — conflict, case-insensitivity across create/clone/undrop, release, and that a reservation does not block AllDatabases.

Verified with go test -race ./libraries/doltcore/sqle/ and the new bats test; gofmt / goimports clean.

Fixes #11280

CloneDatabaseFromRemote held the database-provider write lock across the entire
clone, including the network fetch of chunks from the remote. When a server
serves a --remotesapi-port, that fetch hits a peer's remotesapi whose handler
needs a read lock on this same provider (via SessionDatabase). Two sql-servers
that CALL DOLT_CLONE from each other's remotesapi at the same time therefore
deadlock: each holds its own provider write lock while blocked on the peer, and
neither peer can take the read lock needed to answer. DOLT_PULL is immune
because it operates on an already-registered database and does not hold the
write lock across its fetch.

Narrow the lock scope so the fetch does not run under the provider write lock:
reserve the database name under a brief lock acquisition (a new creatingDatabases
set), release the lock for the network fetch, then re-acquire it only to register
the finished database. The reservation keeps concurrent CREATE DATABASE /
dolt_clone / undrop of the same name mutually exclusive. A shared
databaseNameUnavailableLocked helper performs the availability checks for all
creation paths and preserves the interrupted-create marker behavior
(ErrIncompleteDatabaseDir). Names in creatingDatabases and deletingDatabases are
normalized with formatDbMapKeyName (case-insensitive), matching p.databases.
Unlike deletingDatabases, creatingDatabases deliberately does not gate database
enumeration: an in-progress clone is simply invisible until it registers.

Add a bats regression test (two remotesapi servers cross-cloning concurrently)
and Go unit tests for the reservation semantics.
@reltuk
reltuk merged commit 0dd48c9 into dolthub:main Jul 9, 2026
33 of 41 checks passed
@reltuk

reltuk commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

This is awesome. Thank you for the bug report and thank you for the contribution. I will make a secondary pass on comments and maybe where the testing lands, but in general this approach seems totally reasonable and I had no blocking feedback about the implementation itself.

@reltuk

reltuk commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

If you're curious, follow up PR is here: #11281. It just slightly reworks some comments and function names and moves the test around.

@mccraigmccraig

Copy link
Copy Markdown
Contributor Author

excellent news! I'm using dolt in a local first single master, many follows mesh pattern, and this was quite an issue... now I can get back on official releases 😁

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Deadlock when two sql-servers CALL DOLT_CLONE from each other's --remotesapi-port concurrently

3 participants