Files
ersatztv/ErsatzTV.Core/Metadata/MediaServerProjectionFailureCounter.cs
T
timothy e3645a2840 feat(484): suppress the media-server sweep on projection failures; reject the ratio threshold
Extends MediaServerReconciliationGuard (#477) with a second deterministic refusal: when the
enumeration that produced the incoming set silently dropped items whose projection THREW, the
file-not-found sweep is refused. A dropped item the server did return is indistinguishable
from a deletion at the reconcile step, so a projection regression could otherwise mass-flag a
healthy library FileNotFound (which EmptyTrash then deletes permanently).

Deliberate guard-clause skips (STRM files, virtual items, unsupported types) are explicitly NOT
failures and never suppress a sweep — counting them would permanently disable reconciliation for
any library holding a single STRM file.

The ratio / missing-fraction threshold is REJECTED, not deferred: it is a two-sided heuristic
with no tunable default and no telemetry, and the failure it approximates is exactly observable
via the projection-failure count (a genuine bulk deletion produces zero failures).

Seam is deliberately narrow — the private ProjectTo* contract inside each api client changed from
Option<T> to MediaServerProjectionResult<T> (projected/skipped/failed), the paged helper counts
IsFailure in one place, and the scanner reads it through an optional trailing
MediaServerProjectionFailureCounter on only the five library-level methods that feed a sweep.
The counter is per-enumeration state created by the scanner, never a field on an api client.

fixes #484
2026-07-25 16:36:35 +02:00

24 lines
1.2 KiB
C#

namespace ErsatzTV.Core.Metadata;
// #484: the narrow seam that carries "how many items did the server return that we silently dropped
// because their projection threw?" out of a media-server API client and back to the scanner that owns
// the reconciliation sweep.
//
// Lifetime is deliberately PER ENUMERATION: the scanner that is about to run a sweep creates one
// instance, hands it to the single library-items call whose result it will diff, and reads Count only
// after that enumeration has completed. The API clients are long-lived singletons and scans for
// different libraries run concurrently, so the counter must never be a field on a client or any
// ambient/static state — that would leak one library's failures into another library's sweep decision.
// Increments are interlocked anyway so a paginator that ever fans out stays correct.
public sealed class MediaServerProjectionFailureCounter
{
private int _count;
/// <summary>
/// Number of items the server returned whose projection threw and was swallowed.
/// </summary>
public int Count => Volatile.Read(ref _count);
public void RecordFailure() => Interlocked.Increment(ref _count);
}