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
24 lines
1.2 KiB
C#
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);
|
|
}
|