Skip to content

[Bug]: A source failure is reported as successful completion to subscriptions deferred by SuspendNotifications#1140

Description

@dwcullop

Describe the bug 馃悶

If a cache's source fails while notifications are suspended, any Connect() or Watch() subscription that was deferred by that suspension is told the sequence completed successfully. The error is never delivered.

ObservableCache.Connect() defers when notifications are suspended, waiting for the suspension to lift:

: _suspensionTracker.Value.NotificationsSuspendedObservable.Do(static _ => { }, observer.OnCompleted)
    .Where(static b => !b).Take(1).Select(_ => CreateConnectObservable(predicate, suppressEmptyChangeSets)).Switch();

The second argument to Do maps completion of the suspension subject directly onto observer.OnCompleted. When the source errors, CacheUpdateObserver.OnError calls _suspensionTracker.Value.Dispose(), and that does:

public void Dispose()
{
    _areNotificationsSuspended.OnCompleted();
    _areNotificationsSuspended.Dispose();
}

So the failure path completes the subject, and every deferred subscriber reads that as a successful end of stream.

Watch() has the same shape and the same problem.

This is worse than losing the error. The subscriber's error handling never runs, Finally/Catch see a normal completion, and the consumer is left believing it holds a complete, valid dataset when the source actually failed.

Step to reproduce

var source = new Subject<IChangeSet<int, int>>();
using var cache = new IntermediateCache<int, int>(source);

var normal = "";
using var normalSub = cache.Connect().Subscribe(
    _ => normal += "OnNext;", _ => normal += "OnError;", () => normal += "OnCompleted;");

var deferred = "";
using (cache.SuspendNotifications())
{
    cache.Connect().Subscribe(
        _ => deferred += "OnNext;", _ => deferred += "OnError;", () => deferred += "OnCompleted;");

    source.OnError(new InvalidOperationException("source failed"));
}

// normal   == "OnError;"
// deferred == "OnCompleted;"   <-- the failure was reported as success

Same source, same failure, opposite terminal event, decided purely by whether the subscriber happened to connect during a suspension.

Watch() behaves identically:

using (cache.SuspendNotifications())
{
    cache.Watch(1).Subscribe(_ => { }, _ => saw = "OnError", () => saw = "OnCompleted");
    source.OnError(new InvalidOperationException("source failed"));
}
// saw == "OnCompleted"

Expected behavior

A deferred subscriber should be indistinguishable from an ordinary one. If the source fails, it should receive OnError with the original exception.

DynamicData Version

main

Additional information 鈩癸笍

Found while reviewing #1133, which reuses this deferral shape to defer subscriptions made during an Edit(). That PR does not cause this, but it would widen the trigger considerably: connecting during a suspension is unusual, connecting during an edit is not.

Related to #1136, which was the same deferral path losing OnCompleted. That one is fixed in #1137, but by repairing the Switch operator underneath rather than this Do, so this remains.

A fix likely means giving SuspensionTracker a way to fault the subject rather than complete it, and dropping the Do(..., observer.OnCompleted) so the terminal event flows through on its own.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions