|
23 | 23 | import java.util.Arrays;
|
24 | 24 | import java.util.List;
|
25 | 25 | import java.util.concurrent.ConcurrentHashMap;
|
| 26 | +import java.util.concurrent.CountDownLatch; |
26 | 27 | import java.util.concurrent.atomic.AtomicBoolean;
|
| 28 | +import java.util.concurrent.atomic.AtomicInteger; |
27 | 29 |
|
28 | 30 | import org.junit.Before;
|
29 | 31 | import org.junit.Test;
|
|
33 | 35 | import rx.Observable;
|
34 | 36 | import rx.Observer;
|
35 | 37 | import rx.Subscription;
|
| 38 | +import rx.util.AtomicObservableSubscription; |
| 39 | +import rx.util.SynchronizedObserver; |
36 | 40 | import rx.util.functions.Func1;
|
37 | 41 |
|
38 | 42 | public final class OperationMerge {
|
@@ -115,10 +119,20 @@ private MergeObservable(Observable<Observable<T>> sequences) {
|
115 | 119 | }
|
116 | 120 |
|
117 | 121 | public MergeSubscription call(Observer<T> actualObserver) {
|
| 122 | + |
| 123 | + /** |
| 124 | + * We must synchronize a merge because we subscribe to multiple sequences in parallel that will each be emitting. |
| 125 | + * <p> |
| 126 | + * The calls from each sequence must be serialized. |
| 127 | + * <p> |
| 128 | + * Bug report: https://github.com/Netflix/RxJava/issues/200 |
| 129 | + */ |
| 130 | + SynchronizedObserver<T> synchronizedObserver = new SynchronizedObserver<T>(actualObserver, new AtomicObservableSubscription(ourSubscription)); |
| 131 | + |
118 | 132 | /**
|
119 | 133 | * Subscribe to the parent Observable to get to the children Observables
|
120 | 134 | */
|
121 |
| - sequences.subscribe(new ParentObserver(actualObserver)); |
| 135 | + sequences.subscribe(new ParentObserver(synchronizedObserver)); |
122 | 136 |
|
123 | 137 | /* return our subscription to allow unsubscribing */
|
124 | 138 | return ourSubscription;
|
@@ -380,6 +394,68 @@ public void testMergeArrayWithThreading() {
|
380 | 394 | verify(stringObserver, times(1)).onCompleted();
|
381 | 395 | }
|
382 | 396 |
|
| 397 | + @Test |
| 398 | + public void testSynchronizationOfMultipleSequences() throws Exception { |
| 399 | + final TestASynchronousObservable o1 = new TestASynchronousObservable(); |
| 400 | + final TestASynchronousObservable o2 = new TestASynchronousObservable(); |
| 401 | + |
| 402 | + // use this latch to cause onNext to wait until we're ready to let it go |
| 403 | + final CountDownLatch endLatch = new CountDownLatch(1); |
| 404 | + |
| 405 | + final AtomicInteger concurrentCounter = new AtomicInteger(); |
| 406 | + final AtomicInteger totalCounter = new AtomicInteger(); |
| 407 | + |
| 408 | + @SuppressWarnings("unchecked") |
| 409 | + Observable<String> m = Observable.create(merge(o1, o2)); |
| 410 | + m.subscribe(new Observer<String>() { |
| 411 | + |
| 412 | + @Override |
| 413 | + public void onCompleted() { |
| 414 | + |
| 415 | + } |
| 416 | + |
| 417 | + @Override |
| 418 | + public void onError(Exception e) { |
| 419 | + throw new RuntimeException("failed", e); |
| 420 | + } |
| 421 | + |
| 422 | + @Override |
| 423 | + public void onNext(String v) { |
| 424 | + totalCounter.incrementAndGet(); |
| 425 | + concurrentCounter.incrementAndGet(); |
| 426 | + try { |
| 427 | + // wait here until we're done asserting |
| 428 | + endLatch.await(); |
| 429 | + } catch (InterruptedException e) { |
| 430 | + e.printStackTrace(); |
| 431 | + throw new RuntimeException("failed", e); |
| 432 | + } finally { |
| 433 | + concurrentCounter.decrementAndGet(); |
| 434 | + } |
| 435 | + } |
| 436 | + |
| 437 | + }); |
| 438 | + |
| 439 | + // wait for both observables to send (one should be blocked) |
| 440 | + o1.onNextBeingSent.await(); |
| 441 | + o2.onNextBeingSent.await(); |
| 442 | + |
| 443 | + assertEquals(1, concurrentCounter.get()); |
| 444 | + |
| 445 | + // release so it can finish |
| 446 | + endLatch.countDown(); |
| 447 | + |
| 448 | + try { |
| 449 | + o1.t.join(); |
| 450 | + o2.t.join(); |
| 451 | + } catch (InterruptedException e) { |
| 452 | + throw new RuntimeException(e); |
| 453 | + } |
| 454 | + |
| 455 | + assertEquals(2, totalCounter.get()); |
| 456 | + assertEquals(0, concurrentCounter.get()); |
| 457 | + } |
| 458 | + |
383 | 459 | /**
|
384 | 460 | * unit test from OperationMergeDelayError backported here to show how these use cases work with normal merge
|
385 | 461 | */
|
@@ -452,13 +528,15 @@ public void unsubscribe() {
|
452 | 528 |
|
453 | 529 | private static class TestASynchronousObservable extends Observable<String> {
|
454 | 530 | Thread t;
|
| 531 | + final CountDownLatch onNextBeingSent = new CountDownLatch(1); |
455 | 532 |
|
456 | 533 | @Override
|
457 | 534 | public Subscription subscribe(final Observer<String> observer) {
|
458 | 535 | t = new Thread(new Runnable() {
|
459 | 536 |
|
460 | 537 | @Override
|
461 | 538 | public void run() {
|
| 539 | + onNextBeingSent.countDown(); |
462 | 540 | observer.onNext("hello");
|
463 | 541 | observer.onCompleted();
|
464 | 542 | }
|
|
0 commit comments