Skip to content

Fix that a source failure is reported as successful completion to deferred subscriptions - #1141

Open
dwcullop wants to merge 1 commit into
reactivemarbles:mainfrom
dwcullop:bugfix/deferred_connect_error_propagation
Open

Fix that a source failure is reported as successful completion to deferred subscriptions#1141
dwcullop wants to merge 1 commit into
reactivemarbles:mainfrom
dwcullop:bugfix/deferred_connect_error_propagation

Conversation

@dwcullop

Copy link
Copy Markdown
Member

Fixes #1140.

A Connect() or Watch() made while notifications are suspended is deferred until the suspension lifts. When the cache's source failed, that deferral reported the failure as a successful completion.

// normal subscriber   -> OnError
// deferred subscriber -> OnCompleted

Same source, same failure, opposite terminal event, decided purely by whether the subscriber happened to connect during a suspension. That is worse than losing the error: the subscriber's error handling never runs, Finally and Catch see a normal end of stream, and the consumer is left believing it holds a complete, valid dataset.

Why

Two things combined.

CacheUpdateObserver.OnError disposed the suspension tracker, and disposal completes the subject the deferral is waiting on:

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

And the deferral mapped that completion straight onto the observer:

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

So the failure path completed the gate, and the Do turned that into observer.OnCompleted.

The change

Fault(Exception) faults the subject rather than completing it, and OnError calls that instead of Dispose(). The exception now has a path to the deferred subscriber.

SelectMany replaces Select followed by Switch, and the Do is dropped. Take(1) means there is only ever one inner sequence, so the two are equivalent for delivery, but SelectMany propagates the gate's terminal event by itself. The deferral is then self-contained rather than depending on the behaviour of another operator, and the terminal event travels through the chain rather than bypassing it.

Watch() had the same shape and is changed the same way.

Tests

  • OnErrorFiresIfCacheFailsWhileSuspended
  • OnErrorFiresIfCacheFailsWhileWatchIsSuspended
  • OnErrorFiresIfCacheFailsAfterResumingWhileConnectionWasSuspended

The first two fail without the change. The existing OnCompletedFiresIfCacheDisposedWhileSuspended covers the completion path, which must keep working and does.

Notes

Independent of #1137. That one repairs the cache Switch operator, which is what #1136 was really about; this removes the deferral's dependence on Switch altogether, so the two do not overlap and can land in either order. #1137 also adds tests to SuspendNotificationsFixture, so a trivial conflict is possible depending on merge order.

Related to #1133, which reuses this deferral shape for subscriptions made during an Edit(). That PR would widen the trigger from suspensions to ordinary edits, so this is worth landing first.

One unrelated failure appears on this branch and on main: ConcurrentSuspendDuringResumeDoesNotCorrupt fails when run in isolation. That is #1131, which #1132 addresses.

A Connect() or Watch() made while notifications are suspended is deferred until the
suspension lifts. When the cache's source failed, that deferral reported the failure
as a successful completion, so the subscriber's error handling never ran and its data
looked complete.

Two things caused it. The suspension tracker was disposed on failure, which completes
the subject that the deferral is waiting on, and the deferral mapped that completion
straight onto observer.OnCompleted. Faulting the subject instead carries the exception,
and dropping the Do lets the terminal event travel through the chain rather than
bypassing it.

SelectMany replaces Select followed by Switch. Take(1) means there is only ever one
inner sequence, so the two are equivalent for delivery, but SelectMany propagates the
gate's terminal event by itself. That leaves the deferral self-contained rather than
dependent on the behaviour of another operator.
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.

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

1 participant