Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: List MergeManyChangeSets for Cache ChangeSets #744

Merged

Conversation

dwcullop
Copy link
Member

@dwcullop dwcullop commented Oct 25, 2023

Description

This PR adds MergeManyChangeSets for List ChangeSets where the child changeset is a Cache ChangeSet. In other words, it allows you to create a Cache ChangeSet from a child cache of items in a Source List.

Functionality is identical to that in #743 except the input changeset type is list instead of cache.

Dependencies

What was once a single PR is now three. While the other two are independent, this PR requires them both to work (or else the tests won't pass and/or it might not build).

@dwcullop dwcullop changed the title Feature: List MergeManyChangeSets for Cache Changeset Feature: List MergeManyChangeSets for Cache ChangeSets Oct 25, 2023
@RolandPheasant
Copy link
Collaborator

@dwcullop I just pushed a commit to this PR which adds some replace tests. See 53feeeb

@RolandPheasant
Copy link
Collaborator

@dwcullop I've work it out.

If you change the following:

var sourceListOfCaches = _source
    .WhereReasonsAreNot(ListChangeReason.Moved, ListChangeReason.Refresh)
    .Transform(obj => new ChangeSetCache<TDestination, TDestinationKey>(_changeSetSelector(obj)))
    .Synchronize(locker)
    .AsObservableList();

to

var sourceListOfCaches = _source
    .Transform(obj => new ChangeSetCache<TDestination, TDestinationKey>(_changeSetSelector(obj)))
    .Synchronize(locker)
    .AsObservableList();

The problem goes away. The reason is WhereReasonsAreNot removes the index from all changes because the index is unreliable when reasons are removed. I think I used to rely on that operator internally but due to such issues I stopped using it.

That said, we should always mitigate against such things in the operators (in case someone is using WhereReasonsAreNot). So for safety the replace method in Transformer should become:

case ListChangeReason.Replace:
    {
        var change = item.Item;

        if (change.CurrentIndex == -1 || change.PreviousIndex == -1)
        {
            // Find the original, with it's corresponding index
            var previous = transformed.First(x => x.Source.Equals(change.Previous.Value));
            var index = transformed.IndexOf(previous);
            if (index == -1)
            {
                throw new UnspecifiedIndexException($"Cannot find index of {change.Previous.Value}");
            }

            transformed[index] = _containerFactory(change.Current, previous.Destination, index);
        }
        else
        {
            Optional<TDestination> previous = transformed[change.PreviousIndex].Destination;
            if (change.CurrentIndex == change.PreviousIndex)
            {
                transformed[change.CurrentIndex] = _containerFactory(change.Current, previous, change.CurrentIndex);
            }
            else
            {
                transformed.RemoveAt(change.PreviousIndex);
                transformed.Insert(change.CurrentIndex, _containerFactory(change.Current, Optional<TDestination>.None, change.CurrentIndex));
            }
        }

        break;
    }

@dwcullop
Copy link
Member Author

@dwcullop I've work it out.

Hurray! That resolved the issue! PR has been updated and I'll mark it as ready!

@dwcullop dwcullop marked this pull request as ready for review November 21, 2023 19:00
@dwcullop dwcullop self-assigned this Nov 21, 2023
Copy link

codecov bot commented Nov 21, 2023

Codecov Report

Attention: 408 lines in your changes are missing coverage. Please review.

Comparison is base (53d5f6d) 64.74% compared to head (37c1e86) 65.85%.
Report is 5 commits behind head on main.

Files Patch % Lines
src/DynamicData/Cache/ObservableCacheEx.cs 53.48% 19 Missing and 1 partial ⚠️
src/DynamicData/List/ObservableListEx.cs 69.56% 13 Missing and 1 partial ⚠️
src/DynamicData/Aggregation/AvgEx.cs 73.68% 10 Missing ⚠️
src/DynamicData/Aggregation/SumEx.cs 74.35% 10 Missing ⚠️
src/DynamicData/ObservableChangeSet.cs 58.33% 10 Missing ⚠️
src/DynamicData/Platforms/net45/ParallelEx.cs 25.00% 7 Missing and 2 partials ⚠️
src/DynamicData/Aggregation/StdDevEx.cs 76.66% 5 Missing and 2 partials ⚠️
src/DynamicData/List/Internal/GroupOnProperty.cs 0.00% 7 Missing ⚠️
src/DynamicData/Aggregation/CountEx.cs 25.00% 6 Missing ⚠️
src/DynamicData/Cache/Internal/CacheUpdater.cs 53.84% 6 Missing ⚠️
... and 126 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #744      +/-   ##
==========================================
+ Coverage   64.74%   65.85%   +1.11%     
==========================================
  Files         226      228       +2     
  Lines       11459    11161     -298     
  Branches     2334     2299      -35     
==========================================
- Hits         7419     7350      -69     
+ Misses       3083     2877     -206     
+ Partials      957      934      -23     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

}

[Fact]
public void MergedObservableWillFailIfSourceFails()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we also test that errors from child streams propagate out?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I decidedly did not want errors from child streams to tear down the whole thing. I could be convinced that is the wrong behavior, but my thinking was that an error on the child stream shouldn't disrupt all of the other child streams or the main stream.

// then
receivedError.Should().Be(expectedError);
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following off of the bug I fixed yesterday, could/should we add a test to cover downstream-teardown (I.E. unsubscription) behavior? Internally, this means cleaning up all the outstanding child stream subscriptions, right?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We probably should do. I wonder whether we should raise an issue and gradually trawl through the system.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't expect that there's a systemic issue with not handling downstream-teardown, that was just a defect I introduced a few days ago. But it is a behavior worth testing, wherever we have resources that need cleanup.

Perhaps instead what we need is a guidelines document for testing operators.

// when a source item is removed, all of its sub-items need to be removed
var removedItems = shared
.OnItemRemoved(mc => changeTracker.RemoveItems(mc.Cache.KeyValues, observer))
.Subscribe();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.Subscribe() calls here should probably all be .SubscribeSafe() as its documentation recommends its usage within operators. This literally just try/catches the .Subscribe() call and feeds anything it catches to onError. This is important for operators, because it prevents things like .OnNext() or .OnCompleted() from throwing, when operators that orchestrate different streams together (like, say .Concat()) are involved.

I was also going to suggest passing observer.OnError to this subscription, but I'm less sold on that, after thinking it through. Since the source here is shared with .Publish() the only errors that can occur on this subscription (that aren't already going to be propagated from the "main" subscription) are from .OnItemRemoved(), which doesn't involve any user code. If there's a bug on our part, that's going to throw out of .Subscribe() or .OnNext() or .OnComplete() or whatever the user was doing that triggered the removal operation. But maybe that's actually preferable, since it's not the user's fault?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since it's a shared Observable, I don't think they all need to be SubscribeSafe. This one in particular is only used internally, so if it throws something is really wrong.

Maybe the one below should be SubscribeSafe so that the consumer can detect a bad subscription via their OnError. That one also passes in observer.OnError.

But I do like Roland's suggestion of uniformly addressing these as a single PR.

Copy link
Collaborator

@RolandPheasant RolandPheasant left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's get this in, and @JakenVeina we should review places where we can improve usages in Subscription (safe or not) here and throughout the system in a subsequent PR

@dwcullop dwcullop merged commit 5df6831 into reactivemarbles:main Nov 22, 2023
3 checks passed
@dwcullop dwcullop deleted the feature/list-cache-merge-changesets branch November 22, 2023 21:44
Copy link

github-actions bot commented Dec 7, 2023

This pull request has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Dec 7, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants