Rx.NET: Combine observables in order

时间秒杀一切 提交于 2019-12-08 03:43:05

问题


I have 2 IConnectableObservables where one is replaying old historic messages and the other is emitting fresh current values:

HistoricObservable: - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - ...
CurrentObservable:    - - - - - 5 - 6 - 7 - 8 - 9 - 10 - ...

How can I merge them into a single observable such that I get the full (correct) sequence from both observables, but also drop the subscription and call Dispose on the HistoricObservable subscription once I've started emitting values from CurrentObservable.

MergedObservable: - 1 - 2 - 3 - 4 - 56 - 7 - 8 - 9 - 10 - ...

My messages are identified by a Guid, so the solution can only compare them using Equal and can't rely on any ordering other than how it's emitted by each of the observables.

In short I'm looking to populate a the method:

public static IObservable<T> MergeObservables<T>(
    IObservable<T> historicObservable,
    IObservable<T> currentObservable)
    where T : IEquatable<T>
{
    throw new NotImplementedException();
}

The MergedObservable should keep emitting values from HistoricObservable without waiting for the first value from CurrentObservable, and if the first value from CurrentObservable has already been emitted previously then MergedObservable should skip over any values in CurrentObservable already emitted, dispose of the subscription to HistoricObservable, and start taking all new values from CurrentObservable. I also don't want to immediately switch over when the first object is emitted by the CurrentObservable until I get to that point in the HistoricObservable so I've been having a hard time trying to use TakeWhile/TakeUntil. I've been having some minor success with CombineLatest to save state, but I'm thinking there's probably a better way.

Test Cases

For the following test cases assume each message is represented by a GUID as follows:

A = E021ED8F-F0B7-44A1-B099-9878C6400F34
B = 1139570D-8465-4D7D-982F-E83A183619DE
C = 0AA2422E-19D9-49A7-9E8C-C9333FC46C46
D = F77D0714-2A02-4154-A44C-E593FFC16E3F
E = 14570189-4AAD-4D60-8780-BCDC1D23273D
F = B42983F0-5161-4165-A2F7-074698ECCE77
G = D2506881-F8AB-447F-96FA-896AEAAD1D0A
H = 3063CB7F-CD25-4287-85C3-67C609FA5679
I = 91200C69-CC59-4488-9FBA-AD2D181FD276
J = 2BEA364E-BE86-48FF-941C-4894CEF7A257
K = 67375907-8587-4D77-9C58-3E3254666303
L = C37C2259-C81A-4BC6-BF02-C96A34011479
M = E6F709BE-8910-42AD-A100-2801697496B0
N = 8741D0BB-EDA9-4735-BBAF-CE95629E880D

1) If the historic observable never catches up to the current observable then the merged observable should never emit anything from the current observable

Historic: - A - B - C - D - E - F - G - H|
Current:    - - - - - - - - - - - - - - - I - J - K - L - M - N|
Merged:   - A - B - C - D - E - F - G - H|

2) As soon as the historic observable reaches the first value emitted by the current observable then the merged observable should immediately emit all values previously emitted by current observable and disconnect from the historic observable.

Historic: - A - B - C - D - E - F - G - H - I - J|
Current:  - - - - - - E - F - G - H - I - J|
Merged:   - A - B - C - D - EF-G- H - I - J|

3) The solution should be able to handle values coming from the current observable before the historic observable.

Historic: - - - - - A - B - C - D - E - F - G - H - I - J|
Current:  - - C - D - E - F - G - H - I - J - K - L - M - N|
Merged:   - - - - - A - B - CDEF-G-H- I - J - K - L - M - N|

4) If the values from current observable have already been emitted then the solution should skip over them until a new value is emitted.

Historic: - A - B - C - D - E - F - G - H - I - J|
Current:  - - - - - - - - B - C - D - E - F - G - H - I - J|
Merged:   - A - B - C - D - - - - - - E - F - G - H - I - J|

5) For my use cases I am guaranteed that the current observable will be a subset of historic, but for the sake of completeness I would imagine the solution would continue pulling from the historic observable thinking that the first element will occur at a later point

Historic: - - - - - E - F - G - H - I - J - ... - Z - A|
Current:  - - A - B - C - D - E - F - G - H - I - J|
Merged:   - - - - - E - F - G - H - I - J - ... - Z - ABCDEFGHIJ|

6) I'm also guaranteed that the historic observable won't differ from the current observable once they're synced up, but if for some reason they do the merged observable should have already disconnected from it and won't pick up any differences

Historic: - A - B - C - D - E - D - C - B - A|
Current:  - - - - - - E - F - G - H - I - J|
Merged:   - A - B - C - D - EF-G- H - I - J|

The help with creating a working solution, here's some input data:

var historic = new Subject<int>();
var current = new Subject<int>();

// query & subscription goes here

historic.OnNext(1);
historic.OnNext(2);
current.OnNext(5);
historic.OnNext(3);
current.OnNext(6);
historic.OnNext(4);
current.OnNext(7);
historic.OnNext(5);
current.OnNext(8);
historic.OnNext(6);
current.OnNext(9);
historic.OnNext(7);
current.OnNext(10);

A correct solution should produce the numbers from 1 to 10.


回答1:


Assuming you want distinct results irrespective of the order of appearance, maybe this approach works too:

       var replayCurrent = current.Replay();
        replayCurrent.Connect();


        var merged = historic
            .Scan(
                new { history = new List<string>(), firstVal = (string)null },
                (state, val) =>
                { state.history.Add(val); return state; }
                )
            .Merge(
                current.Take(1).Select(v => new { history = (List<string>)null, firstVal = v })

                )
            .Scan(new { history = (List<string>)null, firstVal = (string)null },
                (state, val) =>
                new { history = val.history ?? state.history, firstVal = val.firstVal ?? state.firstVal })
            .TakeWhile(v => 
                (null==v.firstVal || ( null!=v.firstVal && !v.history.Contains(v.firstVal)))
                )
            .Select(v=>v.history.Last())
            .Concat(replayCurrent)
            .Distinct();

        merged.Subscribe(x => Console.WriteLine(x));



