Skip to content

Automatic instance cleanup#1007

Merged
SimonCropp merged 6 commits into
mainfrom
automatic-instance-cleanup
Jul 21, 2026
Merged

Automatic instance cleanup#1007
SimonCropp merged 6 commits into
mainfrom
automatic-instance-cleanup

Conversation

@SimonCropp

Copy link
Copy Markdown
Owner

No description provided.

The per run cleanup only sees instances that still have a data directory
under the data root. Once that directory is gone, cleared with the temp
directory or by the cleanup itself, nothing points at the instance and the
system databases, logs and traces LocalDB keeps for it are never reclaimed.
On top of that, deleting an instance leaves its logs and event files behind
as an abandoned directory. Both accumulate indefinitely.

The directory LocalDB keeps for each instance is now swept at startup:

 - A directory with no registered instance behind it is residue from an
   already deleted instance and is removed.
 - A registered instance with no data directory, and not running, is an
   orphan and is stopped, deleted, and its directory removed.

Only directories untouched for InstanceCleanupThreshold, defaulting to 30
days, are processed. This keeps the startup scan cheap and avoids racing an
instance being created or in use. Configurable via the
LocalDBInstanceCleanupDays environment variable; zero disables it.

Attribution is by state, not authorship: residue has no live instance so it
is safe to remove whatever created it, and only instances with no data
directory are treated as orphans, so an instance in active use is left
alone. The default instances LocalDB manages itself are always skipped.

The test suite sets the threshold to zero in its module initializer, so
running the tests does not sweep the real instance root as a side effect.
A first run against a large backlog would otherwise stop and delete every
orphaned instance and remove every residue directory in one startup, which
is disk bound and adds a noticeable delay to that run.

At most InstanceCleanupLimit removals happen per run, defaulting to 20, so
the backlog drains over several runs. Skips are cheap and do not count
against the limit, so recently touched or in use directories never crowd
out the stale ones. Configurable via the LocalDBInstanceCleanupLimit
environment variable; zero removes the limit. DeleteOrphanInstances remains
the way to reclaim everything at once.
LocalDB instances are shared by everything on the machine that uses LocalDB,
and nothing in an instance records what created it. Reclaiming one therefore
meant guessing, and the sweep could remove an instance belonging to something
else entirely.

A marker file is now written into the directory LocalDB keeps for each
instance this library starts, and the ongoing sweep only reclaims marked
instances. Marking on start as well as on create means instances that predate
marking are picked up the next time they are used. Marking is best effort: an
instance that cannot be marked is simply never reclaimed automatically.

Instances that predate marking cannot be told apart from instances belonging
to anything else, so they get a single best effort pass, recorded under
%LocalAppData%\LocalDb so it happens once per machine. That pass removes
directories left behind by already deleted instances, which have no instance
to harm, and orphans that still carry the shrunk model.mdf this library leaves
behind. Nothing else is touched, so it can never remove a foreign instance.

The pass is only recorded as done when the sweep actually ran, so a sweep that
could not start does not consume the one chance the backlog gets.

This replaces the per run cap. The ongoing sweep now only ever considers
instances known to be ours, which is a much smaller set.
The only instances DeleteOrphanInstances reclaimed that the automatic sweep
does not are the unmarked ones that carry no sign of having been created
here. Removing those is exactly what the marker exists to prevent, so the
API contradicted the model it now sits behind.

It was also never able to clean up after itself: it enumerated registered
instances, so directories left behind by already deleted instances were
invisible to it, and those are the bulk of what accumulates. Anything the
one time pass deliberately leaves is now left for SqlLocalDB.exe delete.

Residue gets a short threshold of its own rather than the instance
threshold. It has no instance to race, only the window where LocalDB has
created the directory but not yet registered the instance, and the full
threshold would strand any residue not yet old enough when the backlog pass
runs, since that pass only happens once.

IsDefaultInstance moves to DirectoryCleaner, its only remaining caller.
The readme described the data root cleanup as stopping a running instance
before deleting its files. It has stopped, deleted, and removed the
directory LocalDB keeps for the instance since that cleanup was changed,
so both the prose and the flowchart node understated what it does.

The sweep of the directory LocalDB keeps for each instance was not
documented at all, though it is where the growth being reclaimed actually
accumulates. Adds a flowchart for the decision it makes per directory:
whether the directory is LocalDB managed, backed by a registered instance
or only residue, marked as created here, still tracked by a data
directory, and whether the one time backlog pass applies.
@SimonCropp SimonCropp added this to the 25.0.0 milestone Jul 21, 2026
@SimonCropp
SimonCropp merged commit c53668d into main Jul 21, 2026
5 of 6 checks passed
@SimonCropp
SimonCropp deleted the automatic-instance-cleanup branch July 21, 2026 04:14
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