Skip to content

Split enumeration-miss cause + extract EnumerationFailureTracker + de-duplicate error telemetry - #2076

Open
tyrielv wants to merge 1 commit into
microsoft:masterfrom
tyrielv:tyrielv/enum-miss-split
Open

Split enumeration-miss cause + extract EnumerationFailureTracker + de-duplicate error telemetry#2076
tyrielv wants to merge 1 commit into
microsoft:masterfrom
tyrielv:tyrielv/enum-miss-split

Conversation

@tyrielv

@tyrielv tyrielv commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

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 EnumerationFailureTracker class.
No behavior change - metadata and log volume only; the returned HResult is 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 Evicted vs Unknown. Analysis
of that Unknown bucket shows two distinct causes that need to be told apart, plus a per-machine event
storm that inflates the signal:

  • The kernel (ProjFS) can deliver a GetDirectoryEnumeration that races the EndDirectoryEnumeration
    for 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.
  • A caller re-enumerating a handle whose Start GVFS lost (for example across a provider restart) can
    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 EnumerationFailureTracker class. Extracts the classification, once-per-ID de-duplication,
    and the bounded tracking maps into a cohesive, independently unit-testable class (mirroring the
    MissingTreeTracker pattern). 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-free ConcurrentDictionary state; a coarse lock would
    serialize the hot enumeration path. The virtualizer keeps only the eviction policy (deciding what
    is stale) and delegates the bookkeeping.

  • Split the miss reason. EnumerationFailureReason renames Unknown -> NeverSeen and adds
    EndedRecently:

    • 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 kernel
      close/query race; outside gvfs.exe's control).
    • NeverSeen - GVFS never held the ID: never started, or from before a provider restart (outside
      gvfs.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. ClassifyMiss
      attributes Evicted (most actionable) first, then EndedRecently, then NeverSeen.
  • 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.
  • Tracker unit tests cover classification, the Evicted-over-EndedRecently precedence, the eviction
    undo (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.

@tyrielv
tyrielv force-pushed the tyrielv/enum-miss-split branch 2 times, most recently from 9d73a62 to 8419327 Compare August 5, 2026 23:04
@tyrielv tyrielv changed the title Split enumeration-miss cause (EndedRecently vs NeverSeen) + de-duplicate error telemetry [hold for #2071] Split enumeration-miss cause (EndedRecently vs NeverSeen) + de-duplicate error telemetry Aug 5, 2026
@tyrielv
tyrielv marked this pull request as ready for review August 5, 2026 23:04
@tyrielv
tyrielv marked this pull request as draft August 5, 2026 23:26
@tyrielv
tyrielv force-pushed the tyrielv/enum-miss-split branch 3 times, most recently from c0232ae to 28d1485 Compare August 7, 2026 21:31
@tyrielv tyrielv changed the title Split enumeration-miss cause (EndedRecently vs NeverSeen) + de-duplicate error telemetry Split enumeration-miss cause + extract EnumerationFailureTracker + de-duplicate error telemetry Aug 7, 2026
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
tyrielv force-pushed the tyrielv/enum-miss-split branch from 28d1485 to cf6ad76 Compare August 7, 2026 21:36
@tyrielv
tyrielv marked this pull request as ready for review August 7, 2026 21:37
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