Skip to content

Commit 7b386b8

Browse files
authored
Avoid blocking WithLatestFrom value updates (#2630)
Reimplement WithLatestFrom on a two-lock strategy, one gate serializing downstream delivery and a separate lock guarding only the cached latest value, so updates from the value sequence no longer wait on downstream processing of a source element. A slow downstream observer now delays only further source elements, not refreshes of the latest value, for example when software-timestamping a data sequence against a high-rate clock. Observable behavior is otherwise unchanged; the two-lock logic follows the current dotnet/reactive WithLatestFrom. Closes #2600
1 parent d93ac22 commit 7b386b8

2 files changed

Lines changed: 203 additions & 4 deletions

File tree

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Reactive.Linq;
4+
using System.Reactive.Subjects;
5+
using Bonsai.Reactive;
6+
using Microsoft.VisualStudio.TestTools.UnitTesting;
7+
8+
namespace Bonsai.Core.Tests
9+
{
10+
[TestClass]
11+
public class WithLatestFromTests
12+
{
13+
static IObservable<Tuple<TSource, TOther>> Process<TSource, TOther>(
14+
IObservable<TSource> source,
15+
IObservable<TOther> other)
16+
{
17+
return new WithLatestFrom().Process(source, other);
18+
}
19+
20+
[TestMethod]
21+
public void Process_SourceEmitsAfterValueSeen_EmitsPairWithLatestValue()
22+
{
23+
var source = new Subject<int>();
24+
var other = new Subject<string>();
25+
var results = new List<Tuple<int, string>>();
26+
using (Process(source, other).Subscribe(results.Add))
27+
{
28+
other.OnNext("a");
29+
source.OnNext(1);
30+
other.OnNext("b");
31+
source.OnNext(2);
32+
}
33+
34+
CollectionAssert.AreEqual(
35+
new[] { Tuple.Create(1, "a"), Tuple.Create(2, "b") },
36+
results);
37+
}
38+
39+
[TestMethod]
40+
public void Process_SourceCompletes_CompletesResult()
41+
{
42+
var source = new Subject<int>();
43+
var other = new Subject<string>();
44+
var completed = false;
45+
using (Process(source, other).Subscribe(_ => { }, () => completed = true))
46+
{
47+
other.OnNext("a");
48+
source.OnCompleted();
49+
}
50+
51+
Assert.IsTrue(completed);
52+
}
53+
54+
[TestMethod]
55+
public void Process_ValueSequenceCompletes_ContinuesWithLastValue()
56+
{
57+
var source = new Subject<int>();
58+
var other = new Subject<string>();
59+
var results = new List<Tuple<int, string>>();
60+
var completed = false;
61+
using (Process(source, other).Subscribe(results.Add, () => completed = true))
62+
{
63+
other.OnNext("a");
64+
other.OnCompleted();
65+
source.OnNext(1);
66+
source.OnNext(2);
67+
}
68+
69+
Assert.IsFalse(completed);
70+
CollectionAssert.AreEqual(
71+
new[] { Tuple.Create(1, "a"), Tuple.Create(2, "a") },
72+
results);
73+
}
74+
75+
[TestMethod]
76+
public void Process_SourceEmitsBeforeAnyValue_DropsNotification()
77+
{
78+
var source = new Subject<int>();
79+
var other = new Subject<string>();
80+
var results = new List<Tuple<int, string>>();
81+
using (Process(source, other).Subscribe(results.Add))
82+
{
83+
source.OnNext(1);
84+
other.OnNext("a");
85+
source.OnNext(2);
86+
}
87+
88+
CollectionAssert.AreEqual(
89+
new[] { Tuple.Create(2, "a") },
90+
results);
91+
}
92+
93+
[TestMethod]
94+
public void Process_SourceErrors_FaultsDownstream()
95+
{
96+
var source = new Subject<int>();
97+
var other = new Subject<string>();
98+
var error = new InvalidOperationException();
99+
Exception observed = null;
100+
using (Process(source, other).Subscribe(_ => { }, ex => observed = ex))
101+
{
102+
other.OnNext("a");
103+
source.OnError(error);
104+
}
105+
106+
Assert.AreSame(error, observed);
107+
}
108+
109+
[TestMethod]
110+
public void Process_ValueSequenceErrors_FaultsDownstream()
111+
{
112+
var source = new Subject<int>();
113+
var other = new Subject<string>();
114+
var error = new InvalidOperationException();
115+
Exception observed = null;
116+
using (Process(source, other).Subscribe(_ => { }, ex => observed = ex))
117+
{
118+
other.OnError(error);
119+
}
120+
121+
Assert.AreSame(error, observed);
122+
}
123+
124+
[TestMethod]
125+
public void Process_ValueAvailableSynchronously_EmitsOnImmediateSourceElement()
126+
{
127+
var source = Observable.Return(1);
128+
var other = Observable.Return("a");
129+
var results = new List<Tuple<int, string>>(Process(source, other).ToList().Wait());
130+
131+
CollectionAssert.AreEqual(
132+
new[] { Tuple.Create(1, "a") },
133+
results);
134+
}
135+
}
136+
}

Bonsai.Core/Reactive/WithLatestFrom.cs

Lines changed: 67 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
using System;
22
using System.ComponentModel;
3+
using System.Reactive;
4+
using System.Reactive.Disposables;
35
using System.Reactive.Linq;
6+
using System.Threading;
47
using System.Xml.Serialization;
58

69
namespace Bonsai.Reactive
@@ -35,10 +38,70 @@ public IObservable<Tuple<TSource, TOther>> Process<TSource, TOther>(
3538
IObservable<TSource> source,
3639
IObservable<TOther> other)
3740
{
38-
return source.Publish(ps =>
39-
ps.CombineLatest(other, (xs, ys) => Tuple.Create(xs, ys))
40-
.Sample(ps)
41-
.TakeUntil(ps.IgnoreElements().LastOrDefaultAsync()));
41+
return Observable.Create<Tuple<TSource, TOther>>(observer =>
42+
{
43+
var gate = new object();
44+
var latestGate = new object();
45+
var hasLatest = false;
46+
var latest = default(TOther);
47+
var otherDisposable = new SingleAssignmentDisposable();
48+
var otherObserver = Observer.Create<TOther>(
49+
value =>
50+
{
51+
lock (latestGate)
52+
{
53+
latest = value;
54+
}
55+
56+
if (!Volatile.Read(ref hasLatest))
57+
{
58+
Volatile.Write(ref hasLatest, true);
59+
}
60+
},
61+
error =>
62+
{
63+
lock (gate)
64+
{
65+
observer.OnError(error);
66+
}
67+
},
68+
otherDisposable.Dispose);
69+
otherDisposable.Disposable = other.SubscribeSafe(otherObserver);
70+
71+
var sourceObserver = Observer.Create<TSource>(
72+
value =>
73+
{
74+
if (Volatile.Read(ref hasLatest))
75+
{
76+
TOther latestValue;
77+
lock (latestGate)
78+
{
79+
latestValue = latest;
80+
}
81+
82+
lock (gate)
83+
{
84+
observer.OnNext(Tuple.Create(value, latestValue));
85+
}
86+
}
87+
},
88+
error =>
89+
{
90+
lock (gate)
91+
{
92+
observer.OnError(error);
93+
}
94+
},
95+
() =>
96+
{
97+
lock (gate)
98+
{
99+
observer.OnCompleted();
100+
}
101+
});
102+
var sourceDisposable = source.SubscribeSafe(sourceObserver);
103+
return new CompositeDisposable(otherDisposable, sourceDisposable);
104+
});
42105
}
43106
}
44107
}

0 commit comments

Comments
 (0)