Skip to content

Fix multi-process race in Wrapper.Start (LOCALDB_ERROR_INSTANCE_DOES_NOT_EXIST)#929

Merged
SimonCropp merged 6 commits intoSimonCropp:mainfrom
BrandtVavasour:main
May 6, 2026
Merged

Fix multi-process race in Wrapper.Start (LOCALDB_ERROR_INSTANCE_DOES_NOT_EXIST)#929
SimonCropp merged 6 commits intoSimonCropp:mainfrom
BrandtVavasour:main

Conversation

@BrandtVavasour
Copy link
Copy Markdown
Contributor

Summary

Wrapper.Start is unsafe under concurrent calls — both within one process and across processes — when template.mdf is missing. Symptoms include:

  • 0x89C50107 (LOCALDB_ERROR_INSTANCE_DOES_NOT_EXIST)
  • SQL deadlocks during CREATE DATABASE [template]
  • template.mdf "file not found" errors

Real-world trigger: two dotnet test hosts (e.g. CLI + Rider) sharing a LocalDB user instance. Once the wrapper directory is empty, every Start hits the destructive StopAndDelete + CleanStart branch and races every other process doing the same.

Fix

Serialize Wrapper.Start per instance name with two layers, both held until startupTask completes:

  • In-process: static ConcurrentDictionary<string, SemaphoreSlim> keyed by instance name. (The old per-instance semaphoreSlim field was declared but never used.)
  • Cross-process: a FileStream opened with FileShare.None on %TEMP%\localdb_wrapper_<instanceName>.lock. Used in preference to Mutex because file handles aren't thread-affine, so the lock survives await.

Tests added

  • ConcurrentStartTests — single-process race (deterministic SQL deadlock without the fix).
  • MultiProcessConcurrentStartTests — symmetric multi-process race using a new LocalDb.MultiProcessHelper exe spawned via Process.Start.
  • InstanceDoesNotExistRaceTests — asymmetric killer/victim child processes that deterministically reproduce the exact 0x89C50107 error code.

All three race tests fail without the fix and pass with it. The 23 existing WrapperTests still pass.

See src/LocalDb.MultiProcessHelper/README.md for the full root-cause writeup and per-piece rationale.

Brandt and others added 6 commits May 5, 2026 08:11
Implement serialization of the `Wrapper.Start` method for LocalDB
instances using both in-process and cross-process locks. For in-process
synchronization, employ a `ConcurrentDictionary` to manage
`SemaphoreSlim` instances keyed by instance name, ensuring that two
`Wrapper` instances within the same process don't race on the same
LocalDB instance. For cross-process synchronization, use a lock file
in `%TEMP%` with `FileShare.None`, preventing other processes from
starting until the current instance has completed its startup task.

This fix addresses the concurrency issues demonstrated by
`ConcurrentStartTests` in single and multi-process scenarios. Such
synchronization ensures no template rebuilds occur unexpectedly,
avoiding potential races during internal state setup. The
`WrapperTests` suite remains passing, confirming that existing
functionality is preserved.

These changes are motivated by the need to prevent the awkward
interactions that can arise when multiple processes or threads attempt
to interact with the same LocalDB instance simultaneously, posing
risks of database corruption or test flakiness.
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.

2 participants