Split enumeration-miss cause + extract EnumerationFailureTracker + de-duplicate error telemetry - #2076
Open
tyrielv wants to merge 1 commit into
Open
Split enumeration-miss cause + extract EnumerationFailureTracker + de-duplicate error telemetry#2076tyrielv wants to merge 1 commit into
tyrielv wants to merge 1 commit into
Conversation
tyrielv
force-pushed
the
tyrielv/enum-miss-split
branch
2 times, most recently
from
August 5, 2026 23:04
9d73a62 to
8419327
Compare
tyrielv
marked this pull request as ready for review
August 5, 2026 23:04
tyrielv
marked this pull request as draft
August 5, 2026 23:26
tyrielv
force-pushed
the
tyrielv/enum-miss-split
branch
3 times, most recently
from
August 7, 2026 21:31
c0232ae to
28d1485
Compare
GetDirectoryEnumeration logs "Failed to find active enumeration ID" when an enumeration ID is absent. Every non-eviction miss carried the reason Unknown, which hid two different causes: - EndedRecently: ProjFS delivered a Get that raced or followed the End for the same enumeration (a benign kernel close/query race). - NeverSeen: GVFS never held the ID (it never started, or it predates a provider restart). The classification, the once-per-ID error de-duplication, and the bounded tracking maps are extracted into a new EnumerationFailureTracker class (mirroring the MissingTreeTracker pattern) so the policy is cohesive and unit testable on its own. The tracker owns all three "why is this ID absent" maps - recently evicted, recently ended, and recently reported - and exposes RecordEvicted, RecordEnded, ClassifyMiss, and TryReserveReport. The virtualizer records an end (and an eviction) before removing the ID from activeEnumerations, so a racing Get always finds the ID in one collection or the other; ClassifyMiss attributes Evicted (most actionable), then EndedRecently, then NeverSeen. The old Unknown value is renamed NeverSeen. De-duplicate the error: a caller that re-enumerates a lost handle can emit the same error a very large number of times on one machine. The tracker emits the full error once per ID within a window; the first occurrence still logs at Error, so the machine-based signal stays intact. The returned HResult does not change. The tracker prunes its maps on a throttle from every record point, so no map can grow unbounded when one callback (e.g. End) stops arriving - the never-ended scenario this instrumentation targets. It keeps lock-free ConcurrentDictionary state; a coarse lock would serialize the hot enumeration path. The EnumerationFailureReason values are a case-sensitive contract consumed by the release-readiness telemetry dashboard; its cause bucketing must add EndedRecently and NeverSeen. Tests: EnumerationFailureTracker is unit-tested directly (classification, Evicted-over-EndedRecently precedence, eviction undo, dedup, prune/retention); the virtualizer tests cover the wiring, the record-before-remove ordering, and the Evicted-over-EndedRecently precedence end to end. Assisted-by: Claude Opus 4.8 Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
tyrielv
force-pushed
the
tyrielv/enum-miss-split
branch
from
August 7, 2026 21:36
28d1485 to
cf6ad76
Compare
tyrielv
marked this pull request as ready for review
August 7, 2026 21:37
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.
What
Extends the directory-enumeration cause tagging from #2071 with a finer split, de-duplicates the error
telemetry, and moves the whole classification/bookkeeping into a new
EnumerationFailureTrackerclass.No behavior change - metadata and log volume only; the returned
HResultis unchanged.Why
"Directory enumeration failure" ("Failed to find active enumeration ID") is the largest named
regression signature in the field telemetry. #2071 tags the miss as
EvictedvsUnknown. Analysisof that
Unknownbucket shows two distinct causes that need to be told apart, plus a per-machine eventstorm that inflates the signal:
GetDirectoryEnumerationthat races theEndDirectoryEnumerationfor the same handle - a query in flight while the directory handle is closing, or the querying
process dying mid-enumeration. This is a benign close/query race, not an ID GVFS never held.
re-issue the same failing Get in a tight loop, producing a very large number of error events on one
machine and drowning the machine-based signal.
Changes
New
EnumerationFailureTrackerclass. Extracts the classification, once-per-ID de-duplication,and the bounded tracking maps into a cohesive, independently unit-testable class (mirroring the
MissingTreeTrackerpattern). It owns all three "why is this ID absent" maps - recently evicted,recently ended, and recently reported - and exposes
RecordEvicted,RecordEnded,ClassifyMiss,and
TryReserveReport. It keeps lock-freeConcurrentDictionarystate; a coarse lock wouldserialize the hot enumeration path. The virtualizer keeps only the eviction policy (deciding what
is stale) and delegates the bookkeeping.
Split the miss reason.
EnumerationFailureReasonrenamesUnknown->NeverSeenand addsEndedRecently:Evicted- GVFS's own stale-enumeration eviction removed a live enumeration (self-inflicted).EndedRecently- a Get raced or followed the End for the same enumeration (benign kernelclose/query race; outside gvfs.exe's control).
NeverSeen- GVFS never held the ID: never started, or from before a provider restart (outsidegvfs.exe's control).
The virtualizer records an end (and an eviction) via the tracker before removing the ID from
activeEnumerations, so a racing Get always finds it in one collection or the other.ClassifyMissattributes
Evicted(most actionable) first, thenEndedRecently, thenNeverSeen.De-duplicate the error telemetry. The tracker emits the full error once per enumeration ID within
a window; repeats from a caller's retry loop are suppressed. The first occurrence still logs at Error,
so the machine-based signal is preserved.
Bound the tracking maps. The tracker prunes on a throttle from every record point, so no map can
grow unbounded when one callback (e.g. End) stops arriving - the never-ended scenario this
instrumentation targets.
Testing
EnumerationFailureTrackerTests+WindowsFileSystemVirtualizerTests: 44/44 passed.Evicted-over-EndedRecentlyprecedence, the evictionundo (lost-race), the dedup, and the prune/retention of each map. The virtualizer tests cover the
end-to-end wiring, the record-before-remove ordering, and the precedence through the real callbacks.