回答2:


Try this:

var historic = new Subject<int>();
var current = new Subject<int>();

var subscription =
    Observable
        .Defer(() =>
        {
            var c = int.MaxValue;
            return
                current
                    .Do(x => { if (c == int.MaxValue) c = x; })
                    .Publish(pc =>
                        historic
                            .TakeWhile(h => h < c)
                            .Publish(ph =>
                                Observable
                                    .Merge(
                                        ph.Delay(x => pc.FirstOrDefaultAsync(y => x < y)),
                                        pc.Delay(y => ph.FirstOrDefaultAsync(x => y < x)))))
                    .Distinct();
        })
        .Subscribe(x => Console.WriteLine(x));

historic.OnNext(1);
historic.OnNext(2);
current.OnNext(5);
historic.OnNext(3);
current.OnNext(6);
historic.OnNext(4);
current.OnNext(7);
historic.OnNext(5);
current.OnNext(8);
historic.OnNext(6);
current.OnNext(9);
historic.OnNext(7);
current.OnNext(10);

That gives the 1 to 10 in order. It might need a bit of tweaking to get the right historic and current values out.


To confirm that the right values are being emitted, try this:

var subscription =
    Observable
        .Defer(() =>
        {
            var c = int.MaxValue;
            return
                current
                    .Do(x => { if (c == int.MaxValue) c = x; })
                    .Select(x => new { source = "current", value = x })
                    .Publish(pc =>
                        historic
                            .TakeWhile(h => h < c)
                            .Finally(() => Console.WriteLine("!"))
                            .Select(x => new { source = "historic", value = x })
                            .Publish(ph =>
                                Observable
                                    .Merge(
                                        ph.Delay(x => pc.FirstOrDefaultAsync(y => x.value < y.value)),
                                        pc.Delay(y => ph.FirstOrDefaultAsync(x => y.value < x.value)))))
                    .Distinct(x => x.value);
        })
        .Subscribe(x => Console.WriteLine($"{x.source}:{x.value}"));

The output is:

historic:1
historic:2
historic:3
historic:4
current:5
current:6
current:7
!
current:8
current:9
current:10

The ! shows where the historical observable is disposed.




回答3:


Coming back to this I finally made some progress on the direction that I've been going in. Got it to go through all the test cases in the description so I think this will be good enough for my use cases. Constructive feedback is always appreciated.

public static IObservable<T> CombineObservables<T>(
    IObservable<T> historicObservable,
    IObservable<T> currentObservable)
    where T : IEquatable<T>
{
    var cachedCurrent = currentObservable.Replay();
    cachedCurrent.Connect();

    var firstMessage = cachedCurrent.FirstAsync();

    var emittedHistoryItems = new List<T>();

    var part1 = historicObservable.TakeUntil(firstMessage)
                                  .Do(x => emittedHistoryItems.Add(x));

    var part2 = historicObservable.CombineLatest(firstMessage, Tuple.Create)
                                  .TakeWhile(x =>
                                             {
                                                 var historyItem = x.Item1;
                                                 var first = x.Item2;

                                                 return !emittedHistoryItems.Any(y => y.Equals(first)) && !historyItem.Equals(first);
                                             })
                                  .Select(x => x.Item1)
                                  .Do(x => emittedHistoryItems.Add(x));

    var part3 = cachedCurrent.SkipWhile(x => emittedHistoryItems.Contains(x));

    return part1.Concat(part2).Concat(part3);
}

fiddle example: https://dotnetfiddle.net/6BqfiW




回答4:


If I understand your question correctly, you could

  • Make an observable that emits two observables
  • The first is the 'historic' and can be injected with StartsWith
  • The second is the 'current' and is only supplied on the first element of the current, which would mean you would need to subscribe to that observable with Take(1) or something to get the first element, and .Select the observable itself. (You can add .Where and .Scan as needed to compare the values in the 2 sequences)
  • Combine the two with .Switch. Switch will unsub from Historic and follow the Current from then on.

UPDATE:

I don't think the example data reflects the problem, at least not as I understand it. Try this

var historic = new Subject<int>();
var current = new Subject<int>();
var observables = new Subject<IObservable<int>>();
int[] tracker = new int[] { int.MaxValue, int.MaxValue };

var merged = historic
                .Select(x => new { source = (int)0, val = x })
                .Merge(current.Select(y => new { source = (int)1, val = y }))
                .Scan((state, v) => {
                        tracker[v.source] = v.val;
                        return v; })
                .Do(x => {
                    if ((tracker[1] <= tracker[0]) && tracker[0] != int.MaxValue)
                        observables.OnNext(current.StartWith(x.val));
                    })
                .Where(x => x.source == 0)
                .Select(x => x.val);

var streamSelector = observables.Switch();

streamSelector.Subscribe(x => Console.WriteLine(x));
observables.OnNext(merged);

historic.OnNext(1);
historic.OnNext(2);
current.OnNext(5);
historic.OnNext(3);
current.OnNext(6);
historic.OnNext(4);
current.OnNext(7);
historic.OnNext(5);
current.OnNext(8);
historic.OnNext(6);
current.OnNext(9);
historic.OnNext(7);
current.OnNext(10);


来源:https://stackoverflow.com/questions/50298555/rx-net-combine-observables-in-order

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!