From 0554dc062b7120a36a646f3ee97894f46351d4b5 Mon Sep 17 00:00:00 2001 From: pron Date: Mon, 10 Feb 2014 18:14:14 +0200 Subject: [PATCH 001/422] initial commit --- .gitignore | 1 + rxjava-contrib/rxjava-quasar/README.md | 35 +++++ rxjava-contrib/rxjava-quasar/build.gradle | 40 +++++ .../java/rx/quasar/ChannelObservable.java | 103 +++++++++++++ .../java/rx/quasar/NewFiberScheduler.java | 145 ++++++++++++++++++ .../rx/quasar/OnSubscribeFromChannel.java | 61 ++++++++ .../resources/META-INF/suspendable-supers | 2 + .../java/rx/quasar/ChannelObservableTest.java | 86 +++++++++++ .../java/rx/quasar/NewFiberSchedulerTest.java | 32 ++++ settings.gradle | 3 +- 10 files changed, 507 insertions(+), 1 deletion(-) create mode 100644 rxjava-contrib/rxjava-quasar/README.md create mode 100644 rxjava-contrib/rxjava-quasar/build.gradle create mode 100644 rxjava-contrib/rxjava-quasar/src/main/java/rx/quasar/ChannelObservable.java create mode 100644 rxjava-contrib/rxjava-quasar/src/main/java/rx/quasar/NewFiberScheduler.java create mode 100644 rxjava-contrib/rxjava-quasar/src/main/java/rx/quasar/OnSubscribeFromChannel.java create mode 100644 rxjava-contrib/rxjava-quasar/src/main/resources/META-INF/suspendable-supers create mode 100644 rxjava-contrib/rxjava-quasar/src/test/java/rx/quasar/ChannelObservableTest.java create mode 100644 rxjava-contrib/rxjava-quasar/src/test/java/rx/quasar/NewFiberSchedulerTest.java diff --git a/.gitignore b/.gitignore index f69630c23c..606575380d 100644 --- a/.gitignore +++ b/.gitignore @@ -62,6 +62,7 @@ bin/ # NetBeans specific files/directories .nbattrs +/.nb-gradle/profiles/private/ # Scala build *.cache diff --git a/rxjava-contrib/rxjava-quasar/README.md b/rxjava-contrib/rxjava-quasar/README.md new file mode 100644 index 0000000000..ab031fcda3 --- /dev/null +++ b/rxjava-contrib/rxjava-quasar/README.md @@ -0,0 +1,35 @@ +# rxjava-quasar + +Integrates RxJava with [Quasar](https://github.com/puniverse/quasar). +Includes a fiber (lightweight-thread) based scheduler, and an Observable API for Quasar channels. + +Main Classes: + +- [NewFiberScheduler](https://github.com/Netflix/RxJava/blob/master/rxjava-contrib/rxjava-quasar/src/main/java/rx/quasar/NewFiberScheduler.java) +- [ChannelObservable](https://github.com/Netflix/RxJava/blob/master/rxjava-contrib/rxjava-quasar/src/main/java/rx/quasar/ChannelObservable.java) + + +# Binaries + +Binaries and dependency information for Maven, Ivy, Gradle and others can be found at [http://search.maven.org](http://search.maven.org/#search%7Cga%7C1%7Ccom.netflix.rxjava). + +Example for [Maven](http://search.maven.org/#search%7Cga%7C1%7Ca%3A%22rxjava-apache-http%22): + +```xml + + com.netflix.rxjava + rxjava-quasar + x.y.z + +``` + +and for Ivy: + +```xml + +``` + +# Sample Usage + + + diff --git a/rxjava-contrib/rxjava-quasar/build.gradle b/rxjava-contrib/rxjava-quasar/build.gradle new file mode 100644 index 0000000000..72586db6a1 --- /dev/null +++ b/rxjava-contrib/rxjava-quasar/build.gradle @@ -0,0 +1,40 @@ +apply plugin: 'osgi' + +sourceCompatibility = JavaVersion.VERSION_1_6 +targetCompatibility = JavaVersion.VERSION_1_7 + +configurations { + quasar +} + +repositories { + mavenLocal() + mavenCentral() + maven { url "https://oss.sonatype.org/content/repositories/snapshots" } +} + +dependencies { + compile project(':rxjava-core') + compile 'co.paralleluniverse:quasar-core:0.5.0-SNAPSHOT' + quasar 'co.paralleluniverse:quasar-core:0.5.0-SNAPSHOT' + testCompile project(":rxjava-core").sourceSets.test.output + provided 'junit:junit-dep:4.10' + provided 'org.mockito:mockito-core:1.8.5' +} + +jar { + manifest { + name = 'rxjava-quasar' + instruction 'Bundle-Vendor', 'Netflix' + instruction 'Bundle-DocURL', 'https://github.com/Netflix/RxJava' + instruction 'Import-Package', '!org.junit,!junit.framework,!org.mockito.*,*' + } +} + +tasks.withType(Test) { + jvmArgs "-javaagent:${configurations.quasar.iterator().next()}" // =vdmc (verbose, debug, allow monitors, check class) +} + +tasks.withType(JavaExec) { + jvmArgs "-javaagent:${configurations.quasar.iterator().next()}" // =vdmc (verbose, debug, allow monitors, check class) +} diff --git a/rxjava-contrib/rxjava-quasar/src/main/java/rx/quasar/ChannelObservable.java b/rxjava-contrib/rxjava-quasar/src/main/java/rx/quasar/ChannelObservable.java new file mode 100644 index 0000000000..17b8201887 --- /dev/null +++ b/rxjava-contrib/rxjava-quasar/src/main/java/rx/quasar/ChannelObservable.java @@ -0,0 +1,103 @@ +/** + * Copyright 2014 Netflix, Inc. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */package rx.quasar; + +import co.paralleluniverse.fibers.SuspendExecution; +import co.paralleluniverse.fibers.Suspendable; +import co.paralleluniverse.strands.Strand; +import co.paralleluniverse.strands.channels.ReceivePort; +import co.paralleluniverse.strands.channels.SendPort; +import rx.Observable; +import rx.Observer; +import rx.Scheduler; + +/** + * + */ +public class ChannelObservable { + + /** + * Converts an {@link Iterable} sequence into an Observable that emits each message received on the channel. + *

+ * + *

+ * @param channel + * the source {@link ReceivePort} + * @param + * the type of messages on the channel and the type of items to be + * emitted by the resulting Observable + * @return an Observable that emits each message received on the source {@link ReceivePort} + * @see RxJava Wiki: from() + */ + public final static Observable from(ReceivePort channel) { + return Observable.create(new OnSubscribeFromChannel(channel)); + } + + /** + * Converts an {@link Iterable} sequence into an Observable that operates on the specified + * scheduler, emitting each message received on the channel. + *

+ * + *

+ * @param channel + * the source {@link ReceivePort} + * @param scheduler + * the scheduler on which the Observable is to emit the messages received on the channel + * @param + * the type of messages on the channel and the type of items to be + * emitted by the resulting Observable + * @return an Observable that emits each message received on the source {@link ReceivePort}, on the + * specified scheduler + * @see RxJava Wiki: from() + * @see MSDN: Observable.ToObservable + */ + public final static Observable from(ReceivePort channel, Scheduler scheduler) { + return Observable.create(new OnSubscribeFromChannel(channel)).subscribeOn(scheduler); + } + + /** + * Converts a {@link SendPort} channel into an {@link Observer}. + *

+ * @param the type of messages that can be sent to the channel and the type of items to be + * received by the Observer + * @param channel the target {@link SendPort} + * @return + */ + public final static Observer to(final SendPort channel) { + return new Observer() { + + @Override + @Suspendable + public void onNext(T t) { + try { + channel.send(t); + } catch (InterruptedException ex) { + Strand.interrupted(); + } catch (SuspendExecution ex) { + throw new AssertionError(ex); + } + } + + @Override + public void onCompleted() { + channel.close(); + } + + @Override + public void onError(Throwable e) { + } + }; + } +} diff --git a/rxjava-contrib/rxjava-quasar/src/main/java/rx/quasar/NewFiberScheduler.java b/rxjava-contrib/rxjava-quasar/src/main/java/rx/quasar/NewFiberScheduler.java new file mode 100644 index 0000000000..ec6720d4b2 --- /dev/null +++ b/rxjava-contrib/rxjava-quasar/src/main/java/rx/quasar/NewFiberScheduler.java @@ -0,0 +1,145 @@ +/** + * Copyright 2014 Netflix, Inc. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package rx.quasar; + +import co.paralleluniverse.fibers.DefaultFiberScheduler; +import co.paralleluniverse.fibers.Fiber; +import co.paralleluniverse.fibers.FiberScheduler; +import co.paralleluniverse.fibers.SuspendExecution; +import co.paralleluniverse.strands.SuspendableRunnable; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import rx.Scheduler; +import rx.Subscription; +import rx.subscriptions.CompositeSubscription; +import rx.subscriptions.Subscriptions; +import rx.util.functions.Action1; + +/** + * Schedules work on a new fiber. + */ +public class NewFiberScheduler extends Scheduler { + private final static NewFiberScheduler DEFAULT_INSTANCE = new NewFiberScheduler(); + + public static NewFiberScheduler getDefaultInstance() { + return DEFAULT_INSTANCE; + } + + private final FiberScheduler fiberScheduler; + + public NewFiberScheduler(FiberScheduler fiberScheduler) { + if(fiberScheduler == null) + throw new IllegalArgumentException("Fiber scheduler is null"); + if(fiberScheduler == DefaultFiberScheduler.getInstance() && DEFAULT_INSTANCE != null) + throw new IllegalArgumentException("Fiber scheduler is the default FiberScheduler; use getDefaultInstance()"); + this.fiberScheduler = fiberScheduler; + } + + private NewFiberScheduler() { + this(DefaultFiberScheduler.getInstance()); + } + + @Override + public Subscription schedule(Action1 action) { + EventLoopScheduler innerScheduler = new EventLoopScheduler(); + innerScheduler.schedule(action); + return innerScheduler.innerSubscription; + } + + @Override + public Subscription schedule(Action1 action, long delayTime, TimeUnit unit) { + EventLoopScheduler innerScheduler = new EventLoopScheduler(); + innerScheduler.schedule(action, delayTime, unit); + return innerScheduler.innerSubscription; + } + + private class EventLoopScheduler extends Scheduler.Inner implements Subscription { + private final CompositeSubscription innerSubscription = new CompositeSubscription(); + + private EventLoopScheduler() { + } + + @Override + public void schedule(final Action1 action) { + if (innerSubscription.isUnsubscribed()) { + // don't schedule, we are unsubscribed + return; + } + + final AtomicReference sf = new AtomicReference(); + Subscription s = Subscriptions.from(new Fiber(fiberScheduler, new SuspendableRunnable() { + + @Override + public void run() throws SuspendExecution { + try { + if (innerSubscription.isUnsubscribed()) { + return; + } + action.call(EventLoopScheduler.this); + } finally { + // remove the subscription now that we're completed + Subscription s = sf.get(); + if (s != null) { + innerSubscription.remove(s); + } + } + } + }).start()); + + sf.set(s); + innerSubscription.add(s); + } + + @Override + public void schedule(final Action1 action, final long delayTime, final TimeUnit unit) { + final AtomicReference sf = new AtomicReference(); + + Subscription s = Subscriptions.from(new Fiber(fiberScheduler, new SuspendableRunnable() { + + @Override + public void run() throws InterruptedException, SuspendExecution { + Fiber.sleep(delayTime, unit); + try { + if (innerSubscription.isUnsubscribed()) { + return; + } + // now that the delay is past schedule the work to be done for real on the UI thread + action.call(EventLoopScheduler.this); + } finally { + // remove the subscription now that we're completed + Subscription s = sf.get(); + if (s != null) { + innerSubscription.remove(s); + } + } + } + }).start()); + + sf.set(s); + innerSubscription.add(s); + } + + @Override + public void unsubscribe() { + innerSubscription.unsubscribe(); + } + + @Override + public boolean isUnsubscribed() { + return innerSubscription.isUnsubscribed(); + } + } +} diff --git a/rxjava-contrib/rxjava-quasar/src/main/java/rx/quasar/OnSubscribeFromChannel.java b/rxjava-contrib/rxjava-quasar/src/main/java/rx/quasar/OnSubscribeFromChannel.java new file mode 100644 index 0000000000..77275213a1 --- /dev/null +++ b/rxjava-contrib/rxjava-quasar/src/main/java/rx/quasar/OnSubscribeFromChannel.java @@ -0,0 +1,61 @@ +/** + * Copyright 2014 Netflix, Inc. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package rx.quasar; + +import co.paralleluniverse.fibers.Suspendable; +import co.paralleluniverse.strands.channels.ReceivePort; +import rx.Observable.OnSubscribe; +import rx.Subscriber; + +/** + * Converts a {@link ReceivePort} into an Observable that emits each message received on the channel. + *

+ * + */ +public final class OnSubscribeFromChannel implements OnSubscribe { + + final ReceivePort channel; + + public OnSubscribeFromChannel(ReceivePort channel) { + this.channel = channel; + } + + @Override + @Suspendable + public void call(Subscriber o) { + for (;;) { + T m; + + try { + m = channel.receive(); + if (m == null) + break; + if (o.isUnsubscribed()) { + return; + } + } catch (InterruptedException e) { + break; + } catch (Exception e) { + o.onError(e); + continue; + } + + o.onNext(m); + } + + o.onCompleted(); + } +} diff --git a/rxjava-contrib/rxjava-quasar/src/main/resources/META-INF/suspendable-supers b/rxjava-contrib/rxjava-quasar/src/main/resources/META-INF/suspendable-supers new file mode 100644 index 0000000000..290b476b46 --- /dev/null +++ b/rxjava-contrib/rxjava-quasar/src/main/resources/META-INF/suspendable-supers @@ -0,0 +1,2 @@ +rx.util.functions.Action1.call +rx.Observer.onNext diff --git a/rxjava-contrib/rxjava-quasar/src/test/java/rx/quasar/ChannelObservableTest.java b/rxjava-contrib/rxjava-quasar/src/test/java/rx/quasar/ChannelObservableTest.java new file mode 100644 index 0000000000..c66f4ba6b8 --- /dev/null +++ b/rxjava-contrib/rxjava-quasar/src/test/java/rx/quasar/ChannelObservableTest.java @@ -0,0 +1,86 @@ +/** + * Copyright 2014 Netflix, Inc. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package rx.quasar; + +import co.paralleluniverse.fibers.SuspendExecution; +import co.paralleluniverse.fibers.Suspendable; +import co.paralleluniverse.strands.Strand; +import co.paralleluniverse.strands.channels.Channel; +import co.paralleluniverse.strands.channels.Channels; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicBoolean; +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; +import org.junit.Test; +import rx.Observer; + +public class ChannelObservableTest { + private static final int BUFFER_SIZE = 0; + private static final Channels.OverflowPolicy OVERFLOW_POLICY = Channels.OverflowPolicy.BLOCK; + + @Test + public void testObservableFromChannel() throws Exception { + final Channel c = Channels.newChannel(BUFFER_SIZE, OVERFLOW_POLICY); + + final Queue result = new ConcurrentLinkedQueue(); + final AtomicBoolean completed = new AtomicBoolean(); + + ChannelObservable.from(c).subscribeOn(NewFiberScheduler.getDefaultInstance()).subscribe(new Observer() { + @Override + @Suspendable + public void onNext(String t) { +// try { + System.out.println("GOT: " + t); + assertTrue(Strand.isCurrentFiber()); + System.out.println("GOT2: " + t); + //Strand.sleep(100); + System.out.println("GOT3: " + t); + result.add(t); + System.out.println("GOT4: " + t); +// } catch(InterruptedException e) { +// +// } catch (SuspendExecution e) { +// System.err.println("WHA????? " + e); +// throw new AssertionError(e); +// } + } + + @Override + public void onCompleted() { + completed.set(true); + } + + @Override + public void onError(Throwable e) { + + } + }); + + c.send("a"); + c.send("b"); + c.send("c"); + c.close(); + + Thread.sleep(500); + + assertThat(new ArrayList(result), equalTo(Arrays.asList("a", "b", "c"))); + assertThat(completed.get(), is(true)); + } +} diff --git a/rxjava-contrib/rxjava-quasar/src/test/java/rx/quasar/NewFiberSchedulerTest.java b/rxjava-contrib/rxjava-quasar/src/test/java/rx/quasar/NewFiberSchedulerTest.java new file mode 100644 index 0000000000..93ef9d73b9 --- /dev/null +++ b/rxjava-contrib/rxjava-quasar/src/test/java/rx/quasar/NewFiberSchedulerTest.java @@ -0,0 +1,32 @@ +/** + * Copyright 2014 Netflix, Inc. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package rx.quasar; + +import org.junit.Rule; +import org.junit.rules.TestRule; +import org.junit.rules.Timeout; +import rx.Scheduler; +import rx.schedulers.AbstractSchedulerConcurrencyTests; + +public class NewFiberSchedulerTest extends AbstractSchedulerConcurrencyTests { +// @Rule +// public TestRule globalTimeout = new Timeout(20000); + + @Override + protected Scheduler getScheduler() { + return NewFiberScheduler.getDefaultInstance(); + } +} diff --git a/settings.gradle b/settings.gradle index 2122bb8ff6..807fc32a13 100644 --- a/settings.gradle +++ b/settings.gradle @@ -10,4 +10,5 @@ include 'rxjava-core', \ 'rxjava-contrib:rxjava-apache-http', \ 'rxjava-contrib:rxjava-string', \ 'rxjava-contrib:rxjava-async-util', \ -'rxjava-contrib:rxjava-computation-expressions' +'rxjava-contrib:rxjava-computation-expressions',\ +'rxjava-contrib:rxjava-quasar' From 6c7ac7f81f6be7a6dc4383a959d00647d97306ee Mon Sep 17 00:00:00 2001 From: pron Date: Mon, 10 Feb 2014 18:50:23 +0200 Subject: [PATCH 002/422] tests pass --- .../java/rx/quasar/ChannelObservable.java | 2 + .../src/main/resources/META-INF/suspendables | 2 + .../java/rx/quasar/ChannelObservableTest.java | 52 +++++++++++-------- 3 files changed, 34 insertions(+), 22 deletions(-) create mode 100644 rxjava-contrib/rxjava-quasar/src/main/resources/META-INF/suspendables diff --git a/rxjava-contrib/rxjava-quasar/src/main/java/rx/quasar/ChannelObservable.java b/rxjava-contrib/rxjava-quasar/src/main/java/rx/quasar/ChannelObservable.java index 17b8201887..072733982f 100644 --- a/rxjava-contrib/rxjava-quasar/src/main/java/rx/quasar/ChannelObservable.java +++ b/rxjava-contrib/rxjava-quasar/src/main/java/rx/quasar/ChannelObservable.java @@ -22,6 +22,7 @@ import rx.Observable; import rx.Observer; import rx.Scheduler; +import rx.util.OnErrorNotImplementedException; /** * @@ -97,6 +98,7 @@ public void onCompleted() { @Override public void onError(Throwable e) { + throw new OnErrorNotImplementedException(e); } }; } diff --git a/rxjava-contrib/rxjava-quasar/src/main/resources/META-INF/suspendables b/rxjava-contrib/rxjava-quasar/src/main/resources/META-INF/suspendables new file mode 100644 index 0000000000..7ce00d743a --- /dev/null +++ b/rxjava-contrib/rxjava-quasar/src/main/resources/META-INF/suspendables @@ -0,0 +1,2 @@ +rx.Observable.subscribe +rx.observers.SafeSubscriber.onNext diff --git a/rxjava-contrib/rxjava-quasar/src/test/java/rx/quasar/ChannelObservableTest.java b/rxjava-contrib/rxjava-quasar/src/test/java/rx/quasar/ChannelObservableTest.java index c66f4ba6b8..c7af56da2d 100644 --- a/rxjava-contrib/rxjava-quasar/src/test/java/rx/quasar/ChannelObservableTest.java +++ b/rxjava-contrib/rxjava-quasar/src/test/java/rx/quasar/ChannelObservableTest.java @@ -22,44 +22,38 @@ import co.paralleluniverse.strands.channels.Channels; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collection; import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.atomic.AtomicBoolean; import static org.hamcrest.CoreMatchers.*; import static org.junit.Assert.*; import org.junit.Test; +import rx.Observable; import rx.Observer; public class ChannelObservableTest { - private static final int BUFFER_SIZE = 0; - private static final Channels.OverflowPolicy OVERFLOW_POLICY = Channels.OverflowPolicy.BLOCK; - @Test public void testObservableFromChannel() throws Exception { - final Channel c = Channels.newChannel(BUFFER_SIZE, OVERFLOW_POLICY); + final Channel c = Channels.newChannel(0); + + System.out.println("===== " + c); final Queue result = new ConcurrentLinkedQueue(); final AtomicBoolean completed = new AtomicBoolean(); - - ChannelObservable.from(c).subscribeOn(NewFiberScheduler.getDefaultInstance()).subscribe(new Observer() { + + ChannelObservable.from(c, NewFiberScheduler.getDefaultInstance()).subscribe(new Observer() { @Override @Suspendable public void onNext(String t) { -// try { + try { System.out.println("GOT: " + t); assertTrue(Strand.isCurrentFiber()); - System.out.println("GOT2: " + t); - //Strand.sleep(100); - System.out.println("GOT3: " + t); + Strand.sleep(100); result.add(t); - System.out.println("GOT4: " + t); -// } catch(InterruptedException e) { -// -// } catch (SuspendExecution e) { -// System.err.println("WHA????? " + e); -// throw new AssertionError(e); -// } + } catch (InterruptedException e) { + } catch (SuspendExecution e) { + throw new AssertionError(e); + } } @Override @@ -69,18 +63,32 @@ public void onCompleted() { @Override public void onError(Throwable e) { - + } }); - + c.send("a"); c.send("b"); c.send("c"); c.close(); - + Thread.sleep(500); - + assertThat(new ArrayList(result), equalTo(Arrays.asList("a", "b", "c"))); assertThat(completed.get(), is(true)); } + + @Test + public void testObserverChannel() throws Exception { + final Channel c = Channels.newChannel(10); // must use a buffer, otherwise will block on subscribe + + System.out.println("===== " + c); + + Observable.from(Arrays.asList("a", "b", "c")).subscribe(ChannelObservable.to(c)); + + assertThat(c.receive(), equalTo("a")); + assertThat(c.receive(), equalTo("b")); + assertThat(c.receive(), equalTo("c")); + assertThat(c.receive(), is(nullValue())); + } } From dd94e7b6a188ba2adeb6fe5b87d311be59e1fcc3 Mon Sep 17 00:00:00 2001 From: pron Date: Tue, 11 Feb 2014 01:04:11 +0200 Subject: [PATCH 003/422] Added ChannelObservable.subscribe --- rxjava-contrib/rxjava-quasar/README.md | 13 +- .../java/rx/quasar/ChannelObservable.java | 159 +++++++++++++++++- .../rx/quasar/OnSubscribeFromChannel.java | 2 +- .../resources/META-INF/suspendable-supers | 25 ++- .../src/main/resources/META-INF/suspendables | 33 +++- .../java/rx/quasar/ChannelObservableTest.java | 35 ++++ 6 files changed, 261 insertions(+), 6 deletions(-) diff --git a/rxjava-contrib/rxjava-quasar/README.md b/rxjava-contrib/rxjava-quasar/README.md index ab031fcda3..5c67aaee2b 100644 --- a/rxjava-contrib/rxjava-quasar/README.md +++ b/rxjava-contrib/rxjava-quasar/README.md @@ -29,7 +29,18 @@ and for Ivy: ``` -# Sample Usage +# Usage +As always when using Quasar, the java agent has to be started by adding the following JVM option: +``` +-javaagent:path-to-quasar-jar.jar +``` + +Alternatively, you can use AOT instrumentation (see [the Quasar documentation](http://docs.paralleluniverse.co/quasar/#instrumentation)). + +Or, if you're running in Tomcat, you can use the Quasar class loader (see [the Comsat documentation](http://docs.paralleluniverse.co/comsat/#enabling-comsat)). + +`Observer`, `Function` or `Action` method implementations can call fiber-blocking operations when using the `NewFiberScheduler` if the method is annotated with `@Suspendable`. +(rx-core `Observer`s, `Function`s or `Action`s that manipulate, transform or delegate to other `Observer`s, `Function`s or `Action`s are automatically instrumented). diff --git a/rxjava-contrib/rxjava-quasar/src/main/java/rx/quasar/ChannelObservable.java b/rxjava-contrib/rxjava-quasar/src/main/java/rx/quasar/ChannelObservable.java index 072733982f..bdfed38b7c 100644 --- a/rxjava-contrib/rxjava-quasar/src/main/java/rx/quasar/ChannelObservable.java +++ b/rxjava-contrib/rxjava-quasar/src/main/java/rx/quasar/ChannelObservable.java @@ -17,17 +17,24 @@ import co.paralleluniverse.fibers.SuspendExecution; import co.paralleluniverse.fibers.Suspendable; import co.paralleluniverse.strands.Strand; +import co.paralleluniverse.strands.Timeout; +import co.paralleluniverse.strands.channels.Channel; +import co.paralleluniverse.strands.channels.Channels; import co.paralleluniverse.strands.channels.ReceivePort; import co.paralleluniverse.strands.channels.SendPort; +import java.util.concurrent.TimeUnit; import rx.Observable; import rx.Observer; import rx.Scheduler; +import rx.util.Exceptions; import rx.util.OnErrorNotImplementedException; /** - * + * This class contains static methods that connect {@link Observable}s and {@link Channel}s. */ -public class ChannelObservable { +public final class ChannelObservable { + private ChannelObservable() { + } /** * Converts an {@link Iterable} sequence into an Observable that emits each message received on the channel. @@ -102,4 +109,152 @@ public void onError(Throwable e) { } }; } + + /** + * Creates a {@link ReceivePort} subscribed to an {@link Observable}. + *

+ * @param the type of messages emitted by the observable and received on the channel. + * @param bufferSize the channel's buffer size + * @param policy the channel's {@link Channels.OverflowPolicy OverflowPolicy} + * @param o the observable + * @return A new channel with the given buffer size and overflow policy that will receive all events emitted by the observable. + */ + public final static ReceivePort subscribe(int bufferSize, Channels.OverflowPolicy policy, Observable o) { + final ChannelWithErrors channel = new ChannelWithErrors(Channels.newChannel(bufferSize, policy)); + + o.subscribe(new Observer() { + @Override + @Suspendable + public void onNext(T t) { + try { + channel.sendPort().send(t); + } catch (InterruptedException ex) { + Strand.interrupted(); + } catch (SuspendExecution ex) { + throw new AssertionError(ex); + } + } + + @Override + public void onCompleted() { + channel.sendPort().close(); + } + + @Override + public void onError(Throwable e) { + channel.error(e); + } + }); + return channel.receivePort(); + } + + /** + * Creates a {@link ReceivePort} subscribed to an {@link Observable}. + *

+ * @param the type of messages emitted by the observable and received on the channel. + * @param bufferSize the channel's buffer size + * @param policy the channel's {@link Channels.OverflowPolicy OverflowPolicy} + * @param o the observable + * @param scheduler the scheduler used to emit the observable's events + * @return A new channel with the given buffer size and overflow policy that will receive all events emitted by the observable. + */ + public final static ReceivePort subscribe(int bufferSize, Channels.OverflowPolicy policy, Observable o, Scheduler scheduler) { + final ChannelWithErrors channel = new ChannelWithErrors(Channels.newChannel(bufferSize, policy)); + + o.subscribe(new Observer() { + @Override + @Suspendable + public void onNext(T t) { + try { + channel.sendPort().send(t); + } catch (InterruptedException ex) { + Strand.interrupted(); + } catch (SuspendExecution ex) { + throw new AssertionError(ex); + } + } + + @Override + public void onCompleted() { + channel.sendPort().close(); + } + + @Override + public void onError(Throwable e) { + channel.error(e); + } + }, scheduler); + return channel.receivePort(); + } + + private static class ChannelWithErrors { + private final Channel ch; + + public ChannelWithErrors(Channel ch) { + this.ch = ch; + } + + @Suspendable + public void error(Throwable t) { + try { + ch.send(new ThrowableWrapper(t)); + ch.close(); + } catch (InterruptedException e) { + } catch (SuspendExecution e) { + throw new AssertionError(e); + } + } + + public ReceivePort receivePort() { + return new ReceivePort() { + @Override + public T receive() throws SuspendExecution, InterruptedException { + return get(ch.receive()); + } + + @Override + public T receive(long timeout, TimeUnit unit) throws SuspendExecution, InterruptedException { + return get(ch.receive(timeout, unit)); + } + + @Override + public T receive(Timeout timeout) throws SuspendExecution, InterruptedException { + return get(ch.receive(timeout)); + } + + @Override + public T tryReceive() { + return get(ch.tryReceive()); + } + + @Override + public void close() { + ch.close(); + } + + @Override + public boolean isClosed() { + return ch.isClosed(); + } + }; + } + + public SendPort sendPort() { + return (SendPort) ch; + } + + private T get(Object m) { + if (m instanceof ThrowableWrapper) + throw Exceptions.propagate(((ThrowableWrapper) m).t); + return (T) m; + } + + private static class ThrowableWrapper { + final Throwable t; + + public ThrowableWrapper(Throwable t) { + this.t = t; + } + } + } } diff --git a/rxjava-contrib/rxjava-quasar/src/main/java/rx/quasar/OnSubscribeFromChannel.java b/rxjava-contrib/rxjava-quasar/src/main/java/rx/quasar/OnSubscribeFromChannel.java index 77275213a1..9644828947 100644 --- a/rxjava-contrib/rxjava-quasar/src/main/java/rx/quasar/OnSubscribeFromChannel.java +++ b/rxjava-contrib/rxjava-quasar/src/main/java/rx/quasar/OnSubscribeFromChannel.java @@ -50,7 +50,7 @@ public void call(Subscriber o) { break; } catch (Exception e) { o.onError(e); - continue; + return; } o.onNext(m); diff --git a/rxjava-contrib/rxjava-quasar/src/main/resources/META-INF/suspendable-supers b/rxjava-contrib/rxjava-quasar/src/main/resources/META-INF/suspendable-supers index 290b476b46..e547d95ce6 100644 --- a/rxjava-contrib/rxjava-quasar/src/main/resources/META-INF/suspendable-supers +++ b/rxjava-contrib/rxjava-quasar/src/main/resources/META-INF/suspendable-supers @@ -1,2 +1,25 @@ -rx.util.functions.Action1.call rx.Observer.onNext +rx.Observer.onError +rx.Observer.onCompleted +rx.util.functions.Action0.call +rx.util.functions.Action1.call +rx.util.functions.Action2.call +rx.util.functions.Action3.call +rx.util.functions.Action4.call +rx.util.functions.Action5.call +rx.util.functions.Action6.call +rx.util.functions.Action7.call +rx.util.functions.Action8.call +rx.util.functions.Action9.call +rx.util.functions.ActionN.call +rx.util.functions.Func0.call +rx.util.functions.Func1.call +rx.util.functions.Func2.call +rx.util.functions.Func3.call +rx.util.functions.Func4.call +rx.util.functions.Func5.call +rx.util.functions.Func6.call +rx.util.functions.Func7.call +rx.util.functions.Func8.call +rx.util.functions.Func9.call +rx.util.functions.FuncN.call diff --git a/rxjava-contrib/rxjava-quasar/src/main/resources/META-INF/suspendables b/rxjava-contrib/rxjava-quasar/src/main/resources/META-INF/suspendables index 7ce00d743a..9a93b3572f 100644 --- a/rxjava-contrib/rxjava-quasar/src/main/resources/META-INF/suspendables +++ b/rxjava-contrib/rxjava-quasar/src/main/resources/META-INF/suspendables @@ -1,2 +1,33 @@ +rx.Observer.onNext +rx.Observer.onError +rx.Observer.onCompleted rx.Observable.subscribe -rx.observers.SafeSubscriber.onNext +rx.util.functions.Actions.Actions$1.call +rx.util.functions.Actions.Actions$2.call +rx.util.functions.Actions.Actions$3.call +rx.util.functions.Actions.Actions$4.call +rx.util.functions.Actions.Actions$5.call +rx.util.functions.Actions.Actions$6.call +rx.util.functions.Actions.Actions$7.call +rx.util.functions.Actions.Actions$8.call +rx.util.functions.Actions.Actions$9.call +rx.util.functions.Actions.Actions$10.call +rx.util.functions.Actions.Actions$11.call +rx.util.functions.Actions.Actions$12.call +rx.util.functions.Actions.Actions$13.call +rx.util.functions.Actions.Actions$14.call +rx.util.functions.Actions.Functions$1.call +rx.util.functions.Actions.Functions$2.call +rx.util.functions.Actions.Functions$3.call +rx.util.functions.Actions.Functions$4.call +rx.util.functions.Actions.Functions$5.call +rx.util.functions.Actions.Functions$6.call +rx.util.functions.Actions.Functions$7.call +rx.util.functions.Actions.Functions$8.call +rx.util.functions.Actions.Functions$9.call +rx.util.functions.Actions.Functions$10.call +rx.util.functions.Actions.Functions$11.call +rx.util.functions.Actions.Functions$12.call +rx.util.functions.Actions.Functions$13.call +rx.util.functions.Actions.Functions$14.call +rx.util.functions.Actions.Functions$15.call diff --git a/rxjava-contrib/rxjava-quasar/src/test/java/rx/quasar/ChannelObservableTest.java b/rxjava-contrib/rxjava-quasar/src/test/java/rx/quasar/ChannelObservableTest.java index c7af56da2d..d2d2f11065 100644 --- a/rxjava-contrib/rxjava-quasar/src/test/java/rx/quasar/ChannelObservableTest.java +++ b/rxjava-contrib/rxjava-quasar/src/test/java/rx/quasar/ChannelObservableTest.java @@ -20,6 +20,7 @@ import co.paralleluniverse.strands.Strand; import co.paralleluniverse.strands.channels.Channel; import co.paralleluniverse.strands.channels.Channels; +import co.paralleluniverse.strands.channels.ReceivePort; import java.util.ArrayList; import java.util.Arrays; import java.util.Queue; @@ -30,6 +31,7 @@ import org.junit.Test; import rx.Observable; import rx.Observer; +import rx.subjects.PublishSubject; public class ChannelObservableTest { @Test @@ -91,4 +93,37 @@ public void testObserverChannel() throws Exception { assertThat(c.receive(), equalTo("c")); assertThat(c.receive(), is(nullValue())); } + + @Test + public void testObserverChannel2() throws Exception { + ReceivePort c = ChannelObservable.subscribe(10, Channels.OverflowPolicy.BLOCK, Observable.from(Arrays.asList("a", "b", "c"))); + + assertThat(c.receive(), equalTo("a")); + assertThat(c.receive(), equalTo("b")); + assertThat(c.receive(), equalTo("c")); + assertThat(c.receive(), is(nullValue())); + } + + @Test + public void testObserverChannelWithError() throws Exception { + PublishSubject o = PublishSubject.create(); + ReceivePort c = ChannelObservable.subscribe(10, Channels.OverflowPolicy.BLOCK, o); + + o.onNext("a"); + o.onError(new MyException()); + o.onNext("c"); + + assertThat(c.receive(), equalTo("a")); + try { + c.receive(); + fail(); + } catch(MyException e) { + + } + assertThat(c.receive(), is(nullValue())); + } + + static class MyException extends RuntimeException { + + } } From a2ac4aa8058455ed47626c430e94bef9fa32263d Mon Sep 17 00:00:00 2001 From: pron Date: Tue, 11 Feb 2014 16:43:38 +0200 Subject: [PATCH 004/422] Use a custom SuspendableClassifier --- .../java/rx/quasar/ChannelObservable.java | 48 ++++------- .../rx/quasar/RxSuspendableClassifier.java | 79 +++++++++++++++++++ ...se.fibers.instrument.SuspendableClassifier | 1 + .../src/main/resources/META-INF/suspendables | 32 -------- 4 files changed, 94 insertions(+), 66 deletions(-) create mode 100644 rxjava-contrib/rxjava-quasar/src/main/java/rx/quasar/RxSuspendableClassifier.java create mode 100644 rxjava-contrib/rxjava-quasar/src/main/resources/META-INF/services/co.paralleluniverse.fibers.instrument.SuspendableClassifier diff --git a/rxjava-contrib/rxjava-quasar/src/main/java/rx/quasar/ChannelObservable.java b/rxjava-contrib/rxjava-quasar/src/main/java/rx/quasar/ChannelObservable.java index bdfed38b7c..605eb04dc8 100644 --- a/rxjava-contrib/rxjava-quasar/src/main/java/rx/quasar/ChannelObservable.java +++ b/rxjava-contrib/rxjava-quasar/src/main/java/rx/quasar/ChannelObservable.java @@ -28,6 +28,10 @@ import rx.Scheduler; import rx.util.Exceptions; import rx.util.OnErrorNotImplementedException; +import rx.util.functions.Action2; +import rx.util.functions.Actions; +import rx.util.functions.Func1; +import rx.util.functions.Functions; /** * This class contains static methods that connect {@link Observable}s and {@link Channel}s. @@ -122,45 +126,21 @@ public void onError(Throwable e) { public final static ReceivePort subscribe(int bufferSize, Channels.OverflowPolicy policy, Observable o) { final ChannelWithErrors channel = new ChannelWithErrors(Channels.newChannel(bufferSize, policy)); - o.subscribe(new Observer() { - @Override - @Suspendable - public void onNext(T t) { - try { - channel.sendPort().send(t); - } catch (InterruptedException ex) { - Strand.interrupted(); - } catch (SuspendExecution ex) { - throw new AssertionError(ex); - } - } + System.out.println(Functions.fromFunc(new Func1() { @Override - public void onCompleted() { - channel.sendPort().close(); + public String call(String t1) { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. } + })); + System.out.println(Actions.toFunc(new Action2() { @Override - public void onError(Throwable e) { - channel.error(e); + public void call(String t1, String t2) { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. } - }); - return channel.receivePort(); - } - - /** - * Creates a {@link ReceivePort} subscribed to an {@link Observable}. - *

- * @param the type of messages emitted by the observable and received on the channel. - * @param bufferSize the channel's buffer size - * @param policy the channel's {@link Channels.OverflowPolicy OverflowPolicy} - * @param o the observable - * @param scheduler the scheduler used to emit the observable's events - * @return A new channel with the given buffer size and overflow policy that will receive all events emitted by the observable. - */ - public final static ReceivePort subscribe(int bufferSize, Channels.OverflowPolicy policy, Observable o, Scheduler scheduler) { - final ChannelWithErrors channel = new ChannelWithErrors(Channels.newChannel(bufferSize, policy)); - + })); + o.subscribe(new Observer() { @Override @Suspendable @@ -183,7 +163,7 @@ public void onCompleted() { public void onError(Throwable e) { channel.error(e); } - }, scheduler); + }); return channel.receivePort(); } diff --git a/rxjava-contrib/rxjava-quasar/src/main/java/rx/quasar/RxSuspendableClassifier.java b/rxjava-contrib/rxjava-quasar/src/main/java/rx/quasar/RxSuspendableClassifier.java new file mode 100644 index 0000000000..8a985cbfdb --- /dev/null +++ b/rxjava-contrib/rxjava-quasar/src/main/java/rx/quasar/RxSuspendableClassifier.java @@ -0,0 +1,79 @@ +/** + * Copyright 2014 Netflix, Inc. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package rx.quasar; + +import co.paralleluniverse.fibers.instrument.MethodDatabase; +import co.paralleluniverse.fibers.instrument.SimpleSuspendableClassifier; +import co.paralleluniverse.fibers.instrument.SuspendableClassifier; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +public class RxSuspendableClassifier implements SuspendableClassifier { + private static final Set CORE_PACKAGES = new HashSet(Arrays.asList(new String[]{ + "rx", "rx.joins", "rx.observables", "rx.observers", "rx.operators", "rx.plugins", "rx.schedulers", + "rx.subjects", "rx.subscriptions", "rx.util", "rx.util.functions" + })); + + private static final Set EXCEPTIONS = new HashSet(Arrays.asList(new String[]{ + "rx/observers/SynchronizedObserver", + "rx/schedulers/AbstractSchedulerTests$ConcurrentObserverValidator",})); + + private static final Set OBSERVER_METHODS = new HashSet(Arrays.asList(new String[]{ + "onNext(Ljava/lang/Object;)V", "onCompleted()V", "onError(Ljava/lang/Throwable;)V" + })); + + private static final String FUNCTION_METHOD = "call"; + + @Override + public MethodDatabase.SuspendableType isSuspendable(MethodDatabase db, String className, String superClassName, String[] interfaces, String methodName, String methodDesc, String methodSignature, String[] methodExceptions) { + MethodDatabase.SuspendableType s = null; + if (isCoreRx(className) && !EXCEPTIONS.contains(className)) { + if (isObserverImplementation(db, className, superClassName, interfaces, methodName, methodDesc)) + s = MethodDatabase.SuspendableType.SUSPENDABLE; + else if (isUtilFunction(db, className, superClassName, interfaces, methodName, methodDesc)) + s = MethodDatabase.SuspendableType.SUSPENDABLE; + } + // System.out.println("-- " + className + "." + methodName + ": " + s); + return s; + } + + private boolean isCoreRx(String className) { + return CORE_PACKAGES.contains(packageOf(className)); + } + + private static boolean isObserverImplementation(MethodDatabase db, String className, String superClassName, String[] interfaces, String methodName, String methodDesc) { + return !className.equals("rx/Observer") + && OBSERVER_METHODS.contains(methodName + methodDesc) + && SimpleSuspendableClassifier.extendsOrImplements("rx/Observer", db, className, superClassName, interfaces); + } + + private static boolean isUtilFunction(MethodDatabase db, String className, String superClassName, String[] interfaces, String methodName, String methodDesc) { + return (className.startsWith("rx/util/functions/Functions") || className.startsWith("rx/util/functions/Actions")) + && methodName.equals(FUNCTION_METHOD) + && (SimpleSuspendableClassifier.extendsOrImplements("rx/util/functions/Function", db, className, superClassName, interfaces) + || SimpleSuspendableClassifier.extendsOrImplements("rx/util/functions/Action", db, className, superClassName, interfaces)); + } + + private static String packageOf(String className) { + try { + return className.substring(0, className.lastIndexOf('/')).replace('/', '.'); + } catch (RuntimeException e) { + System.err.println("???? " + className); + throw e; + } + } +} diff --git a/rxjava-contrib/rxjava-quasar/src/main/resources/META-INF/services/co.paralleluniverse.fibers.instrument.SuspendableClassifier b/rxjava-contrib/rxjava-quasar/src/main/resources/META-INF/services/co.paralleluniverse.fibers.instrument.SuspendableClassifier new file mode 100644 index 0000000000..72a1acf3c9 --- /dev/null +++ b/rxjava-contrib/rxjava-quasar/src/main/resources/META-INF/services/co.paralleluniverse.fibers.instrument.SuspendableClassifier @@ -0,0 +1 @@ +rx.quasar.RxSuspendableClassifier diff --git a/rxjava-contrib/rxjava-quasar/src/main/resources/META-INF/suspendables b/rxjava-contrib/rxjava-quasar/src/main/resources/META-INF/suspendables index 9a93b3572f..d364ba2406 100644 --- a/rxjava-contrib/rxjava-quasar/src/main/resources/META-INF/suspendables +++ b/rxjava-contrib/rxjava-quasar/src/main/resources/META-INF/suspendables @@ -1,33 +1 @@ -rx.Observer.onNext -rx.Observer.onError -rx.Observer.onCompleted rx.Observable.subscribe -rx.util.functions.Actions.Actions$1.call -rx.util.functions.Actions.Actions$2.call -rx.util.functions.Actions.Actions$3.call -rx.util.functions.Actions.Actions$4.call -rx.util.functions.Actions.Actions$5.call -rx.util.functions.Actions.Actions$6.call -rx.util.functions.Actions.Actions$7.call -rx.util.functions.Actions.Actions$8.call -rx.util.functions.Actions.Actions$9.call -rx.util.functions.Actions.Actions$10.call -rx.util.functions.Actions.Actions$11.call -rx.util.functions.Actions.Actions$12.call -rx.util.functions.Actions.Actions$13.call -rx.util.functions.Actions.Actions$14.call -rx.util.functions.Actions.Functions$1.call -rx.util.functions.Actions.Functions$2.call -rx.util.functions.Actions.Functions$3.call -rx.util.functions.Actions.Functions$4.call -rx.util.functions.Actions.Functions$5.call -rx.util.functions.Actions.Functions$6.call -rx.util.functions.Actions.Functions$7.call -rx.util.functions.Actions.Functions$8.call -rx.util.functions.Actions.Functions$9.call -rx.util.functions.Actions.Functions$10.call -rx.util.functions.Actions.Functions$11.call -rx.util.functions.Actions.Functions$12.call -rx.util.functions.Actions.Functions$13.call -rx.util.functions.Actions.Functions$14.call -rx.util.functions.Actions.Functions$15.call From f4ea0737c846a0a441eb2dbab27b6d69a099d064 Mon Sep 17 00:00:00 2001 From: pron Date: Wed, 12 Feb 2014 13:51:07 +0200 Subject: [PATCH 005/422] minor fix in classifier --- .../src/main/java/rx/quasar/RxSuspendableClassifier.java | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/rxjava-contrib/rxjava-quasar/src/main/java/rx/quasar/RxSuspendableClassifier.java b/rxjava-contrib/rxjava-quasar/src/main/java/rx/quasar/RxSuspendableClassifier.java index 8a985cbfdb..14b24f294d 100644 --- a/rxjava-contrib/rxjava-quasar/src/main/java/rx/quasar/RxSuspendableClassifier.java +++ b/rxjava-contrib/rxjava-quasar/src/main/java/rx/quasar/RxSuspendableClassifier.java @@ -69,11 +69,6 @@ private static boolean isUtilFunction(MethodDatabase db, String className, Strin } private static String packageOf(String className) { - try { - return className.substring(0, className.lastIndexOf('/')).replace('/', '.'); - } catch (RuntimeException e) { - System.err.println("???? " + className); - throw e; - } + return className.substring(0, Math.max(0, className.lastIndexOf('/'))).replace('/', '.'); } } From 53fb292f3a6130c91e320ee5721f6affee0cd460 Mon Sep 17 00:00:00 2001 From: pron Date: Mon, 17 Feb 2014 19:48:38 +0200 Subject: [PATCH 006/422] map Observable.onError(Throwable) to SendPort.close(Throwable) in --- .../java/rx/quasar/ChannelObservable.java | 87 ++----------------- .../java/rx/quasar/ChannelObservableTest.java | 7 +- 2 files changed, 10 insertions(+), 84 deletions(-) diff --git a/rxjava-contrib/rxjava-quasar/src/main/java/rx/quasar/ChannelObservable.java b/rxjava-contrib/rxjava-quasar/src/main/java/rx/quasar/ChannelObservable.java index 605eb04dc8..67d09dde4a 100644 --- a/rxjava-contrib/rxjava-quasar/src/main/java/rx/quasar/ChannelObservable.java +++ b/rxjava-contrib/rxjava-quasar/src/main/java/rx/quasar/ChannelObservable.java @@ -17,17 +17,13 @@ import co.paralleluniverse.fibers.SuspendExecution; import co.paralleluniverse.fibers.Suspendable; import co.paralleluniverse.strands.Strand; -import co.paralleluniverse.strands.Timeout; import co.paralleluniverse.strands.channels.Channel; import co.paralleluniverse.strands.channels.Channels; import co.paralleluniverse.strands.channels.ReceivePort; import co.paralleluniverse.strands.channels.SendPort; -import java.util.concurrent.TimeUnit; import rx.Observable; import rx.Observer; import rx.Scheduler; -import rx.util.Exceptions; -import rx.util.OnErrorNotImplementedException; import rx.util.functions.Action2; import rx.util.functions.Actions; import rx.util.functions.Func1; @@ -109,7 +105,7 @@ public void onCompleted() { @Override public void onError(Throwable e) { - throw new OnErrorNotImplementedException(e); + channel.close(e); } }; } @@ -124,7 +120,7 @@ public void onError(Throwable e) { * @return A new channel with the given buffer size and overflow policy that will receive all events emitted by the observable. */ public final static ReceivePort subscribe(int bufferSize, Channels.OverflowPolicy policy, Observable o) { - final ChannelWithErrors channel = new ChannelWithErrors(Channels.newChannel(bufferSize, policy)); + final Channel channel = Channels.newChannel(bufferSize, policy); System.out.println(Functions.fromFunc(new Func1() { @@ -146,7 +142,7 @@ public void call(String t1, String t2) { @Suspendable public void onNext(T t) { try { - channel.sendPort().send(t); + channel.send(t); } catch (InterruptedException ex) { Strand.interrupted(); } catch (SuspendExecution ex) { @@ -156,85 +152,14 @@ public void onNext(T t) { @Override public void onCompleted() { - channel.sendPort().close(); + channel.close(); } @Override public void onError(Throwable e) { - channel.error(e); + channel.close(e); } }); - return channel.receivePort(); - } - - private static class ChannelWithErrors { - private final Channel ch; - - public ChannelWithErrors(Channel ch) { - this.ch = ch; - } - - @Suspendable - public void error(Throwable t) { - try { - ch.send(new ThrowableWrapper(t)); - ch.close(); - } catch (InterruptedException e) { - } catch (SuspendExecution e) { - throw new AssertionError(e); - } - } - - public ReceivePort receivePort() { - return new ReceivePort() { - @Override - public T receive() throws SuspendExecution, InterruptedException { - return get(ch.receive()); - } - - @Override - public T receive(long timeout, TimeUnit unit) throws SuspendExecution, InterruptedException { - return get(ch.receive(timeout, unit)); - } - - @Override - public T receive(Timeout timeout) throws SuspendExecution, InterruptedException { - return get(ch.receive(timeout)); - } - - @Override - public T tryReceive() { - return get(ch.tryReceive()); - } - - @Override - public void close() { - ch.close(); - } - - @Override - public boolean isClosed() { - return ch.isClosed(); - } - }; - } - - public SendPort sendPort() { - return (SendPort) ch; - } - - private T get(Object m) { - if (m instanceof ThrowableWrapper) - throw Exceptions.propagate(((ThrowableWrapper) m).t); - return (T) m; - } - - private static class ThrowableWrapper { - final Throwable t; - - public ThrowableWrapper(Throwable t) { - this.t = t; - } - } + return channel; } } diff --git a/rxjava-contrib/rxjava-quasar/src/test/java/rx/quasar/ChannelObservableTest.java b/rxjava-contrib/rxjava-quasar/src/test/java/rx/quasar/ChannelObservableTest.java index d2d2f11065..cce90923de 100644 --- a/rxjava-contrib/rxjava-quasar/src/test/java/rx/quasar/ChannelObservableTest.java +++ b/rxjava-contrib/rxjava-quasar/src/test/java/rx/quasar/ChannelObservableTest.java @@ -20,6 +20,7 @@ import co.paralleluniverse.strands.Strand; import co.paralleluniverse.strands.channels.Channel; import co.paralleluniverse.strands.channels.Channels; +import co.paralleluniverse.strands.channels.ProducerException; import co.paralleluniverse.strands.channels.ReceivePort; import java.util.ArrayList; import java.util.Arrays; @@ -117,10 +118,10 @@ public void testObserverChannelWithError() throws Exception { try { c.receive(); fail(); - } catch(MyException e) { - + } catch(ProducerException e) { + assertThat(e.getCause(), instanceOf(MyException.class)); } - assertThat(c.receive(), is(nullValue())); + assertThat(c.isClosed(), is(true)); } static class MyException extends RuntimeException { From 43719c7dfd29922dd45a007923405958aa540f2d Mon Sep 17 00:00:00 2001 From: pron Date: Mon, 17 Feb 2014 20:08:01 +0200 Subject: [PATCH 007/422] - --- .../java/rx/quasar/ChannelObservable.java | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/rxjava-contrib/rxjava-quasar/src/main/java/rx/quasar/ChannelObservable.java b/rxjava-contrib/rxjava-quasar/src/main/java/rx/quasar/ChannelObservable.java index 67d09dde4a..45bcaa01c6 100644 --- a/rxjava-contrib/rxjava-quasar/src/main/java/rx/quasar/ChannelObservable.java +++ b/rxjava-contrib/rxjava-quasar/src/main/java/rx/quasar/ChannelObservable.java @@ -24,10 +24,6 @@ import rx.Observable; import rx.Observer; import rx.Scheduler; -import rx.util.functions.Action2; -import rx.util.functions.Actions; -import rx.util.functions.Func1; -import rx.util.functions.Functions; /** * This class contains static methods that connect {@link Observable}s and {@link Channel}s. @@ -121,21 +117,6 @@ public void onError(Throwable e) { */ public final static ReceivePort subscribe(int bufferSize, Channels.OverflowPolicy policy, Observable o) { final Channel channel = Channels.newChannel(bufferSize, policy); - - System.out.println(Functions.fromFunc(new Func1() { - - @Override - public String call(String t1) { - throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. - } - })); - System.out.println(Actions.toFunc(new Action2() { - - @Override - public void call(String t1, String t2) { - throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. - } - })); o.subscribe(new Observer() { @Override From 6bece88c6941a754a8cc054bbe9946f1e7c0c615 Mon Sep 17 00:00:00 2001 From: pron Date: Thu, 20 Feb 2014 20:08:46 +0200 Subject: [PATCH 008/422] Added ChannelObservable.get --- .../java/rx/quasar/ChannelObservable.java | 82 ++++++++++++++++-- .../java/rx/quasar/ChannelObservableTest.java | 84 ++++++++++++++++++- 2 files changed, 157 insertions(+), 9 deletions(-) diff --git a/rxjava-contrib/rxjava-quasar/src/main/java/rx/quasar/ChannelObservable.java b/rxjava-contrib/rxjava-quasar/src/main/java/rx/quasar/ChannelObservable.java index 45bcaa01c6..97e9af950b 100644 --- a/rxjava-contrib/rxjava-quasar/src/main/java/rx/quasar/ChannelObservable.java +++ b/rxjava-contrib/rxjava-quasar/src/main/java/rx/quasar/ChannelObservable.java @@ -14,6 +14,7 @@ * limitations under the License. */package rx.quasar; +import co.paralleluniverse.fibers.FiberAsync; import co.paralleluniverse.fibers.SuspendExecution; import co.paralleluniverse.fibers.Suspendable; import co.paralleluniverse.strands.Strand; @@ -21,6 +22,9 @@ import co.paralleluniverse.strands.channels.Channels; import co.paralleluniverse.strands.channels.ReceivePort; import co.paralleluniverse.strands.channels.SendPort; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import rx.Observable; import rx.Observer; import rx.Scheduler; @@ -45,7 +49,7 @@ private ChannelObservable() { * @return an Observable that emits each message received on the source {@link ReceivePort} * @see RxJava Wiki: from() */ - public final static Observable from(ReceivePort channel) { + public static Observable from(ReceivePort channel) { return Observable.create(new OnSubscribeFromChannel(channel)); } @@ -67,7 +71,7 @@ public final static Observable from(ReceivePort channel) { * @see RxJava Wiki: from() * @see MSDN: Observable.ToObservable */ - public final static Observable from(ReceivePort channel, Scheduler scheduler) { + public static Observable from(ReceivePort channel, Scheduler scheduler) { return Observable.create(new OnSubscribeFromChannel(channel)).subscribeOn(scheduler); } @@ -79,7 +83,7 @@ public final static Observable from(ReceivePort channel, Scheduler sch * @param channel the target {@link SendPort} * @return */ - public final static Observer to(final SendPort channel) { + public static Observer to(final SendPort channel) { return new Observer() { @Override @@ -115,9 +119,9 @@ public void onError(Throwable e) { * @param o the observable * @return A new channel with the given buffer size and overflow policy that will receive all events emitted by the observable. */ - public final static ReceivePort subscribe(int bufferSize, Channels.OverflowPolicy policy, Observable o) { + public static ReceivePort subscribe(int bufferSize, Channels.OverflowPolicy policy, Observable o) { final Channel channel = Channels.newChannel(bufferSize, policy); - + o.subscribe(new Observer() { @Override @Suspendable @@ -143,4 +147,72 @@ public void onError(Throwable e) { }); return channel; } + + /** + * Takes an observable that generates at most one value, blocks until it completes and returns the result. + * If the observable completes before a value has been emitted, this method returns {@code null}. + * It the observable fails, this function throws an {@link ExecutionException} that wraps the observable's exception. + * + * @param o the observable + * @return the observable's result, or {@code null} if the observable completes before a value is emitted. + * @throws ExecutionException if the observable fails + */ + public static T get(final Observable o) throws ExecutionException, SuspendExecution, InterruptedException { + return new AsyncObservable(o).run(); + } + + /** + * Takes an observable that generates at most one value, blocks until it completes or the timeout expires, and returns the result. + * If the observable completes before a value has been emitted, this method returns {@code null}. + * It the observable fails, this function throws an {@link ExecutionException} that wraps the observable's exception. + * + * @param o the observable + * @param timeout the maximum time this method will blcok + * @param unit the timeout's time unit + * @return the observable's result, or {@code null} if the observable completes before a value is emitted. + * @throws ExecutionException if the observable fails + * @throws TimeoutException if the timeout expires before the observable completes + */ + public static T get(final Observable o, long timeout, TimeUnit unit) throws ExecutionException, SuspendExecution, InterruptedException, TimeoutException { + return new AsyncObservable(o).run(timeout, unit); + } + + private static class AsyncObservable extends FiberAsync implements Observer { + private final Observable o; + + public AsyncObservable(Observable o) { + this.o = o; + } + + @Override + protected Void requestAsync() { + o.subscribe(this); + return null; + } + + @Override + public void onNext(T t) { + if (isCompleted()) + throw new IllegalStateException("Operation already completed"); + asyncCompleted(t); + } + + @Override + public void onError(Throwable e) { + if (isCompleted()) + throw new IllegalStateException("Operation already completed"); + asyncFailed(e); + } + + @Override + public void onCompleted() { + if (!isCompleted()) + asyncCompleted(null); + } + + @Override + protected ExecutionException wrapException(Throwable t) { + return new ExecutionException(t); + } + } } diff --git a/rxjava-contrib/rxjava-quasar/src/test/java/rx/quasar/ChannelObservableTest.java b/rxjava-contrib/rxjava-quasar/src/test/java/rx/quasar/ChannelObservableTest.java index cce90923de..63ef447af3 100644 --- a/rxjava-contrib/rxjava-quasar/src/test/java/rx/quasar/ChannelObservableTest.java +++ b/rxjava-contrib/rxjava-quasar/src/test/java/rx/quasar/ChannelObservableTest.java @@ -15,9 +15,11 @@ */ package rx.quasar; +import co.paralleluniverse.fibers.Fiber; import co.paralleluniverse.fibers.SuspendExecution; import co.paralleluniverse.fibers.Suspendable; import co.paralleluniverse.strands.Strand; +import co.paralleluniverse.strands.SuspendableCallable; import co.paralleluniverse.strands.channels.Channel; import co.paralleluniverse.strands.channels.Channels; import co.paralleluniverse.strands.channels.ProducerException; @@ -26,6 +28,7 @@ import java.util.Arrays; import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ExecutionException; import java.util.concurrent.atomic.AtomicBoolean; import static org.hamcrest.CoreMatchers.*; import static org.junit.Assert.*; @@ -113,18 +116,91 @@ public void testObserverChannelWithError() throws Exception { o.onNext("a"); o.onError(new MyException()); o.onNext("c"); - + assertThat(c.receive(), equalTo("a")); try { c.receive(); fail(); - } catch(ProducerException e) { + } catch (ProducerException e) { assertThat(e.getCause(), instanceOf(MyException.class)); } assertThat(c.isClosed(), is(true)); } - + + @Test + public void whenGetThenBlockAndReturnResult() throws Exception { + final PublishSubject o = PublishSubject.create(); + + Fiber f = new Fiber(new SuspendableCallable() { + + @Override + public String run() throws SuspendExecution, InterruptedException { + try { + return ChannelObservable.get(o); + } catch (ExecutionException e) { + throw new AssertionError(); + } + } + }).start(); + + Thread.sleep(100); + + o.onNext("foo"); + o.onCompleted(); + + assertThat(f.get(), equalTo("foo")); + } + + @Test + public void whenGetAndObservableFailsThenThrowExecutionException() throws Exception { + final PublishSubject o = PublishSubject.create(); + + Fiber f = new Fiber(new SuspendableCallable() { + + @Override + public String run() throws SuspendExecution, InterruptedException { + try { + return ChannelObservable.get(o); + } catch (ExecutionException e) { + return e.getCause().getMessage(); + } + } + }).start(); + + Thread.sleep(100); + + o.onError(new Exception("ohoh")); + + assertThat(f.get(), equalTo("ohoh")); + } + + @Test + public void whenGetAndObservableEmitsTwoValuesThenBlowup() throws Exception { + final PublishSubject o = PublishSubject.create(); + + Fiber f = new Fiber(new SuspendableCallable() { + + @Override + public String run() throws SuspendExecution, InterruptedException { + try { + return ChannelObservable.get(o); + } catch (ExecutionException e) { + throw new AssertionError(); + } + } + }).start(); + + Thread.sleep(100); + + o.onNext("foo"); + try { + o.onNext("bar"); + fail(); + } catch (Exception e) { + } + } + static class MyException extends RuntimeException { - + } } From a1f7c029b0bd2e0b6883eff481fc7a74eb014d58 Mon Sep 17 00:00:00 2001 From: Samuel Gruetter Date: Fri, 21 Feb 2014 18:59:42 +0100 Subject: [PATCH 009/422] Observable.sample(Observable) in Scala --- .../src/main/scala/rx/lang/scala/Observable.scala | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/Observable.scala b/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/Observable.scala index 9243ec3e6c..f120d72900 100644 --- a/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/Observable.scala +++ b/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/Observable.scala @@ -1132,6 +1132,21 @@ trait Observable[+T] toScalaObservable[T](asJavaObservable.sample(duration.length, duration.unit, scheduler)) } + /** + * Return an Observable that emits the results of sampling the items emitted by the source Observable + * whenever the specified sampler Observable emits an item or completes. + * + * + * + * @param sampler + * the Observable to use for sampling the source Observable + * @return an Observable that emits the results of sampling the items emitted by this Observable whenever + * the sampler Observable emits an item or completes + */ + def sample(sampler: Observable[Any]): Observable[T] = { + toScalaObservable[T](asJavaObservable.sample(sampler)) + } + /** * Returns an Observable that applies a function of your choosing to the first item emitted by a * source Observable, then feeds the result of that function along with the second item emitted From 045d4f9d22f234bfdac4912a90dde79054ccc4f1 Mon Sep 17 00:00:00 2001 From: Samuel Gruetter Date: Fri, 21 Feb 2014 19:00:14 +0100 Subject: [PATCH 010/422] tweak RxScalaDemo (and Olympics example) --- .../rx/lang/scala/examples/Olympics.scala | 28 +++++++++++++---- .../rx/lang/scala/examples/RxScalaDemo.scala | 30 +++++++++++-------- 2 files changed, 39 insertions(+), 19 deletions(-) diff --git a/language-adaptors/rxjava-scala/src/examples/scala/rx/lang/scala/examples/Olympics.scala b/language-adaptors/rxjava-scala/src/examples/scala/rx/lang/scala/examples/Olympics.scala index e91de4b51d..dba0019cdb 100644 --- a/language-adaptors/rxjava-scala/src/examples/scala/rx/lang/scala/examples/Olympics.scala +++ b/language-adaptors/rxjava-scala/src/examples/scala/rx/lang/scala/examples/Olympics.scala @@ -22,6 +22,7 @@ object Olympics { case class Medal(val year: Int, val games: String, val discipline: String, val medal: String, val athlete: String, val country: String) def mountainBikeMedals: Observable[Medal] = Observable.items( + duration(100 millis), // a short delay because medals are only awarded some time after the Games began Observable.items( Medal(1996, "Atlanta 1996", "cross-country men", "Gold", "Bart BRENTJENS", "Netherlands"), Medal(1996, "Atlanta 1996", "cross-country women", "Gold", "Paola PEZZO", "Italy"), @@ -69,18 +70,33 @@ object Olympics { ).concat // speed it up :D - val fourYears = 4000.millis + val oneYear = 1000.millis - val neverUsedDummyMedal = Medal(3333, "?", "?", "?", "?", "?") + //val neverUsedDummyMedal = Medal(3333, "?", "?", "?", "?", "?") - def fourYearsEmpty: Observable[Medal] = { + /** runs an infinite loop, and returns Bottom type (Nothing) */ + def getNothing: Nothing = { + println("You shouldn't have called this method ;-)") + getNothing + } + + /** returns an Observable which emits no elements and completes after a duration of d */ + def duration(d: Duration): Observable[Nothing] = Observable.interval(d).take(1).filter(_ => false).map(_ => getNothing) + + def fourYearsEmpty: Observable[Medal] = duration(4*oneYear) + + def yearTicks: Observable[Int] = + (Observable.from(1996 to 2014) zip (Observable.items(-1) ++ Observable.interval(oneYear))).map(_._1) + + /* + def fourYearsEmptyOld: Observable[Medal] = { // TODO this should return an observable which emits nothing during fourYears and then completes // Because of https://github.com/Netflix/RxJava/issues/388, we get non-terminating tests // And this https://github.com/Netflix/RxJava/pull/289#issuecomment-24738668 also causes problems // So we don't use this: - // Observable.interval(fourYears).take(1).map(i => neverUsedDummyMedal).filter(m => false) + Observable.interval(fourYears).take(1).map(i => neverUsedDummyMedal).filter(m => false) // But we just return empty, which completes immediately - Observable.empty - } + // Observable.empty + }*/ } \ No newline at end of file diff --git a/language-adaptors/rxjava-scala/src/examples/scala/rx/lang/scala/examples/RxScalaDemo.scala b/language-adaptors/rxjava-scala/src/examples/scala/rx/lang/scala/examples/RxScalaDemo.scala index 6b6faf4f7d..c75ad9ea57 100644 --- a/language-adaptors/rxjava-scala/src/examples/scala/rx/lang/scala/examples/RxScalaDemo.scala +++ b/language-adaptors/rxjava-scala/src/examples/scala/rx/lang/scala/examples/RxScalaDemo.scala @@ -32,7 +32,7 @@ import org.scalatest.junit.JUnitSuite import rx.lang.scala._ import rx.lang.scala.schedulers._ -@Ignore // Since this doesn't do automatic testing, don't increase build time unnecessarily +//@Ignore // Since this doesn't do automatic testing, don't increase build time unnecessarily class RxScalaDemo extends JUnitSuite { @Test def intervalExample() { @@ -78,13 +78,9 @@ class RxScalaDemo extends JUnitSuite { val first = Observable.from(List(10, 11, 12)) val second = Observable.from(List(10, 11, 12)) - val b1 = (first zip second) map (p => p._1 == p._2) forall (b => b) + val b = (first zip second) forall { case (a, b) => a == b } - val equality = (a: Any, b: Any) => a == b - val b2 = (first zip second) map (p => equality(p._1, p._2)) forall (b => b) - - assertTrue(b1.toBlockingObservable.single) - assertTrue(b2.toBlockingObservable.single) + assertTrue(b.toBlockingObservable.single) } @Test def testObservableComparisonWithForComprehension() { @@ -93,7 +89,7 @@ class RxScalaDemo extends JUnitSuite { val booleans = for ((n1, n2) <- (first zip second)) yield (n1 == n2) - val b1 = booleans.forall(_ == true) // without `== true`, b1 is assigned the forall function + val b1 = booleans.forall(identity) assertTrue(b1.toBlockingObservable.single) } @@ -216,7 +212,6 @@ class RxScalaDemo extends JUnitSuite { }).flatten.toBlockingObservable.foreach(println(_)) } - @Ignore // TODO something's bad here @Test def timingTest1() { val numbersByModulo3 = Observable.interval(1000 millis).take(9).groupBy(_ % 3) @@ -224,10 +219,14 @@ class RxScalaDemo extends JUnitSuite { (for ((modulo, numbers) <- numbersByModulo3) yield { println("Observable for modulo" + modulo + " started at t = " + (System.currentTimeMillis - t0)) - numbers.take(1) // <- TODO very unexpected - //numbers + numbers.map(n => s"${n} is in the modulo-$modulo group") }).flatten.toBlockingObservable.foreach(println(_)) } + + @Test def testOlympicYearTicks() { + Olympics.yearTicks.subscribe(println(_)) + waitFor(Olympics.yearTicks) + } @Test def groupByExample() { val medalsByCountry = Olympics.mountainBikeMedals.groupBy(medal => medal.country) @@ -238,8 +237,10 @@ class RxScalaDemo extends JUnitSuite { firstMedalOfEachCountry.subscribe(medal => { println(s"${medal.country} wins its first medal in ${medal.year}") }) + + Olympics.yearTicks.subscribe(year => println(s"\nYear $year starts.")) - waitFor(firstMedalOfEachCountry) + waitFor(Olympics.yearTicks) } @Test def groupByUntilExample() { @@ -469,7 +470,10 @@ class RxScalaDemo extends JUnitSuite { def output(s: String): Unit = println(s) - // blocks until obs has completed + /** Subscribes to obs and waits until obs has completed. Note that if you subscribe to + * obs yourself and also call waitFor(obs), all side-effects of subscribing to obs + * will happen twice. + */ def waitFor[T](obs: Observable[T]): Unit = { obs.toBlockingObservable.toIterable.last } From 54adc1ed1a31549219412b7d4cee133b84239252 Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Fri, 21 Feb 2014 14:09:51 -0800 Subject: [PATCH 011/422] Operator: doOnTerminate Like finallyDo but before emitting the terminal state instead of after. --- rxjava-core/src/main/java/rx/Observable.java | 39 ++++++++++++++++++-- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/rxjava-core/src/main/java/rx/Observable.java b/rxjava-core/src/main/java/rx/Observable.java index 2224fa3c8e..dc47e9175f 100644 --- a/rxjava-core/src/main/java/rx/Observable.java +++ b/rxjava-core/src/main/java/rx/Observable.java @@ -4293,17 +4293,17 @@ public final Observable doOnEach(final Action1> onNot Observer observer = new Observer() { @Override public final void onCompleted() { - onNotification.call(new Notification()); + onNotification.call(Notification.createOnCompleted()); } @Override public final void onError(Throwable e) { - onNotification.call(new Notification(e)); + onNotification.call(Notification.createOnError(e)); } @Override public final void onNext(T v) { - onNotification.call(new Notification(v)); + onNotification.call(Notification.createOnNext(v)); } }; @@ -4387,6 +4387,39 @@ public final void onNext(T args) { return lift(new OperatorDoOnEach(observer)); } + + /** + * Modifies an Observable so that it invokes an action when it calls {@code onCompleted} or {@code onError}

+ * + *

+ * This differs from {@code finallyDo} in that this happens BEFORE onCompleted/onError are emitted. + * + * @param onTerminate + * the action to invoke when the source Observable calls {@code onCompleted} or {@code onError} + * @return the source Observable with the side-effecting behavior applied + * @see RxJava Wiki: doOnCompleted() + * @see MSDN: Observable.Do + */ + public final Observable doOnTerminate(final Action0 onTerminate) { + Observer observer = new Observer() { + @Override + public final void onCompleted() { + onTerminate.call(); + } + + @Override + public final void onError(Throwable e) { + onTerminate.call(); + } + + @Override + public final void onNext(T args) { + } + + }; + + return lift(new OperatorDoOnEach(observer)); + } /** * Returns an Observable that emits the single item at a specified index in a sequence of emissions from a From c7f7d5787dcff1019ba9e411b1353a9be2f1faf2 Mon Sep 17 00:00:00 2001 From: Todd Nine Date: Wed, 12 Feb 2014 20:33:37 -0700 Subject: [PATCH 012/422] Added additional test to prove issue --- rxjava-core/src/test/java/rx/ZipTests.java | 27 ++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/rxjava-core/src/test/java/rx/ZipTests.java b/rxjava-core/src/test/java/rx/ZipTests.java index 97928a5092..262f7dcb74 100644 --- a/rxjava-core/src/test/java/rx/ZipTests.java +++ b/rxjava-core/src/test/java/rx/ZipTests.java @@ -15,6 +15,10 @@ */ package rx; +import static org.junit.Assert.*; + +import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -31,6 +35,7 @@ import rx.functions.Action1; import rx.functions.Func1; import rx.functions.Func2; +import rx.functions.FuncN; import rx.observables.GroupedObservable; public class ZipTests { @@ -91,6 +96,28 @@ public void testCovarianceOfZip() { Observable. zip(horrors, ratings, combine); } + /** + * Occasionally zip may be invoked with 0 observables. This blocks indefinitely instead + * of immediately invoking zip with 0 argument. + */ + @Test(timeout = 5000) + public void nonBlockingObservable() { + + final Object invoked = new Object(); + + Collection> observables = Collections.emptyList(); + + Observable result = Observable.zip(observables, new FuncN() { + @Override + public Object call(final Object... args) { + assertEquals("No argument should have been passed", 0, args.length); + return invoked; + } + }); + + assertSame(invoked, result.toBlockingObservable().last()); + } + Func2 combine = new Func2() { @Override public ExtendedResult call(Media m, Rating r) { From 243bd3a68b1e478ffa38b603c2b5d18845772860 Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Fri, 21 Feb 2014 14:30:10 -0800 Subject: [PATCH 013/422] BugFix: Zip with 0 Observables Fixes bug reported at https://github.com/Netflix/RxJava/pull/868 --- .../main/java/rx/operators/OperatorZip.java | 14 +++- rxjava-core/src/test/java/rx/ZipTests.java | 7 +- .../java/rx/operators/OperatorZipTest.java | 68 +++++++++++++++++++ 3 files changed, 85 insertions(+), 4 deletions(-) diff --git a/rxjava-core/src/main/java/rx/operators/OperatorZip.java b/rxjava-core/src/main/java/rx/operators/OperatorZip.java index f211e42815..21da75d417 100644 --- a/rxjava-core/src/main/java/rx/operators/OperatorZip.java +++ b/rxjava-core/src/main/java/rx/operators/OperatorZip.java @@ -106,9 +106,14 @@ public OperatorZip(Func9 f) { public Subscriber call(final Subscriber observer) { return new Subscriber(observer) { + boolean started = false; + @Override public void onCompleted() { - // we only complete once a child Observable completes or errors + if (!started) { + // this means we have not received a valid onNext before termination so we emit the onCompleted + observer.onCompleted(); + } } @Override @@ -118,7 +123,12 @@ public void onError(Throwable e) { @Override public void onNext(Observable[] observables) { - new Zip(observables, observer, zipFunction).zip(); + if (observables == null || observables.length == 0) { + observer.onCompleted(); + } else { + started = true; + new Zip(observables, observer, zipFunction).zip(); + } } }; diff --git a/rxjava-core/src/test/java/rx/ZipTests.java b/rxjava-core/src/test/java/rx/ZipTests.java index 262f7dcb74..8735cb2e66 100644 --- a/rxjava-core/src/test/java/rx/ZipTests.java +++ b/rxjava-core/src/test/java/rx/ZipTests.java @@ -97,10 +97,12 @@ public void testCovarianceOfZip() { } /** - * Occasionally zip may be invoked with 0 observables. This blocks indefinitely instead + * Occasionally zip may be invoked with 0 observables. Test that we don't block indefinitely instead * of immediately invoking zip with 0 argument. + * + * We now expect an IllegalArgumentException since last() requires at least one value and nothing will be emitted. */ - @Test(timeout = 5000) + @Test(expected = IllegalArgumentException.class) public void nonBlockingObservable() { final Object invoked = new Object(); @@ -110,6 +112,7 @@ public void nonBlockingObservable() { Observable result = Observable.zip(observables, new FuncN() { @Override public Object call(final Object... args) { + System.out.println("received: " + args); assertEquals("No argument should have been passed", 0, args.length); return invoked; } diff --git a/rxjava-core/src/test/java/rx/operators/OperatorZipTest.java b/rxjava-core/src/test/java/rx/operators/OperatorZipTest.java index e96c8b84b0..7862c9f49a 100644 --- a/rxjava-core/src/test/java/rx/operators/OperatorZipTest.java +++ b/rxjava-core/src/test/java/rx/operators/OperatorZipTest.java @@ -22,6 +22,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -42,6 +43,7 @@ import rx.functions.Func3; import rx.functions.FuncN; import rx.functions.Functions; +import rx.observers.TestSubscriber; import rx.subjects.PublishSubject; import rx.subscriptions.Subscriptions; @@ -986,6 +988,72 @@ public void call(String s) { assertEquals("OnCompleted_null-OnCompleted_null", list.get(3)); } + @Test + public void testZipEmptyObservables() { + + Observable o = Observable.zip(Observable. empty(), Observable. empty(), new Func2() { + + @Override + public String call(Integer t1, String t2) { + return t1 + "-" + t2; + } + + }); + + final ArrayList list = new ArrayList(); + o.subscribe(new Action1() { + + @Override + public void call(String s) { + System.out.println(s); + list.add(s); + } + }); + + assertEquals(0, list.size()); + } + + @Test + public void testZipEmptyList() { + + final Object invoked = new Object(); + Collection> observables = Collections.emptyList(); + + Observable o = Observable.zip(observables, new FuncN() { + @Override + public Object call(final Object... args) { + assertEquals("No argument should have been passed", 0, args.length); + return invoked; + } + }); + + TestSubscriber ts = new TestSubscriber(); + o.subscribe(ts); + ts.awaitTerminalEvent(200, TimeUnit.MILLISECONDS); + ts.assertReceivedOnNext(Collections.emptyList()); + } + + /** + * Expect IllegalArgumentException instead of blocking forever as zip should emit onCompleted and no onNext + * and last() expects at least a single response. + */ + @Test(expected = IllegalArgumentException.class) + public void testZipEmptyListBlocking() { + + final Object invoked = new Object(); + Collection> observables = Collections.emptyList(); + + Observable o = Observable.zip(observables, new FuncN() { + @Override + public Object call(final Object... args) { + assertEquals("No argument should have been passed", 0, args.length); + return invoked; + } + }); + + o.toBlockingObservable().last(); + } + Observable OBSERVABLE_OF_5_INTEGERS = OBSERVABLE_OF_5_INTEGERS(new AtomicInteger()); Observable OBSERVABLE_OF_5_INTEGERS(final AtomicInteger numEmitted) { From afc4da1aaee94fb724271dfce3fb2e0ee98a31c0 Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Fri, 21 Feb 2014 14:48:06 -0800 Subject: [PATCH 014/422] Delete Deprecated onSubscribeStart That Doesn't Work - It's messy to make this work so deleting it instead. - Better to move forward in the 0.17 release than try and make this work for the very small percentage (probably only Netflix) that uses it. --- .../main/java/rx/plugins/RxJavaObservableExecutionHook.java | 6 ------ 1 file changed, 6 deletions(-) diff --git a/rxjava-core/src/main/java/rx/plugins/RxJavaObservableExecutionHook.java b/rxjava-core/src/main/java/rx/plugins/RxJavaObservableExecutionHook.java index 5668a1b416..01e6c97507 100644 --- a/rxjava-core/src/main/java/rx/plugins/RxJavaObservableExecutionHook.java +++ b/rxjava-core/src/main/java/rx/plugins/RxJavaObservableExecutionHook.java @@ -49,12 +49,6 @@ public abstract class RxJavaObservableExecutionHook { * original {@link Func1}<{@link Subscriber}{@code }, {@link Subscription}> to be executed * @return {@link Func1}<{@link Subscriber}{@code }, {@link Subscription}> function that can be modified, decorated, replaced or just returned as a pass-thru. */ - @Deprecated - public OnSubscribeFunc onSubscribeStart(Observable observableInstance, OnSubscribeFunc onSubscribe) { - // pass-thru by default - return onSubscribe; - } - public OnSubscribe onSubscribeStart(Observable observableInstance, final OnSubscribe onSubscribe) { // pass-thru by default return onSubscribe; From 7162d8bff019f1473537a0d3b8a9b7b4240109bf Mon Sep 17 00:00:00 2001 From: Bob T Builder Date: Fri, 21 Feb 2014 23:00:11 +0000 Subject: [PATCH 015/422] [Gradle Release Plugin] - pre tag commit: '0.17.0-RC4'. --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 80d3fa81f1..91a402ea6d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -version=0.17.0-RC4-SNAPSHOT +version=0.17.0-RC4 From 3a1c5f51cf645a4c4134e19e3d178935482a3284 Mon Sep 17 00:00:00 2001 From: Bob T Builder Date: Fri, 21 Feb 2014 23:00:15 +0000 Subject: [PATCH 016/422] [Gradle Release Plugin] - new version commit: '0.17.0-RC5-SNAPSHOT'. --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 91a402ea6d..2f793be8d8 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -version=0.17.0-RC4 +version=0.17.0-RC5-SNAPSHOT From 747f97ed6fd67d25ee22600475d71ca74bf289a6 Mon Sep 17 00:00:00 2001 From: David Gross Date: Fri, 21 Feb 2014 16:58:51 -0800 Subject: [PATCH 017/422] customize diagram/javadocs for doOnTerminate() --- rxjava-core/src/main/java/rx/Observable.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/rxjava-core/src/main/java/rx/Observable.java b/rxjava-core/src/main/java/rx/Observable.java index dc47e9175f..2226e6c798 100644 --- a/rxjava-core/src/main/java/rx/Observable.java +++ b/rxjava-core/src/main/java/rx/Observable.java @@ -4389,15 +4389,16 @@ public final void onNext(T args) { } /** - * Modifies an Observable so that it invokes an action when it calls {@code onCompleted} or {@code onError}

- * + * Modifies an Observable so that it invokes an action when it calls {@code onCompleted} or {@code onError}. + *

+ * *

* This differs from {@code finallyDo} in that this happens BEFORE onCompleted/onError are emitted. * * @param onTerminate * the action to invoke when the source Observable calls {@code onCompleted} or {@code onError} * @return the source Observable with the side-effecting behavior applied - * @see RxJava Wiki: doOnCompleted() + * @see RxJava Wiki: doOnTerminate() * @see MSDN: Observable.Do */ public final Observable doOnTerminate(final Action0 onTerminate) { From ed983eef3fc2c82509404c03770f2948b73fee41 Mon Sep 17 00:00:00 2001 From: Samuel Gruetter Date: Mon, 24 Feb 2014 10:31:50 +0100 Subject: [PATCH 018/422] some RxScalaDemo tweaks --- .../rx/lang/scala/examples/RxScalaDemo.scala | 48 +++++++++++-------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/language-adaptors/rxjava-scala/src/examples/scala/rx/lang/scala/examples/RxScalaDemo.scala b/language-adaptors/rxjava-scala/src/examples/scala/rx/lang/scala/examples/RxScalaDemo.scala index c75ad9ea57..21ea16ee0f 100644 --- a/language-adaptors/rxjava-scala/src/examples/scala/rx/lang/scala/examples/RxScalaDemo.scala +++ b/language-adaptors/rxjava-scala/src/examples/scala/rx/lang/scala/examples/RxScalaDemo.scala @@ -32,7 +32,16 @@ import org.scalatest.junit.JUnitSuite import rx.lang.scala._ import rx.lang.scala.schedulers._ -//@Ignore // Since this doesn't do automatic testing, don't increase build time unnecessarily +/** + * Demo how the different operators can be used. In Eclipse, you can right-click + * a test and choose "Run As" > "Scala JUnit Test". + * + * For each operator added to Observable.java, we add a little usage demo here. + * It does not need to test the functionality (that's already done by the tests in + * RxJava core), but it should demonstrate how it can be used, to make sure that + * the method signature makes sense. + */ +@Ignore // Since this doesn't do automatic testing, don't increase build time unnecessarily class RxScalaDemo extends JUnitSuite { @Test def intervalExample() { @@ -251,30 +260,36 @@ class RxScalaDemo extends JUnitSuite { } @Test def combineLatestExample() { - val first_counter = Observable.interval(250 millis) - val second_counter = Observable.interval(550 millis) - val combined_counter = first_counter.combineLatest(second_counter, + val firstCounter = Observable.interval(250 millis) + val secondCounter = Observable.interval(550 millis) + val combinedCounter = firstCounter.combineLatest(secondCounter, (x: Long, y: Long) => List(x,y)) take 10 - combined_counter subscribe {x => println(s"Emitted group: $x")} + combinedCounter subscribe {x => println(s"Emitted group: $x")} + waitFor(combinedCounter) } + @Test def olympicsExampleWithoutPublish() { + val medals = Olympics.mountainBikeMedals.doOnEach(_ => println("onNext")) + medals.subscribe(println(_)) // triggers an execution of medals Observable + waitFor(medals) // triggers another execution of medals Observable + } - @Test def olympicsExample() { - val medals = Olympics.mountainBikeMedals.publish - medals.subscribe(println(_)) + @Test def olympicsExampleWithPublish() { + val medals = Olympics.mountainBikeMedals.doOnEach(_ => println("onNext")).publish + medals.subscribe(println(_)) // triggers an execution of medals Observable medals.connect - //waitFor(medals) + waitFor(medals) // triggers another execution of medals Observable } @Test def exampleWithoutPublish() { - val unshared = List(1 to 4).toObservable + val unshared = Observable.from(1 to 4) unshared.subscribe(n => println(s"subscriber 1 gets $n")) unshared.subscribe(n => println(s"subscriber 2 gets $n")) } @Test def exampleWithPublish() { - val unshared = List(1 to 4).toObservable + val unshared = Observable.from(1 to 4) val shared = unshared.publish shared.subscribe(n => println(s"subscriber 1 gets $n")) shared.subscribe(n => println(s"subscriber 2 gets $n")) @@ -403,7 +418,7 @@ class RxScalaDemo extends JUnitSuite { } @Test def timestampExample() { - val timestamped = Observable.interval(100 millis).take(3).timestamp.toBlockingObservable + val timestamped = Observable.interval(100 millis).take(6).timestamp.toBlockingObservable for ((millis, value) <- timestamped if value > 0) { println(value + " at t = " + millis) } @@ -442,15 +457,6 @@ class RxScalaDemo extends JUnitSuite { val oc3: rx.Notification[_ <: Int] = oc2.asJavaNotification val oc4: rx.Notification[_ <: Any] = oc2.asJavaNotification } - - @Test def elementAtReplacement() { - assertEquals("b", List("a", "b", "c").toObservable.drop(1).first.toBlockingObservable.single) - } - - @Test def elementAtOrDefaultReplacement() { - assertEquals("b", List("a", "b", "c").toObservable.drop(1).firstOrElse("!").toBlockingObservable.single) - assertEquals("!!", List("a", "b", "c").toObservable.drop(10).firstOrElse("!!").toBlockingObservable.single) - } @Test def takeWhileWithIndexAlternative { val condition = true From 4258d776b0bceabef67285a8c48ca527ddf61514 Mon Sep 17 00:00:00 2001 From: George Campbell Date: Tue, 11 Feb 2014 21:44:26 -0800 Subject: [PATCH 019/422] Changes made while integrating it with our internal system --- .../java/rx/operators/DebugSubscriber.java | 53 ++++++--- .../java/rx/operators/DebugSubscription.java | 25 ++++- .../src/main/java/rx/plugins/DebugHook.java | 57 +++++++--- .../java/rx/plugins/DebugNotification.java | 102 ++++++++++-------- .../src/test/java/rx/debug/DebugHookTest.java | 74 +++++++++---- 5 files changed, 211 insertions(+), 100 deletions(-) diff --git a/rxjava-contrib/rxjava-debug/src/main/java/rx/operators/DebugSubscriber.java b/rxjava-contrib/rxjava-debug/src/main/java/rx/operators/DebugSubscriber.java index 276e245e74..f75c6b5662 100644 --- a/rxjava-contrib/rxjava-debug/src/main/java/rx/operators/DebugSubscriber.java +++ b/rxjava-contrib/rxjava-debug/src/main/java/rx/operators/DebugSubscriber.java @@ -4,47 +4,72 @@ import rx.Observer; import rx.Subscriber; import rx.functions.Action1; +import rx.functions.Action2; import rx.functions.Func1; import rx.plugins.DebugNotification; -public final class DebugSubscriber extends Subscriber { +public final class DebugSubscriber extends Subscriber { private final Func1 onNextHook; - final Action1 events; - final Observer o; - Operator from = null; - Operator to = null; + private final Func1 start; + private final Action1 complete; + private final Action2 error; + private final Observer o; + private Operator from = null; + private Operator to = null; public DebugSubscriber( Func1 onNextHook, - Action1 _events, + Func1 start, + Action1 complete, + Action2 error, Subscriber _o, Operator _out, Operator _in) { super(_o); - this.events = _events; + this.start = start; + this.complete = complete; + this.error = error; this.o = _o; this.onNextHook = onNextHook; this.from = _out; this.to = _in; - this.add(new DebugSubscription(this)); + this.add(new DebugSubscription(this, start, complete, error)); } @Override public void onCompleted() { - events.call(DebugNotification.createOnCompleted(o, from, to)); - o.onCompleted(); + final DebugNotification n = DebugNotification.createOnCompleted(o, from, to); + C context = start.call(n); + try { + o.onCompleted(); + complete.call(context); + } catch (Throwable e) { + error.call(context, e); + } } @Override public void onError(Throwable e) { - events.call(DebugNotification.createOnError(o, from, e, to)); - o.onError(e); + final DebugNotification n = DebugNotification.createOnError(o, from, e, to); + C context = start.call(n); + try { + o.onError(e); + complete.call(context); + } catch (Throwable e2) { + error.call(context, e2); + } } @Override public void onNext(T t) { - events.call(DebugNotification.createOnNext(o, from, t, to)); - o.onNext(onNextHook.call(t)); + final DebugNotification n = DebugNotification.createOnNext(o, from, t, to); + C context = start.call(n); + try { + o.onNext(onNextHook.call(t)); + complete.call(context); + } catch (Throwable e) { + error.call(context, e); + } } public Operator getFrom() { diff --git a/rxjava-contrib/rxjava-debug/src/main/java/rx/operators/DebugSubscription.java b/rxjava-contrib/rxjava-debug/src/main/java/rx/operators/DebugSubscription.java index e82960d5fe..256fd8798a 100644 --- a/rxjava-contrib/rxjava-debug/src/main/java/rx/operators/DebugSubscription.java +++ b/rxjava-contrib/rxjava-debug/src/main/java/rx/operators/DebugSubscription.java @@ -1,19 +1,34 @@ package rx.operators; import rx.Subscription; +import rx.functions.Action1; +import rx.functions.Action2; +import rx.functions.Func1; import rx.plugins.DebugNotification; -final class DebugSubscription implements Subscription { - private final DebugSubscriber debugObserver; +final class DebugSubscription implements Subscription { + private final DebugSubscriber debugObserver; + private final Func1 start; + private final Action1 complete; + private final Action2 error; - DebugSubscription(DebugSubscriber debugObserver) { + DebugSubscription(DebugSubscriber debugObserver, Func1 start, Action1 complete, Action2 error) { this.debugObserver = debugObserver; + this.start = start; + this.complete = complete; + this.error = error; } @Override public void unsubscribe() { - debugObserver.events.call(DebugNotification. createUnsubscribe(debugObserver.o, debugObserver.from, debugObserver.to)); - debugObserver.unsubscribe(); + final DebugNotification n = DebugNotification. createUnsubscribe(debugObserver.getActual(), debugObserver.getFrom(), debugObserver.getTo()); + C context = start.call(n); + try { + debugObserver.unsubscribe(); + complete.call(context); + } catch (Throwable e) { + error.call(context, e); + } } @Override diff --git a/rxjava-contrib/rxjava-debug/src/main/java/rx/plugins/DebugHook.java b/rxjava-contrib/rxjava-debug/src/main/java/rx/plugins/DebugHook.java index 31d54d87ce..3e5d5eba9a 100644 --- a/rxjava-contrib/rxjava-debug/src/main/java/rx/plugins/DebugHook.java +++ b/rxjava-contrib/rxjava-debug/src/main/java/rx/plugins/DebugHook.java @@ -6,6 +6,7 @@ import rx.Subscriber; import rx.Subscription; import rx.functions.Action1; +import rx.functions.Action2; import rx.functions.Actions; import rx.functions.Func1; import rx.functions.Functions; @@ -17,9 +18,11 @@ * * @author gscampbell */ -public class DebugHook extends RxJavaObservableExecutionHook { +public class DebugHook extends RxJavaObservableExecutionHook { private final Func1 onNextHook; - private final Action1 events; + private final Func1 start; + private final Action1 complete; + private final Action2 error; /** * Creates a new instance of the DebugHook RxJava plug-in that can be passed into @@ -31,18 +34,26 @@ public class DebugHook extends RxJavaObservableExecutionHook { * @param events * This action is invoked as each notification is generated */ - public DebugHook(Func1 onNextDataHook, Action1 events) { + public DebugHook(Func1 onNextDataHook, Func1 start, Action1 complete, Action2 error) { + this.complete = complete; + this.error = error; this.onNextHook = onNextDataHook == null ? Functions.identity() : onNextDataHook; - this.events = events == null ? Actions.empty() : events; + this.start = (Func1) (start == null ? Actions.empty() : start); } @Override - public OnSubscribe onSubscribeStart(Observable observableInstance, final OnSubscribe f) { + public OnSubscribe onSubscribeStart(final Observable observableInstance, final OnSubscribe f) { return new OnSubscribe() { @Override public void call(Subscriber o) { - events.call(DebugNotification.createSubscribe(o, f)); - f.call(wrapOutbound(null, o)); + C context = start.call(DebugNotification.createSubscribe(o, observableInstance, f)); + try { + f.call(wrapOutbound(null, o)); + complete.call(context); + } + catch(Throwable e) { + error.call(context, e); + } } }; } @@ -54,12 +65,7 @@ public Subscription onSubscribeReturn(Observable observableInst @Override public OnSubscribe onCreate(final OnSubscribe f) { - return new OnSubscribe() { - @Override - public void call(Subscriber o) { - f.call(wrapInbound(null, o)); - } - }; + return new OnCreateWrapper(f); } @Override @@ -81,19 +87,36 @@ public Subscription onAdd(Subscriber subscriber, Subscription s) { private Subscriber wrapOutbound(Operator bind, Subscriber o) { if (o instanceof DebugSubscriber) { if (bind != null) - ((DebugSubscriber) o).setFrom(bind); + ((DebugSubscriber) o).setFrom(bind); return o; } - return new DebugSubscriber(onNextHook, events, o, bind, null); + return new DebugSubscriber(onNextHook, start, complete, error, o, bind, null); } @SuppressWarnings("unchecked") private Subscriber wrapInbound(Operator bind, Subscriber o) { if (o instanceof DebugSubscriber) { if (bind != null) - ((DebugSubscriber) o).setTo(bind); + ((DebugSubscriber) o).setTo(bind); return o; } - return new DebugSubscriber(onNextHook, events, o, null, bind); + return new DebugSubscriber(onNextHook, start, complete, error, o, null, bind); + } + + public final class OnCreateWrapper implements OnSubscribe { + private final OnSubscribe f; + + private OnCreateWrapper(OnSubscribe f) { + this.f = f; + } + + @Override + public void call(Subscriber o) { + f.call(wrapInbound(null, o)); + } + + public OnSubscribe getActual() { + return f; + } } } diff --git a/rxjava-contrib/rxjava-debug/src/main/java/rx/plugins/DebugNotification.java b/rxjava-contrib/rxjava-debug/src/main/java/rx/plugins/DebugNotification.java index f4144fb2b0..f22d4e31ef 100644 --- a/rxjava-contrib/rxjava-debug/src/main/java/rx/plugins/DebugNotification.java +++ b/rxjava-contrib/rxjava-debug/src/main/java/rx/plugins/DebugNotification.java @@ -1,103 +1,119 @@ package rx.plugins; -import rx.Notification; +import rx.Observable; import rx.Observable.OnSubscribe; import rx.Observable.Operator; import rx.Observer; import rx.observers.SafeSubscriber; import rx.operators.DebugSubscriber; -public class DebugNotification { +public class DebugNotification { public static enum Kind { OnNext, OnError, OnCompleted, Subscribe, Unsubscribe } - private final OnSubscribe source; + private final Observable source; + private final OnSubscribe sourceFunc; private final Operator from; private final Kind kind; - private final Notification notification; private final Operator to; - private final long nanoTime; - private final long threadId; - private Observer o; + private final Throwable throwable; + private final T value; + private final Observer observer; - public static DebugNotification createSubscribe(Observer o, OnSubscribe source) { + public static DebugNotification createSubscribe(Observer o, Observable source, OnSubscribe sourceFunc) { Operator to = null; Operator from = null; + if (o instanceof SafeSubscriber) { + o = ((SafeSubscriber) o).getActual(); + } if (o instanceof DebugSubscriber) { - to = ((DebugSubscriber) o).getTo(); - from = ((DebugSubscriber) o).getFrom(); + to = ((DebugSubscriber) o).getTo(); + from = ((DebugSubscriber) o).getFrom(); o = ((DebugSubscriber) o).getActual(); } - return new DebugNotification(o, from, Kind.Subscribe, null, to, source); + if (sourceFunc instanceof DebugHook.OnCreateWrapper) { + sourceFunc = ((DebugHook.OnCreateWrapper) sourceFunc).getActual(); + } + return new DebugNotification(o, from, Kind.Subscribe, null, null, to, source, sourceFunc); } - public static DebugNotification createOnNext(Observer o, Operator from, T t, Operator to) { - return new DebugNotification(o, from, Kind.OnNext, Notification.createOnNext(t), to, null); + public static DebugNotification createOnNext(Observer o, Operator from, T t, Operator to) { + return new DebugNotification(o, from, Kind.OnNext, t, null, to, null, null); } - public static DebugNotification createOnError(Observer o, Operator from, Throwable e, Operator to) { - return new DebugNotification(o, from, Kind.OnError, Notification. createOnError(e), to, null); + public static DebugNotification createOnError(Observer o, Operator from, Throwable e, Operator to) { + return new DebugNotification(o, from, Kind.OnError, null, e, to, null, null); } - public static DebugNotification createOnCompleted(Observer o, Operator from, Operator to) { - return new DebugNotification(o, from, Kind.OnCompleted, Notification. createOnCompleted(), to, null); + public static DebugNotification createOnCompleted(Observer o, Operator from, Operator to) { + return new DebugNotification(o, from, Kind.OnCompleted, null, null, to, null, null); } - public static DebugNotification createUnsubscribe(Observer o, Operator from, Operator to) { - return new DebugNotification(o, from, Kind.Unsubscribe, null, to, null); + public static DebugNotification createUnsubscribe(Observer o, Operator from, Operator to) { + return new DebugNotification(o, from, Kind.Unsubscribe, null, null, to, null, null); } - private DebugNotification(Observer o, Operator from, Kind kind, Notification notification, Operator to, OnSubscribe source) { - this.o = (o instanceof SafeSubscriber) ? ((SafeSubscriber) o).getActual() : o; + private DebugNotification(Observer o, Operator from, Kind kind, T value, Throwable throwable, Operator to, Observable source, OnSubscribe sourceFunc) { + this.observer = (o instanceof SafeSubscriber) ? ((SafeSubscriber) o).getActual() : o; this.from = from; this.kind = kind; - this.notification = notification; + this.value = value; + this.throwable = throwable; this.to = to; this.source = source; - this.nanoTime = System.nanoTime(); - this.threadId = Thread.currentThread().getId(); + this.sourceFunc = sourceFunc; + } + + public Observer getObserver() { + return observer; } public Operator getFrom() { return from; } - public Notification getNotification() { - return notification; + public T getValue() { + return value; } - public Operator getTo() { - return to; + public Throwable getThrowable() { + return throwable; } - public long getNanoTime() { - return nanoTime; - } - - public long getThreadId() { - return threadId; + public Operator getTo() { + return to; } public Kind getKind() { return kind; } + + public Observable getSource() { + return source; + } + + public OnSubscribe getSourceFunc() { + return sourceFunc; + } @Override + /** + * Does a very bad job of making JSON like string. + */ public String toString() { final StringBuilder s = new StringBuilder("{"); - s.append(" \"nano\": ").append(nanoTime); - s.append(", \"thread\": ").append(threadId); - s.append(", \"observer\": \"").append(o.getClass().getName()).append("@").append(Integer.toHexString(o.hashCode())).append("\""); + s.append("\"observer\": \"").append(observer.getClass().getName()).append("@").append(Integer.toHexString(observer.hashCode())).append("\""); s.append(", \"type\": \"").append(kind).append("\""); - if (notification != null) { - if (notification.hasValue()) - s.append(", \"value\": \"").append(notification.getValue()).append("\""); - if (notification.hasThrowable()) - s.append(", \"exception\": \"").append(notification.getThrowable().getMessage().replace("\\", "\\\\").replace("\"", "\\\"")).append("\""); - } + if (kind == Kind.OnNext) + // not json safe + s.append(", \"value\": \"").append(value).append("\""); + if (kind == Kind.OnError) + s.append(", \"exception\": \"").append(throwable.getMessage().replace("\\", "\\\\").replace("\"", "\\\"")).append("\""); if (source != null) s.append(", \"source\": \"").append(source.getClass().getName()).append("@").append(Integer.toHexString(source.hashCode())).append("\""); + if (sourceFunc != null) + s.append(", \"sourceFunc\": \"").append(sourceFunc.getClass().getName()).append("@").append(Integer.toHexString(sourceFunc.hashCode())).append("\""); if (from != null) s.append(", \"from\": \"").append(from.getClass().getName()).append("@").append(Integer.toHexString(from.hashCode())).append("\""); if (to != null) diff --git a/rxjava-contrib/rxjava-debug/src/test/java/rx/debug/DebugHookTest.java b/rxjava-contrib/rxjava-debug/src/test/java/rx/debug/DebugHookTest.java index 92c6cdd0dd..ea69258259 100644 --- a/rxjava-contrib/rxjava-debug/src/test/java/rx/debug/DebugHookTest.java +++ b/rxjava-contrib/rxjava-debug/src/test/java/rx/debug/DebugHookTest.java @@ -11,14 +11,17 @@ import org.junit.Before; import org.junit.Ignore; import org.junit.Test; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; -import rx.Notification; import rx.Observable; -import rx.Observer; +import rx.Subscriber; import rx.functions.Action1; +import rx.functions.Action2; import rx.functions.Func1; import rx.plugins.DebugHook; import rx.plugins.DebugNotification; +import rx.plugins.DebugNotification.Kind; import rx.plugins.PlugReset; import rx.plugins.RxJavaPlugins; @@ -32,25 +35,52 @@ public void reset() { @Test @Ignore public void testSimple() { - Action1 events = mock(Action1.class); - final DebugHook hook = new DebugHook(null, events); + Func1 start = mock(Func1.class); + Action1 complete = mock(Action1.class); + Action2 error = mock(Action2.class); + final DebugHook hook = new DebugHook(null, start, complete, error); RxJavaPlugins.getInstance().registerObservableExecutionHook(hook); Observable.empty().subscribe(); - verify(events, times(1)).call(subscribe()); - verify(events, times(1)).call(onCompleted()); + verify(start, times(1)).call(subscribe()); + verify(start, times(1)).call(onCompleted()); + + verify(complete, times(2)).call(any()); + + verify(error, never()).call(any(), any()); } @Test public void testOneOp() { - Action1 events = mock(Action1.class); - final DebugHook hook = new DebugHook(null, events); + Func1, Object> start = mock(Func1.class); + doAnswer(new Answer() { + public Object answer(InvocationOnMock invocation) throws Throwable { + Object context = new Object(); + System.out.println("start: " + context.hashCode() + " " + invocation.getArguments()[0]); + return context; + } + }).when(start).call(any(DebugNotification.class)); + Action1 complete = mock(Action1.class); + doAnswer(new Answer() { + public Object answer(InvocationOnMock invocation) throws Throwable { + System.out.println("complete: " + invocation.getArguments()[0].hashCode()); + return null; + } + }).when(complete).call(any()); + Action2 error = mock(Action2.class); + doAnswer(new Answer() { + public Object answer(InvocationOnMock invocation) throws Throwable { + System.out.println("error: " + invocation.getArguments()[1].hashCode()); + return null; + } + }).when(error).call(any(), any(Throwable.class)); + final DebugHook hook = new DebugHook(null, start, complete, error); RxJavaPlugins.getInstance().registerObservableExecutionHook(hook); Observable.from(Arrays.asList(1, 3)).flatMap(new Func1>() { @Override public Observable call(Integer it) { return Observable.from(Arrays.asList(it, it + 1)); } - }).take(3).subscribe(new Observer() { + }).take(3).subscribe(new Subscriber() { @Override public void onCompleted() { } @@ -63,23 +93,26 @@ public void onError(Throwable e) { public void onNext(Integer t) { } }); - verify(events, atLeast(3)).call(subscribe()); - verify(events, times(4)).call(onNext(1)); + verify(start, atLeast(3)).call(subscribe()); + verify(start, times(4)).call(onNext(1)); // one less because it originates from the inner observable sent to merge - verify(events, times(3)).call(onNext(2)); - verify(events, times(4)).call(onNext(3)); + verify(start, times(3)).call(onNext(2)); + verify(start, times(4)).call(onNext(3)); // because the take unsubscribes - verify(events, never()).call(onNext(4)); + verify(start, never()).call(onNext(4)); + + verify(complete, atLeast(14)).call(any()); + + verify(error, never()).call(any(), any(Throwable.class)); } - private static DebugNotification onNext(final T value) { - return argThat(new BaseMatcher>() { + private static DebugNotification onNext(final T value) { + return argThat(new BaseMatcher>() { @Override public boolean matches(Object item) { if (item instanceof DebugNotification) { - DebugNotification dn = (DebugNotification) item; - Notification n = dn.getNotification(); - return n != null && n.hasValue() && n.getValue().equals(value); + DebugNotification dn = (DebugNotification) item; + return dn.getKind() == Kind.OnNext && dn.getValue().equals(value); } return false; } @@ -115,8 +148,7 @@ private static DebugNotification onCompleted() { public boolean matches(Object item) { if (item instanceof DebugNotification) { DebugNotification dn = (DebugNotification) item; - Notification n = dn.getNotification(); - return n != null && n.isOnCompleted(); + return dn.getKind() == Kind.OnCompleted; } return false; } From f92eb0e45a4fa89e4f0c9dc7b115aa30fc6c7d58 Mon Sep 17 00:00:00 2001 From: Samuel Gruetter Date: Mon, 24 Feb 2014 21:15:04 +0100 Subject: [PATCH 020/422] add Subscriber and Observable creation from `Subscriber[T] => Unit` --- .../rx/lang/scala/examples/RxScalaDemo.scala | 38 ++++++++++++-- .../scala/ImplicitFunctionConversions.scala | 9 +++- .../scala/rx/lang/scala/JavaConversions.scala | 4 ++ .../main/scala/rx/lang/scala/Observable.scala | 38 ++++++++++++-- .../main/scala/rx/lang/scala/Observer.scala | 36 ++++++++----- .../main/scala/rx/lang/scala/Subscriber.scala | 52 +++++++++++++++++++ 6 files changed, 153 insertions(+), 24 deletions(-) create mode 100644 language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/Subscriber.scala diff --git a/language-adaptors/rxjava-scala/src/examples/scala/rx/lang/scala/examples/RxScalaDemo.scala b/language-adaptors/rxjava-scala/src/examples/scala/rx/lang/scala/examples/RxScalaDemo.scala index 21ea16ee0f..e7c09e769c 100644 --- a/language-adaptors/rxjava-scala/src/examples/scala/rx/lang/scala/examples/RxScalaDemo.scala +++ b/language-adaptors/rxjava-scala/src/examples/scala/rx/lang/scala/examples/RxScalaDemo.scala @@ -463,15 +463,43 @@ class RxScalaDemo extends JUnitSuite { List("a", "b").toObservable.zipWithIndex.takeWhile{case (elem, index) => condition}.map(_._1) } - @Test def createExample() { + def calculateElement(index: Int): String = { + println("omg I'm calculating so hard") + index match { + case 0 => "a" + case 1 => "b" + case _ => throw new IllegalArgumentException + } + } + + /** + * This is a bad way of using Observable.create, because even if the consumer unsubscribes, + * all elements are calculated. + */ + @Test def createExampleBad() { val o = Observable.create[String](observer => { - // this is bad because you cannot unsubscribe! - observer.onNext("a") - observer.onNext("b") + observer.onNext(calculateElement(0)) + observer.onNext(calculateElement(1)) observer.onCompleted() Subscription {} }) - o.subscribe(println(_)) + o.take(1).subscribe(println(_)) + } + + /** + * This is the good way of doing it: If the consumer unsubscribes, no more elements are + * calculated. + */ + @Test def createExampleGood() { + val o = Observable[String](subscriber => { + var i = 0 + while (i < 2 && !subscriber.isUnsubscribed) { + subscriber.onNext(calculateElement(i)) + i += 1 + } + if (!subscriber.isUnsubscribed) subscriber.onCompleted() + }) + o.take(1).subscribe(println(_)) } def output(s: String): Unit = println(s) diff --git a/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/ImplicitFunctionConversions.scala b/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/ImplicitFunctionConversions.scala index 3c6aa5c29a..423ab119c9 100644 --- a/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/ImplicitFunctionConversions.scala +++ b/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/ImplicitFunctionConversions.scala @@ -17,10 +17,8 @@ package rx.lang.scala import java.lang.Exception import java.{ lang => jlang } - import scala.language.implicitConversions import scala.collection.Seq - import rx.functions._ import rx.lang.scala.JavaConversions._ @@ -56,6 +54,13 @@ object ImplicitFunctionConversions { } } + implicit def scalaAction1ToOnSubscribe[T](f: Subscriber[T] => Unit) = + new rx.Observable.OnSubscribe[T] { + def call(s: rx.Subscriber[_ >: T]): Unit = { + f(s) + } + } + implicit def scalaByNameParamToFunc0[B](param: => B): Func0[B] = new Func0[B] { def call(): B = param diff --git a/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/JavaConversions.scala b/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/JavaConversions.scala index d003842be4..9ebd7744d7 100644 --- a/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/JavaConversions.scala +++ b/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/JavaConversions.scala @@ -32,6 +32,10 @@ object JavaConversions { implicit def toScalaSubscription(s: rx.Subscription): Subscription = Subscription(s) + implicit def toJavaSubscriber[T](s: Subscriber[T]): rx.Subscriber[_ >: T] = s.asJavaSubscriber + + implicit def toScalaSubscriber[T](s: rx.Subscriber[_ >: T]): Subscriber[T] = Subscriber(s) + implicit def scalaSchedulerToJavaScheduler(s: Scheduler): rx.Scheduler = s.asJavaScheduler implicit def javaSchedulerToScalaScheduler(s: rx.Scheduler): Scheduler = Scheduler(s) diff --git a/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/Observable.scala b/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/Observable.scala index f120d72900..1466afca3c 100644 --- a/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/Observable.scala +++ b/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/Observable.scala @@ -2272,9 +2272,6 @@ object Observable { * should invoke the Observer's [[rx.lang.scala.Observer.onNext onNext]], [[rx.lang.scala.Observer.onError onError]], and [[rx.lang.scala.Observer.onCompleted onCompleted]] methods * appropriately. * - * A well-formed Observable must invoke either the Observer's `onCompleted` method - * exactly once or its `onError` method exactly once. - * * See Rx Design Guidelines (PDF) * for detailed information. * @@ -2296,6 +2293,41 @@ object Observable { })) } + /* + Note: It's dangerous to have two overloads where one takes an `Observer[T] => Subscription` + function and the other takes a `Subscriber[T] => Unit` function, because expressions like + `o => Subscription{}` have both of these types. + So we call the old create method "create", and the new create method "apply". + Try it out yourself here: + def foo[T]: Unit = { + val fMeant: Observer[T] => Subscription = o => Subscription{} + val fWrong: Subscriber[T] => Unit = o => Subscription{} + } + */ + + /** + * Returns an Observable that will execute the specified function when a someone subscribes to it. + * + * + * + * Write the function you pass so that it behaves as an Observable: It should invoke the + * Subscriber's `onNext`, `onError`, and `onCompleted` methods appropriately. + * + * See Rx Design Guidelines (PDF) for detailed + * information. + * + * @tparam T + * the type of the items that this Observable emits + * @param f + * a function that accepts a `Subscriber[T]`, and invokes its `onNext`, + * `onError`, and `onCompleted` methods as appropriate + * @return an Observable that, when someone subscribes to it, will execute the specified + * function + */ + def apply[T](f: Subscriber[T] => Unit): Observable[T] = { + toScalaObservable(rx.Observable.create(f)) + } + /** * Returns an Observable that invokes an [[rx.lang.scala.Observer]]'s [[rx.lang.scala.Observer.onError onError]] * method when the Observer subscribes to it. diff --git a/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/Observer.scala b/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/Observer.scala index 6e956dd1a1..e7e9ad5bf1 100644 --- a/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/Observer.scala +++ b/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/Observer.scala @@ -56,7 +56,7 @@ trait Observer[-T] { } -object Observer { +object Observer extends ObserverFactoryMethods[Observer] { /** * Scala calls XXX; Java receives XXX. @@ -72,17 +72,25 @@ object Observer { } - def apply[T]( ): Observer[T] = apply[T]((v:T)=>(), (e: Throwable)=>(), ()=>()) - def apply[T](onNext: T=>Unit ): Observer[T] = apply[T](onNext, (e: Throwable)=>(), ()=>()) - def apply[T](onNext: T=>Unit, onError: Throwable=>Unit ): Observer[T] = apply[T](onNext, onError, ()=>()) - def apply[T](onNext: T=>Unit, onCompleted: ()=>Unit): Observer[T] = apply[T](onNext, (e: Throwable)=>(), onCompleted) - def apply[T](onNext: T=>Unit, onError: Throwable=>Unit, onCompleted: ()=>Unit): Observer[T] = { - val n = onNext; val e = onError; val c = onCompleted - // Java calls XXX; Scala receives XXX. - Observer(new rx.Observer[T] { - override def onNext(value: T): Unit = n(value) - override def onError(error: Throwable): Unit = e(error) - override def onCompleted(): Unit = c() - }) + def apply[T](onNext: T => Unit, onError: Throwable => Unit, onCompleted: () => Unit): Observer[T] = { + val n = onNext; val e = onError; val c = onCompleted + // Java calls XXX; Scala receives XXX. + Observer(new rx.Observer[T] { + override def onNext(value: T): Unit = n(value) + override def onError(error: Throwable): Unit = e(error) + override def onCompleted(): Unit = c() + }) } -} \ No newline at end of file +} + + +private [scala] trait ObserverFactoryMethods[P[_] <: Observer[_]] { + + def apply[T](onNext: T => Unit, onError: Throwable => Unit, onCompleted: () => Unit): P[T] + + def apply[T]( ): P[T] = apply[T]((v:T)=>(), (e: Throwable)=>(), ()=>()) + def apply[T](onNext: T=>Unit ): P[T] = apply[T](onNext, (e: Throwable)=>(), ()=>()) + def apply[T](onNext: T=>Unit, onError: Throwable=>Unit ): P[T] = apply[T](onNext, onError, ()=>()) + def apply[T](onNext: T=>Unit, onCompleted: ()=>Unit): P[T] = apply[T](onNext, (e: Throwable)=>(), onCompleted) +} + diff --git a/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/Subscriber.scala b/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/Subscriber.scala new file mode 100644 index 0000000000..9f7091123e --- /dev/null +++ b/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/Subscriber.scala @@ -0,0 +1,52 @@ +package rx.lang.scala + +trait Subscriber[-T] extends Observer[T] with Subscription { + + self => + + private [scala] override val asJavaObserver: rx.Observer[_ >: T] = asJavaSubscriber + private [scala] override val asJavaSubscription: rx.Subscription = asJavaSubscriber + + private [scala] val asJavaSubscriber: rx.Subscriber[_ >: T] = new rx.Subscriber[T] { + def onNext(value: T): Unit = self.onNext(value) + def onError(error: Throwable): Unit = self.onError(error) + def onCompleted(): Unit = self.onCompleted() + } + + /** + * Used to register an unsubscribe callback. + */ + final def add(s: Subscription): Unit = { + asJavaSubscriber.add(s.asJavaSubscription) + } + + override final def unsubscribe(): Unit = { + asJavaSubscriber.unsubscribe() + } + + override final def isUnsubscribed: Boolean = { + asJavaSubscriber.isUnsubscribed() + } + +} + +object Subscriber extends ObserverFactoryMethods[Subscriber] { + + private[scala] def apply[T](subscriber: rx.Subscriber[T]): Subscriber[T] = new Subscriber[T] { + override val asJavaSubscriber = subscriber + + override def onNext(value: T): Unit = asJavaSubscriber.onNext(value) + override def onError(error: Throwable): Unit = asJavaSubscriber.onError(error) + override def onCompleted(): Unit = asJavaSubscriber.onCompleted() + } + + def apply[T](onNext: T => Unit, onError: Throwable => Unit, onCompleted: () => Unit): Subscriber[T] = { + val n = onNext; val e = onError; val c = onCompleted + // Java calls XXX; Scala receives XXX. + Subscriber(new rx.Subscriber[T] { + override def onNext(value: T): Unit = n(value) + override def onError(error: Throwable): Unit = e(error) + override def onCompleted(): Unit = c() + }) + } +} From b7cfcba520de9824267ed43c6a895ff62aa4f75e Mon Sep 17 00:00:00 2001 From: Samuel Gruetter Date: Mon, 24 Feb 2014 23:42:36 +0100 Subject: [PATCH 021/422] deprecate rx.lang.scala.Observable.create[T](Observer[T]=>Subscription) --- .../rxjava-scala/src/main/scala/rx/lang/scala/Observable.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/Observable.scala b/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/Observable.scala index 1466afca3c..6794770232 100644 --- a/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/Observable.scala +++ b/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/Observable.scala @@ -2285,6 +2285,7 @@ object Observable { * @return * an Observable that, when an [[rx.lang.scala.Observer]] subscribes to it, will execute the given function. */ + @deprecated("Use `apply[T](Subscriber[T] => Unit)` instead", "0.17.0") def create[T](func: Observer[T] => Subscription): Observable[T] = { toScalaObservable[T](rx.Observable.create(new OnSubscribeFunc[T] { def onSubscribe(t1: rx.Observer[_ >: T]): rx.Subscription = { From f18e4d2d901695070a7876f6dc9f06a71629fa5c Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Mon, 24 Feb 2014 21:42:41 -0800 Subject: [PATCH 022/422] Localized Operator Error Handling - use the lift function rather than try/catch in subscribe since this catches at the operator level rather than for an entire sequence - unit tests with onErrorResumeNext demonstrating the use cases --- rxjava-core/src/main/java/rx/Observable.java | 12 ++- .../java/rx/exceptions/OnErrorThrowable.java | 8 ++ .../java/rx/operators/OperatorMapTest.java | 13 ++- ...ratorOnErrorResumeNextViaFunctionTest.java | 83 +++++++++++++++++++ 4 files changed, 111 insertions(+), 5 deletions(-) diff --git a/rxjava-core/src/main/java/rx/Observable.java b/rxjava-core/src/main/java/rx/Observable.java index dc47e9175f..d310f3cff7 100644 --- a/rxjava-core/src/main/java/rx/Observable.java +++ b/rxjava-core/src/main/java/rx/Observable.java @@ -266,7 +266,17 @@ public Observable lift(final Operator lift) { return new Observable(new OnSubscribe() { @Override public void call(Subscriber o) { - f.call(hook.onLift(lift).call(o)); + try { + f.call(hook.onLift(lift).call(o)); + } catch (Throwable e) { + // localized capture of errors rather than it skipping all operators + // and ending up in the try/catch of the subscribe method which then + // prevents onErrorResumeNext and other similar approaches to error handling + if (e instanceof OnErrorNotImplementedException) { + throw (OnErrorNotImplementedException) e; + } + o.onError(e); + } } }); } diff --git a/rxjava-core/src/main/java/rx/exceptions/OnErrorThrowable.java b/rxjava-core/src/main/java/rx/exceptions/OnErrorThrowable.java index 7044dd37ca..bf255432ea 100644 --- a/rxjava-core/src/main/java/rx/exceptions/OnErrorThrowable.java +++ b/rxjava-core/src/main/java/rx/exceptions/OnErrorThrowable.java @@ -59,6 +59,14 @@ public static OnErrorThrowable from(Throwable t) { * @return Throwable e passed in */ public static Throwable addValueAsLastCause(Throwable e, Object value) { + Throwable lastCause = Exceptions.getFinalCause(e); + if (lastCause != null && lastCause instanceof OnNextValue) { + // purposefully using == for object reference check + if (((OnNextValue) lastCause).getValue() == value) { + // don't add another + return e; + } + } Exceptions.addCause(e, new OnNextValue(value)); return e; } diff --git a/rxjava-core/src/test/java/rx/operators/OperatorMapTest.java b/rxjava-core/src/test/java/rx/operators/OperatorMapTest.java index 5cf5cfd1c3..fd091bdaef 100644 --- a/rxjava-core/src/test/java/rx/operators/OperatorMapTest.java +++ b/rxjava-core/src/test/java/rx/operators/OperatorMapTest.java @@ -29,6 +29,7 @@ import rx.Observable; import rx.Observer; import rx.Subscriber; +import rx.exceptions.OnErrorNotImplementedException; import rx.functions.Action1; import rx.functions.Func1; import rx.functions.Func2; @@ -171,7 +172,7 @@ public String call(String s) { public void call(Throwable t1) { t1.printStackTrace(); } - + }); m.subscribe(stringObserver); @@ -255,7 +256,7 @@ public Integer call(Integer i) { }).toBlockingObservable().single(); } - @Test(expected = RuntimeException.class) + @Test(expected = OnErrorNotImplementedException.class) public void verifyExceptionIsThrownIfThereIsNoExceptionHandler() { Observable.OnSubscribe creator = new Observable.OnSubscribe() { @@ -273,7 +274,6 @@ public void call(Subscriber observer) { @Override public Observable call(Object object) { - return Observable.from(object); } }; @@ -299,7 +299,12 @@ public void call(Object object) { } }; - Observable.create(creator).flatMap(manyMapper).map(mapper).subscribe(onNext); + try { + Observable.create(creator).flatMap(manyMapper).map(mapper).subscribe(onNext); + } catch (RuntimeException e) { + e.printStackTrace(); + throw e; + } } private static Map getMap(String prefix) { diff --git a/rxjava-core/src/test/java/rx/operators/OperatorOnErrorResumeNextViaFunctionTest.java b/rxjava-core/src/test/java/rx/operators/OperatorOnErrorResumeNextViaFunctionTest.java index b071d863d6..7c7d7207e9 100644 --- a/rxjava-core/src/test/java/rx/operators/OperatorOnErrorResumeNextViaFunctionTest.java +++ b/rxjava-core/src/test/java/rx/operators/OperatorOnErrorResumeNextViaFunctionTest.java @@ -26,7 +26,9 @@ import org.mockito.Mockito; import rx.Observable; +import rx.Observable.Operator; import rx.Observer; +import rx.Subscriber; import rx.Subscription; import rx.functions.Func1; import rx.observers.TestSubscriber; @@ -143,6 +145,87 @@ public Observable call(Throwable t1) { verify(observer, times(0)).onCompleted(); } + /** + * Test that we receive the onError if an exception is thrown from an operator that + * does not have manual try/catch handling like map does. + */ + @Test + public void testOnErrorResumeReceivesErrorFromPreviousNonProtectedOperator() { + TestSubscriber ts = new TestSubscriber(); + Observable.from(1).lift(new Operator() { + + @Override + public Subscriber call(Subscriber t1) { + throw new RuntimeException("failed"); + } + + }).onErrorResumeNext(new Func1>() { + + @Override + public Observable call(Throwable t1) { + if (t1.getMessage().equals("failed")) { + return Observable.from("success"); + } else { + return Observable.error(t1); + } + } + + }).subscribe(ts); + + ts.assertTerminalEvent(); + System.out.println(ts.getOnNextEvents()); + ts.assertReceivedOnNext(Arrays.asList("success")); + } + + /** + * Test that we receive the onError if an exception is thrown from an operator that + * does not have manual try/catch handling like map does. + */ + @Test + public void testOnErrorResumeReceivesErrorFromPreviousNonProtectedOperatorOnNext() { + TestSubscriber ts = new TestSubscriber(); + Observable.from(1).lift(new Operator() { + + @Override + public Subscriber call(Subscriber t1) { + return new Subscriber() { + + @Override + public void onCompleted() { + throw new RuntimeException("failed"); + } + + @Override + public void onError(Throwable e) { + throw new RuntimeException("failed"); + } + + @Override + public void onNext(Integer t) { + throw new RuntimeException("failed"); + } + + }; + } + + }).onErrorResumeNext(new Func1>() { + + @Override + public Observable call(Throwable t1) { + if (t1.getMessage().equals("failed")) { + return Observable.from("success"); + } else { + return Observable.error(t1); + } + } + + }).subscribe(ts); + + ts.assertTerminalEvent(); + System.out.println(ts.getOnNextEvents()); + ts.assertReceivedOnNext(Arrays.asList("success")); + } + private static class TestObservable implements Observable.OnSubscribeFunc { final Subscription s; From 1b13e56e0155d3bf79da76e178675cb72c24d800 Mon Sep 17 00:00:00 2001 From: Dave Ray Date: Tue, 18 Feb 2014 11:33:19 -0800 Subject: [PATCH 023/422] Initial import of indigena-rx Imported code, fixed namespaces and adusted one test for changes since 0.14.1. --- .../src/main/clojure/rx/lang/clojure/base.clj | 75 +++ .../main/clojure/rx/lang/clojure/blocking.clj | 57 ++ .../main/clojure/rx/lang/clojure/chunk.clj | 97 ++++ .../src/main/clojure/rx/lang/clojure/core.clj | 544 ++++++++++++++++++ .../main/clojure/rx/lang/clojure/future.clj | 88 +++ .../main/clojure/rx/lang/clojure/graph.clj | 137 +++++ .../main/clojure/rx/lang/clojure/realized.clj | 127 ++++ .../clojure/rx/lang/clojure/base_test.clj | 38 ++ .../clojure/rx/lang/clojure/blocking_test.clj | 18 + .../clojure/rx/lang/clojure/chunk_test.clj | 56 ++ .../clojure/rx/lang/clojure/core_test.clj | 351 +++++++++++ .../clojure/rx/lang/clojure/future_test.clj | 37 ++ .../clojure/rx/lang/clojure/graph_test.clj | 118 ++++ .../clojure/rx/lang/clojure/realized_test.clj | 130 +++++ 14 files changed, 1873 insertions(+) create mode 100644 language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/base.clj create mode 100644 language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/blocking.clj create mode 100644 language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/chunk.clj create mode 100644 language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj create mode 100644 language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/future.clj create mode 100644 language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/graph.clj create mode 100644 language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/realized.clj create mode 100644 language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/base_test.clj create mode 100644 language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/blocking_test.clj create mode 100644 language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/chunk_test.clj create mode 100644 language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/core_test.clj create mode 100644 language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/future_test.clj create mode 100644 language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/graph_test.clj create mode 100644 language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/realized_test.clj diff --git a/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/base.clj b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/base.clj new file mode 100644 index 0000000000..0d1c181062 --- /dev/null +++ b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/base.clj @@ -0,0 +1,75 @@ +(ns rx.lang.clojure.base + "Generic, low-level rx helpers." + (:refer-clojure :exclude [merge]) + (:require [rx.lang.clojure.interop :as iop]) + (:import [rx Observable Observer Subscription] + [rx.observables BlockingObservable] + [rx.subscriptions Subscriptions])) + +(def ^:private -ns- *ns*) +(set! *warn-on-reflection* true) + +(defn wrap-on-completed + "Wrap handler with code that automaticaly calls rx.Observable.onCompleted." + [handler] + (fn [^Observer observer] + (handler observer) + (.onCompleted observer))) + +(defn wrap-on-error + "Wrap handler with code that automaticaly calls (on-error) if an exception is thrown" + [handler] + (fn [^Observer observer] + (try + (handler observer) + (catch Exception e + (.onError observer e))))) + +(defn ^Observable merge + "Observable.merge, renamed because merge means something else in Clojure + + os is one of: + + * An Iterable of Observables to merge + * An Observable> to merge + " + [os] + (cond + (instance? Iterable os) + (Observable/merge (Observable/from ^Iterable os)) + (instance? Observable os) + (Observable/merge ^Observable os) + :else + (throw (IllegalArgumentException. (str "Don't know how to merge " (type os)))))) + +(defn ^Observable merge-delay-error + "Observable.mergeDelayError, renamed because merge means something else in Clojure" + [os] + (cond + (instance? java.util.List os) + (Observable/mergeDelayError ^java.util.List os) + (instance? Observable os) + (Observable/mergeDelayError ^Observable os) + :else + (throw (IllegalArgumentException. (str "Don't know how to merge " (type os)))))) + +(defn ^Observable zip + "Observable.zip. You want map." + ([f ^Observable a ^Observable b] (Observable/zip a b (iop/fn* f))) + ([f ^Observable a ^Observable b ^Observable c] (Observable/zip a b c (iop/fn* f))) + ([f ^Observable a ^Observable b ^Observable c ^Observable d] (Observable/zip a b c d (iop/fn* f))) + ([f a b c d & more] + ; recurse on more and then pull everything together with 4 parameter version + (zip (fn [a b c more-value] + (apply f a b c more-value)) + a + b + c + (apply zip vector d more)))) + +(defmacro zip-let + [bindings & body] + (let [pairs (clojure.core/partition 2 bindings) + names (clojure.core/mapv clojure.core/first pairs) + values (clojure.core/map second pairs)] + `(zip (fn ~names ~@body) ~@values))) diff --git a/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/blocking.clj b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/blocking.clj new file mode 100644 index 0000000000..9455636b11 --- /dev/null +++ b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/blocking.clj @@ -0,0 +1,57 @@ +(ns rx.lang.clojure.blocking + "Blocking operators and functions. These should never be used in + production code except at the end of an async chain to convert from + rx land back to sync land. For example, to produce a servlet response." + (:refer-clojure :exclude [first into]) + (:require [rx.lang.clojure.core :as rx]) + (:import [rx Observable] + [rx.observables BlockingObservable])) + +(def ^:private -ns- *ns*) +(set! *warn-on-reflection* true) + +(defn ^BlockingObservable ->blocking + "Convert an Observable to a BlockingObservable" + [^Observable o] + (.toBlockingObservable o)) + +(defn first + "*Blocks* and waits for the first value emitted by the given observable. + + If an error is produced it is thrown. + + See: + clojure.core/first + rx/first + " + [observable] + (let [result (clojure.core/promise)] + (rx/subscribe (->> observable (rx/take 1)) + #(clojure.core/deliver result [:value %]) + #(clojure.core/deliver result [:error %]) + #(clojure.core/deliver result nil)) + (if-let [[type v] @result] + (case type + :value v + :error (throw v))))) + +(defn single + "*Blocks* and waits for the first value emitted by the given observable. + + An error is thrown if more then one value is produced. + " + [observable] + (.single (->blocking observable))) + +(defn into + "*Blocks* and pours the elements emitted by the given observables into + to. + + If an error is produced it is thrown. + + See: + clojure.core/into + rx/into + " + [to from-observable] + (first (rx/into to from-observable))) diff --git a/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/chunk.clj b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/chunk.clj new file mode 100644 index 0000000000..d44c26f2c2 --- /dev/null +++ b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/chunk.clj @@ -0,0 +1,97 @@ +(ns rx.lang.clojure.chunk + (:refer-clojure :exclude [chunk]) + (:require [rx.lang.clojure.core :as rx] + [rx.lang.clojure.base :as rx-base])) + +(def ^:private -ns- *ns*) +(set! *warn-on-reflection* true) + +(defn chunk + "Same as rx.Observable.merge(Observable>) but the input Observables + are \"chunked\" so that at most chunk-size of them are \"in flight\" at any given + time. + + The order of the input Observables is not preserved. + + The main purpose here is to allow a large number of Hystrix observables to + be processed in a controlled way so that the Hystrix execution queues aren't + overwhelmed. + + Example: + + (->> users + (map #(-> (GetUserCommand. %) .toObservable)) + (chunk 10)) + + See: + http://netflix.github.io/RxJava/javadoc/rx/Observable.html#merge(rx.Observable) + http://netflix.github.io/RxJava/javadoc/rx/Observable.html#mergeDelayError(rx.Observable) + " + ([chunk-size observable-source] (chunk chunk-size {} observable-source)) + ([chunk-size options observable-source] + (let [new-state-atom #(atom {:in-flight #{} ; observables currently in-flight + :buffered [] ; observables waiting to be emitted + :complete false ; true if observable-source is complete + :observer % }) ; the observer + ps #(do (printf "%s/%d/%d%n" + (:complete %) + (-> % :buffered count) + (-> % :in-flight count)) + (flush)) + + ; Given the current state, returns [action new-state]. action is the + ; next Observable or Throwable to emit, or :complete if we're done. + next-state (fn [{:keys [complete buffered in-flight] :as old}] + (cond + (empty? buffered) [complete old] + + (< (count in-flight) chunk-size) (let [next-o (first buffered)] + [next-o + (-> old + (update-in [:buffered] next) + (update-in [:in-flight] conj next-o))]) + + :else [nil old])) + + ; Advance the state, performing side-effects as necessary + advance! (fn advance! [state-atom] + (let [old-state @state-atom + [action new-state] (next-state old-state)] + (if (compare-and-set! state-atom old-state new-state) + (let [observer (:observer new-state)] + (if (:debug options) (ps new-state)) + (cond + (= :complete action) + (rx/on-completed observer) + + (instance? Throwable action) + (rx/on-error observer action) + + (instance? rx.Observable action) + (rx/on-next observer + (.finallyDo ^rx.Observable action + (reify rx.util.functions.Action0 + (call [this] + (swap! state-atom update-in [:in-flight] disj action) + (advance! state-atom))))))) + (recur state-atom)))) + + subscribe (fn [state-atom] + (rx/subscribe observable-source + (fn [o] + (swap! state-atom update-in [:buffered] conj o) + (advance! state-atom)) + + (fn [e] + (swap! state-atom assoc :complete e) + (advance! state-atom)) + + (fn [] + (swap! state-atom assoc :complete :complete) + (advance! state-atom)))) + observable (rx/fn->o (fn [observer] + (subscribe (new-state-atom observer)))) ] + (if (:delay-error? options) + (rx.Observable/mergeDelayError observable) + (rx.Observable/merge observable))))) + diff --git a/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj new file mode 100644 index 0000000000..e807882e4c --- /dev/null +++ b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj @@ -0,0 +1,544 @@ +(ns rx.lang.clojure.core + (:refer-clojure :exclude [concat cons do drop drop-while empty + filter future + interpose into keep keep-indexed + map mapcat map-indexed + merge next partition reduce reductions + rest seq some sort sort-by split-with + take take-while throw]) + (:require [rx.lang.clojure.interop :as iop] + [rx.lang.clojure.base :as base] + [rx.lang.clojure.graph :as graph] + [rx.lang.clojure.realized :as realized]) + (:import [rx Observable Observer Subscription] + [rx.observables BlockingObservable] + [rx.subscriptions Subscriptions] + [rx.util.functions Action0 Action1 Func0 Func1 Func2])) + +(set! *warn-on-reflection* true) + +(declare map map-indexed reduce take take-while) + +(defn ^Func1 fn->predicate + "Turn f into a predicate that returns true/false like Rx predicates should" + [f] + (iop/fn* (comp boolean f))) + +;################################################################################ + +(defn observable? + "Returns true if o is an rx.Observable" + [o] + (instance? Observable o)) + +;################################################################################ + +(defn on-next + "Call onNext on the given observer." + [^Observer o value] + (.onNext o value)) + +(defn on-completed + "Call onCompleted on the given observer." + [^Observer o] + (.onCompleted o)) + +(defn on-error + "Call onError on the given observer." + [^Observer o e] + (.onError o e)) + +(defn on-error-return + [^Observable o f] + (.onErrorReturn o f)) + +;################################################################################ + +(defn ^Subscription subscribe + ([^Observable o on-next-action] + (.subscribe o ^Action1 (iop/action* on-next-action))) + ([^Observable o on-next-action on-error-action] + (.subscribe o ^Action1 (iop/action* on-next-action) ^Action1 (iop/action* on-error-action))) + ([^Observable o on-next-action on-error-action on-completed-action] + (.subscribe o ^Action1 (iop/action* on-next-action) ^Action1 (iop/action* on-error-action) ^Action0 (iop/action* on-completed-action)))) + +(defn chain + "Like subscribe, but any omitted handlers pass-through to the next observable." + ([from to] + (chain from to #(on-next to %))) + ([from to on-next-action] + (chain from to on-next-action #(on-error to %))) + ([from to on-next-action on-error-action] + (chain from to on-next-action on-error-action #(on-completed to))) + ([from to on-next-action on-error-action on-completed-action] + (subscribe from on-next-action on-error-action on-completed-action))) + +(defn unsubscribe + "Unsubscribe from Subscription s and return it." + [^Subscription s] + (.unsubscribe s) + s) + +(defn ^Subscription fn->subscription + "Create a new subscription that calls the given no-arg handler function when + unsubscribe is called + + See: + rx.subscriptions.Subscriptions/create + " + [handler] + (Subscriptions/create ^Action0 (iop/action* handler))) + +;################################################################################ + +(defn ^Observable never [] (Observable/never)) +(defn ^Observable empty [] (Observable/empty)) + +(defn ^Observable return + "Returns an observable that emits a single value. + + See: + Observable/just + " + [value] + (Observable/just value)) + +(defn ^Observable fn->o + "Create an observable from the given handler. When subscribed to, (f observer) + is called at which point, f can start emitting values, etc." + [f] + (Observable/create (iop/fn* f))) + +(defn ^Observable seq->o + "Make an observable out of some seq-able thing. The rx equivalent of clojure.core/seq." + [xs] + (if xs + (Observable/from ^Iterable xs) + (empty))) + +;################################################################################ + +(defn cache + "caches the observable value so that multiple subscribers don't re-evaluate it" + [^Observable xs] + (.cache xs)) + +(defn cons + "cons x to the beginning of xs" + [x xs] + (fn->o (fn [target] + (on-next target x) + (chain xs target)))) + +(defn ^Observable concat + "Concatenate the given Observables one after the another. + + Note that xs is separate Observables which are concatentated. To concatenate an + Observable of Observables, use concat* + + See: + rx.Observable/concat + concat* + " + [& xs] + (Observable/concat (seq->o xs))) + +(defn ^Observable concat* + "Concatenate the given Observable of Observables one after another. + + See: + rx.Observable/concat + concat + " + [^Observable os] + (Observable/concat os)) + +(defn ^Observable do + "Returns a new Observable that, for each x in Observable xs, executes (do-fn x), + presumably for its side effects, and then passes x along unchanged. + + If do-fn throws an exception, that exception is emitted via onError and the sequence + is finished. + + Example: + + (->> (rx/seq->o [1 2 3]) + (rx/do println) + ...) + + Will print 1, 2, 3. + " + ([do-fn xs] + (fn->o (fn [target] + (let [state (atom {:sub nil + :error nil }) + on-next-fn (fn [v] + ; since we may not be able to unsubscribe, drop + ; anything after an error + (let [{:keys [sub error]} @state] + (if-not error + (try + (do-fn v) + (on-next target v) + (catch Throwable e + (reset! state {:error e :sub nil}) + (if sub + (unsubscribe sub)) + (on-error target e))))))] + (let [sub (chain xs target on-next-fn)] + ; dependening on xs, this may not be reached until after the sequence + ; is complete. + (swap! state update-in [:sub] (constantly sub)) + sub)))))) + +(defn ^Observable drop + [n ^Observable xs] + (.skip xs n)) + +(defn ^Observable drop-while + [p xs] + (fn->o (fn [target] + (let [dropping (atom true)] + (chain xs + target + (fn [v] + (when (or (not @dropping) + (not (reset! dropping (p v)))) + (on-next target v)))))))) + +(defn ^Observable filter + [p ^Observable xs] + (.filter xs (fn->predicate p))) + +(defn interpose + [sep xs] + (fn->o (fn [target] + (let [first? (atom true)] + (chain xs + target + (fn [v] + (if-not (compare-and-set! first? true false) + (on-next target sep)) + (on-next target v))))))) + +(defn into + "Returns an observable that emits a single value which is all of the + values of from-observable conjoined onto to + + See: + clojure.core/into + rx.Observable/toList + " + [to ^Observable from-observable] + (->> from-observable + .toList + (map (partial clojure.core/into to)))) + +(defn keep + [f xs] + (filter (complement nil?) (map xs f))) + +(defn keep + [f xs] + (filter (complement nil?) (map f xs))) + +(defn keep-indexed + [f xs] + (filter (complement nil?) (map-indexed f xs))) + +(defn ^Observable map + "Map a function over an observable sequence. Unlike clojure.core/map, only supports up + to 4 simultaneous source sequences at the moment." + ([f ^Observable xs] (.map xs (iop/fn* f))) + ([f xs & observables] (apply base/zip f xs observables))) + +(defn ^Observable mapcat + "Returns an observable which, for each value x in xs, calls (f x), which must + return an Observable. The resulting observables are concatentated together + into one observable. + + See: + clojure.core/mapcat + rx.Observable/mapMany + " + ([f ^Observable xs] (.mapMany xs (iop/fn* f))) + ; TODO multi-arg version + ) + +(defn map-indexed + "Returns an observable that invokes (f index value) for each value of the input + observable. index starts at 0. + + See: + clojure.core/map-indexed + " + [f xs] + (fn->o (fn [target] + (let [n (atom -1)] + (chain xs + target + (fn [v] (on-next target (f (swap! n inc) v)))))))) + +(defn merge + " + Returns an observable that emits a single map that consists of the rest of the + maps emitted by the input observable conj-ed onto the first. If a key occurs + in more than one map, the mapping from the latter (left-to-right) will be the + mapping in the result. + + NOTE: This is very different from rx.Observable/merge. See rx.base/merge for that + one. + + See: + clojure.core/merge + " + [maps] + (reduce clojure.core/merge {} maps)) + +(def next + "Returns an observable that emits all of the first element of the input observable. + + See: + clojure.core/next + " + (partial drop 1)) + +; TODO partition. Use Buffer whenever it's implemented. + +(defn ^Observable reduce + ([f ^Observable xs] (.reduce xs (iop/fn* f))) + ([f val ^Observable xs] (.reduce xs val (iop/fn* f)))) + +(defn ^Observable reductions + ([f ^Observable xs] (.scan xs (iop/fn* f))) + ([f val ^Observable xs] (.scan xs val (iop/fn* f)))) + +(def rest + "Same as rx/next" + next) + +(defn some + "Returns an observable that emits the first logical true value of (pred x) for + any x in xs, else completes immediately. + + See: + clojure.core/some + " + [p ^Observable xs] + (fn->o (fn [target] + (chain xs + target + (fn [v] + (when-let [result (p v)] + (on-next target result) + (on-completed target))))))) + +(defn sort + "Returns an observable that emits a single value which is a sorted sequence + of the items in coll, where the sort order is determined by comparing + items. If no comparator is supplied, uses compare. comparator must + implement java.util.Comparator. + + See: + clojure.core/sort + " + ([coll] (sort clojure.core/compare coll)) + ([comp ^Observable coll] + (.toSortedList coll (iop/fn [a b] + ; force to int so rxjava doesn't have a fit + (int (comp a b)))))) + +(defn sort-by + "Returns an observable that emits a single value which is a sorted sequence + of the items in coll, where the sort order is determined by comparing + (keyfn item). If no comparator is supplied, uses compare. comparator must + implement java.util.Comparator. + + See: + clojure.core/sort-by + " + ([keyfn coll] (sort-by keyfn clojure.core/compare coll)) + ([keyfn comp ^Observable coll] + (.toSortedList coll (iop/fn [a b] + ; force to int so rxjava doesn't have a fit + (int (comp (keyfn a) (keyfn b))))))) + +(defn split-with + "Returns an observable that emits a pair of observables + + [(take-while p xs) (drop-while p xs)] + + See: + rx.lang.clojure/take-while + rx.lang.clojure/drop-while + clojure.core/split-with + " + [p xs] + (return [(take-while p xs) (drop-while p xs)])) + +(defn ^Observable take + "Returns an observable that emits the first n elements of xs. + + See: + clojure.core/take + " + [n ^Observable xs] + {:pre [(>= n 0)]} + (.take xs n)) + +(defn take-while + [p xs] + (fn->o (fn [target] + (chain xs + target + (fn [v] + (if (p v) + (on-next target v) + (on-completed target))))))) + +;################################################################################; + +(defn throw + "Returns an Observable the simply emits the given exception with on-error + + See: + http://netflix.github.io/RxJava/javadoc/rx/Observable.html#error(java.lang.Exception) + " + [^Exception e] + (Observable/error e)) + +(defn catch* + "Returns an observable that, when Observable o triggers an error, e, continues with + Observable returned by (apply f e args) if (p e) is true. If (p e) returns a Throwable + that value is passed as e. + + If p is a class object, a normal instance? check is performed rather than calling it + as a function. If the value returned by (p e) is not true, the error is propagated. + + Examples: + + (-> my-observable + + ; On IllegalArgumentException, just emit 1 + (catch* IllegalArgumentException (fn [e] (rx/return 1))) + + ; If exception message contains \"WAT\", emit [\\W \\A \\T] + (catch* #(-> % .getMessage (.contains \"WAT\")) (rx/seq->o [\\W \\A \\T]))) + + See: + + http://netflix.github.io/RxJava/javadoc/rx/Observable.html#onErrorResumeNext(rx.util.functions.Func1) + " + [^Observable o p f & args] + (let [p (if (class? p) + (fn [e] (.isInstance ^Class p e)) + p)] + (.onErrorResumeNext o + ^Func1 (iop/fn [e] + (if-let [maybe-e (p e)] + (apply f (if (instance? Throwable maybe-e) maybe-e e) args) + (rx.lang.clojure.core/throw e)))))) + +(defmacro catch + "Macro version of catch*. + + The body of the catch is wrapped in an implicit (do). It must evaluate to an Observable. + + Example: + + (-> my-observable + ; just emit 0 on IllegalArgumentException + (catch IllegalArgumentException e + (rx/return 0)) + + (catch DependencyException e + (if (.isMinor e) + (rx/return 0) + (rx/throw (WebException. 503))))) + + See: + catch* + " + [o p binding & body] + `(catch* ~o ~p (fn [~binding] ~@body))) + +(defn finally* + "Returns an Observable that, as a side-effect, executes (apply f args) when the given + Observable completes regardless of success or failure. + + Example: + + (-> my-observable + (finally* (fn [] (println \"Done\")))) + + " + [^Observable o f & args] + (.finallyDo o ^Action0 (iop/action [] (apply f args)))) + +(defmacro finally + "Macro version of finally*. + + Example: + + (-> my-observable + (finally (println \"Done\"))) + + See: + finally* + " + [o & body] + `(finally* ~o (fn [] ~@body))) + +;################################################################################; + +(defn generator* + "Creates an observable that calls (f observable & args) which should emit a sequence. + + Automatically calls on-completed on return, or on-error if any exception is thrown. + + Subscribers will block. + + Examples: + + ; An observable that emits just 99 + (generator* on-next 99) + " + [f & args] + (fn->o (-> (fn [observer] + (apply f observer args) + (Subscriptions/empty)) + base/wrap-on-completed + base/wrap-on-error))) + +(defmacro generator + "Create an observable that executes body which should emit a sequence. bindings + should be a single [observer] argument. + + Automatically calls on-completed on return, or on-error if any exception is thrown. + + Subscribe will block. + + Examples: + + ; make an observer that emits [0 1 2 3 4] + (generator [observer] + (dotimes [i 5] + (on-next observer i))) + + " + [bindings & body] + `(generator* (fn ~bindings ~@body))) + +;################################################################################; + +; Import public graph symbols here. I want them in this namespace, but implementing +; them here with all the clojure.core symbols excluded is a pain. +(intern *ns* (with-meta 'let-o* (meta #'graph/let-o*)) @#'graph/let-o*) +(intern *ns* (with-meta 'let-o (meta #'graph/let-o)) @#'graph/let-o) + +;################################################################################; + +; Import some public realized symbols here. I want them in this namespace, but implementing +; them here with all the clojure.core symbols excluded is a pain. +(intern *ns* (with-meta 'let-realized (meta #'realized/let-realized)) @#'realized/let-realized) + diff --git a/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/future.clj b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/future.clj new file mode 100644 index 0000000000..7f6a78761d --- /dev/null +++ b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/future.clj @@ -0,0 +1,88 @@ +(ns rx.lang.clojure.future + (:refer-clojure :exclude [future]) + (:require [rx.lang.clojure.interop :as iop] + [rx.lang.clojure.core :as rx :refer [fn->o fn->subscription]] + [rx.lang.clojure.base :as base])) + +(def ^:private -ns- *ns*) +(set! *warn-on-reflection* true) + +(defn default-runner + "Default runner creator function. Creates futures on Clojure's default future thread pool." + [f] + (future-call f)) + +(defn future-generator* + "Same as rx/generator* except f is invoked in a separate thread. + + runner is a function that takes a no-arg function argument and returns a future + representing the execution of that function. + + subscribe will not block. + + See: + rx.lang.clojure.core/generator* + rx.lang.clojure.future/future-generator + " + [runner f & args] + {:pre [(ifn? runner) (ifn? f)]} + (fn->o (fn [observer] + (let [wrapped (-> (fn [o] + (apply f o args)) + base/wrap-on-completed + base/wrap-on-error) + fu (runner #(wrapped observer))] + (fn->subscription #(future-cancel fu)))))) + +(defmacro future-generator + "Same as rx/generator macro except body is invoked in a separate thread. + + runner is a function that takes a no-arg function argument and returns a future + representing the execution of that function. + + subscribe will not block. + + See: + rx.lang.clojure.core/generator* + rx.lang.clojure.future/future-generator + " + [runner bindings & body] + `(future-generator* ~runner (fn ~bindings ~@body))) + +(defn future* + "Execute (f & args) in a separate thread and pass the result to onNext. + If an exception is thrown, onError is called with the exception. + + runner is a function that takes a no-arg function argument and returns a future + representing the execution of that function. + + Returns an Observable. + " + [runner f & args] + {:pre [(ifn? runner) (ifn? f)]} + (fn->o (fn [observer] + (let [wrapped (-> #(rx/on-next % (apply f args)) + base/wrap-on-completed + base/wrap-on-error) + fu (runner #(wrapped observer))] + (fn->subscription #(future-cancel fu)))))) + +(defmacro future + "Executes body in a separate thread and passes the single result to onNext. + If an exception occurs, onError is called. + + Returns an Observable + + runner is a function that takes a no-arg function argument and returns a future + representing the execution of that function. + + Examples: + + (subscribe (rx/future rx/default-runner + (slurp \"input.txt\")) + (fn [v] (println \"Got: \" v))) + ; eventually outputs content of input.txt + " + [runner & body] + `(future* ~runner (fn [] ~@body))) + diff --git a/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/graph.clj b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/graph.clj new file mode 100644 index 0000000000..18187d29ac --- /dev/null +++ b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/graph.clj @@ -0,0 +1,137 @@ +(ns rx.lang.clojure.graph + "This is an implementation namespace. Don't use it directly. Use the symbols + in rx.lang.clojure.core + " + (:require [clojure.set :as set])) + +(def ^:private -ns- *ns*) +(set! *warn-on-reflection* true) + +(defn ^:private ->let-o*-observable + [^rx.Observable o n name] + (if (= n 1) + o + ; TODO This is a shortcut. We know the expected number of subscriptions so + ; we only need to cache values until we get the nth subscription at which + ; point, it just becomes a pass through. I haven't found a cache/replay-ish + ; operator that gives this level of control over the cached values + (.cache o))) + +(defn let-o* + "Given a graph description, returns an observable that emits a single + map of observables all hooked up and ready for subscription. + + A graph is a map from name to a map with keys: + + :deps A vector of dependency names + :factory A function that takes a map from name to Observable + for the names in :deps and returns an Observable + + Returns a map from name to Observable. Additionally, there will be a + ::non-terminals key in the map with a vector of non-terminal names. + + See: + let-o + " + [description] + (let [in-dep-counts (->> description + vals + (mapcat :deps) + frequencies) + terminals (set/difference (set (keys description)) (set (keys in-dep-counts))) + non-terminals (vec (keys in-dep-counts)) + + resolve-node (fn resolve-node [state {:keys [id deps factory] :as node}] + (let [existing (state id)] + (cond + ; It's already resolving up the stack. We've hit a cycle. + (= ::resolving existing) (throw (IllegalArgumentException. (format "Cycle found at '%s'" id))) + + ; It's already resolved. Done. + (not (nil? existing)) state + + :else + ; recursively resolve dependencies + (let [new-state (reduce (fn [s dep] + (if-let [dep-node (description dep)] + (resolve-node s (assoc dep-node :id dep)) + (throw (IllegalArgumentException. (format "Unknown node '%s' referenced from '%s'" dep id))))) + (assoc state id ::resolving) + deps) + ; execute the factory function and wrap it in an observable that delays dependencies + o (-> (select-keys new-state deps) + factory + (->let-o*-observable (in-dep-counts id 1) id))] + ; return the updated state with the resolved node + (assoc new-state id o)))))] + ; resolve the graph and build the result map + (-> (reduce (fn [s [id node]] + (resolve-node s (assoc node :id id))) + {} + description) + (select-keys terminals) + (assoc ::non-terminals non-terminals)))) + +(defmacro let-o + "Similar to clojure.core/let, but bindings are Observables and the result of the body + must be an Observable. Binding names must start with ?. Binding order doesn't matter + and any binding is visible to all other expressions as long as no cycles are produced + in the resulting Observable expression. + + The key difference here is that the macro can identify the dependencies between Observables + and correctly connect them, protecting from variations in subscribe behavior as well as + the idiosyncracies of setting up multiple subscriptions to Observables. + + This is only very useful for constructing graphs of Observables where you'd usually have + to fiddle around with publish, connect, replay and all that stuff. If you have a linear + sequence of operators, just chain them together. + + Current limitations: + + * All Observables are cache()'d so watch out for large sequences. This will be + fixed eventually. + * let-o cannot be nested. Some deep-walking macro-magic will be required for this. + + Example: + + ; Note that both ?c and ?d depend on ?b and the result Observable depends on + ; ?c and ?d. + (let-o [?a (rx/return 99) + ?b (... some observable network request ...) + ?c (rx/map vector ?a ?b) + ?d (rx/map ... ?b)] + (rx/map vector ?c ?d)) + + See: + let-o* + " + [bindings & result-body] + (let [sym->dep-sym (fn [s] + (when (and (symbol? s) + (not (namespace s)) + (.startsWith (name s) "?")) + s)) + body->dep-syms (fn [body] + (->> body + flatten + (keep sym->dep-sym) + distinct + vec)) + ->node-map (fn [[id & body]] + (let [dep-syms (body->dep-syms body) + dep-keys (->> dep-syms (map (comp keyword name)) vec)] + [(keyword (name id)) {:deps dep-keys + :factory `(fn [{:keys ~dep-syms}] ~@body) }])) + node-map (let [base-map (->> bindings + (partition 2) + (map ->node-map) + (into {})) + result-dep-syms (body->dep-syms result-body)] + (assoc base-map + :rx.lang.clojure.core/result + {:deps (mapv keyword result-dep-syms) + :factory `(fn [{:keys ~result-dep-syms}] ~@result-body) }))] + `(->> ~node-map + let-o* + :rx.lang.clojure.core/result))) + diff --git a/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/realized.clj b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/realized.clj new file mode 100644 index 0000000000..7747c71881 --- /dev/null +++ b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/realized.clj @@ -0,0 +1,127 @@ +(ns rx.lang.clojure.realized + (:require [rx.lang.clojure.interop :as iop] + [rx.lang.clojure.base :as rx-base])) + +(def ^:private -ns- *ns*) +(set! *warn-on-reflection* true) + +(defrecord ^:private PostProc [o f]) + +(defn all + "Tell realized map to capture all output of the observable, not just the last one" + [o] + (->PostProc o identity)) + +(defn only + "Tell realized map to capture the only value emitted by the observable. + If there are 0 or more than one values, an IllegalStateException is thrown + which should propagate to onError. + + This is the default mode of realized-map and let-realized. + " + [o] + (->PostProc o (fn [values] + (condp = (count values) + 1 (first values) + (throw (IllegalStateException. "Observable did not produce exactly one value")))))) + +(defn ^:private ->post-proc + [v] + (cond + (instance? rx.Observable v) (only v) + (instance? PostProc v) v + (vector? v) (->PostProc (first v) + (apply comp (reverse (next v)))) + :else (->post-proc (rx.Observable/just v)))) + +(defn realized-map + "See let-realized. + + Given a map from key to observable, returns an observable that emits a single + map from the same keys to the values emitted by their corresponding observable. + + keyvals is a list of key/value pairs where key is a key in the emitted map and val + can be one of the following: + + rx.Observable The only value of the emitted sequence is bound to the key. This is the + default since this is often a singleton response from a request. If the + Observable produces 0 or more than 1 values, an IllegalStateException is + produced. + + vector The first element of the vector must be an Observable. Remaining elements + are functions applied in sequence to the list of values emitted by the + observable. For example [my-observable first] will result in a single + value in the emitted map rather than a vector of values. + + other The value is placed in the emitted map as is + + Note the observable can also be wrapped with realized/all to get the full list rather than + just the last value. + + The purpose of this is to simplify the messy pattern of mapping observables to + single key maps, merging and then folding all the separate maps together. So code + like this: + + (rx/merge (rx-base/merge (->> (user-info-o user-id) + (rx/map (fn [u] {:user u}))) + (->> (user-likes-o user-id) + (rx/map (fn [u] {:likes u}))))) + + becomes: + + (realized-map :user (user-info-o user-id) + :likes (user-likes-o user-id)) + + See: + let-realized + " + [& keyvals] + (let [o (->> keyvals + (partition 2) + ; generate a sequence of observables + (map (fn [[k v]] + (let [{:keys [^rx.Observable o f]} (->post-proc v)] + ; pour the observable into a single list and apply post-proc func to it + (-> o + .toList + (.map (iop/fn [list] {k (f list)})))))))] + + (-> o + rx-base/merge ; funnel all the observables into a single sequence + (.reduce {} (iop/fn* merge))))) ; do the map merge dance + +(defn ^rx.Observable realized-map* + "Same as realized-map, but takes a map argument rather than key-value pairs." + [map-description] + (apply realized-map (apply concat map-description))) + +(defmacro let-realized + "'let' version of realized map. + + (let-realized [a (make-observable)] + (* 2 a)) + + is equivalent to: + + (->> (realized-map :a (make-observable)) + (map (fn [{:keys [a]}] (* 2 a)))) + + That is, it eliminates the repition of the map keys when you want to do something + with the final result. + + Evaluates to an Observable that emits the value of the let body. + + See: + rx.lang.clojure.realized/realized-map + rx.lang.clojure.realized/all + " + [bindings & body] + (let [b-parts (partition 2 bindings) + b-map (->> b-parts + (map (fn [[k v]] + [(keyword (name k)) v])) + (into {})) + b-names (mapv first b-parts)] + `(.map (realized-map* ~b-map) + (iop/fn [{:keys ~b-names}] ~@body)))) + diff --git a/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/base_test.clj b/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/base_test.clj new file mode 100644 index 0000000000..985e7eeb1f --- /dev/null +++ b/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/base_test.clj @@ -0,0 +1,38 @@ +(ns rx.lang.clojure.base-test + (:require [rx.lang.clojure.core :as rx] + [rx.lang.clojure.base :as b] + [rx.lang.clojure.blocking :as blocking] + [rx.lang.clojure.future :as f] + ) + (:require [clojure.test :refer [deftest testing is]])) + +(deftest test-zip + (testing "is happy with less than 4 args" + (is (= [[1 2 3]] (blocking/into [] (b/zip vector + (rx/seq->o [1]) (rx/seq->o [2]) (rx/seq->o [3])))))) + (testing "is happy with more than 4 args" + (is (= [[1 2 3 4 5 6 7 8]] + (blocking/into [] (b/zip vector + (rx/seq->o [1]) + (rx/seq->o [2]) + (rx/seq->o [3]) + (rx/seq->o [4]) + (rx/seq->o [5]) + (rx/seq->o [6]) + (rx/seq->o [7]) + (rx/seq->o [8]))))))) + +(deftest test-merge + (is (= [[1 3 5] [2 4 6]] + (let [r (blocking/into [] + (b/merge [(f/future-generator f/default-runner [o] + (doseq [x [1 3 5]] + (Thread/sleep 10) + (rx/on-next o x))) + (f/future-generator f/default-runner [o] + (doseq [x [2 4 6]] + (Thread/sleep 10) + (rx/on-next o x)))]))] + ; make sure each sequence maintained original order + [(keep #{1 3 5} r) + (keep #{2 4 6} r) ])))) diff --git a/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/blocking_test.clj b/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/blocking_test.clj new file mode 100644 index 0000000000..0963c14bab --- /dev/null +++ b/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/blocking_test.clj @@ -0,0 +1,18 @@ +(ns rx.lang.clojure.blocking-test + (:require [rx.lang.clojure.blocking :as b] + [rx.lang.clojure.core :as rx] + [clojure.test :refer [deftest testing is]])) + +(deftest test-first + (testing "returns first element of observable" + (is (= 1 (b/first (rx/return 1))))) + (testing "returns nil for empty observable" + (is (nil? (b/first (rx/empty)))))) + +(deftest test-single + (testing "returns one element" + (is (= 1 (b/single (rx/return 1))))) + (testing "throw if empty" + (is (thrown? java.lang.IllegalArgumentException (b/single (rx/empty))))) + (testing "throw if many" + (is (thrown? java.lang.IllegalArgumentException (b/single (rx/seq->o [1 2])))))) diff --git a/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/chunk_test.clj b/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/chunk_test.clj new file mode 100644 index 0000000000..c0d8974e8b --- /dev/null +++ b/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/chunk_test.clj @@ -0,0 +1,56 @@ +(ns rx.lang.clojure.chunk-test + (:require [rx.lang.clojure.chunk :as rx-chunk] + [rx.lang.clojure.core :as rx] + [rx.lang.clojure.future :as rx-future] + [rx.lang.clojure.blocking :as rx-blocking] + [clojure.test :refer [deftest testing is]])) + + +(deftest test-chunk + (let [n 20 + chunk-size 10 + factory (rx-future/future-generator rx-future/default-runner [o] + (doseq [i (range n)] + (Thread/sleep (rand-int 50)) + (rx/on-next o (rx-future/future rx-future/default-runner + (let [t (rand-int 500)] + (Thread/sleep t)) + i))))] + (is (= (range n) + (sort (rx-blocking/into [] + (rx-chunk/chunk chunk-size {:debug true} factory))))))) + +(deftest test-chunk-with-error + (testing "error from source is propagated" + (let [n 20 + chunk-size 4 + factory (rx-future/future-generator rx-future/default-runner [o] + (doseq [i (range n)] + (Thread/sleep (rand-int 50)) + (rx/on-next o (rx-future/future rx-future/default-runner + (let [t (rand-int 1000)] + (Thread/sleep t)) + i))) + (throw (IllegalArgumentException. "hi")))] + (is (thrown-with-msg? IllegalArgumentException #"hi" + (rx-blocking/into [] + (rx-chunk/chunk chunk-size {:debug true} factory)))))) + + (testing "error from single observable is propagated" + (let [n 20 + chunk-size 4 + factory (rx-future/future-generator rx-future/default-runner [o] + (doseq [i (range n)] + (Thread/sleep (rand-int 50)) + (rx/on-next o (rx-future/future rx-future/default-runner + (let [t (rand-int 1000)] + (throw (IllegalArgumentException. "byebye")) + (Thread/sleep t)) + i))))] + (is (thrown? rx.exceptions.CompositeException + (rx-blocking/into [] + (rx-chunk/chunk chunk-size + {:debug true + :delay-error? true } + factory))))))) + diff --git a/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/core_test.clj b/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/core_test.clj new file mode 100644 index 0000000000..7af636345d --- /dev/null +++ b/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/core_test.clj @@ -0,0 +1,351 @@ +(ns rx.lang.clojure.core-test + (:require [rx.lang.clojure.core :as rx] + [rx.lang.clojure.blocking :as b] + [clojure.test :refer [deftest is testing]])) +(comment + (rx/subscribe + (->> + (rx/map (fn [a b] (hash-map :a a :b b)) (rx/seq->o [1 2 3 4 5 6]) + (rx/seq->o ["x" "y" 9 10 "z"])) + (rx/map #(update-in % [:a] (partial * 2))) + (rx/filter (comp not number? :b)) + (rx/concat (rx/seq->o (range 3))) + (rx/drop 1) + (rx/take 6) + (rx/into [])) + println + println)) + +(deftest test-observable? + (is (rx/observable? (rx/return 99))) + (is (not (rx/observable? "I'm not an observable")))) + +(deftest test-subscribe + (testing "subscribe overload with only onNext" + (let [o (rx/return 1) + called (atom nil)] + (rx/subscribe o (fn [v] (swap! called (fn [_] v)))) + (is (= 1 @called))))) + +(deftest test-generator + (testing "calls on-completed automatically" + (let [o (rx/generator [o]) + called (atom nil)] + (rx/subscribe o (fn [v]) (fn [_]) #(reset! called "YES")) + (is (= "YES" @called)))) + + (testing "exceptions automatically go to on-error" + (let [expected (IllegalArgumentException. "hi") + actual (atom nil)] + (rx/subscribe (rx/generator [o] (throw expected)) + (fn [v]) + #(reset! actual %)) + (is (identical? expected @actual))))) + +(deftest test-seq->o + (is (= [] (b/into [] (rx/seq->o [])))) + (is (= [] (b/into [] (rx/seq->o nil)))) + (is (= [0 1 2 3] (b/first (rx/into [] (rx/seq->o (range 4)))))) + (is (= #{0 1 2 3} (b/first (rx/into #{} (rx/seq->o (range 4)))))) + (is (= {:a 1 :b 2 :c 3} (b/first (rx/into {} (rx/seq->o [[:a 1] [:b 2] [:c 3]])))))) + +(deftest test-return + (is (= [0] (b/into [] (rx/return 0))))) + +(deftest test-cache + (let [value (atom 0) + o (->> + (rx/return 0) + (rx/map (fn [x] (swap! value inc))) + (rx/cache))] + (is (= 1 (b/single o))) + (is (= 1 @value)) + (is (= 1 (b/single o))) + (is (= 1 @value)) + (is (= 1 (b/single o))))) + +(deftest test-cons + (is (= [1] (b/into [] (rx/cons 1 (rx/empty))))) + (is (= [1 2 3 4] (b/into [] (rx/cons 1 (rx/seq->o [2 3 4])))))) + +(deftest test-concat + (is (= [:q :r] + (b/into [] (rx/concat (rx/seq->o [:q :r]))))) + (is (= [:q :r 1 2 3] + (b/into [] (rx/concat (rx/seq->o [:q :r]) + (rx/seq->o [1 2 3])))))) + +(deftest test-concat* + (is (= [:q :r] + (b/into [] (rx/concat* (rx/return (rx/seq->o [:q :r])))))) + (is (= [:q :r 1 2 3] + (b/into [] (rx/concat* (rx/seq->o [(rx/seq->o [:q :r]) + (rx/seq->o [1 2 3])])))))) + +(deftest test-do + (testing "calls a function with each element" + (let [collected (atom [])] + (is (= [1 2 3] + (->> (rx/seq->o [1 2 3]) + (rx/do (fn [v] + (swap! collected conj (* 2 v)))) + (rx/do (partial println "GOT")) + (b/into [])))) + (is (= [2 4 6] @collected)))) + (testing "ends sequence with onError if action code throws an exception" + (let [collected (atom []) + o (->> (rx/seq->o [1 2 3]) + (rx/do (fn [v] + (if (= v 2) + (throw (IllegalStateException. (str "blah" v))) + (swap! collected conj (* 99 v))))))] + (is (thrown-with-msg? IllegalStateException #"blah2" + (b/into [] o))) + (is (= [99] @collected))))) + +(deftest test-drop-while + (is (= (into [] (drop-while even? [2 4 6 8 1 2 3])) + (b/into [] (rx/drop-while even? (rx/seq->o [2 4 6 8 1 2 3]))))) + (is (= (into [] (drop-while even? [2 4 6 8 1 2 3])) + (b/into [] (rx/drop-while even? (rx/seq->o [2 4 6 8 1 2 3])))))) + +(deftest test-filter + (is (= (into [] (->> [:a :b :c :d :e :f :G :e] + (filter #{:b :e :G}))) + (b/into [] (->> (rx/seq->o [:a :b :c :d :e :f :G :e]) + (rx/filter #{:b :e :G})))))) + +(deftest test-interpose + (is (= (interpose \, [1 2 3]) + (b/into [] (rx/interpose \, (rx/seq->o [1 2 3])))))) + +(deftest test-into + (is (= (into [6 7 8] [9 10 [11]]) + (b/first (rx/into [6 7 8] (rx/seq->o [9 10 [11]])))))) + +(deftest test-keep + (is (= (into [] (keep identity [true true false])) + (b/into [] (rx/keep identity (rx/seq->o [true true false]))))) + + (is (= (into [] (keep #(if (even? %) (* 2 %)) (range 9))) + (b/into [] (rx/keep #(if (even? %) (* 2 %)) (rx/seq->o (range 9))))))) + +(deftest test-keep-indexed + (is (= (into [] (keep-indexed (fn [i v] + (if (even? i) v)) + [true true false])) + (b/into [] (rx/keep-indexed (fn [i v] + (if (even? i) v)) + (rx/seq->o [true true false])))))) + +(deftest test-map + (is (= (into {} (map (juxt identity name) + [:q :r :s :t :u])) + (b/into {} (rx/map (juxt identity name) + (rx/seq->o [:q :r :s :t :u]))))) + (is (= (into [] (map vector + [:q :r :s :t :u] + (range 10) + ["a" "b" "c" "d" "e"] )) + (b/into [] (rx/map vector + (rx/seq->o [:q :r :s :t :u]) + (rx/seq->o (range 10) ) + (rx/seq->o ["a" "b" "c" "d" "e"] ))))) + ; check > 4 arg case + (is (= (into [] (map vector + [:q :r :s :t :u] + [:q :r :s :t :u] + [:q :r :s :t :u] + (range 10) + (range 10) + (range 10) + ["a" "b" "c" "d" "e"] + ["a" "b" "c" "d" "e"] + ["a" "b" "c" "d" "e"])) + (b/into [] (rx/map vector + (rx/seq->o [:q :r :s :t :u]) + (rx/seq->o [:q :r :s :t :u]) + (rx/seq->o [:q :r :s :t :u]) + (rx/seq->o (range 10)) + (rx/seq->o (range 10)) + (rx/seq->o (range 10)) + (rx/seq->o ["a" "b" "c" "d" "e"]) + (rx/seq->o ["a" "b" "c" "d" "e"]) + (rx/seq->o ["a" "b" "c" "d" "e"])))))) + +(deftest test-map-indexed + (is (= (map-indexed vector [:a :b :c]) + (b/into [] (rx/map-indexed vector (rx/seq->o [:a :b :c])))))) + +(deftest test-merge + (is (= [{:a 1 :b 2 :c 3 :d 4}] + (b/into [] (rx/merge (rx/seq->o [{:a 1 :d 0} {:b 2} {:c 3} {:d 4} ])))))) + +(deftest test-mapcat + (let [f (fn [v] [v (* v v)]) + xs (range 10)] + (is (= (mapcat f xs) + (b/into [] (rx/mapcat (comp rx/seq->o f) (rx/seq->o xs)))))) + (comment + (is (= (into [] (mapcat vector + [:q :r :s :t :u] + (range 10) + ["a" "b" "c" "d" "e"] )) + (b/into [] (rx/mapcat vector + (rx/seq->o [:q :r :s :t :u]) + (rx/seq->o (range 10) ) + (rx/seq->o ["a" "b" "c" "d" "e"] ))))))) + +(deftest test-next + (let [in [:q :r :s :t :u]] + (is (= (next in) (b/into [] (rx/next (rx/seq->o in))))))) + +(deftest test-rest + (let [in [:q :r :s :t :u]] + (is (= (rest in) (b/into [] (rx/rest (rx/seq->o in))))))) + +(deftest test-reduce + (is (= (reduce + 0 (range 4)) + (b/first (rx/reduce + 0 (rx/seq->o (range 4))))))) + +(deftest test-reductions + (is (= (into [] (reductions + 0 (range 4))) + (b/into [] (rx/reductions + 0 (rx/seq->o (range 4))))))) + +(deftest test-some + (is (= [:r] (b/into [] (rx/some #{:r :s :t} (rx/seq->o [:q :v :r]))))) + (is (= [] (b/into [] (rx/some #{:r :s :t} (rx/seq->o [:q :v])))))) + +(deftest test-sort + (is (= [[]] (b/into [] (rx/sort (rx/empty))))) + (is (= [[1 2 3]] + (b/into [] (rx/sort (rx/seq->o [3 1 2]))))) + (is (= [[3 2 1]] + (b/into [] (rx/sort (fn [a b] (- (compare a b))) (rx/seq->o [2 1 3])))))) + +(deftest test-sort-by + (is (= [[]] (b/into [] (rx/sort-by :foo (rx/empty))))) + (is (= [[{:foo 1} {:foo 2} {:foo 3}]] + (b/into [] (rx/sort-by :foo (rx/seq->o [{:foo 2}{:foo 1}{:foo 3}]))))) + (is (= [[{:foo 3} {:foo 2} {:foo 1}]] + (b/into [] (rx/sort-by :foo (fn [a b] (- (compare a b))) (rx/seq->o [{:foo 2}{:foo 1}{:foo 3}])))))) + +(deftest test-split-with + (is (= (split-with (partial >= 3) (range 6)) + (->> (rx/seq->o (range 6)) + (rx/split-with (partial >= 3)) + b/first + (map (partial b/into [])))))) + +(deftest test-take-while + (is (= (into [] (take-while even? [2 4 6 8 1 2 3])) + (b/into [] (rx/take-while even? (rx/seq->o [2 4 6 8 1 2 3])))))) + +(deftest test-throw + (let [expected (IllegalArgumentException. "HI") + called (atom nil)] + (rx/subscribe (rx/throw expected) + (fn [_]) + (fn [e] (reset! called expected)) + (fn [_])) + (is (identical? expected @called)))) + +(deftest test-catch* + (testing "Is just a passthrough if there's no error" + (is (= [1 2 3] + (b/into [] + (-> + (rx/seq->o [1 2 3]) + (rx/catch* Exception (fn [e] (throw "OH NO")))))))) + + (testing "Can catch a particular exception type and continue with an observable" + (is (= [1 2 4 5 6 "foo"] + (b/into [] + (-> + (rx/generator [o] + (rx/on-next o 1) + (rx/on-next o 2) + (rx/on-error o (IllegalStateException. "foo"))) + (rx/catch* IllegalStateException + (fn [e] + (rx/seq->o [4 5 6 (.getMessage e)])))))))) + + (testing "if exception isn't matched, it's passed to on-error" + (let [expected (IllegalArgumentException. "HI") + called (atom nil)] + (rx/subscribe (-> + (rx/generator [o] + (rx/on-next o 1) + (rx/on-next o 2) + (rx/on-error o expected)) + (rx/catch* IllegalStateException (fn [e] + (rx/return "WAT?")))) + (fn [_]) + (fn [e] (reset! called expected)) + (fn [_])) + (is (identical? expected @called)))) + + (testing "if p returns Throwable, that's passed as e" + (let [cause (IllegalArgumentException. "HI") + wrapper (java.util.concurrent.ExecutionException. cause)] + (is (= [cause] + (b/into [] + (-> + (rx/generator [o] + (rx/on-error o wrapper)) + (rx/catch #(.getCause %) e + (rx/return e))))))))) + + +(deftest test-finally + (testing "Supports a finally clause" + (testing "called on completed" + (let [completed (atom nil) + called (atom nil)] + (rx/subscribe (-> + (rx/seq->o [1 2 3]) + (rx/finally* (fn [extra] (reset! called (str "got " extra))) + "it")) + (fn [_]) + (fn [_] (throw (IllegalStateException. "WAT"))) + (fn [] (reset! completed "DONE"))) + (is (= "got it" @called)) + (is (= "DONE" @completed)))) + + (testing "called on error" + (let [expected (IllegalStateException. "expected") + completed (atom nil) + called (atom nil)] + (rx/subscribe (-> + (rx/generator [o] + (rx/on-next o 1) + (rx/on-next o 2) + (rx/on-error o expected)) + (rx/finally + (reset! called "got it"))) + (fn [_]) + (fn [e] (reset! completed e)) + (fn [] (throw (IllegalStateException. "WAT")))) + (is (= "got it" @called)) + (is (identical? expected @completed)))))) + +;################################################################################ + +(deftest test-graph-imports + (is (= 99 + (-> {:a {:deps [] :factory (fn [_] (rx/return 99))}} + rx/let-o* + :a + b/single))) + (is (= 100 + (b/single (rx/let-o [?a (rx/return 100)] + ?a))))) + +;################################################################################ + +(deftest test-realized-imports + (is (= {:a 1 :b 2} + (->> (rx/let-realized [a (rx/return 1) + b (rx/return 2)] + {:a a :b b}) + b/single)))) diff --git a/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/future_test.clj b/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/future_test.clj new file mode 100644 index 0000000000..dccf89c908 --- /dev/null +++ b/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/future_test.clj @@ -0,0 +1,37 @@ +(ns rx.lang.clojure.future-test + (:require [rx.lang.clojure.core :as rx] + [rx.lang.clojure.blocking :as blocking] + [rx.lang.clojure.future :as f]) + (:require [clojure.test :refer [deftest testing is]])) + +(deftest test-future-generator + (is (not= [(.getId (Thread/currentThread))] + (blocking/into [] + (f/future-generator f/default-runner + [observer] + (rx/on-next observer (.getId (Thread/currentThread)))))))) + +(deftest test-future + (is (= [15] (blocking/into [] (f/future* f/default-runner + 1 2 3 4 5)))) + (is (= [15] (blocking/into [] (f/future f/default-runner (println "HI") (+ 1 2 3 4 5))))) ) + + +(comment (rx/subscribe (f/future* f/default-runner + 1 2 3 4 5) + (fn [v] (println "RESULT: " v)) + (fn [e] (println "ERROR: " e)) + #(println "COMPLETED"))) + +(comment (rx/subscribe (f/future f/default-runner + (Thread/sleep 5000) + (+ 100 200)) + (fn [v] (println "RESULT: " v)) + (fn [e] (println "ERROR: " e)) + #(println "COMPLETED"))) + +(comment (rx/subscribe (f/future f/default-runner + (Thread/sleep 2000) + (throw (Exception. "Failed future"))) + (fn [v] (println "RESULT: " v)) + (fn [e] (println "ERROR: " e)) + #(println "COMPLETED"))) + diff --git a/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/graph_test.clj b/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/graph_test.clj new file mode 100644 index 0000000000..2b3fdde98c --- /dev/null +++ b/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/graph_test.clj @@ -0,0 +1,118 @@ +(ns rx.lang.clojure.graph-test + (:require [rx.lang.clojure.graph :as graph] + [rx.lang.clojure.core :as rx] + [rx.lang.clojure.future :as rx-future] + [rx.lang.clojure.blocking :as rx-blocking] + [clojure.test :refer [deftest testing is]])) + +(deftest test-let-o* + (testing "throws on cycle" + (is (thrown-with-msg? IllegalArgumentException #"Cycle found" + (graph/let-o* {:a {:deps [:a]}})))) + + (testing "throws on unknown" + (is (thrown-with-msg? IllegalArgumentException #"Unknown node" + (graph/let-o* {:a {:deps [:b]}})))) + + (testing "it works in a simple case" + (let [d {:a {:deps [] + :factory (fn [_] (rx/seq->o [1 2 3 4 5]))} + :b {:deps [:a] + :factory (fn [{:keys [a]}] (rx/map #(* % %) a)) } + :c {:deps [:a :b] + :factory (fn [{:keys [a b]}] (rx/map #(+ %1 %2) a b)) } + :d {:deps [:c :b] + :factory (fn [{:keys [c b]}] (rx/map #(+ %1 %2) c b)) } + } + f (graph/let-o* d) ] + (println f) + ; (n^2 + n) + n^2 + (is (= [3 10 21 36 55] + (rx-blocking/into [] (:d f))))))) + +(deftest test-let-o + (testing "it works" + (let [f (graph/let-o [?a (rx/seq->o [1 2 3]) + ?b (rx/seq->o [4 5 6])] + (rx/map + ?a ?b))] + (is (= [5 7 9] + (rx-blocking/into [] f))))) + + (testing "it still works" + (is (= {:a 99 :b 100 :z "hi"} + (rx-blocking/single + (-> (let [z (rx/return "hi")] ; an observable from "somewhere else" + (graph/let-o + [?a (rx-future/future rx-future/default-runner (Thread/sleep 50) 99) + ?b (rx-future/future rx-future/default-runner (Thread/sleep 500) 100) + ?c (rx/map #(hash-map :a %1 :b %2 :z %3) ?a ?b ?z) + ?z z] + (rx/merge ?c))))))))) + +(deftest test-complicated-graph + ; These funcs model network requests for various stuff. They all return observable. + (let [request-vhs (fn [] + (rx-future/future-generator rx-future/default-runner + [o] + (Thread/sleep 50) + (doseq [i (range 3)] + (rx/on-next o {:id i})))) + request-user (fn [id] + (rx-future/future rx-future/default-runner + (Thread/sleep (rand-int 250)) + {:id id + :name (str "friend" id) })) + request-ab (fn [u] + (rx-future/future rx-future/default-runner + (Thread/sleep (rand-int 250)) + {:user-id (:id u) + :cell (* 2 (:id u))})) + + request-video-md (fn [v] + (rx/return {:video v + :title (str "title" (:id v)) })) + + ; Now we can stitch all these requests together into an rx graph to + ; produce a response. + o (graph/let-o [?user-info (rx-future/future rx-future/default-runner + (Thread/sleep 20) + {:name "Bob" + :id 12345 + :friend-ids [1 2 3] }) + + ?friends (->> ?user-info + (rx/mapcat (fn [ui] + (rx/mapcat request-user + (rx/seq->o (:friend-ids ui)))))) + + ?ab (->> (rx/concat ?user-info ?friends) + (rx/mapcat request-ab)) + + ?ab-lookup (->> ?ab + (rx/map (juxt :user-id #(dissoc % :user-id))) + (rx/into {})) + + ?vhs (request-vhs) + + + ?metadata (->> ?vhs + (rx/mapcat request-video-md))] + (rx/map (fn [u m f ab-lookup] + {:user (dissoc u :friend-ids) + :videos m + :friends (sort-by :id f) + :ab ab-lookup}) + ?user-info + (rx/into [] ?metadata) + (rx/into [] ?friends) + ?ab-lookup))] + + (is (= {:user {:name "Bob" :id 12345} + :videos [{:video {:id 0} :title "title0"} + {:video {:id 1} :title "title1"} + {:video {:id 2} :title "title2"}] + :friends [{:name "friend1" :id 1}{:name "friend2" :id 2}{:name "friend3" :id 3}] + :ab {12345 {:cell 24690} 1 {:cell 2} 2 {:cell 4} 3 {:cell 6}} } + (rx-blocking/single o))))) + + diff --git a/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/realized_test.clj b/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/realized_test.clj new file mode 100644 index 0000000000..3bf9b16872 --- /dev/null +++ b/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/realized_test.clj @@ -0,0 +1,130 @@ +(ns rx.lang.clojure.realized-test + (:require [rx.lang.clojure.realized :as r] + [rx.lang.clojure.core :as rx] + [rx.lang.clojure.future :as rx-future] + [rx.lang.clojure.blocking :as rx-blocking] + [clojure.test :refer [deftest testing is]])) + + + +(deftest test-realized-map + (testing "Turns map of observables into observable of map" + (let [o (r/realized-map :a (r/all (rx/seq->o [1 2 3])) + :a2 (rx/seq->o [99 100 101]) + :b (rx/return "hi") + :c [(->> [1 2 3] + rx/seq->o + (rx/map #(* % %))) + next] + :d (rx/return "just one") + :e "just a value") + result (rx-blocking/single o)] + (is (= {:a [1 2 3] + :a2 101 + :b "hi" + :c [4 9] + :d "just one" + :e "just a value" } + result))))) + +(deftest test-realized-map + (testing "works like realized-map, but takes a map instead of key/value pairs" + (is (= {:a [1 2] + :b 500 } + (->> {:a (r/all (rx/seq->o [1 2])) + :b 500 } + r/realized-map* + rx-blocking/single))))) + +(deftest test-let-realized + (is (= {:a* 2 + :b* 500 + :c* 1000 } + (->> (r/let-realized [a [(rx/seq->o [1 2]) last] + b 500 + c (rx/return 1000) ] + {:a* a + :b* b + :c* c }) + rx-blocking/single)))) + +(deftest test-only + (testing "raises IllegalStateException if sequence is empty" + (is (thrown-with-msg? IllegalStateException #"did not produce" + (->> (r/let-realized [a (rx/seq->o [1 2])] + {:a a}) + rx-blocking/single))) + ; Just to be sure, make sure it goes through onError. + (let [values (atom []) + errors (atom [])] + (rx/subscribe (r/let-realized [a (rx/seq->o [1 2])] + {:a a}) + #(swap! values conj %) + #(swap! errors conj %)) + (is (empty? @values)) + (is (= 1 (count @errors))) + (let [[e] @errors] + (is (instance? IllegalStateException e)))))) + +(deftest test-all + (testing "collects all values from an observable" + (is (= [1 2 3] + (->> (r/let-realized [a (r/all (rx/seq->o [1 2 3]))] + a) + rx-blocking/single))))) + +; Playing with some expressing some of the video stuff with this. +(comment + (->> (get-list-of-lists user-id) + (rx/mapcat (fn [list] + (->> (video-list->videos list) + (rx/take 10)))) + (rx/mapcat (fn [video] + (->> (r/let-realized [md (video->metadata video) + bm (video->bookmark video) + rt (video->rating video user-id)] + {:id (:id video) + :title (:title md) + :length (:duration md) + :bookmark bm + :rating {:actual (:actual-star-rating rt) + :average (:average-star-rating rt) + :predicted (:predicted-star-rating rt) } }))))) + + (->> (get-list-of-lists user-id) + (rx/mapcat (fn [list] + (->> (video-list->videos list) + (rx/take 10)))) + (rx/mapcat (fn [video] + (->> (r/realized-map :md (video->metadata video) + :bm (video->bookmark video) + :rt (video->rating video user-id)) + (rx/map (fn [{:keys [md bm rt]}] + {:id (:id video) + :title (:title md) + :length (:duration md) + :bookmark bm + :rating {:actual (:actual-star-rating rt) + :average (:average-star-rating rt) + :predicted (:predicted-star-rating rt) } })))))) + + (->> (get-list-of-lists user-id) + (rx/mapcat (fn [list] + (->> (video-list->videos list) + (rx/take 10)))) + (rx/mapcat (fn [video] + (->> (r/realized-map :id (:id video) + :md [(video->metadata video) + first + #(select-keys % [:title :duration])] + :bookmark (video->bookmark video) + :rating [(video->rating video user-id) + first + #(hash-map :actual (:actual-star-rating %) + :average (:average-star-rating %) + :predicted (:predicted-star-rating %))]) + (rx/map (fn [m] + (-> m + (merge (:md m)) + (dissoc :md))))))))) + From 80ba1b3bd041a3dcfd58d8c295ce45cdc6076fe1 Mon Sep 17 00:00:00 2001 From: Dave Ray Date: Tue, 18 Feb 2014 14:28:14 -0800 Subject: [PATCH 024/422] Replace chain with operators. General core cleanup --- .../src/main/clojure/rx/lang/clojure/core.clj | 230 ++++++++++-------- .../clojure/rx/lang/clojure/core_test.clj | 62 +++-- 2 files changed, 181 insertions(+), 111 deletions(-) diff --git a/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj index e807882e4c..ac3a21e647 100644 --- a/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj +++ b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj @@ -1,6 +1,6 @@ (ns rx.lang.clojure.core (:refer-clojure :exclude [concat cons do drop drop-while empty - filter future + filter first future interpose into keep keep-indexed map mapcat map-indexed merge next partition reduce reductions @@ -10,14 +10,14 @@ [rx.lang.clojure.base :as base] [rx.lang.clojure.graph :as graph] [rx.lang.clojure.realized :as realized]) - (:import [rx Observable Observer Subscription] + (:import [rx Observable Observer Subscriber Subscription Observable$Operator Observable$OnSubscribe] [rx.observables BlockingObservable] [rx.subscriptions Subscriptions] [rx.util.functions Action0 Action1 Func0 Func1 Func2])) (set! *warn-on-reflection* true) -(declare map map-indexed reduce take take-while) +(declare concat map map-indexed reduce take take-while) (defn ^Func1 fn->predicate "Turn f into a predicate that returns true/false like Rx predicates should" @@ -62,16 +62,58 @@ ([^Observable o on-next-action on-error-action on-completed-action] (.subscribe o ^Action1 (iop/action* on-next-action) ^Action1 (iop/action* on-error-action) ^Action0 (iop/action* on-completed-action)))) -(defn chain - "Like subscribe, but any omitted handlers pass-through to the next observable." - ([from to] - (chain from to #(on-next to %))) - ([from to on-next-action] - (chain from to on-next-action #(on-error to %))) - ([from to on-next-action on-error-action] - (chain from to on-next-action on-error-action #(on-completed to))) - ([from to on-next-action on-error-action on-completed-action] - (subscribe from on-next-action on-error-action on-completed-action))) +(defn ^Subscriber ->subscriber + "" + ([o on-next-action] (->subscriber o on-next-action nil nil)) + ([o on-next-action on-error-action] (->subscriber o on-next-action on-error-action)) + ([^Subscriber o on-next-action on-error-action on-completed-action] + (proxy [Subscriber] [o] + (onCompleted [] + (if on-completed-action + (on-completed-action o) + (on-completed o))) + (onError [e] + (if on-error-action + (on-error-action o e) + (on-error o e))) + (onNext [t] + (if on-next-action + (on-next-action o t) + (on-next o t)))))) + +(defn ^Observable$Operator ->operator + "Create a basic Operator with the given handler fns. If a handler is omitted or nil + it's treated as a pass-through. + + on-next-action Passed Subscriber and value + on-error-action Passed Throwable + on-completed-action No-args + + See: + lift + rx.Observable$Operator + " + [input] + {:pre [(fn? input)]} + (reify Observable$Operator + (call [this o] + (input o)))) + +(defn lift + "Lift the Operator op over the given Observable xs + + Example: + + (->> my-observable + (rx/lift (rx/->operator ...)) + ...) + + See: + rx.Observable/lift + ->operator + " + [^Observable$Operator op ^Observable xs] + (.lift xs op)) (defn unsubscribe "Unsubscribe from Subscription s and return it." @@ -79,6 +121,16 @@ (.unsubscribe s) s) +(defn unsubscribed? + "Returns true if the given Subscription (or Subscriber) is unsubscribed. + + See: + rx.Observable/create + fn->o + " + [^Subscription s] + (.isUnsubscribed s)) + (defn ^Subscription fn->subscription "Create a new subscription that calls the given no-arg handler function when unsubscribe is called @@ -89,6 +141,19 @@ [handler] (Subscriptions/create ^Action0 (iop/action* handler))) +(defn ^Observable fn->o + "Create an Observable from the given function. + + When subscribed to, (f subscriber) is called at which point, f can start emitting values, etc. + The passed subscriber is of type rx.Subscriber. + + See: + rx.Subscriber + rx.Observable/create + " + [f] + (Observable/create ^Observable$OnSubscribe (iop/action* f))) + ;################################################################################ (defn ^Observable never [] (Observable/never)) @@ -103,12 +168,6 @@ [value] (Observable/just value)) -(defn ^Observable fn->o - "Create an observable from the given handler. When subscribed to, (f observer) - is called at which point, f can start emitting values, etc." - [f] - (Observable/create (iop/fn* f))) - (defn ^Observable seq->o "Make an observable out of some seq-able thing. The rx equivalent of clojure.core/seq." [xs] @@ -119,16 +178,17 @@ ;################################################################################ (defn cache - "caches the observable value so that multiple subscribers don't re-evaluate it" + "caches the observable value so that multiple subscribers don't re-evaluate it. + + See: + rx.Observable/cache" [^Observable xs] (.cache xs)) (defn cons "cons x to the beginning of xs" [x xs] - (fn->o (fn [target] - (on-next target x) - (chain xs target)))) + (concat (return x) xs)) (defn ^Observable concat "Concatenate the given Observables one after the another. @@ -162,64 +222,46 @@ Example: - (->> (rx/seq->o [1 2 3]) - (rx/do println) - ...) + (->> (rx/seq->o [1 2 3]) + (rx/do println) + ...) - Will print 1, 2, 3. + Will print 1, 2, 3. " - ([do-fn xs] - (fn->o (fn [target] - (let [state (atom {:sub nil - :error nil }) - on-next-fn (fn [v] - ; since we may not be able to unsubscribe, drop - ; anything after an error - (let [{:keys [sub error]} @state] - (if-not error - (try - (do-fn v) - (on-next target v) - (catch Throwable e - (reset! state {:error e :sub nil}) - (if sub - (unsubscribe sub)) - (on-error target e))))))] - (let [sub (chain xs target on-next-fn)] - ; dependening on xs, this may not be reached until after the sequence - ; is complete. - (swap! state update-in [:sub] (constantly sub)) - sub)))))) + [do-fn xs] + (map #(do (do-fn %) %) xs)) (defn ^Observable drop [n ^Observable xs] (.skip xs n)) (defn ^Observable drop-while - [p xs] - (fn->o (fn [target] - (let [dropping (atom true)] - (chain xs - target - (fn [v] - (when (or (not @dropping) - (not (reset! dropping (p v)))) - (on-next target v)))))))) + [p ^Observable xs] + (.skipWhile xs (fn->predicate p))) (defn ^Observable filter [p ^Observable xs] (.filter xs (fn->predicate p))) +(defn ^Observable first + "Returns an Observable that emits the first item emitted by xs, or an + empty Observable if xs is empty. + + See: + rx.Observable/takeFirst + " + [^Observable xs] + (.takeFirst xs)) + (defn interpose [sep xs] - (fn->o (fn [target] - (let [first? (atom true)] - (chain xs - target - (fn [v] - (if-not (compare-and-set! first? true false) - (on-next target sep)) - (on-next target v))))))) + (let [op (->operator (fn [o] + (let [first? (atom true)] + (->subscriber o (fn [o v] + (if-not (compare-and-set! first? true false) + (on-next o sep)) + (on-next o v))))))] + (lift op xs))) (defn into "Returns an observable that emits a single value which is all of the @@ -234,10 +276,6 @@ .toList (map (partial clojure.core/into to)))) -(defn keep - [f xs] - (filter (complement nil?) (map xs f))) - (defn keep [f xs] (filter (complement nil?) (map f xs))) @@ -273,12 +311,13 @@ clojure.core/map-indexed " [f xs] - (fn->o (fn [target] - (let [n (atom -1)] - (chain xs - target - (fn [v] (on-next target (f (swap! n inc) v)))))))) + (let [op (->operator (fn [o] + (let [n (atom -1)] + (->subscriber o + (fn [o v] (on-next o (f (swap! n inc) v)))))))] + (lift op xs))) +; TODO which merge goes here? (defn merge " Returns an observable that emits a single map that consists of the rest of the @@ -296,7 +335,7 @@ (reduce clojure.core/merge {} maps)) (def next - "Returns an observable that emits all of the first element of the input observable. + "Returns an observable that emits all but the first element of the input observable. See: clojure.core/next @@ -325,13 +364,10 @@ clojure.core/some " [p ^Observable xs] - (fn->o (fn [target] - (chain xs - target - (fn [v] - (when-let [result (p v)] - (on-next target result) - (on-completed target))))))) + (->> xs + (map p) + (filter identity) + first)) (defn sort "Returns an observable that emits a single value which is a sorted sequence @@ -387,14 +423,15 @@ (.take xs n)) (defn take-while - [p xs] - (fn->o (fn [target] - (chain xs - target - (fn [v] - (if (p v) - (on-next target v) - (on-completed target))))))) + "Returns an Observable that emits xs until the first x such that + (p x) is falsey. + + See: + clojure.core/take-while + rx.Observable/takeWhile + " + [p ^Observable xs] + (.takeWhile xs (fn->predicate p))) ;################################################################################; @@ -402,9 +439,9 @@ "Returns an Observable the simply emits the given exception with on-error See: - http://netflix.github.io/RxJava/javadoc/rx/Observable.html#error(java.lang.Exception) + rx.Observable/error " - [^Exception e] + [^Throwable e] (Observable/error e)) (defn catch* @@ -444,6 +481,9 @@ The body of the catch is wrapped in an implicit (do). It must evaluate to an Observable. + Note that the source observable is the first argument so this won't mix well with ->> + threading. + Example: (-> my-observable @@ -504,9 +544,7 @@ (generator* on-next 99) " [f & args] - (fn->o (-> (fn [observer] - (apply f observer args) - (Subscriptions/empty)) + (fn->o (-> #(apply f % args) base/wrap-on-completed base/wrap-on-error))) diff --git a/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/core_test.clj b/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/core_test.clj index 7af636345d..80652f461b 100644 --- a/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/core_test.clj +++ b/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/core_test.clj @@ -1,20 +1,7 @@ (ns rx.lang.clojure.core-test (:require [rx.lang.clojure.core :as rx] [rx.lang.clojure.blocking :as b] - [clojure.test :refer [deftest is testing]])) -(comment - (rx/subscribe - (->> - (rx/map (fn [a b] (hash-map :a a :b b)) (rx/seq->o [1 2 3 4 5 6]) - (rx/seq->o ["x" "y" 9 10 "z"])) - (rx/map #(update-in % [:a] (partial * 2))) - (rx/filter (comp not number? :b)) - (rx/concat (rx/seq->o (range 3))) - (rx/drop 1) - (rx/take 6) - (rx/into [])) - println - println)) + [clojure.test :refer [deftest is testing are]])) (deftest test-observable? (is (rx/observable? (rx/return 99))) @@ -27,6 +14,45 @@ (rx/subscribe o (fn [v] (swap! called (fn [_] v)))) (is (= 1 @called))))) +(deftest test-fn->predicate + (are [f arg result] (= result (.call (rx/fn->predicate f) arg)) + identity nil false + identity false false + identity 1 true + identity "true" true + identity true true)) + +(deftest test-fn->subscription + (let [called (atom 0) + s (rx/fn->subscription #(swap! called inc))] + (is (identical? s (rx/unsubscribe s))) + (is (= 1 @called)))) + +(deftest test-unsubscribed? + (let [s (rx/fn->subscription #())] + (is (not (rx/unsubscribed? s))) + (rx/unsubscribe s) + (is (rx/unsubscribed? s)))) + + +(deftest test-fn->o + (let [o (rx/fn->o (fn [s] + (rx/on-next s 0) + (rx/on-next s 1) + (when-not (rx/unsubscribed? s) (rx/on-next s 2)) + (rx/on-completed s)))] + (is (= [0 1 2] (b/into [] o))))) + +(deftest test-->operator + (let [o (rx/->operator #(rx/->subscriber % + (fn [o v] + (if (even? v) + (rx/on-next o v))))) + result (->> (rx/seq->o [1 2 3 4 5]) + (rx/lift o) + (b/into []))] + (is (= [2 4] result)))) + (deftest test-generator (testing "calls on-completed automatically" (let [o (rx/generator [o]) @@ -38,7 +64,7 @@ (let [expected (IllegalArgumentException. "hi") actual (atom nil)] (rx/subscribe (rx/generator [o] (throw expected)) - (fn [v]) + #() #(reset! actual %)) (is (identical? expected @actual))))) @@ -115,6 +141,12 @@ (b/into [] (->> (rx/seq->o [:a :b :c :d :e :f :G :e]) (rx/filter #{:b :e :G})))))) +(deftest test-first + (is (= [3] + (b/into [] (rx/first (rx/seq->o [3 4 5]))))) + (is (= [] + (b/into [] (rx/first (rx/empty)))))) + (deftest test-interpose (is (= (interpose \, [1 2 3]) (b/into [] (rx/interpose \, (rx/seq->o [1 2 3])))))) From da88202077ffb51833da41e0d515b3f92881e243 Mon Sep 17 00:00:00 2001 From: Dave Ray Date: Tue, 18 Feb 2014 14:57:22 -0800 Subject: [PATCH 025/422] Got rid of base. It was weird. --- .../src/main/clojure/rx/lang/clojure/base.clj | 75 -------------- .../main/clojure/rx/lang/clojure/chunk.clj | 3 +- .../src/main/clojure/rx/lang/clojure/core.clj | 99 +++++++++++++++---- .../main/clojure/rx/lang/clojure/future.clj | 19 ++-- .../main/clojure/rx/lang/clojure/realized.clj | 17 ++-- .../clojure/rx/lang/clojure/base_test.clj | 38 ------- .../clojure/rx/lang/clojure/core_test.clj | 39 +++++++- .../clojure/rx/lang/clojure/graph_test.clj | 2 +- 8 files changed, 133 insertions(+), 159 deletions(-) delete mode 100644 language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/base.clj delete mode 100644 language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/base_test.clj diff --git a/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/base.clj b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/base.clj deleted file mode 100644 index 0d1c181062..0000000000 --- a/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/base.clj +++ /dev/null @@ -1,75 +0,0 @@ -(ns rx.lang.clojure.base - "Generic, low-level rx helpers." - (:refer-clojure :exclude [merge]) - (:require [rx.lang.clojure.interop :as iop]) - (:import [rx Observable Observer Subscription] - [rx.observables BlockingObservable] - [rx.subscriptions Subscriptions])) - -(def ^:private -ns- *ns*) -(set! *warn-on-reflection* true) - -(defn wrap-on-completed - "Wrap handler with code that automaticaly calls rx.Observable.onCompleted." - [handler] - (fn [^Observer observer] - (handler observer) - (.onCompleted observer))) - -(defn wrap-on-error - "Wrap handler with code that automaticaly calls (on-error) if an exception is thrown" - [handler] - (fn [^Observer observer] - (try - (handler observer) - (catch Exception e - (.onError observer e))))) - -(defn ^Observable merge - "Observable.merge, renamed because merge means something else in Clojure - - os is one of: - - * An Iterable of Observables to merge - * An Observable> to merge - " - [os] - (cond - (instance? Iterable os) - (Observable/merge (Observable/from ^Iterable os)) - (instance? Observable os) - (Observable/merge ^Observable os) - :else - (throw (IllegalArgumentException. (str "Don't know how to merge " (type os)))))) - -(defn ^Observable merge-delay-error - "Observable.mergeDelayError, renamed because merge means something else in Clojure" - [os] - (cond - (instance? java.util.List os) - (Observable/mergeDelayError ^java.util.List os) - (instance? Observable os) - (Observable/mergeDelayError ^Observable os) - :else - (throw (IllegalArgumentException. (str "Don't know how to merge " (type os)))))) - -(defn ^Observable zip - "Observable.zip. You want map." - ([f ^Observable a ^Observable b] (Observable/zip a b (iop/fn* f))) - ([f ^Observable a ^Observable b ^Observable c] (Observable/zip a b c (iop/fn* f))) - ([f ^Observable a ^Observable b ^Observable c ^Observable d] (Observable/zip a b c d (iop/fn* f))) - ([f a b c d & more] - ; recurse on more and then pull everything together with 4 parameter version - (zip (fn [a b c more-value] - (apply f a b c more-value)) - a - b - c - (apply zip vector d more)))) - -(defmacro zip-let - [bindings & body] - (let [pairs (clojure.core/partition 2 bindings) - names (clojure.core/mapv clojure.core/first pairs) - values (clojure.core/map second pairs)] - `(zip (fn ~names ~@body) ~@values))) diff --git a/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/chunk.clj b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/chunk.clj index d44c26f2c2..303f8cc090 100644 --- a/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/chunk.clj +++ b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/chunk.clj @@ -1,7 +1,6 @@ (ns rx.lang.clojure.chunk (:refer-clojure :exclude [chunk]) - (:require [rx.lang.clojure.core :as rx] - [rx.lang.clojure.base :as rx-base])) + (:require [rx.lang.clojure.core :as rx])) (def ^:private -ns- *ns*) (set! *warn-on-reflection* true) diff --git a/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj index ac3a21e647..308e363dc2 100644 --- a/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj +++ b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj @@ -7,7 +7,6 @@ rest seq some sort sort-by split-with take take-while throw]) (:require [rx.lang.clojure.interop :as iop] - [rx.lang.clojure.base :as base] [rx.lang.clojure.graph :as graph] [rx.lang.clojure.realized :as realized]) (:import [rx Observable Observer Subscriber Subscription Observable$Operator Observable$OnSubscribe] @@ -156,6 +155,81 @@ ;################################################################################ +(defn wrap-on-completed + "Wrap handler with code that automaticaly calls rx.Observable.onCompleted." + [handler] + (fn [^Observer observer] + (handler observer) + (.onCompleted observer))) + +(defn wrap-on-error + "Wrap handler with code that automaticaly calls (on-error) if an exception is thrown" + [handler] + (fn [^Observer observer] + (try + (handler observer) + (catch Throwable e + (.onError observer e))))) + +(defn ^Observable merge + "Observable.merge, renamed because merge means something else in Clojure + + os is one of: + + * An Iterable of Observables to merge + * An Observable> to merge + + If you want clojure.core/merge, it's just this: + + (rx/reduce clojure.core/merge {} maps) + + " + [os] + (cond + (instance? Iterable os) + (Observable/merge (Observable/from ^Iterable os)) + (instance? Observable os) + (Observable/merge ^Observable os) + :else + (throw (IllegalArgumentException. (str "Don't know how to merge " (type os)))))) + +(defn ^Observable merge-delay-error + "Observable.mergeDelayError, renamed because merge means something else in Clojure" + [os] + (cond + (instance? java.util.List os) + (Observable/mergeDelayError ^java.util.List os) + (instance? Observable os) + (Observable/mergeDelayError ^Observable os) + :else + (throw (IllegalArgumentException. (str "Don't know how to merge " (type os)))))) + +(defn ^Observable zip + "Observable.zip. You want map." + ([f ^Observable a ^Observable b] (Observable/zip a b (iop/fn* f))) + ([f ^Observable a ^Observable b ^Observable c] (Observable/zip a b c (iop/fn* f))) + ([f ^Observable a ^Observable b ^Observable c ^Observable d] (Observable/zip a b c d (iop/fn* f))) + ([f a b c d & more] + ; recurse on more and then pull everything together with 4 parameter version + (zip (fn [a b c more-value] + (apply f a b c more-value)) + a + b + c + (apply zip vector d more)))) + +(defmacro zip-let + [bindings & body] + (let [pairs (clojure.core/partition 2 bindings) + names (clojure.core/mapv clojure.core/first pairs) + values (clojure.core/map second pairs)] + `(zip (fn ~names ~@body) ~@values))) +;################################################################################ + + + + + (defn ^Observable never [] (Observable/never)) (defn ^Observable empty [] (Observable/empty)) @@ -288,7 +362,7 @@ "Map a function over an observable sequence. Unlike clojure.core/map, only supports up to 4 simultaneous source sequences at the moment." ([f ^Observable xs] (.map xs (iop/fn* f))) - ([f xs & observables] (apply base/zip f xs observables))) + ([f xs & observables] (apply zip f xs observables))) (defn ^Observable mapcat "Returns an observable which, for each value x in xs, calls (f x), which must @@ -317,23 +391,6 @@ (fn [o v] (on-next o (f (swap! n inc) v)))))))] (lift op xs))) -; TODO which merge goes here? -(defn merge - " - Returns an observable that emits a single map that consists of the rest of the - maps emitted by the input observable conj-ed onto the first. If a key occurs - in more than one map, the mapping from the latter (left-to-right) will be the - mapping in the result. - - NOTE: This is very different from rx.Observable/merge. See rx.base/merge for that - one. - - See: - clojure.core/merge - " - [maps] - (reduce clojure.core/merge {} maps)) - (def next "Returns an observable that emits all but the first element of the input observable. @@ -545,8 +602,8 @@ " [f & args] (fn->o (-> #(apply f % args) - base/wrap-on-completed - base/wrap-on-error))) + wrap-on-completed + wrap-on-error))) (defmacro generator "Create an observable that executes body which should emit a sequence. bindings diff --git a/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/future.clj b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/future.clj index 7f6a78761d..38fc8fa461 100644 --- a/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/future.clj +++ b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/future.clj @@ -1,8 +1,7 @@ (ns rx.lang.clojure.future (:refer-clojure :exclude [future]) (:require [rx.lang.clojure.interop :as iop] - [rx.lang.clojure.core :as rx :refer [fn->o fn->subscription]] - [rx.lang.clojure.base :as base])) + [rx.lang.clojure.core :as rx])) (def ^:private -ns- *ns*) (set! *warn-on-reflection* true) @@ -26,13 +25,13 @@ " [runner f & args] {:pre [(ifn? runner) (ifn? f)]} - (fn->o (fn [observer] + (rx/fn->o (fn [observer] (let [wrapped (-> (fn [o] (apply f o args)) - base/wrap-on-completed - base/wrap-on-error) + rx/wrap-on-completed + rx/wrap-on-error) fu (runner #(wrapped observer))] - (fn->subscription #(future-cancel fu)))))) + (rx/fn->subscription #(future-cancel fu)))))) (defmacro future-generator "Same as rx/generator macro except body is invoked in a separate thread. @@ -60,12 +59,12 @@ " [runner f & args] {:pre [(ifn? runner) (ifn? f)]} - (fn->o (fn [observer] + (rx/fn->o (fn [observer] (let [wrapped (-> #(rx/on-next % (apply f args)) - base/wrap-on-completed - base/wrap-on-error) + rx/wrap-on-completed + rx/wrap-on-error) fu (runner #(wrapped observer))] - (fn->subscription #(future-cancel fu)))))) + (rx/fn->subscription #(future-cancel fu)))))) (defmacro future "Executes body in a separate thread and passes the single result to onNext. diff --git a/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/realized.clj b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/realized.clj index 7747c71881..e0b455af14 100644 --- a/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/realized.clj +++ b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/realized.clj @@ -1,6 +1,5 @@ (ns rx.lang.clojure.realized - (:require [rx.lang.clojure.interop :as iop] - [rx.lang.clojure.base :as rx-base])) + (:require [rx.lang.clojure.interop :as iop])) (def ^:private -ns- *ns*) (set! *warn-on-reflection* true) @@ -62,10 +61,12 @@ single key maps, merging and then folding all the separate maps together. So code like this: - (rx/merge (rx-base/merge (->> (user-info-o user-id) - (rx/map (fn [u] {:user u}))) - (->> (user-likes-o user-id) - (rx/map (fn [u] {:likes u}))))) + ; TODO update + (->> (rx/merge (->> (user-info-o user-id) + (rx/map (fn [u] {:user u}))) + (->> (user-likes-o user-id) + (rx/map (fn [u] {:likes u})))) + (rx/reduce merge {})) becomes: @@ -86,8 +87,8 @@ .toList (.map (iop/fn [list] {k (f list)})))))))] - (-> o - rx-base/merge ; funnel all the observables into a single sequence + (-> ^Iterable o + (rx.Observable/merge) ; funnel all the observables into a single sequence (.reduce {} (iop/fn* merge))))) ; do the map merge dance (defn ^rx.Observable realized-map* diff --git a/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/base_test.clj b/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/base_test.clj deleted file mode 100644 index 985e7eeb1f..0000000000 --- a/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/base_test.clj +++ /dev/null @@ -1,38 +0,0 @@ -(ns rx.lang.clojure.base-test - (:require [rx.lang.clojure.core :as rx] - [rx.lang.clojure.base :as b] - [rx.lang.clojure.blocking :as blocking] - [rx.lang.clojure.future :as f] - ) - (:require [clojure.test :refer [deftest testing is]])) - -(deftest test-zip - (testing "is happy with less than 4 args" - (is (= [[1 2 3]] (blocking/into [] (b/zip vector - (rx/seq->o [1]) (rx/seq->o [2]) (rx/seq->o [3])))))) - (testing "is happy with more than 4 args" - (is (= [[1 2 3 4 5 6 7 8]] - (blocking/into [] (b/zip vector - (rx/seq->o [1]) - (rx/seq->o [2]) - (rx/seq->o [3]) - (rx/seq->o [4]) - (rx/seq->o [5]) - (rx/seq->o [6]) - (rx/seq->o [7]) - (rx/seq->o [8]))))))) - -(deftest test-merge - (is (= [[1 3 5] [2 4 6]] - (let [r (blocking/into [] - (b/merge [(f/future-generator f/default-runner [o] - (doseq [x [1 3 5]] - (Thread/sleep 10) - (rx/on-next o x))) - (f/future-generator f/default-runner [o] - (doseq [x [2 4 6]] - (Thread/sleep 10) - (rx/on-next o x)))]))] - ; make sure each sequence maintained original order - [(keep #{1 3 5} r) - (keep #{2 4 6} r) ])))) diff --git a/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/core_test.clj b/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/core_test.clj index 80652f461b..2fa3daa0cd 100644 --- a/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/core_test.clj +++ b/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/core_test.clj @@ -1,6 +1,7 @@ (ns rx.lang.clojure.core-test (:require [rx.lang.clojure.core :as rx] [rx.lang.clojure.blocking :as b] + [rx.lang.clojure.future :as f] [clojure.test :refer [deftest is testing are]])) (deftest test-observable? @@ -53,6 +54,37 @@ (b/into []))] (is (= [2 4] result)))) +(deftest test-zip + (testing "is happy with less than 4 args" + (is (= [[1 2 3]] (b/into [] (rx/zip vector + (rx/seq->o [1]) (rx/seq->o [2]) (rx/seq->o [3])))))) + (testing "is happy with more than 4 args" + (is (= [[1 2 3 4 5 6 7 8]] + (b/into [] (rx/zip vector + (rx/seq->o [1]) + (rx/seq->o [2]) + (rx/seq->o [3]) + (rx/seq->o [4]) + (rx/seq->o [5]) + (rx/seq->o [6]) + (rx/seq->o [7]) + (rx/seq->o [8]))))))) + +(deftest test-merge + (is (= [[1 3 5] [2 4 6]] + (let [r (b/into [] + (rx/merge [(f/future-generator f/default-runner [o] + (doseq [x [1 3 5]] + (Thread/sleep 10) + (rx/on-next o x))) + (f/future-generator f/default-runner [o] + (doseq [x [2 4 6]] + (Thread/sleep 10) + (rx/on-next o x)))]))] + ; make sure each sequence maintained original order + [(keep #{1 3 5} r) + (keep #{2 4 6} r) ])))) + (deftest test-generator (testing "calls on-completed automatically" (let [o (rx/generator [o]) @@ -209,10 +241,6 @@ (is (= (map-indexed vector [:a :b :c]) (b/into [] (rx/map-indexed vector (rx/seq->o [:a :b :c])))))) -(deftest test-merge - (is (= [{:a 1 :b 2 :c 3 :d 4}] - (b/into [] (rx/merge (rx/seq->o [{:a 1 :d 0} {:b 2} {:c 3} {:d 4} ])))))) - (deftest test-mapcat (let [f (fn [v] [v (* v v)]) xs (range 10)] @@ -361,6 +389,7 @@ (is (= "got it" @called)) (is (identical? expected @completed)))))) + ;################################################################################ (deftest test-graph-imports @@ -381,3 +410,5 @@ b (rx/return 2)] {:a a :b b}) b/single)))) + + diff --git a/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/graph_test.clj b/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/graph_test.clj index 2b3fdde98c..cd3cf59788 100644 --- a/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/graph_test.clj +++ b/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/graph_test.clj @@ -47,7 +47,7 @@ ?b (rx-future/future rx-future/default-runner (Thread/sleep 500) 100) ?c (rx/map #(hash-map :a %1 :b %2 :z %3) ?a ?b ?z) ?z z] - (rx/merge ?c))))))))) + (rx/reduce merge {} ?c))))))))) (deftest test-complicated-graph ; These funcs model network requests for various stuff. They all return observable. From 236cbdb7fd227f17869ed854e329dd95465d7253 Mon Sep 17 00:00:00 2001 From: Dave Ray Date: Tue, 18 Feb 2014 17:38:51 -0800 Subject: [PATCH 026/422] Clean up future stuff and docs --- .../src/main/clojure/rx/lang/clojure/core.clj | 83 ++++++++++++------- .../main/clojure/rx/lang/clojure/future.clj | 43 +++++++--- .../clojure/rx/lang/clojure/core_test.clj | 4 +- .../clojure/rx/lang/clojure/future_test.clj | 67 ++++++++++----- 4 files changed, 131 insertions(+), 66 deletions(-) diff --git a/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj index 308e363dc2..938c68cdc3 100644 --- a/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj +++ b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj @@ -9,7 +9,10 @@ (:require [rx.lang.clojure.interop :as iop] [rx.lang.clojure.graph :as graph] [rx.lang.clojure.realized :as realized]) - (:import [rx Observable Observer Subscriber Subscription Observable$Operator Observable$OnSubscribe] + (:import [rx + Observable + Observer Observable$Operator Observable$OnSubscribe + Subscriber Subscription] [rx.observables BlockingObservable] [rx.subscriptions Subscriptions] [rx.util.functions Action0 Action1 Func0 Func1 Func2])) @@ -47,24 +50,29 @@ [^Observer o e] (.onError o e)) -(defn on-error-return - [^Observable o f] - (.onErrorReturn o f)) - ;################################################################################ (defn ^Subscription subscribe + ([^Observable o on-next-action] - (.subscribe o ^Action1 (iop/action* on-next-action))) + (.subscribe o + ^Action1 (iop/action* on-next-action))) + ([^Observable o on-next-action on-error-action] - (.subscribe o ^Action1 (iop/action* on-next-action) ^Action1 (iop/action* on-error-action))) + (.subscribe o + ^Action1 (iop/action* on-next-action) + ^Action1 (iop/action* on-error-action))) + ([^Observable o on-next-action on-error-action on-completed-action] - (.subscribe o ^Action1 (iop/action* on-next-action) ^Action1 (iop/action* on-error-action) ^Action0 (iop/action* on-completed-action)))) + (.subscribe o + ^Action1 (iop/action* on-next-action) + ^Action1 (iop/action* on-error-action) + ^Action0 (iop/action* on-completed-action)))) (defn ^Subscriber ->subscriber "" ([o on-next-action] (->subscriber o on-next-action nil nil)) - ([o on-next-action on-error-action] (->subscriber o on-next-action on-error-action)) + ([o on-next-action on-error-action] (->subscriber o on-next-action on-error-action nil)) ([^Subscriber o on-next-action on-error-action on-completed-action] (proxy [Subscriber] [o] (onCompleted [] @@ -80,8 +88,8 @@ (on-next-action o t) (on-next o t)))))) -(defn ^Observable$Operator ->operator - "Create a basic Operator with the given handler fns. If a handler is omitted or nil +(defn ^Observable$Operator fn->operator + "Create a basic Operator with f. If a handler is omitted or nil it's treated as a pass-through. on-next-action Passed Subscriber and value @@ -92,11 +100,11 @@ lift rx.Observable$Operator " - [input] - {:pre [(fn? input)]} + [f] + {:pre [(fn? f)]} (reify Observable$Operator (call [this o] - (input o)))) + (f o)))) (defn lift "Lift the Operator op over the given Observable xs @@ -104,12 +112,12 @@ Example: (->> my-observable - (rx/lift (rx/->operator ...)) + (rx/lift (rx/fn->operator ...)) ...) See: rx.Observable/lift - ->operator + fn->operator " [^Observable$Operator op ^Observable xs] (.lift xs op)) @@ -160,7 +168,8 @@ [handler] (fn [^Observer observer] (handler observer) - (.onCompleted observer))) + (when-not (unsubscribed? observer) + (.onCompleted observer)))) (defn wrap-on-error "Wrap handler with code that automaticaly calls (on-error) if an exception is thrown" @@ -169,7 +178,8 @@ (try (handler observer) (catch Throwable e - (.onError observer e))))) + (when-not (unsubscribed? observer) + (.onError observer e)))))) (defn ^Observable merge "Observable.merge, renamed because merge means something else in Clojure @@ -194,7 +204,7 @@ (throw (IllegalArgumentException. (str "Don't know how to merge " (type os)))))) (defn ^Observable merge-delay-error - "Observable.mergeDelayError, renamed because merge means something else in Clojure" + "Observable.mergeDelayError" [os] (cond (instance? java.util.List os) @@ -205,7 +215,7 @@ (throw (IllegalArgumentException. (str "Don't know how to merge " (type os)))))) (defn ^Observable zip - "Observable.zip. You want map." + "rx.Observable.zip. You want map." ([f ^Observable a ^Observable b] (Observable/zip a b (iop/fn* f))) ([f ^Observable a ^Observable b ^Observable c] (Observable/zip a b c (iop/fn* f))) ([f ^Observable a ^Observable b ^Observable c ^Observable d] (Observable/zip a b c d (iop/fn* f))) @@ -224,20 +234,32 @@ names (clojure.core/mapv clojure.core/first pairs) values (clojure.core/map second pairs)] `(zip (fn ~names ~@body) ~@values))) -;################################################################################ +;################################################################################ +(defn ^Observable never + "Returns an Observable that never emits any values and never completes. + See: + rx.Observable/never + " + [] + (Observable/never)) +(defn ^Observable empty + "Returns an Observable that completes immediately without emitting any values. -(defn ^Observable never [] (Observable/never)) -(defn ^Observable empty [] (Observable/empty)) + See: + rx.Observable/empty + " + [] + (Observable/empty)) (defn ^Observable return "Returns an observable that emits a single value. See: - Observable/just + rx.Observable/just " [value] (Observable/just value)) @@ -329,7 +351,7 @@ (defn interpose [sep xs] - (let [op (->operator (fn [o] + (let [op (fn->operator (fn [o] (let [first? (atom true)] (->subscriber o (fn [o v] (if-not (compare-and-set! first? true false) @@ -385,7 +407,7 @@ clojure.core/map-indexed " [f xs] - (let [op (->operator (fn [o] + (let [op (fn->operator (fn [o] (let [n (atom -1)] (->subscriber o (fn [o v] (on-next o (f (swap! n inc) v)))))))] @@ -589,16 +611,17 @@ ;################################################################################; (defn generator* - "Creates an observable that calls (f observable & args) which should emit a sequence. + "Creates an observable that calls (f observable & args) which should emit values + with (rx/on-next observable value). Automatically calls on-completed on return, or on-error if any exception is thrown. - Subscribers will block. + f should exit early if (rx/unsubscribed? observable) returns true Examples: ; An observable that emits just 99 - (generator* on-next 99) + (rx/generator* on-next 99) " [f & args] (fn->o (-> #(apply f % args) @@ -611,7 +634,7 @@ Automatically calls on-completed on return, or on-error if any exception is thrown. - Subscribe will block. + The body should exit early if (rx/unsubscribed? observable) returns true Examples: diff --git a/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/future.clj b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/future.clj index 38fc8fa461..ac8d29f5e6 100644 --- a/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/future.clj +++ b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/future.clj @@ -1,4 +1,7 @@ (ns rx.lang.clojure.future + "Functions and macros for making rx-ified futures. That is, run some code in some + other thread and return an Observable of its result. + " (:refer-clojure :exclude [future]) (:require [rx.lang.clojure.interop :as iop] [rx.lang.clojure.core :as rx])) @@ -17,7 +20,8 @@ runner is a function that takes a no-arg function argument and returns a future representing the execution of that function. - subscribe will not block. + Returns an Observable. If the subscriber unsubscribes, the future will be canceled + with clojure.core/future-cancel See: rx.lang.clojure.core/generator* @@ -25,13 +29,14 @@ " [runner f & args] {:pre [(ifn? runner) (ifn? f)]} - (rx/fn->o (fn [observer] - (let [wrapped (-> (fn [o] - (apply f o args)) - rx/wrap-on-completed - rx/wrap-on-error) - fu (runner #(wrapped observer))] - (rx/fn->subscription #(future-cancel fu)))))) + (rx/fn->o (fn [^rx.Subscriber observer] + (let [wrapped (-> (fn [o] + (apply f o args)) + rx/wrap-on-completed + rx/wrap-on-error) + fu (runner #(wrapped observer))] + (.add observer + (rx/fn->subscription #(future-cancel fu))))))) (defmacro future-generator "Same as rx/generator macro except body is invoked in a separate thread. @@ -39,7 +44,16 @@ runner is a function that takes a no-arg function argument and returns a future representing the execution of that function. - subscribe will not block. + Returns an Observable. If the subscriber unsubscribes, the future will be canceled + with clojure.core/future-cancel + + Example: + + (future-generator default-runner + [o] + (rx/on-next o 1) + (Thread/sleep 1000) + (rx/on-next o 2)) See: rx.lang.clojure.core/generator* @@ -55,22 +69,25 @@ runner is a function that takes a no-arg function argument and returns a future representing the execution of that function. - Returns an Observable. + Returns an Observable. If the subscriber unsubscribes, the future will be canceled + with clojure.core/future-cancel " [runner f & args] {:pre [(ifn? runner) (ifn? f)]} - (rx/fn->o (fn [observer] + (rx/fn->o (fn [^rx.Subscriber observer] (let [wrapped (-> #(rx/on-next % (apply f args)) rx/wrap-on-completed rx/wrap-on-error) fu (runner #(wrapped observer))] - (rx/fn->subscription #(future-cancel fu)))))) + (.add observer + (rx/fn->subscription #(future-cancel fu))))))) (defmacro future "Executes body in a separate thread and passes the single result to onNext. If an exception occurs, onError is called. - Returns an Observable + Returns an Observable. If the subscriber unsubscribes, the future will be canceled + with clojure.core/future-cancel runner is a function that takes a no-arg function argument and returns a future representing the execution of that function. diff --git a/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/core_test.clj b/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/core_test.clj index 2fa3daa0cd..c9300fb739 100644 --- a/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/core_test.clj +++ b/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/core_test.clj @@ -44,8 +44,8 @@ (rx/on-completed s)))] (is (= [0 1 2] (b/into [] o))))) -(deftest test-->operator - (let [o (rx/->operator #(rx/->subscriber % +(deftest test-fn->operator + (let [o (rx/fn->operator #(rx/->subscriber % (fn [o v] (if (even? v) (rx/on-next o v))))) diff --git a/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/future_test.clj b/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/future_test.clj index dccf89c908..46682ddc44 100644 --- a/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/future_test.clj +++ b/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/future_test.clj @@ -1,37 +1,62 @@ (ns rx.lang.clojure.future-test (:require [rx.lang.clojure.core :as rx] - [rx.lang.clojure.blocking :as blocking] + [rx.lang.clojure.blocking :as b] [rx.lang.clojure.future :as f]) (:require [clojure.test :refer [deftest testing is]])) (deftest test-future-generator (is (not= [(.getId (Thread/currentThread))] - (blocking/into [] + (b/into [] (f/future-generator f/default-runner [observer] (rx/on-next observer (.getId (Thread/currentThread)))))))) (deftest test-future - (is (= [15] (blocking/into [] (f/future* f/default-runner + 1 2 3 4 5)))) - (is (= [15] (blocking/into [] (f/future f/default-runner (println "HI") (+ 1 2 3 4 5))))) ) + (is (= [15] (b/into [] (f/future* f/default-runner + 1 2 3 4 5)))) + (is (= [15] (b/into [] (f/future f/default-runner (println "HI") (+ 1 2 3 4 5))))) ) +(deftest test-future-exception + (is (= "Caught: boo" + (-> (f/future f/default-runner (throw (java.io.FileNotFoundException. "boo"))) + (rx/catch java.io.FileNotFoundException e + (rx/return (str "Caught: " (.getMessage e)))) + (b/single))))) -(comment (rx/subscribe (f/future* f/default-runner + 1 2 3 4 5) - (fn [v] (println "RESULT: " v)) - (fn [e] (println "ERROR: " e)) - #(println "COMPLETED"))) +(deftest test-future-cancel + (let [exited? (atom nil) + o (f/future f/default-runner + (Thread/sleep 1000) + (reset! exited? true) + "WAT") + result (->> o + (rx/take 0) + (b/into []))] + (Thread/sleep 2000) + (is (= [nil []] + [@exited? result])))) -(comment (rx/subscribe (f/future f/default-runner - (Thread/sleep 5000) - (+ 100 200)) - (fn [v] (println "RESULT: " v)) - (fn [e] (println "ERROR: " e)) - #(println "COMPLETED"))) - -(comment (rx/subscribe (f/future f/default-runner - (Thread/sleep 2000) - (throw (Exception. "Failed future"))) - (fn [v] (println "RESULT: " v)) - (fn [e] (println "ERROR: " e)) - #(println "COMPLETED"))) +(deftest test-future-generator-cancel + (let [exited? (atom nil) + o (f/future-generator f/default-runner + [o] + (rx/on-next o "FIRST") + (Thread/sleep 1000) + (reset! exited? true)) + result (->> o + (rx/take 1) + (b/into []))] + (Thread/sleep 2000) + (is (= [nil ["FIRST"]] + [@exited? result])))) +(deftest test-future-generator-exception + (let [e (java.io.FileNotFoundException. "snake")] + (is (= [1 2 e] + (b/into [] (-> (f/future-generator + f/default-runner + [o] + (rx/on-next o 1) + (rx/on-next o 2) + (throw e)) + (rx/catch java.io.FileNotFoundException e + (rx/return e)))))))) From bfc03d6a2f5590bf1c7f53526cbf4ade5ca12c25 Mon Sep 17 00:00:00 2001 From: Dave Ray Date: Wed, 19 Feb 2014 00:02:16 -0800 Subject: [PATCH 027/422] Some more core operators --- .../src/main/clojure/rx/lang/clojure/core.clj | 244 ++++++++++++++---- .../clojure/rx/lang/clojure/core_test.clj | 120 ++++++--- 2 files changed, 283 insertions(+), 81 deletions(-) diff --git a/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj index 938c68cdc3..4e50cef03c 100644 --- a/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj +++ b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj @@ -1,9 +1,12 @@ (ns rx.lang.clojure.core - (:refer-clojure :exclude [concat cons do drop drop-while empty + (:refer-clojure :exclude [concat cons count cycle + distinct do drop drop-while + empty every? filter first future - interpose into keep keep-indexed + interpose into + keep keep-indexed map mapcat map-indexed - merge next partition reduce reductions + merge next nth partition reduce reductions rest seq some sort sort-by split-with take take-while throw]) (:require [rx.lang.clojure.interop :as iop] @@ -128,6 +131,28 @@ (.unsubscribe s) s) +(defn subscribe-on + "Cause subscriptions to the given observable to happen on the given scheduler. + + Returns a new Observable. + + See: + rx.Observable/subscribeOn + " + [^rx.Scheduler s ^Observable xs] + (.subscribeOn xs s)) + +(defn unsubscribe-on + "Cause unsubscriptions from the given observable to happen on the given scheduler. + + Returns a new Observable. + + See: + rx.Observable/unsubscribeOn + " + [^rx.Scheduler s ^Observable xs] + (.unsubscribeOn xs s)) + (defn unsubscribed? "Returns true if the given Subscription (or Subscriber) is unsubscribed. @@ -161,8 +186,6 @@ [f] (Observable/create ^Observable$OnSubscribe (iop/action* f))) -;################################################################################ - (defn wrap-on-completed "Wrap handler with code that automaticaly calls rx.Observable.onCompleted." [handler] @@ -181,6 +204,14 @@ (when-not (unsubscribed? observer) (.onError observer e)))))) +;################################################################################ + +(defn synchronize + ([^Observable xs] + (.synchronize xs)) + ([lock ^Observable xs] + (.synchronize xs lock))) + (defn ^Observable merge "Observable.merge, renamed because merge means something else in Clojure @@ -214,26 +245,6 @@ :else (throw (IllegalArgumentException. (str "Don't know how to merge " (type os)))))) -(defn ^Observable zip - "rx.Observable.zip. You want map." - ([f ^Observable a ^Observable b] (Observable/zip a b (iop/fn* f))) - ([f ^Observable a ^Observable b ^Observable c] (Observable/zip a b c (iop/fn* f))) - ([f ^Observable a ^Observable b ^Observable c ^Observable d] (Observable/zip a b c d (iop/fn* f))) - ([f a b c d & more] - ; recurse on more and then pull everything together with 4 parameter version - (zip (fn [a b c more-value] - (apply f a b c more-value)) - a - b - c - (apply zip vector d more)))) - -(defmacro zip-let - [bindings & body] - (let [pairs (clojure.core/partition 2 bindings) - names (clojure.core/mapv clojure.core/first pairs) - values (clojure.core/map second pairs)] - `(zip (fn ~names ~@body) ~@values))) ;################################################################################ @@ -309,6 +320,50 @@ [^Observable os] (Observable/concat os)) +(defn count + "Returns an Observable that emits the number of items is xs as a long. + + See: + rx.Observable/longCount + " + [^Observable xs] + (.longCount xs)) + +(defn cycle + "Returns an Observable that emits the items of xs repeatedly, forever. + + TODO: Other sigs. + + See: + rx.Observable/repeat + clojure.core/cycle + " + [^Observable xs] + (.repeat xs)) + +(defn distinct + "Returns an Observable of the elements of Observable xs with duplicates + removed. key-fn, if provided, is a one arg function that determines the + key used to determined duplicates. key-fn defaults to identity. + + This implementation doesn't use rx.Observable/distinct because it doesn't + honor Clojure's equality semantics. + + See: + clojure.core/distinct + " + ([xs] (distinct identity xs)) + ([key-fn ^Observable xs] + (let [op (fn->operator (fn [o] + (let [seen (atom #{})] + (->subscriber o + (fn [o v] + (let [key (key-fn v)] + (when-not (contains? @seen key) + (swap! seen conj key) + (on-next o v))))))))] + (lift op xs)))) + (defn ^Observable do "Returns a new Observable that, for each x in Observable xs, executes (do-fn x), presumably for its side effects, and then passes x along unchanged. @@ -318,14 +373,17 @@ Example: - (->> (rx/seq->o [1 2 3]) - (rx/do println) - ...) + (->> (rx/seq->o [1 2 3]) + (rx/do println) + ...) Will print 1, 2, 3. + + See: + rx.Observable/doOnNext " - [do-fn xs] - (map #(do (do-fn %) %) xs)) + [do-fn ^Observable xs] + (.doOnNext xs (iop/action* do-fn))) (defn ^Observable drop [n ^Observable xs] @@ -335,6 +393,17 @@ [p ^Observable xs] (.skipWhile xs (fn->predicate p))) +(defn ^Observable every? + "Returns an Observable that emits a single true value if (p x) is true for + all x in xs. Otherwise emits false. + + See: + clojure.core/every? + rx.Observable/all + " + [p ^Observable xs] + (.all xs (fn->predicate p))) + (defn ^Observable filter [p ^Observable xs] (.filter xs (fn->predicate p))) @@ -349,6 +418,8 @@ [^Observable xs] (.takeFirst xs)) +; TODO group-by + (defn interpose [sep xs] (let [op (fn->operator (fn [o] @@ -380,11 +451,34 @@ [f xs] (filter (complement nil?) (map-indexed f xs))) +(defn ^Observable map* + "Map a function over an Observable of Observables. + + Each item from the first emitted Observable is the first arg, each + item from the second emitted Observable is the second arg, and so on. + + See: + map + clojure.core/map + rx.Observable/zip + " + [f ^Observable observable] + (Observable/zip observable + ^rx.functions.FuncN (iop/fnN* f))) + (defn ^Observable map - "Map a function over an observable sequence. Unlike clojure.core/map, only supports up - to 4 simultaneous source sequences at the moment." - ([f ^Observable xs] (.map xs (iop/fn* f))) - ([f xs & observables] (apply zip f xs observables))) + "Map a function over one or more observable sequences. + + Each item from the first Observable is the first arg, each item + from the second Observable is the second arg, and so on. + + See: + clojure.core/map + rx.Observable/zip + " + [f & observables] + (Observable/zip ^Iterable observables + ^rx.functions.FuncN (iop/fnN* f))) (defn ^Observable mapcat "Returns an observable which, for each value x in xs, calls (f x), which must @@ -393,11 +487,9 @@ See: clojure.core/mapcat - rx.Observable/mapMany + rx.Observable/flatMap " - ([f ^Observable xs] (.mapMany xs (iop/fn* f))) - ; TODO multi-arg version - ) + ([f ^Observable xs] (.flatMap xs (iop/fn* f)))) (defn map-indexed "Returns an observable that invokes (f index value) for each value of the input @@ -408,9 +500,9 @@ " [f xs] (let [op (fn->operator (fn [o] - (let [n (atom -1)] - (->subscriber o - (fn [o v] (on-next o (f (swap! n inc) v)))))))] + (let [n (atom -1)] + (->subscriber o + (fn [o v] (on-next o (f (swap! n inc) v)))))))] (lift op xs))) (def next @@ -421,7 +513,19 @@ " (partial drop 1)) -; TODO partition. Use Buffer whenever it's implemented. +(defn nth + "Returns an Observable that emits the value at the index in the given + Observable. nth throws an IndexOutOfBoundsException unless not-found + is supplied. + + Note that the Observable is the *first* arg! + " + ([^Observable xs index] + (.elementAt xs index)) + ([^Observable xs index not-found] + (.elementAtOrDefault xs index not-found))) + +; TODO partition. Use window (defn ^Observable reduce ([f ^Observable xs] (.reduce xs (iop/fn* f))) @@ -448,36 +552,76 @@ (filter identity) first)) -(defn sort - "Returns an observable that emits a single value which is a sorted sequence +(defn sorted-list + "Returns an observable that emits a *single value* which is a sorted List of the items in coll, where the sort order is determined by comparing items. If no comparator is supplied, uses compare. comparator must implement java.util.Comparator. + Use sort if you don't want the sequence squashed down to a List. + See: - clojure.core/sort + rx.Observable/toSortedList + sort " - ([coll] (sort clojure.core/compare coll)) + ([coll] (sorted-list clojure.core/compare coll)) ([comp ^Observable coll] (.toSortedList coll (iop/fn [a b] ; force to int so rxjava doesn't have a fit (int (comp a b)))))) -(defn sort-by - "Returns an observable that emits a single value which is a sorted sequence +(defn sorted-list-by + "Returns an observable that emits a *single value* which is a sorted List of the items in coll, where the sort order is determined by comparing (keyfn item). If no comparator is supplied, uses compare. comparator must implement java.util.Comparator. + Use sort-by if you don't want the sequence squashed down to a List. + See: - clojure.core/sort-by + rx.Observable/toSortedList + sort-by " - ([keyfn coll] (sort-by keyfn clojure.core/compare coll)) + ([keyfn coll] (sorted-list-by keyfn clojure.core/compare coll)) ([keyfn comp ^Observable coll] (.toSortedList coll (iop/fn [a b] ; force to int so rxjava doesn't have a fit (int (comp (keyfn a) (keyfn b))))))) +(defn sort + "Returns an observable that emits the items in xs, where the sort order is + determined by comparing items. If no comparator is supplied, uses compare. + comparator must implement java.util.Comparator. + + See: + sorted-list + clojure.core/sort + " + ([xs] + (->> xs + (sorted-list) + (mapcat seq->o))) + ([comp xs] + (->> xs + (sorted-list comp) + (mapcat seq->o)))) + +(defn sort-by + "Returns an observable that emits the items in xs, where the sort order is + determined by comparing (keyfn item). If no comparator is supplied, uses + compare. comparator must implement java.util.Comparator. + + See: + clojure.core/sort-by + " + ([keyfn xs] + (->> (sorted-list-by keyfn xs) + (mapcat seq->o))) + ([keyfn comp ^Observable xs] + (->> xs + (sorted-list-by keyfn comp) + (mapcat seq->o)))) + (defn split-with "Returns an observable that emits a pair of observables diff --git a/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/core_test.clj b/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/core_test.clj index c9300fb739..daefe3c05c 100644 --- a/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/core_test.clj +++ b/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/core_test.clj @@ -54,21 +54,21 @@ (b/into []))] (is (= [2 4] result)))) -(deftest test-zip - (testing "is happy with less than 4 args" - (is (= [[1 2 3]] (b/into [] (rx/zip vector - (rx/seq->o [1]) (rx/seq->o [2]) (rx/seq->o [3])))))) - (testing "is happy with more than 4 args" - (is (= [[1 2 3 4 5 6 7 8]] - (b/into [] (rx/zip vector - (rx/seq->o [1]) - (rx/seq->o [2]) - (rx/seq->o [3]) - (rx/seq->o [4]) - (rx/seq->o [5]) - (rx/seq->o [6]) - (rx/seq->o [7]) - (rx/seq->o [8]))))))) + +(deftest test-syncrhonize + ; I'm going to believe synchronize works and just exercise it + ; here for sanity. + (is (= [1 2 3] + (->> [1 2 3] + (rx/seq->o) + (rx/synchronize) + (b/into [])))) + (let [lock (Object.)] + (is (= [1 2 3] + (->> [1 2 3] + (rx/seq->o) + (rx/synchronize lock) + (b/into [])))))) (deftest test-merge (is (= [[1 3 5] [2 4 6]] @@ -140,6 +140,35 @@ (b/into [] (rx/concat* (rx/seq->o [(rx/seq->o [:q :r]) (rx/seq->o [1 2 3])])))))) +(deftest test-count + (are [xs] (= (count xs) (->> xs (rx/seq->o) (rx/count) (b/single))) + [] + [1] + [5 6 7] + (range 10000))) + +(deftest test-cycle + (is (= [1 2 3 1 2 3 1 2 3 1 2] + (->> [1 2 3] + (rx/seq->o) + (rx/cycle) + (rx/take 11) + (b/into []))))) + +(deftest test-distinct + (let [input [{:a 1} {:a 1} {:b 1} {"a" (int 1)} {:a (int 1)}]] + (is (= (distinct input) + (->> input + (rx/seq->o) + (rx/distinct) + (b/into []))))) + (let [input [{:name "Bob" :x 2} {:name "Jim" :x 99} {:name "Bob" :x 3}]] + (is (= [{:name "Bob" :x 2} {:name "Jim" :x 99}] + (->> input + (rx/seq->o) + (rx/distinct :name) + (b/into [])))))) + (deftest test-do (testing "calls a function with each element" (let [collected (atom [])] @@ -167,6 +196,13 @@ (is (= (into [] (drop-while even? [2 4 6 8 1 2 3])) (b/into [] (rx/drop-while even? (rx/seq->o [2 4 6 8 1 2 3])))))) +(deftest test-every? + (are [xs p result] (= result (->> xs (rx/seq->o) (rx/every? p) (b/single))) + [2 4 6 8] even? true + [2 4 3 8] even? false + [1 2 3 4] #{1 2 3 4} true + [1 2 3 4] #{1 3 4} false)) + (deftest test-filter (is (= (into [] (->> [:a :b :c :d :e :f :G :e] (filter #{:b :e :G}))) @@ -237,6 +273,17 @@ (rx/seq->o ["a" "b" "c" "d" "e"]) (rx/seq->o ["a" "b" "c" "d" "e"])))))) +(deftest test-map* + (is (= [[1 2 3 4 5 6 7 8]] + (b/into [] (rx/map* vector + (rx/seq->o [(rx/seq->o [1]) + (rx/seq->o [2]) + (rx/seq->o [3]) + (rx/seq->o [4]) + (rx/seq->o [5]) + (rx/seq->o [6]) + (rx/seq->o [7]) + (rx/seq->o [8])])))))) (deftest test-map-indexed (is (= (map-indexed vector [:a :b :c]) (b/into [] (rx/map-indexed vector (rx/seq->o [:a :b :c])))))) @@ -245,21 +292,18 @@ (let [f (fn [v] [v (* v v)]) xs (range 10)] (is (= (mapcat f xs) - (b/into [] (rx/mapcat (comp rx/seq->o f) (rx/seq->o xs)))))) - (comment - (is (= (into [] (mapcat vector - [:q :r :s :t :u] - (range 10) - ["a" "b" "c" "d" "e"] )) - (b/into [] (rx/mapcat vector - (rx/seq->o [:q :r :s :t :u]) - (rx/seq->o (range 10) ) - (rx/seq->o ["a" "b" "c" "d" "e"] ))))))) + (b/into [] (rx/mapcat (comp rx/seq->o f) (rx/seq->o xs))))))) (deftest test-next (let [in [:q :r :s :t :u]] (is (= (next in) (b/into [] (rx/next (rx/seq->o in))))))) +(deftest test-nth + (is (= [:a] + (b/into [] (rx/nth (rx/seq->o [:s :b :a :c]) 2)))) + (is (= [:fallback] + (b/into [] (rx/nth (rx/seq->o [:s :b :a :c]) 25 :fallback))))) + (deftest test-rest (let [in [:q :r :s :t :u]] (is (= (rest in) (b/into [] (rx/rest (rx/seq->o in))))))) @@ -276,18 +320,32 @@ (is (= [:r] (b/into [] (rx/some #{:r :s :t} (rx/seq->o [:q :v :r]))))) (is (= [] (b/into [] (rx/some #{:r :s :t} (rx/seq->o [:q :v])))))) -(deftest test-sort - (is (= [[]] (b/into [] (rx/sort (rx/empty))))) +(deftest test-sorted-list + (is (= [[]] (b/into [] (rx/sorted-list (rx/empty))))) (is (= [[1 2 3]] - (b/into [] (rx/sort (rx/seq->o [3 1 2]))))) + (b/into [] (rx/sorted-list (rx/seq->o [3 1 2]))))) (is (= [[3 2 1]] + (b/into [] (rx/sorted-list (fn [a b] (- (compare a b))) (rx/seq->o [2 1 3])))))) + +(deftest test-sorted-list-by + (is (= [[]] (b/into [] (rx/sorted-list-by :foo (rx/empty))))) + (is (= [[{:foo 1} {:foo 2} {:foo 3}]] + (b/into [] (rx/sorted-list-by :foo (rx/seq->o [{:foo 2}{:foo 1}{:foo 3}]))))) + (is (= [[{:foo 3} {:foo 2} {:foo 1}]] + (b/into [] (rx/sorted-list-by :foo (fn [a b] (- (compare a b))) (rx/seq->o [{:foo 2}{:foo 1}{:foo 3}])))))) + +(deftest test-sort + (is (= [] (b/into [] (rx/sort (rx/empty))))) + (is (= [1 2 3] + (b/into [] (rx/sort (rx/seq->o [3 1 2]))))) + (is (= [3 2 1] (b/into [] (rx/sort (fn [a b] (- (compare a b))) (rx/seq->o [2 1 3])))))) (deftest test-sort-by - (is (= [[]] (b/into [] (rx/sort-by :foo (rx/empty))))) - (is (= [[{:foo 1} {:foo 2} {:foo 3}]] + (is (= [] (b/into [] (rx/sort-by :foo (rx/empty))))) + (is (= [{:foo 1} {:foo 2} {:foo 3}] (b/into [] (rx/sort-by :foo (rx/seq->o [{:foo 2}{:foo 1}{:foo 3}]))))) - (is (= [[{:foo 3} {:foo 2} {:foo 1}]] + (is (= [{:foo 3} {:foo 2} {:foo 1}] (b/into [] (rx/sort-by :foo (fn [a b] (- (compare a b))) (rx/seq->o [{:foo 2}{:foo 1}{:foo 3}])))))) (deftest test-split-with From 42763685f9360b5ea83498494208a7fac3506b24 Mon Sep 17 00:00:00 2001 From: Dave Ray Date: Wed, 19 Feb 2014 10:05:24 -0800 Subject: [PATCH 028/422] Update blocking stuff --- .../main/clojure/rx/lang/clojure/blocking.clj | 112 +++++++++++++++--- .../clojure/rx/lang/clojure/blocking_test.clj | 53 ++++++++- 2 files changed, 146 insertions(+), 19 deletions(-) diff --git a/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/blocking.clj b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/blocking.clj index 9455636b11..f81f4b3c64 100644 --- a/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/blocking.clj +++ b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/blocking.clj @@ -1,39 +1,81 @@ (ns rx.lang.clojure.blocking "Blocking operators and functions. These should never be used in production code except at the end of an async chain to convert from - rx land back to sync land. For example, to produce a servlet response." - (:refer-clojure :exclude [first into]) - (:require [rx.lang.clojure.core :as rx]) + rx land back to sync land. For example, to produce a servlet response. + + If you use these, you're a bad person. + " + (:refer-clojure :exclude [first into doseq last]) + (:require [rx.lang.clojure.interop :as iop] [rx.lang.clojure.core :as rx]) (:import [rx Observable] [rx.observables BlockingObservable])) (def ^:private -ns- *ns*) (set! *warn-on-reflection* true) +(defmacro ^:private with-ex-unwrap + [& body] + `(try + ~@body + (catch RuntimeException e# + (throw (or + (and (identical? RuntimeException (class e#)) + (.getCause e#)) + e#))))) + (defn ^BlockingObservable ->blocking - "Convert an Observable to a BlockingObservable" - [^Observable o] - (.toBlockingObservable o)) + "Convert an Observable to a BlockingObservable. + + If o is already a BlockingObservable it's returned unchanged. + " + [o] + (if (instance? BlockingObservable o) + o + (.toBlockingObservable ^Observable o))) + +(defn o->seq + "Returns a lazy sequence of the items emitted by o + + See: + rx.observables.BlockingObservable/getIterator + rx.lang.clojure.core/seq->o + " + [o] + (-> (->blocking o) + (.getIterator) + (iterator-seq))) (defn first "*Blocks* and waits for the first value emitted by the given observable. + If the Observable is empty, returns nil + If an error is produced it is thrown. See: clojure.core/first rx/first + rx.observables.BlockingObservable/first + " + [observable] + (with-ex-unwrap + (.firstOrDefault (->blocking observable) nil))) + +(defn last + "*Blocks* and waits for the last value emitted by the given observable. + + If the Observable is empty, returns nil + + If an error is produced it is thrown. + + See: + clojure.core/last + rx/last + rx.observable.BlockingObservable/last " [observable] - (let [result (clojure.core/promise)] - (rx/subscribe (->> observable (rx/take 1)) - #(clojure.core/deliver result [:value %]) - #(clojure.core/deliver result [:error %]) - #(clojure.core/deliver result nil)) - (if-let [[type v] @result] - (case type - :value v - :error (throw v))))) + (with-ex-unwrap + (.lastOrDefault (->blocking observable) nil))) (defn single "*Blocks* and waits for the first value emitted by the given observable. @@ -41,7 +83,8 @@ An error is thrown if more then one value is produced. " [observable] - (.single (->blocking observable))) + (with-ex-unwrap + (.single (->blocking observable)))) (defn into "*Blocks* and pours the elements emitted by the given observables into @@ -55,3 +98,40 @@ " [to from-observable] (first (rx/into to from-observable))) + +(defn doseq* + "*Blocks* and executes (f x) for each x emitted by xs + + Returns nil. + + See: + doseq + clojure.core/doseq + " + [xs f] + (with-ex-unwrap + (-> (->blocking xs) + (.forEach (rx.lang.clojure.interop/action* f))))) + +(defmacro doseq + "Like clojure.core/doseq except iterates over an observable in a blocking manner. + + Unlike clojure.core/doseq, only supports a single binding + + Returns nil. + + Example: + + (rx-blocking/doseq [{:keys [name]} users-observable] + (println \"User:\" name)) + + See: + doseq* + clojure.core/doseq + " + [bindings & body] + (when (not= (count bindings) 2) + (throw (IllegalArgumentException. (str "sorry, rx/doseq only supports one binding")))) + (let [[k v] bindings] + `(doseq* ~v (fn [~k] ~@body)))) + diff --git a/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/blocking_test.clj b/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/blocking_test.clj index 0963c14bab..df8e9fae1e 100644 --- a/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/blocking_test.clj +++ b/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/blocking_test.clj @@ -3,11 +3,34 @@ [rx.lang.clojure.core :as rx] [clojure.test :refer [deftest testing is]])) +(deftest test-->blocking + (testing "returns a BlockingObservable from an Observable" + (is (instance? rx.observables.BlockingObservable (b/->blocking (rx/return 0))))) + + (testing "is idempotent" + (is (instance? rx.observables.BlockingObservable (b/->blocking (b/->blocking (rx/return 0))))))) + + +(deftest test-o->seq + (is (= [1 2 3] (b/o->seq (rx/seq->o [1 2 3]))))) + (deftest test-first (testing "returns first element of observable" - (is (= 1 (b/first (rx/return 1))))) + (is (= 1 (b/first (rx/seq->o [1 2 3]))))) + (testing "returns nil for empty observable" + (is (nil? (b/first (rx/empty))))) + (testing "rethrows errors" + (is (thrown? java.io.FileNotFoundException + (b/first (rx/throw (java.io.FileNotFoundException. "boo"))))))) + +(deftest test-last + (testing "returns last element of observable" + (is (= 3 (b/last (rx/seq->o [1 2 3]))))) (testing "returns nil for empty observable" - (is (nil? (b/first (rx/empty)))))) + (is (nil? (b/last (rx/empty))))) + (testing "rethrows errors" + (is (thrown? java.io.FileNotFoundException + (b/last (rx/throw (java.io.FileNotFoundException. "boo"))))))) (deftest test-single (testing "returns one element" @@ -15,4 +38,28 @@ (testing "throw if empty" (is (thrown? java.lang.IllegalArgumentException (b/single (rx/empty))))) (testing "throw if many" - (is (thrown? java.lang.IllegalArgumentException (b/single (rx/seq->o [1 2])))))) + (is (thrown? java.lang.IllegalArgumentException (b/single (rx/seq->o [1 2]))))) + (testing "rethrows errors" + (is (thrown? java.io.FileNotFoundException + (b/single (rx/throw (java.io.FileNotFoundException. "boo"))))))) + +(deftest test-into + (is (= [1 2 3] + (b/into [1] (rx/seq->o [2 3])))) + (testing "rethrows errors" + (is (thrown? java.io.FileNotFoundException + (b/into #{} (rx/throw (java.io.FileNotFoundException. "boo"))))))) + +(deftest test-doseq + (is (= (range 3) + (let [capture (atom [])] + (b/doseq [{:keys [value]} (rx/seq->o (map #(hash-map :value %) (range 3)))] + (println value) + (swap! capture conj value)) + @capture))) + + (testing "rethrows errors" + (is (thrown? java.io.FileNotFoundException + (b/doseq [i (rx/seq->o (range 3))] + (throw (java.io.FileNotFoundException. "boo"))))))) + From a2d231461b5aa9dda2ecb384f897681e05534a83 Mon Sep 17 00:00:00 2001 From: Dave Ray Date: Wed, 19 Feb 2014 10:05:38 -0800 Subject: [PATCH 029/422] Experimental warnings --- .../src/main/clojure/rx/lang/clojure/blocking.clj | 4 +++- .../src/main/clojure/rx/lang/clojure/chunk.clj | 8 ++++++-- .../src/main/clojure/rx/lang/clojure/graph.clj | 8 ++++++-- .../src/main/clojure/rx/lang/clojure/realized.clj | 12 +++++++++--- 4 files changed, 24 insertions(+), 8 deletions(-) diff --git a/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/blocking.clj b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/blocking.clj index f81f4b3c64..4512022466 100644 --- a/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/blocking.clj +++ b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/blocking.clj @@ -14,6 +14,8 @@ (set! *warn-on-reflection* true) (defmacro ^:private with-ex-unwrap + "The blocking ops wrap errors stuff in RuntimeException because of stupid Java. + This tries to unwrap them so callers get the exceptions they expect." [& body] `(try ~@body @@ -80,7 +82,7 @@ (defn single "*Blocks* and waits for the first value emitted by the given observable. - An error is thrown if more then one value is produced. + An error is thrown if zero or more then one value is produced. " [observable] (with-ex-unwrap diff --git a/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/chunk.clj b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/chunk.clj index 303f8cc090..2a6695bfb7 100644 --- a/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/chunk.clj +++ b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/chunk.clj @@ -6,7 +6,11 @@ (set! *warn-on-reflection* true) (defn chunk - "Same as rx.Observable.merge(Observable>) but the input Observables + "EXTREMELY EXPERIMENTAL AND SUBJECT TO CHANGE OR DELETION + + TODO RxJava's much bigger since this was written. Is there something built in? + + Same as rx.Observable.merge(Observable>) but the input Observables are \"chunked\" so that at most chunk-size of them are \"in flight\" at any given time. @@ -19,7 +23,7 @@ Example: (->> users - (map #(-> (GetUserCommand. %) .toObservable)) + (rx/map #(-> (GetUserCommand. %) .toObservable)) (chunk 10)) See: diff --git a/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/graph.clj b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/graph.clj index 18187d29ac..a67ebba47c 100644 --- a/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/graph.clj +++ b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/graph.clj @@ -18,7 +18,9 @@ (.cache o))) (defn let-o* - "Given a graph description, returns an observable that emits a single + "EXTREMELY EXPERIMENTAL AND SUBJECT TO CHANGE OR DELETION + + Given a graph description, returns an observable that emits a single map of observables all hooked up and ready for subscription. A graph is a map from name to a map with keys: @@ -73,7 +75,9 @@ (assoc ::non-terminals non-terminals)))) (defmacro let-o - "Similar to clojure.core/let, but bindings are Observables and the result of the body + "EXTREMELY EXPERIMENTAL AND SUBJECT TO CHANGE OR DELETION + + Similar to clojure.core/let, but bindings are Observables and the result of the body must be an Observable. Binding names must start with ?. Binding order doesn't matter and any binding is visible to all other expressions as long as no cycles are produced in the resulting Observable expression. diff --git a/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/realized.clj b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/realized.clj index e0b455af14..2926633924 100644 --- a/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/realized.clj +++ b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/realized.clj @@ -34,7 +34,9 @@ :else (->post-proc (rx.Observable/just v)))) (defn realized-map - "See let-realized. + "EXTREMELY EXPERIMENTAL AND SUBJECT TO CHANGE OR DELETION + + See let-realized. Given a map from key to observable, returns an observable that emits a single map from the same keys to the values emitted by their corresponding observable. @@ -92,12 +94,16 @@ (.reduce {} (iop/fn* merge))))) ; do the map merge dance (defn ^rx.Observable realized-map* - "Same as realized-map, but takes a map argument rather than key-value pairs." + "EXTREMELY EXPERIMENTAL AND SUBJECT TO CHANGE OR DELETION + + Same as realized-map, but takes a map argument rather than key-value pairs." [map-description] (apply realized-map (apply concat map-description))) (defmacro let-realized - "'let' version of realized map. + "EXTREMELY EXPERIMENTAL AND SUBJECT TO CHANGE OR DELETION + + 'let' version of realized map. (let-realized [a (make-observable)] (* 2 a)) From 0da1e888705e9af489cf420ee0ed19c621a8def4 Mon Sep 17 00:00:00 2001 From: Dave Ray Date: Wed, 19 Feb 2014 12:43:37 -0800 Subject: [PATCH 030/422] Update Clojure README --- language-adaptors/rxjava-clojure/README.md | 119 +++++++++++++++------ 1 file changed, 87 insertions(+), 32 deletions(-) diff --git a/language-adaptors/rxjava-clojure/README.md b/language-adaptors/rxjava-clojure/README.md index b2f69b3853..537e72d9f6 100644 --- a/language-adaptors/rxjava-clojure/README.md +++ b/language-adaptors/rxjava-clojure/README.md @@ -1,10 +1,90 @@ -# Clojure Adaptor for RxJava +Clojure bindings for RxJava. +# Binaries + +Binaries and dependency information for Maven, Ivy, Gradle and others can be found at [http://search.maven.org](http://search.maven.org/#search%7Cga%7C1%7Ca%3A%22rxjava-clojure%22). + +Example for Leiningen: + +```clojure +[com.netflix.rxjava/rxjava-clojure "x.y.z"] +``` + +and for Gradle: + +```groovy +compile 'com.netflix.rx:rxjava-clojure:x.y.z' +``` + +and for Maven: + +```xml + + com.netflix.rxjava + rxjava-clojure + x.y.z + +``` + +and for Ivy: + +```xml + +``` + +# Clojure Bindings +This library provides convenient, idiomatic Clojure bindings for RxJava. + +The bindings try to present an API that will be comfortable and familiar to a Clojure programmer that's familiar with the sequence operations in `clojure.core`. It "fixes" several issues with using RxJava with raw Java interop, for example: + +* Argument lists are in the "right" order. So in RxJava, the function applied in `Observable.map` is the second argument, while here it's the first argument with one or more Observables as trailing arguments +* Operators take normal Clojure functions as arguments, bypassing need for the interop described below +* Predicates accomodate Clojure's notion of truth +* Operators are generally names as they would be in `clojure.core` rather than the Rx names + +There is no object wrapping going on. That is, all functions return normal `rx.Observable` objects, so you can always drop back to Java interop for anything that's missing in this wrapper. + +## Basic Usage +Most functionality resides in the `rx.lang.clojure.core` namespace and for the most part looks like normal Clojure sequence manipulation: + +```clojure +(require '[rx.lang.clojure.core :as rx]) + +(->> my-observable + (rx/map (comp clojure.string/lower-case :first-name)) + (rx/map clojure.string/lower-case) + (rx/filter #{"bob"}) + (rx/distinct) + (rx/into [])) +;=> An Observable that emits a single vector of names +``` + +Blocking operators, which are useful for testing, but should otherwise be avoided, reside in `rx.lang.clojure.blocking`. For example: + +```clojure +(require '[rx.lang.clojure.blocking :as rxb]) + +(rxb/doseq [{:keys [first-name]} users-observable] + (println "Hey," first-name)) +;=> nil +``` + +## What's Missing +This library is an ongoing work in progress driven primarily by the needs of one team at Netflix. As such some things are currently missing: + +* Highly-specific operators that we felt cluttered the API and were easily composed from existing operators, especially since we're in not-Java land. For example, `Observable.sumLong()`. +* Most everything involving schedulers +* Most everything involving time +* `Observable.window` and `Observable.buffer`. Who knows which parts of these beasts to wrap? + +Of course, contributions that cover these cases are welcome. + +# Low-level Interop This adaptor provides functions and macros to ease Clojure/RxJava interop. In particular, there are functions and macros for turning Clojure functions and code into RxJava `Func*` and `Action*` interfaces without the tedium of manually reifying the interfaces. -# Basic Usage +## Basic Usage -## Requiring the interop namespace +### Requiring the interop namespace The first thing to do is to require the namespace: ```clojure @@ -19,7 +99,7 @@ or, at the REPL: (require '[rx.lang.clojure.interop :as rx]) ``` -## Using rx/fn +### Using rx/fn Once the namespace is required, you can use the `rx/fn` macro anywhere RxJava wants a `rx.util.functions.Func` object. The syntax is exactly the same as `clojure.core/fn`: ```clojure @@ -34,7 +114,7 @@ If you already have a plain old Clojure function you'd like to use, you can pass (.reduce (rx/fn* +))) ``` -## Using rx/action +### Using rx/action The `rx/action` macro is identical to `rx/fn` except that the object returned implements `rx.util.functions.Action` interfaces. It's used in `subscribe` and other side-effect-y contexts: ```clojure @@ -46,7 +126,7 @@ The `rx/action` macro is identical to `rx/fn` except that the object returned im (rx/action [] (println "Sequence complete")))) ``` -## Using Observable/create +### Using Observable/create As of 0.17, `rx.Observable/create` takes an implementation of `rx.Observable$OnSubscribe` which is basically an alias for `rx.util.functions.Action1` that takes an `rx.Subscriber` as its argument. Thus, you can just use `rx/action` when creating new observables: ```clojure @@ -59,35 +139,10 @@ As of 0.17, `rx.Observable/create` takes an implementation of `rx.Observable$OnS (.onCompleted s))) ``` -# Gotchas +## Gotchas Here are a few things to keep in mind when using this interop: * Keep in mind the (mostly empty) distinction between `Func` and `Action` and which is used in which contexts * If there are multiple Java methods overloaded by `Func` arity, you'll need to use a type hint to let the compiler know which one to choose. * Methods that take a predicate (like filter) expect the predicate to return a boolean value. A function that returns a non-boolean value will result in a `ClassCastException`. -# Binaries - -Binaries and dependency information for Maven, Ivy, Gradle and others can be found at [http://search.maven.org](http://search.maven.org/#search%7Cga%7C1%7Ca%3A%22rxjava-clojure%22). - -Example for Maven: - -```xml - - com.netflix.rxjava - rxjava-clojure - x.y.z - -``` - -and for Ivy: - -```xml - -``` - -and for Leiningen: - -```clojure -[com.netflix.rxjava/rxjava-clojure "x.y.z"] -``` From cf5a20c59ef1533004d205d82cb679fc2f0b63cf Mon Sep 17 00:00:00 2001 From: mike castleman Date: Wed, 19 Feb 2014 16:32:59 -0500 Subject: [PATCH 031/422] fix gradle spec in README presumably the group ID should be the same for gradle as for the other dependency managers. --- language-adaptors/rxjava-clojure/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/language-adaptors/rxjava-clojure/README.md b/language-adaptors/rxjava-clojure/README.md index 537e72d9f6..1a0d9e0af9 100644 --- a/language-adaptors/rxjava-clojure/README.md +++ b/language-adaptors/rxjava-clojure/README.md @@ -13,7 +13,7 @@ Example for Leiningen: and for Gradle: ```groovy -compile 'com.netflix.rx:rxjava-clojure:x.y.z' +compile 'com.netflix.rxjava:rxjava-clojure:x.y.z' ``` and for Maven: From df5503fcec91b1248fc4db913a517fc34ecd7750 Mon Sep 17 00:00:00 2001 From: Dave Ray Date: Wed, 19 Feb 2014 16:46:53 -0800 Subject: [PATCH 032/422] Cleaned up naming of observable operator definition fns --- .../main/clojure/rx/lang/clojure/chunk.clj | 4 +- .../src/main/clojure/rx/lang/clojure/core.clj | 242 +++++++++--------- .../main/clojure/rx/lang/clojure/future.clj | 8 +- .../clojure/rx/lang/clojure/core_test.clj | 23 +- 4 files changed, 139 insertions(+), 138 deletions(-) diff --git a/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/chunk.clj b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/chunk.clj index 2a6695bfb7..d53c8ce322 100644 --- a/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/chunk.clj +++ b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/chunk.clj @@ -92,8 +92,8 @@ (fn [] (swap! state-atom assoc :complete :complete) (advance! state-atom)))) - observable (rx/fn->o (fn [observer] - (subscribe (new-state-atom observer)))) ] + observable (rx/observable* (fn [observer] + (subscribe (new-state-atom observer)))) ] (if (:delay-error? options) (rx.Observable/mergeDelayError observable) (rx.Observable/merge observable))))) diff --git a/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj index 4e50cef03c..4040d7da83 100644 --- a/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj +++ b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj @@ -54,28 +54,14 @@ (.onError o e)) ;################################################################################ +; Tools for creating new operators and observables -(defn ^Subscription subscribe - - ([^Observable o on-next-action] - (.subscribe o - ^Action1 (iop/action* on-next-action))) +(declare unsubscribed?) - ([^Observable o on-next-action on-error-action] - (.subscribe o - ^Action1 (iop/action* on-next-action) - ^Action1 (iop/action* on-error-action))) - - ([^Observable o on-next-action on-error-action on-completed-action] - (.subscribe o - ^Action1 (iop/action* on-next-action) - ^Action1 (iop/action* on-error-action) - ^Action0 (iop/action* on-completed-action)))) - -(defn ^Subscriber ->subscriber +(defn ^Subscriber subscriber "" - ([o on-next-action] (->subscriber o on-next-action nil nil)) - ([o on-next-action on-error-action] (->subscriber o on-next-action on-error-action nil)) + ([o on-next-action] (subscriber o on-next-action nil nil)) + ([o on-next-action on-error-action] (subscriber o on-next-action on-error-action nil)) ([^Subscriber o on-next-action on-error-action on-completed-action] (proxy [Subscriber] [o] (onCompleted [] @@ -91,17 +77,23 @@ (on-next-action o t) (on-next o t)))))) -(defn ^Observable$Operator fn->operator - "Create a basic Operator with f. If a handler is omitted or nil - it's treated as a pass-through. +(defn ^Subscription subscription + "Create a new subscription that calls the given no-arg handler function when + unsubscribe is called + + See: + rx.subscriptions.Subscriptions/create + " + [handler] + (Subscriptions/create ^Action0 (iop/action* handler))) - on-next-action Passed Subscriber and value - on-error-action Passed Throwable - on-completed-action No-args +(defn ^Observable$Operator operator* + "Returns a new implementation of rx.Observable$Operator that calls the given + function with a rx.Subscriber. The function should return a rx.Subscriber. See: - lift - rx.Observable$Operator + lift + rx.Observable$Operator " [f] {:pre [(fn? f)]} @@ -109,22 +101,72 @@ (call [this o] (f o)))) +(defn ^Observable observable* + "Create an Observable from the given function. + + When subscribed to, (f subscriber) is called at which point, f can start emitting values, etc. + The passed subscriber is of type rx.Subscriber. + + See: + rx.Subscriber + rx.Observable/create + " + [f] + (Observable/create ^Observable$OnSubscribe (iop/action* f))) + +(defn wrap-on-completed + "Wrap handler with code that automaticaly calls rx.Observable.onCompleted." + [handler] + (fn [^Observer observer] + (handler observer) + (when-not (unsubscribed? observer) + (.onCompleted observer)))) + +(defn wrap-on-error + "Wrap handler with code that automaticaly calls (on-error) if an exception is thrown" + [handler] + (fn [^Observer observer] + (try + (handler observer) + (catch Throwable e + (when-not (unsubscribed? observer) + (.onError observer e)))))) + (defn lift "Lift the Operator op over the given Observable xs Example: (->> my-observable - (rx/lift (rx/fn->operator ...)) + (rx/lift (rx/operator ...)) ...) See: rx.Observable/lift - fn->operator + operator " [^Observable$Operator op ^Observable xs] (.lift xs op)) +;################################################################################ + +(defn ^Subscription subscribe + + ([^Observable o on-next-action] + (.subscribe o + ^Action1 (iop/action* on-next-action))) + + ([^Observable o on-next-action on-error-action] + (.subscribe o + ^Action1 (iop/action* on-next-action) + ^Action1 (iop/action* on-error-action))) + + ([^Observable o on-next-action on-error-action on-completed-action] + (.subscribe o + ^Action1 (iop/action* on-next-action) + ^Action1 (iop/action* on-error-action) + ^Action0 (iop/action* on-completed-action)))) + (defn unsubscribe "Unsubscribe from Subscription s and return it." [^Subscription s] @@ -158,53 +200,50 @@ See: rx.Observable/create - fn->o + observable* " [^Subscription s] (.isUnsubscribed s)) -(defn ^Subscription fn->subscription - "Create a new subscription that calls the given no-arg handler function when - unsubscribe is called +;################################################################################ +; Functions for creating Observables + +(defn ^Observable never + "Returns an Observable that never emits any values and never completes. See: - rx.subscriptions.Subscriptions/create + rx.Observable/never " - [handler] - (Subscriptions/create ^Action0 (iop/action* handler))) - -(defn ^Observable fn->o - "Create an Observable from the given function. + [] + (Observable/never)) - When subscribed to, (f subscriber) is called at which point, f can start emitting values, etc. - The passed subscriber is of type rx.Subscriber. +(defn ^Observable empty + "Returns an Observable that completes immediately without emitting any values. See: - rx.Subscriber - rx.Observable/create + rx.Observable/empty " - [f] - (Observable/create ^Observable$OnSubscribe (iop/action* f))) + [] + (Observable/empty)) -(defn wrap-on-completed - "Wrap handler with code that automaticaly calls rx.Observable.onCompleted." - [handler] - (fn [^Observer observer] - (handler observer) - (when-not (unsubscribed? observer) - (.onCompleted observer)))) +(defn ^Observable return + "Returns an observable that emits a single value. -(defn wrap-on-error - "Wrap handler with code that automaticaly calls (on-error) if an exception is thrown" - [handler] - (fn [^Observer observer] - (try - (handler observer) - (catch Throwable e - (when-not (unsubscribed? observer) - (.onError observer e)))))) + See: + rx.Observable/just + " + [value] + (Observable/just value)) + +(defn ^Observable seq->o + "Make an observable out of some seq-able thing. The rx equivalent of clojure.core/seq." + [xs] + (if xs + (Observable/from ^Iterable xs) + (empty))) ;################################################################################ +; Operators (defn synchronize ([^Observable xs] @@ -246,44 +285,6 @@ (throw (IllegalArgumentException. (str "Don't know how to merge " (type os)))))) -;################################################################################ - -(defn ^Observable never - "Returns an Observable that never emits any values and never completes. - - See: - rx.Observable/never - " - [] - (Observable/never)) - -(defn ^Observable empty - "Returns an Observable that completes immediately without emitting any values. - - See: - rx.Observable/empty - " - [] - (Observable/empty)) - -(defn ^Observable return - "Returns an observable that emits a single value. - - See: - rx.Observable/just - " - [value] - (Observable/just value)) - -(defn ^Observable seq->o - "Make an observable out of some seq-able thing. The rx equivalent of clojure.core/seq." - [xs] - (if xs - (Observable/from ^Iterable xs) - (empty))) - -;################################################################################ - (defn cache "caches the observable value so that multiple subscribers don't re-evaluate it. @@ -354,14 +355,14 @@ " ([xs] (distinct identity xs)) ([key-fn ^Observable xs] - (let [op (fn->operator (fn [o] - (let [seen (atom #{})] - (->subscriber o - (fn [o v] - (let [key (key-fn v)] - (when-not (contains? @seen key) - (swap! seen conj key) - (on-next o v))))))))] + (let [op (operator* (fn [o] + (let [seen (atom #{})] + (subscriber o + (fn [o v] + (let [key (key-fn v)] + (when-not (contains? @seen key) + (swap! seen conj key) + (on-next o v))))))))] (lift op xs)))) (defn ^Observable do @@ -422,12 +423,12 @@ (defn interpose [sep xs] - (let [op (fn->operator (fn [o] - (let [first? (atom true)] - (->subscriber o (fn [o v] - (if-not (compare-and-set! first? true false) - (on-next o sep)) - (on-next o v))))))] + (let [op (operator* (fn [o] + (let [first? (atom true)] + (subscriber o (fn [o v] + (if-not (compare-and-set! first? true false) + (on-next o sep)) + (on-next o v))))))] (lift op xs))) (defn into @@ -499,10 +500,11 @@ clojure.core/map-indexed " [f xs] - (let [op (fn->operator (fn [o] - (let [n (atom -1)] - (->subscriber o - (fn [o v] (on-next o (f (swap! n inc) v)))))))] + (let [op (operator* (fn [o] + (let [n (atom -1)] + (subscriber o + (fn [o v] + (on-next o (f (swap! n inc) v)))))))] (lift op xs))) (def next @@ -768,9 +770,9 @@ (rx/generator* on-next 99) " [f & args] - (fn->o (-> #(apply f % args) - wrap-on-completed - wrap-on-error))) + (observable* (-> #(apply f % args) + wrap-on-completed + wrap-on-error))) (defmacro generator "Create an observable that executes body which should emit a sequence. bindings diff --git a/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/future.clj b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/future.clj index ac8d29f5e6..0c63be36e8 100644 --- a/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/future.clj +++ b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/future.clj @@ -29,14 +29,14 @@ " [runner f & args] {:pre [(ifn? runner) (ifn? f)]} - (rx/fn->o (fn [^rx.Subscriber observer] + (rx/observable* (fn [^rx.Subscriber observer] (let [wrapped (-> (fn [o] (apply f o args)) rx/wrap-on-completed rx/wrap-on-error) fu (runner #(wrapped observer))] (.add observer - (rx/fn->subscription #(future-cancel fu))))))) + (rx/subscription #(future-cancel fu))))))) (defmacro future-generator "Same as rx/generator macro except body is invoked in a separate thread. @@ -74,13 +74,13 @@ " [runner f & args] {:pre [(ifn? runner) (ifn? f)]} - (rx/fn->o (fn [^rx.Subscriber observer] + (rx/observable* (fn [^rx.Subscriber observer] (let [wrapped (-> #(rx/on-next % (apply f args)) rx/wrap-on-completed rx/wrap-on-error) fu (runner #(wrapped observer))] (.add observer - (rx/fn->subscription #(future-cancel fu))))))) + (rx/subscription #(future-cancel fu))))))) (defmacro future "Executes body in a separate thread and passes the single result to onNext. diff --git a/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/core_test.clj b/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/core_test.clj index daefe3c05c..f23bcc0013 100644 --- a/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/core_test.clj +++ b/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/core_test.clj @@ -23,29 +23,29 @@ identity "true" true identity true true)) -(deftest test-fn->subscription +(deftest test-subscription (let [called (atom 0) - s (rx/fn->subscription #(swap! called inc))] + s (rx/subscription #(swap! called inc))] (is (identical? s (rx/unsubscribe s))) (is (= 1 @called)))) (deftest test-unsubscribed? - (let [s (rx/fn->subscription #())] + (let [s (rx/subscription #())] (is (not (rx/unsubscribed? s))) (rx/unsubscribe s) (is (rx/unsubscribed? s)))) -(deftest test-fn->o - (let [o (rx/fn->o (fn [s] - (rx/on-next s 0) - (rx/on-next s 1) - (when-not (rx/unsubscribed? s) (rx/on-next s 2)) - (rx/on-completed s)))] +(deftest test-observable* + (let [o (rx/observable* (fn [s] + (rx/on-next s 0) + (rx/on-next s 1) + (when-not (rx/unsubscribed? s) (rx/on-next s 2)) + (rx/on-completed s)))] (is (= [0 1 2] (b/into [] o))))) -(deftest test-fn->operator - (let [o (rx/fn->operator #(rx/->subscriber % +(deftest test-operator* + (let [o (rx/operator* #(rx/subscriber % (fn [o v] (if (even? v) (rx/on-next o v))))) @@ -54,7 +54,6 @@ (b/into []))] (is (= [2 4] result)))) - (deftest test-syncrhonize ; I'm going to believe synchronize works and just exercise it ; here for sanity. From f1ff778a8db1b9fc9e9d47bba02e6f1b50088ecc Mon Sep 17 00:00:00 2001 From: Dave Ray Date: Wed, 19 Feb 2014 21:00:12 -0800 Subject: [PATCH 033/422] Implement catch-error-value Automating "rx.exceptions.OnErrorThrowable/addValueAsLastCause" idiom when user code is invoked in onNext. Other minor cleanup. --- .../src/main/clojure/rx/lang/clojure/core.clj | 49 ++++++++++++++--- .../clojure/rx/lang/clojure/core_test.clj | 54 ++++++++++++++++++- 2 files changed, 95 insertions(+), 8 deletions(-) diff --git a/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj index 4040d7da83..1b55535ee3 100644 --- a/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj +++ b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj @@ -39,19 +39,53 @@ ;################################################################################ (defn on-next - "Call onNext on the given observer." + "Call onNext on the given observer and return o." [^Observer o value] - (.onNext o value)) + (.onNext o value) + o) (defn on-completed - "Call onCompleted on the given observer." + "Call onCompleted on the given observer and return o." [^Observer o] - (.onCompleted o)) + (.onCompleted o) + o) (defn on-error - "Call onError on the given observer." + "Call onError on the given observer and return o." [^Observer o e] - (.onError o e)) + (.onError o e) + o) + +(defmacro catch-error-value + "Experimental + + TODO: Better name, better abstraction. + + Evaluate body and return its value. If an exception e is thrown, inject the + given value into the exception's cause and call (on-error error-observer e), + returning e. + + This is meant to facilitate implementing Observers that call user-supplied code + safely. The general pattern is something like: + + (fn [o v] + (rx/catch-error-value o v + (rx/on-next o (some-func v)))) + + If (some-func v) throws an exception, it is caught, v is injected into the + exception's cause (with OnErrorThrowable/addValueAsLastCause) and + (rx/on-error o e) is invoked. + + See: + rx.exceptions.OnErrorThrowable/addValueAsLastCause + " + [error-observer value & body] + `(try + ~@body + (catch Throwable e# + (on-error ~error-observer + (rx.exceptions.OnErrorThrowable/addValueAsLastCause e# ~value)) + e#))) ;################################################################################ ; Tools for creating new operators and observables @@ -504,7 +538,8 @@ (let [n (atom -1)] (subscriber o (fn [o v] - (on-next o (f (swap! n inc) v)))))))] + (catch-error-value o v + (on-next o (f (swap! n inc) v))))))))] (lift op xs))) (def next diff --git a/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/core_test.clj b/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/core_test.clj index f23bcc0013..8def4ec506 100644 --- a/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/core_test.clj +++ b/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/core_test.clj @@ -8,6 +8,48 @@ (is (rx/observable? (rx/return 99))) (is (not (rx/observable? "I'm not an observable")))) +(deftest test-on-next + (testing "calls onNext" + (let [called (atom []) + o (reify rx.Observer (onNext [this value] (swap! called conj value)))] + (is (identical? o (rx/on-next o 1))) + (is (= [1] @called))))) + +(deftest test-on-completed + (testing "calls onCompleted" + (let [called (atom 0) + o (reify rx.Observer (onCompleted [this] (swap! called inc)))] + (is (identical? o (rx/on-completed o))) + (is (= 1 @called))))) + +(deftest test-on-error + (testing "calls onError" + (let [called (atom []) + e (java.io.FileNotFoundException. "yum") + o (reify rx.Observer (onError [this e] (swap! called conj e)))] + (is (identical? o (rx/on-error o e))) + (is (= [e] @called))))) + +(deftest test-catch-error-value + (testing "if no exception, returns body" + (let [o (reify rx.Observer)] + (is (= 3 (rx/catch-error-value o 99 + (+ 1 2)))))) + + (testing "exceptions call onError on observable and inject value in exception" + (let [called (atom []) + e (java.io.FileNotFoundException. "boo") + o (reify rx.Observer + (onError [this e] + (swap! called conj e))) + result (rx/catch-error-value o 100 + (throw e)) + cause (.getCause e)] + (is (identical? e result)) + (is (= [e] @called)) + (when (is (instance? rx.exceptions.OnErrorThrowable$OnNextValue cause)) + (is (= 100 (.getValue cause))))))) + (deftest test-subscribe (testing "subscribe overload with only onNext" (let [o (rx/return 1) @@ -285,7 +327,17 @@ (rx/seq->o [8])])))))) (deftest test-map-indexed (is (= (map-indexed vector [:a :b :c]) - (b/into [] (rx/map-indexed vector (rx/seq->o [:a :b :c])))))) + (b/into [] (rx/map-indexed vector (rx/seq->o [:a :b :c]))))) + (testing "exceptions from fn have error value injected" + (try + (->> (rx/seq->o [:a :b :c]) + (rx/map-indexed (fn [i v] + (if (= 1 i) + (throw (java.io.FileNotFoundException. "blah"))) + v)) + (b/into [])) + (catch java.io.FileNotFoundException e + (is (= :b (-> e .getCause .getValue))))))) (deftest test-mapcat (let [f (fn [v] [v (* v v)]) From feb16d08967e10f413e7fdb641a07e44553555fe Mon Sep 17 00:00:00 2001 From: Dave Ray Date: Wed, 19 Feb 2014 21:06:45 -0800 Subject: [PATCH 034/422] more README --- language-adaptors/rxjava-clojure/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/language-adaptors/rxjava-clojure/README.md b/language-adaptors/rxjava-clojure/README.md index 1a0d9e0af9..14f6b312be 100644 --- a/language-adaptors/rxjava-clojure/README.md +++ b/language-adaptors/rxjava-clojure/README.md @@ -69,6 +69,12 @@ Blocking operators, which are useful for testing, but should otherwise be avoide ;=> nil ``` +## Open Issues + +* The missing stuff mentioned below +* `group-by` +* There are some functions for defining customer Observables and Operators (`subscriber`, `operator*`, `observable*`). I don't think these are really enough for serious operator implementation, but I'm hesitant to guess at an abstraction at this point. These will probably change dramatically. + ## What's Missing This library is an ongoing work in progress driven primarily by the needs of one team at Netflix. As such some things are currently missing: From 963cfd5af091eb4595be6d1cffe2544d4a15bd57 Mon Sep 17 00:00:00 2001 From: Dave Ray Date: Wed, 19 Feb 2014 21:58:45 -0800 Subject: [PATCH 035/422] Implement group-by Note that since 02ccc4d727a9297f14219549208757c6e0efce2a, the val-fn variant of groupBy is unimplemented so for now an exception is thrown if it's used. --- language-adaptors/rxjava-clojure/README.md | 2 +- .../src/main/clojure/rx/lang/clojure/core.clj | 33 +++++++++++++++++-- .../clojure/rx/lang/clojure/core_test.clj | 30 +++++++++++++++++ 3 files changed, 62 insertions(+), 3 deletions(-) diff --git a/language-adaptors/rxjava-clojure/README.md b/language-adaptors/rxjava-clojure/README.md index 14f6b312be..4c5bdafe46 100644 --- a/language-adaptors/rxjava-clojure/README.md +++ b/language-adaptors/rxjava-clojure/README.md @@ -72,7 +72,7 @@ Blocking operators, which are useful for testing, but should otherwise be avoide ## Open Issues * The missing stuff mentioned below -* `group-by` +* `group-by` val-fn variant isn't implemented in RxJava * There are some functions for defining customer Observables and Operators (`subscriber`, `operator*`, `observable*`). I don't think these are really enough for serious operator implementation, but I'm hesitant to guess at an abstraction at this point. These will probably change dramatically. ## What's Missing diff --git a/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj index 1b55535ee3..a7a0555934 100644 --- a/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj +++ b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj @@ -3,6 +3,7 @@ distinct do drop drop-while empty every? filter first future + group-by interpose into keep keep-indexed map mapcat map-indexed @@ -16,7 +17,9 @@ Observable Observer Observable$Operator Observable$OnSubscribe Subscriber Subscription] - [rx.observables BlockingObservable] + [rx.observables + BlockingObservable + GroupedObservable] [rx.subscriptions Subscriptions] [rx.util.functions Action0 Action1 Func0 Func1 Func2])) @@ -453,7 +456,33 @@ [^Observable xs] (.takeFirst xs)) -; TODO group-by +(defn ^Observable group-by + "Returns an Observable of clojure.lang.MapEntry where the key is the result of + (key-fn x) and the val is an Observable of (val-fn x) for each key. If val-fn is + omitted, it defaults to identity. + + This returns a clojure.lang.MapEntry rather than rx.observables.GroupedObservable + for some vague consistency with clojure.core/group-by and so that clojure.core/key, + clojure.core/val and destructuring will work as expected. + + See: + clojure.core/group-by + rx.Observable/groupBy + rx.observables.GroupedObservable + " + ([key-fn ^Observable xs] + (->> (.groupBy xs (iop/fn* key-fn)) + (map (fn [^GroupedObservable go] + (clojure.lang.MapEntry. (.getKey go) go))))) + ([key-fn val-fn ^Observable xs] + ; TODO reinstate once this is implemented + ; see https://github.com/Netflix/RxJava/commit/02ccc4d727a9297f14219549208757c6e0efce2a + (throw (UnsupportedOperationException. "groupBy with val-fn is currently unimplemented in RxJava")) + (->> (.groupBy xs + (iop/fn* key-fn) + (iop/fn* val-fn)) + (map (fn [^GroupedObservable go] + (clojure.lang.MapEntry. (.getKey go) go)))))) (defn interpose [sep xs] diff --git a/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/core_test.clj b/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/core_test.clj index 8def4ec506..c59be6a034 100644 --- a/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/core_test.clj +++ b/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/core_test.clj @@ -256,6 +256,36 @@ (is (= [] (b/into [] (rx/first (rx/empty)))))) +(deftest test-group-by + (let [xs [{:k :a :v 1} {:k :b :v 2} {:k :a :v 3} {:k :c :v 4}]] + (testing "with just a key-fn" + (is (= [[:a {:k :a :v 1}] + [:b {:k :b :v 2}] + [:a {:k :a :v 3}] + [:c {:k :c :v 4}]] + (->> xs + (rx/seq->o) + (rx/group-by :k) + (rx/mapcat (fn [[k vo :as me]] + (is (instance? clojure.lang.MapEntry me)) + (rx/map #(vector k %) vo))) + (b/into []))))) + + ; TODO reinstate once this is implemented + ; see https://github.com/Netflix/RxJava/commit/02ccc4d727a9297f14219549208757c6e0efce2a + #_(testing "with a val-fn" + (is (= [[:a 1] + [:b 2] + [:a 3] + [:c 4]] + (->> xs + (rx/seq->o) + (rx/group-by :k :v) + (rx/mapcat (fn [[k vo :as me]] + (is (instance? clojure.lang.MapEntry me)) + (rx/map #(vector k %) vo))) + (b/into []))))) + )) (deftest test-interpose (is (= (interpose \, [1 2 3]) (b/into [] (rx/interpose \, (rx/seq->o [1 2 3])))))) From 3714db96bbdcd50e8680d189d9e1c77de3018dc4 Mon Sep 17 00:00:00 2001 From: Dave Ray Date: Wed, 19 Feb 2014 23:41:01 -0800 Subject: [PATCH 036/422] Implement interleave and interleave* --- .../src/main/clojure/rx/lang/clojure/core.clj | 31 +++++++++++++++++-- .../clojure/rx/lang/clojure/core_test.clj | 31 +++++++++++++++++-- 2 files changed, 58 insertions(+), 4 deletions(-) diff --git a/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj index a7a0555934..7a86f6d4a0 100644 --- a/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj +++ b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj @@ -4,7 +4,7 @@ empty every? filter first future group-by - interpose into + interleave interpose into keep keep-indexed map mapcat map-indexed merge next nth partition reduce reductions @@ -25,7 +25,7 @@ (set! *warn-on-reflection* true) -(declare concat map map-indexed reduce take take-while) +(declare concat* concat map* map map-indexed reduce take take-while) (defn ^Func1 fn->predicate "Turn f into a predicate that returns true/false like Rx predicates should" @@ -484,6 +484,33 @@ (map (fn [^GroupedObservable go] (clojure.lang.MapEntry. (.getKey go) go)))))) +(defn interleave* + "Returns an Observable of the first item in each Observable emitted by observables, then + the second etc. + + observables is an Observable of Observables + + See: + interleave + clojure.core/interleave + " + [observables] + (->> (map* #(seq->o %&) observables) + (concat*))) + +(defn interleave + "Returns an Observable of the first item in each Observable, then the second etc. + + Each argument is an individual Observable + + See: + observable* + clojure.core/interleave + " + [o1 & observables] + (->> (apply map #(seq->o %&) o1 observables) + (concat*))) + (defn interpose [sep xs] (let [op (operator* (fn [o] diff --git a/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/core_test.clj b/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/core_test.clj index c59be6a034..2362638e50 100644 --- a/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/core_test.clj +++ b/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/core_test.clj @@ -284,8 +284,35 @@ (rx/mapcat (fn [[k vo :as me]] (is (instance? clojure.lang.MapEntry me)) (rx/map #(vector k %) vo))) - (b/into []))))) - )) + (b/into []))))))) + +(deftest test-interleave + (are [inputs] (= (apply interleave inputs) + (->> (apply rx/interleave (map rx/seq->o inputs)) + (b/into []))) + [[] []] + [[] [1]] + [(range 5) (range 10) (range 10) (range 3)] + [(range 50) (range 10)] + [(range 5) (range 10 60) (range 10) (range 50)]) + + ; one-arg case, not supported by clojure.core/interleave + (is (= (range 10) + (->> (rx/interleave (rx/seq->o (range 10))) + (b/into []))))) + +(deftest test-interleave* + (are [inputs] (= (apply interleave inputs) + (->> (rx/interleave* (->> inputs + (map rx/seq->o) + (rx/seq->o))) + (b/into []))) + [[] []] + [[] [1]] + [(range 5) (range 10) (range 10) (range 3)] + [(range 50) (range 10)] + [(range 5) (range 10 60) (range 10) (range 50)])) + (deftest test-interpose (is (= (interpose \, [1 2 3]) (b/into [] (rx/interpose \, (rx/seq->o [1 2 3])))))) From 02ff779811751a4b032675c237782d22d310720f Mon Sep 17 00:00:00 2001 From: Dave Ray Date: Thu, 20 Feb 2014 08:44:10 -0800 Subject: [PATCH 037/422] Implemented partition-all There isn't really an rxjava impl equivalent to clojure.core/partition, so it's omitted. --- .../src/main/clojure/rx/lang/clojure/core.clj | 14 ++++++++-- .../clojure/rx/lang/clojure/core_test.clj | 27 +++++++++++++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj index 7a86f6d4a0..f74df0eed6 100644 --- a/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj +++ b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj @@ -7,7 +7,7 @@ interleave interpose into keep keep-indexed map mapcat map-indexed - merge next nth partition reduce reductions + merge next nth partition-all reduce reductions rest seq some sort sort-by split-with take take-while throw]) (:require [rx.lang.clojure.interop :as iop] @@ -618,7 +618,17 @@ ([^Observable xs index not-found] (.elementAtOrDefault xs index not-found))) -; TODO partition. Use window +(defn ^Observable partition-all + "Returns an Observable of Observables of n items each, at offsets step + apart. If step is not supplied, defaults to n, i.e. the partitions + do not overlap. May include partitions with fewer than n items at the end. + + See: + clojure.core/partition-all + rx.Observable/window + " + ([n ^Observable xs] (.window xs (int n))) + ([n step ^Observable xs] (.window xs (int n) (int step)))) (defn ^Observable reduce ([f ^Observable xs] (.reduce xs (iop/fn* f))) diff --git a/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/core_test.clj b/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/core_test.clj index 2362638e50..47db658c6c 100644 --- a/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/core_test.clj +++ b/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/core_test.clj @@ -416,6 +416,33 @@ (let [in [:q :r :s :t :u]] (is (= (rest in) (b/into [] (rx/rest (rx/seq->o in))))))) +(deftest test-partition-all + (are [input-size part-size step] (= (->> (range input-size) + (partition-all part-size step)) + (->> (range input-size) + (rx/seq->o) + (rx/partition-all part-size step) + (rx/map #(rx/into [] %)) + (rx/concat*) + (b/into []))) + 0 1 1 + 10 2 2 + 10 3 2 + 15 30 4) + + (are [input-size part-size] (= (->> (range input-size) + (partition-all part-size)) + (->> (range input-size) + (rx/seq->o) + (rx/partition-all part-size) + (rx/map #(rx/into [] %)) + (rx/concat*) + (b/into []))) + 0 1 + 10 2 + 10 3 + 15 30)) + (deftest test-reduce (is (= (reduce + 0 (range 4)) (b/first (rx/reduce + 0 (rx/seq->o (range 4))))))) From 1ffe5fb9c8581185e0e090c2b4c8d71cb91ce488 Mon Sep 17 00:00:00 2001 From: Dave Ray Date: Thu, 20 Feb 2014 10:04:53 -0800 Subject: [PATCH 038/422] Avoid intermediate toList for blocking/into --- .../src/main/clojure/rx/lang/clojure/blocking.clj | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/blocking.clj b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/blocking.clj index 4512022466..feee933225 100644 --- a/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/blocking.clj +++ b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/blocking.clj @@ -99,7 +99,8 @@ rx/into " [to from-observable] - (first (rx/into to from-observable))) + (with-ex-unwrap + (clojure.core/into to (o->seq from-observable)))) (defn doseq* "*Blocks* and executes (f x) for each x emitted by xs From 7f4f07ed83df6e01619f888d557eec3675ac203c Mon Sep 17 00:00:00 2001 From: Dave Ray Date: Thu, 20 Feb 2014 12:09:46 -0800 Subject: [PATCH 039/422] Update merge/merge-delay-error impls. Split merge into merge/merge* for consistency with other functions that can take one or more observables or observable of observables. --- .../src/main/clojure/rx/lang/clojure/core.clj | 52 +++++++++++-------- .../clojure/rx/lang/clojure/core_test.clj | 48 ++++++++++++----- 2 files changed, 63 insertions(+), 37 deletions(-) diff --git a/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj index f74df0eed6..129306f43d 100644 --- a/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj +++ b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj @@ -288,39 +288,45 @@ ([lock ^Observable xs] (.synchronize xs lock))) -(defn ^Observable merge - "Observable.merge, renamed because merge means something else in Clojure +(defn merge* + "Merge an Observable of Observables into a single Observable + + If you want clojure.core/merge, it's just this: - os is one of: + (rx/reduce clojure.core/merge {} maps) - * An Iterable of Observables to merge - * An Observable> to merge + See: + merge + merge-delay-error* + rx.Observable/merge + " + [^Observable xs] + (Observable/merge xs)) + +(defn ^Observable merge + "Merge one or more Observables into a single observable. If you want clojure.core/merge, it's just this: (rx/reduce clojure.core/merge {} maps) + See: + merge* + merge-delay-error + rx.Observable/merge " - [os] - (cond - (instance? Iterable os) - (Observable/merge (Observable/from ^Iterable os)) - (instance? Observable os) - (Observable/merge ^Observable os) - :else - (throw (IllegalArgumentException. (str "Don't know how to merge " (type os)))))) + [& os] + (merge* (seq->o os))) -(defn ^Observable merge-delay-error - "Observable.mergeDelayError" - [os] - (cond - (instance? java.util.List os) - (Observable/mergeDelayError ^java.util.List os) - (instance? Observable os) - (Observable/mergeDelayError ^Observable os) - :else - (throw (IllegalArgumentException. (str "Don't know how to merge " (type os)))))) +(defn ^Observable merge-delay-error* + "Same as merge*, but all values are emitted before errors are propagated" + [^Observable xs] + (Observable/mergeDelayError xs)) +(defn ^Observable merge-delay-error + "Same as merge, but all values are emitted before errors are propagated" + [& os] + (merge-delay-error* (seq->o os))) (defn cache "caches the observable value so that multiple subscribers don't re-evaluate it. diff --git a/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/core_test.clj b/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/core_test.clj index 47db658c6c..f9a47dd99b 100644 --- a/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/core_test.clj +++ b/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/core_test.clj @@ -111,20 +111,40 @@ (rx/synchronize lock) (b/into [])))))) -(deftest test-merge - (is (= [[1 3 5] [2 4 6]] - (let [r (b/into [] - (rx/merge [(f/future-generator f/default-runner [o] - (doseq [x [1 3 5]] - (Thread/sleep 10) - (rx/on-next o x))) - (f/future-generator f/default-runner [o] - (doseq [x [2 4 6]] - (Thread/sleep 10) - (rx/on-next o x)))]))] - ; make sure each sequence maintained original order - [(keep #{1 3 5} r) - (keep #{2 4 6} r) ])))) +(let [expected-result [[1 3 5] [2 4 6]] + sleepy-o #(f/future-generator f/default-runner [o] + (doseq [x %] + (Thread/sleep 10) + (rx/on-next o x))) + make-inputs (fn [] (mapv sleepy-o expected-result)) + make-output (fn [r] [(keep #{1 3 5} r) + (keep #{2 4 6} r)])] + (deftest test-merge* + (is (= expected-result + (->> (make-inputs) + (rx/seq->o) + (rx/merge*) + (b/into []) + (make-output))))) + (deftest test-merge + (is (= expected-result + (->> (make-inputs) + (apply rx/merge) + (b/into []) + (make-output))))) + (deftest test-merge-delay-error* + (is (= expected-result + (->> (make-inputs) + (rx/seq->o) + (rx/merge-delay-error*) + (b/into []) + (make-output))))) + (deftest test-merge-delay-error + (is (= expected-result + (->> (make-inputs) + (apply rx/merge-delay-error) + (b/into []) + (make-output)))))) (deftest test-generator (testing "calls on-completed automatically" From 67cf57e2a37618c1134ad3cf1e1a87a50111a226 Mon Sep 17 00:00:00 2001 From: Dave Ray Date: Thu, 20 Feb 2014 12:17:28 -0800 Subject: [PATCH 040/422] Updates from @mbossenbroek's feedback. --- .../src/main/clojure/rx/lang/clojure/core.clj | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj index 129306f43d..c87d6d2e1b 100644 --- a/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj +++ b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj @@ -96,7 +96,7 @@ (declare unsubscribed?) (defn ^Subscriber subscriber - "" + "Experimental, subject to change or deletion." ([o on-next-action] (subscriber o on-next-action nil nil)) ([o on-next-action on-error-action] (subscriber o on-next-action on-error-action nil)) ([^Subscriber o on-next-action on-error-action on-completed-action] @@ -125,7 +125,9 @@ (Subscriptions/create ^Action0 (iop/action* handler))) (defn ^Observable$Operator operator* - "Returns a new implementation of rx.Observable$Operator that calls the given + "Experimental, subject to change or deletion. + + Returns a new implementation of rx.Observable$Operator that calls the given function with a rx.Subscriber. The function should return a rx.Subscriber. See: @@ -188,6 +190,13 @@ ;################################################################################ (defn ^Subscription subscribe + "Subscribe to the given observable. + + on-X-action is a normal clojure function. + + See: + rx.Observable/subscribe + " ([^Observable o on-next-action] (.subscribe o @@ -283,6 +292,11 @@ ; Operators (defn synchronize + "Synchronize execution. + + See: + rx.Observable/synchronize + " ([^Observable xs] (.synchronize xs)) ([lock ^Observable xs] From 2b597fc19fa70ac48423bbe285a2b38241354077 Mon Sep 17 00:00:00 2001 From: Dave Ray Date: Thu, 20 Feb 2014 13:54:19 -0800 Subject: [PATCH 041/422] interpose docstring --- .../rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj index c87d6d2e1b..28ef283e30 100644 --- a/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj +++ b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj @@ -532,6 +532,11 @@ (concat*))) (defn interpose + "Returns an Observable of the elements of xs separated by sep + + See: + clojure.core/interpose + " [sep xs] (let [op (operator* (fn [o] (let [first? (atom true)] From c573cfd87c6f13ed037fc0e7a31155dcc0b95aca Mon Sep 17 00:00:00 2001 From: Dave Ray Date: Thu, 20 Feb 2014 15:17:07 -0800 Subject: [PATCH 042/422] Reimplement into without toList --- .../src/main/clojure/rx/lang/clojure/core.clj | 9 +++++---- .../src/test/clojure/rx/lang/clojure/core_test.clj | 11 ++++++++--- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj index 28ef283e30..2a61e0ed20 100644 --- a/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj +++ b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj @@ -554,10 +554,11 @@ clojure.core/into rx.Observable/toList " - [to ^Observable from-observable] - (->> from-observable - .toList - (map (partial clojure.core/into to)))) + [to ^Observable from] + ; clojure.core/into uses transients if to is IEditableCollection + ; I don't think we have any guarantee that all on-next calls will be on the + ; same thread, so we can't do that here. + (reduce conj to from)) (defn keep [f xs] diff --git a/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/core_test.clj b/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/core_test.clj index f9a47dd99b..accd20cfed 100644 --- a/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/core_test.clj +++ b/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/core_test.clj @@ -115,7 +115,7 @@ sleepy-o #(f/future-generator f/default-runner [o] (doseq [x %] (Thread/sleep 10) - (rx/on-next o x))) + (rx/on-next o x))) make-inputs (fn [] (mapv sleepy-o expected-result)) make-output (fn [r] [(keep #{1 3 5} r) (keep #{2 4 6} r)])] @@ -338,8 +338,13 @@ (b/into [] (rx/interpose \, (rx/seq->o [1 2 3])))))) (deftest test-into - (is (= (into [6 7 8] [9 10 [11]]) - (b/first (rx/into [6 7 8] (rx/seq->o [9 10 [11]])))))) + (are [input to] (= (into to input) + (b/single (rx/into to (rx/seq->o input)))) + [6 7 8] [9 10 [11]] + #{} [1 2 3 2 4 5] + {} [[1 2] [3 2] [4 5]] + {} [] + '() (range 50))) (deftest test-keep (is (= (into [] (keep identity [true true false])) From c51054ea4d335997f7f26aed77937d44c83f0a3d Mon Sep 17 00:00:00 2001 From: Dave Ray Date: Thu, 20 Feb 2014 15:35:36 -0800 Subject: [PATCH 043/422] Update generator docstring --- .../src/main/clojure/rx/lang/clojure/core.clj | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj index 2a61e0ed20..1ddf30b09b 100644 --- a/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj +++ b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj @@ -432,7 +432,7 @@ Example: (->> (rx/seq->o [1 2 3]) - (rx/do println) + (rx/do println) ...) Will print 1, 2, 3. @@ -884,8 +884,8 @@ ;################################################################################; (defn generator* - "Creates an observable that calls (f observable & args) which should emit values - with (rx/on-next observable value). + "Creates an observable that calls (f observer & args) which should emit values + with (rx/on-next observer value). Automatically calls on-completed on return, or on-error if any exception is thrown. @@ -902,8 +902,8 @@ wrap-on-error))) (defmacro generator - "Create an observable that executes body which should emit a sequence. bindings - should be a single [observer] argument. + "Create an observable that executes body which should emit values with + (rx/on-next observer value) where observer comes from bindings. Automatically calls on-completed on return, or on-error if any exception is thrown. From ea22e7ae8d00315234511125262a237b75245ce3 Mon Sep 17 00:00:00 2001 From: Dave Ray Date: Thu, 20 Feb 2014 17:21:09 -0800 Subject: [PATCH 044/422] Make catch and finally ->> friendly Base on @mbossenbroek's feedback. Seems like an improvement. --- .../src/main/clojure/rx/lang/clojure/core.clj | 75 +++++++++++-------- .../clojure/rx/lang/clojure/core_test.clj | 66 +++++++--------- .../clojure/rx/lang/clojure/future_test.clj | 25 ++++--- 3 files changed, 87 insertions(+), 79 deletions(-) diff --git a/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj index 1ddf30b09b..8865fa0b21 100644 --- a/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj +++ b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj @@ -798,7 +798,7 @@ (defn catch* "Returns an observable that, when Observable o triggers an error, e, continues with - Observable returned by (apply f e args) if (p e) is true. If (p e) returns a Throwable + Observable returned by (f e) if (p e) is true. If (p e) returns a Throwable that value is passed as e. If p is a class object, a normal instance? check is performed rather than calling it @@ -806,26 +806,30 @@ Examples: - (-> my-observable + (->> my-observable - ; On IllegalArgumentException, just emit 1 - (catch* IllegalArgumentException (fn [e] (rx/return 1))) + ; On IllegalArgumentException, just emit 1 + (catch* IllegalArgumentException + (fn [e] (rx/return 1))) - ; If exception message contains \"WAT\", emit [\\W \\A \\T] - (catch* #(-> % .getMessage (.contains \"WAT\")) (rx/seq->o [\\W \\A \\T]))) + ; If exception message contains \"WAT\", emit [\\W \\A \\T] + (catch* (fn [e] (-> e .getMessage (.contains \"WAT\"))) + (fn [e] (rx/seq->o [\\W \\A \\T])))) See: - + rx.Observable/onErrorResumeNext http://netflix.github.io/RxJava/javadoc/rx/Observable.html#onErrorResumeNext(rx.util.functions.Func1) " - [^Observable o p f & args] + [p f ^Observable o] (let [p (if (class? p) (fn [e] (.isInstance ^Class p e)) p)] (.onErrorResumeNext o ^Func1 (iop/fn [e] (if-let [maybe-e (p e)] - (apply f (if (instance? Throwable maybe-e) maybe-e e) args) + (f (if (instance? Throwable maybe-e) + maybe-e + e)) (rx.lang.clojure.core/throw e)))))) (defmacro catch @@ -833,53 +837,64 @@ The body of the catch is wrapped in an implicit (do). It must evaluate to an Observable. - Note that the source observable is the first argument so this won't mix well with ->> - threading. + Note that the source observable is the last argument so this works with ->> but may look + slightly odd when used standalone. Example: - (-> my-observable - ; just emit 0 on IllegalArgumentException - (catch IllegalArgumentException e - (rx/return 0)) + (->> my-observable + ; just emit 0 on IllegalArgumentException + (catch IllegalArgumentException e + (rx/return 0)) - (catch DependencyException e - (if (.isMinor e) - (rx/return 0) - (rx/throw (WebException. 503))))) + (catch DependencyException e + (if (.isMinor e) + (rx/return 0) + (rx/throw (WebException. 503))))) See: catch* " - [o p binding & body] - `(catch* ~o ~p (fn [~binding] ~@body))) + {:arglists '([p binding & body observable])} + [p binding & body] + (let [o (last body) + body (butlast body)] + `(catch* ~p + (fn [~binding] ~@body) + ~o))) (defn finally* - "Returns an Observable that, as a side-effect, executes (apply f args) when the given + "Returns an Observable that, as a side-effect, executes (f) when the given Observable completes regardless of success or failure. Example: - (-> my-observable - (finally* (fn [] (println \"Done\")))) + (->> my-observable + (finally* (fn [] (println \"Done\")))) " - [^Observable o f & args] - (.finallyDo o ^Action0 (iop/action [] (apply f args)))) + [f ^Observable o] + (.finallyDo o ^Action0 (iop/action* f))) (defmacro finally "Macro version of finally*. + Note that the source observable is the last argument so this works with ->> but may look + slightly odd when used standalone. + Example: - (-> my-observable - (finally (println \"Done\"))) + (->> my-observable + (finally (println \"Done\"))) See: finally* " - [o & body] - `(finally* ~o (fn [] ~@body))) + {:arglists '([& body observable])} + [& body] + (let [o (last body) + body (butlast body)] + `(finally* (fn [] ~@body) ~o))) ;################################################################################; diff --git a/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/core_test.clj b/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/core_test.clj index accd20cfed..89f1a4caf4 100644 --- a/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/core_test.clj +++ b/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/core_test.clj @@ -531,33 +531,30 @@ (deftest test-catch* (testing "Is just a passthrough if there's no error" (is (= [1 2 3] - (b/into [] - (-> - (rx/seq->o [1 2 3]) - (rx/catch* Exception (fn [e] (throw "OH NO")))))))) + (->> (rx/seq->o [1 2 3]) + (rx/catch* Exception (fn [e] (throw "OH NO"))) + (b/into []))))) (testing "Can catch a particular exception type and continue with an observable" (is (= [1 2 4 5 6 "foo"] - (b/into [] - (-> - (rx/generator [o] - (rx/on-next o 1) - (rx/on-next o 2) - (rx/on-error o (IllegalStateException. "foo"))) - (rx/catch* IllegalStateException - (fn [e] - (rx/seq->o [4 5 6 (.getMessage e)])))))))) + (->> (rx/generator [o] + (rx/on-next o 1) + (rx/on-next o 2) + (rx/on-error o (IllegalStateException. "foo"))) + (rx/catch* IllegalStateException + (fn [e] + (rx/seq->o [4 5 6 (.getMessage e)]))) + (b/into []))))) (testing "if exception isn't matched, it's passed to on-error" (let [expected (IllegalArgumentException. "HI") called (atom nil)] - (rx/subscribe (-> - (rx/generator [o] - (rx/on-next o 1) - (rx/on-next o 2) - (rx/on-error o expected)) - (rx/catch* IllegalStateException (fn [e] - (rx/return "WAT?")))) + (rx/subscribe (->> (rx/generator [o] + (rx/on-next o 1) + (rx/on-next o 2) + (rx/on-error o expected)) + (rx/catch* IllegalStateException (fn [e] + (rx/return "WAT?")))) (fn [_]) (fn [e] (reset! called expected)) (fn [_])) @@ -567,12 +564,10 @@ (let [cause (IllegalArgumentException. "HI") wrapper (java.util.concurrent.ExecutionException. cause)] (is (= [cause] - (b/into [] - (-> - (rx/generator [o] - (rx/on-error o wrapper)) - (rx/catch #(.getCause %) e - (rx/return e))))))))) + (->> (rx/throw wrapper) + (rx/catch #(.getCause %) e + (rx/return e)) + (b/into []))))))) (deftest test-finally @@ -580,10 +575,8 @@ (testing "called on completed" (let [completed (atom nil) called (atom nil)] - (rx/subscribe (-> - (rx/seq->o [1 2 3]) - (rx/finally* (fn [extra] (reset! called (str "got " extra))) - "it")) + (rx/subscribe (->> (rx/seq->o [1 2 3]) + (rx/finally* (fn [] (reset! called (str "got it"))))) (fn [_]) (fn [_] (throw (IllegalStateException. "WAT"))) (fn [] (reset! completed "DONE"))) @@ -594,13 +587,12 @@ (let [expected (IllegalStateException. "expected") completed (atom nil) called (atom nil)] - (rx/subscribe (-> - (rx/generator [o] - (rx/on-next o 1) - (rx/on-next o 2) - (rx/on-error o expected)) - (rx/finally - (reset! called "got it"))) + (rx/subscribe (->> (rx/generator [o] + (rx/on-next o 1) + (rx/on-next o 2) + (rx/on-error o expected)) + (rx/finally + (reset! called "got it"))) (fn [_]) (fn [e] (reset! completed e)) (fn [] (throw (IllegalStateException. "WAT")))) diff --git a/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/future_test.clj b/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/future_test.clj index 46682ddc44..ae2bf4c81f 100644 --- a/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/future_test.clj +++ b/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/future_test.clj @@ -17,10 +17,10 @@ (deftest test-future-exception (is (= "Caught: boo" - (-> (f/future f/default-runner (throw (java.io.FileNotFoundException. "boo"))) - (rx/catch java.io.FileNotFoundException e - (rx/return (str "Caught: " (.getMessage e)))) - (b/single))))) + (->> (f/future f/default-runner (throw (java.io.FileNotFoundException. "boo"))) + (rx/catch java.io.FileNotFoundException e + (rx/return (str "Caught: " (.getMessage e)))) + (b/single))))) (deftest test-future-cancel (let [exited? (atom nil) @@ -52,11 +52,12 @@ (deftest test-future-generator-exception (let [e (java.io.FileNotFoundException. "snake")] (is (= [1 2 e] - (b/into [] (-> (f/future-generator - f/default-runner - [o] - (rx/on-next o 1) - (rx/on-next o 2) - (throw e)) - (rx/catch java.io.FileNotFoundException e - (rx/return e)))))))) + (->> (f/future-generator + f/default-runner + [o] + (rx/on-next o 1) + (rx/on-next o 2) + (throw e)) + (rx/catch java.io.FileNotFoundException e + (rx/return e)) + (b/into [])))))) From ee45b4494123849b9a18738c9d8b81c145464ee9 Mon Sep 17 00:00:00 2001 From: Dave Ray Date: Thu, 20 Feb 2014 20:44:38 -0800 Subject: [PATCH 045/422] Eliminate sorted-list variants and cleanup sort tests --- .../src/main/clojure/rx/lang/clojure/core.clj | 41 ++------------- .../clojure/rx/lang/clojure/core_test.clj | 52 ++++++++++--------- 2 files changed, 32 insertions(+), 61 deletions(-) diff --git a/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj index 8865fa0b21..5f007abbe2 100644 --- a/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj +++ b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj @@ -681,36 +681,7 @@ (filter identity) first)) -(defn sorted-list - "Returns an observable that emits a *single value* which is a sorted List - of the items in coll, where the sort order is determined by comparing - items. If no comparator is supplied, uses compare. comparator must - implement java.util.Comparator. - - Use sort if you don't want the sequence squashed down to a List. - - See: - rx.Observable/toSortedList - sort - " - ([coll] (sorted-list clojure.core/compare coll)) - ([comp ^Observable coll] - (.toSortedList coll (iop/fn [a b] - ; force to int so rxjava doesn't have a fit - (int (comp a b)))))) - -(defn sorted-list-by - "Returns an observable that emits a *single value* which is a sorted List - of the items in coll, where the sort order is determined by comparing - (keyfn item). If no comparator is supplied, uses compare. comparator must - implement java.util.Comparator. - - Use sort-by if you don't want the sequence squashed down to a List. - - See: - rx.Observable/toSortedList - sort-by - " +(defn ^:private sorted-list-by ([keyfn coll] (sorted-list-by keyfn clojure.core/compare coll)) ([keyfn comp ^Observable coll] (.toSortedList coll (iop/fn [a b] @@ -723,16 +694,13 @@ comparator must implement java.util.Comparator. See: - sorted-list clojure.core/sort " ([xs] - (->> xs - (sorted-list) - (mapcat seq->o))) + (sort clojure.core/compare xs)) ([comp xs] (->> xs - (sorted-list comp) + (sorted-list-by identity comp) (mapcat seq->o)))) (defn sort-by @@ -744,8 +712,7 @@ clojure.core/sort-by " ([keyfn xs] - (->> (sorted-list-by keyfn xs) - (mapcat seq->o))) + (sort-by keyfn clojure.core/compare xs)) ([keyfn comp ^Observable xs] (->> xs (sorted-list-by keyfn comp) diff --git a/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/core_test.clj b/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/core_test.clj index 89f1a4caf4..f0c3242ce5 100644 --- a/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/core_test.clj +++ b/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/core_test.clj @@ -480,33 +480,37 @@ (is (= [:r] (b/into [] (rx/some #{:r :s :t} (rx/seq->o [:q :v :r]))))) (is (= [] (b/into [] (rx/some #{:r :s :t} (rx/seq->o [:q :v])))))) -(deftest test-sorted-list - (is (= [[]] (b/into [] (rx/sorted-list (rx/empty))))) - (is (= [[1 2 3]] - (b/into [] (rx/sorted-list (rx/seq->o [3 1 2]))))) - (is (= [[3 2 1]] - (b/into [] (rx/sorted-list (fn [a b] (- (compare a b))) (rx/seq->o [2 1 3])))))) - -(deftest test-sorted-list-by - (is (= [[]] (b/into [] (rx/sorted-list-by :foo (rx/empty))))) - (is (= [[{:foo 1} {:foo 2} {:foo 3}]] - (b/into [] (rx/sorted-list-by :foo (rx/seq->o [{:foo 2}{:foo 1}{:foo 3}]))))) - (is (= [[{:foo 3} {:foo 2} {:foo 1}]] - (b/into [] (rx/sorted-list-by :foo (fn [a b] (- (compare a b))) (rx/seq->o [{:foo 2}{:foo 1}{:foo 3}])))))) - (deftest test-sort - (is (= [] (b/into [] (rx/sort (rx/empty))))) - (is (= [1 2 3] - (b/into [] (rx/sort (rx/seq->o [3 1 2]))))) - (is (= [3 2 1] - (b/into [] (rx/sort (fn [a b] (- (compare a b))) (rx/seq->o [2 1 3])))))) + (are [in cmp] (= (if cmp + (sort cmp in) + (sort in)) + (->> in + (rx/seq->o) + (#(if cmp (rx/sort cmp %) (rx/sort %))) + (b/into []))) + [] nil + [] (comp - compare) + [3 1 2] nil + [1 2 3] nil + [1 2 3] (comp - compare) + [2 1 3] (comp - compare))) (deftest test-sort-by - (is (= [] (b/into [] (rx/sort-by :foo (rx/empty))))) - (is (= [{:foo 1} {:foo 2} {:foo 3}] - (b/into [] (rx/sort-by :foo (rx/seq->o [{:foo 2}{:foo 1}{:foo 3}]))))) - (is (= [{:foo 3} {:foo 2} {:foo 1}] - (b/into [] (rx/sort-by :foo (fn [a b] (- (compare a b))) (rx/seq->o [{:foo 2}{:foo 1}{:foo 3}])))))) + (are [rin cmp] (let [in (map #(hash-map :foo %) rin)] + (= (if cmp + (sort-by :foo cmp in) + (sort-by :foo in)) + (->> in + (rx/seq->o) + (#(if cmp (rx/sort-by :foo cmp %) (rx/sort-by :foo %))) + (b/into [])))) + [] nil + [] (comp - compare) + [3 1 2] nil + [1 2 3] nil + [1 2 3] (comp - compare) + [2 1 3] (comp - compare))) + (deftest test-split-with (is (= (split-with (partial >= 3) (range 6)) From 9269d4e9e00db7c2edb68d471b47dad76b3da827 Mon Sep 17 00:00:00 2001 From: Dave Ray Date: Thu, 20 Feb 2014 21:26:18 -0800 Subject: [PATCH 046/422] Eliminated macro version of future stuff based on feedback. --- .../main/clojure/rx/lang/clojure/future.clj | 97 ++++++------------- .../clojure/rx/lang/clojure/chunk_test.clj | 55 ++++++----- .../clojure/rx/lang/clojure/core_test.clj | 10 +- .../clojure/rx/lang/clojure/future_test.clj | 40 ++++---- .../clojure/rx/lang/clojure/graph_test.clj | 44 +++++---- 5 files changed, 113 insertions(+), 133 deletions(-) diff --git a/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/future.clj b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/future.clj index 0c63be36e8..83b56b27b6 100644 --- a/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/future.clj +++ b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/future.clj @@ -2,20 +2,17 @@ "Functions and macros for making rx-ified futures. That is, run some code in some other thread and return an Observable of its result. " - (:refer-clojure :exclude [future]) (:require [rx.lang.clojure.interop :as iop] [rx.lang.clojure.core :as rx])) (def ^:private -ns- *ns*) (set! *warn-on-reflection* true) -(defn default-runner - "Default runner creator function. Creates futures on Clojure's default future thread pool." - [f] - (future-call f)) +(defn future* + "Exerimental/Possibly a bad idea -(defn future-generator* - "Same as rx/generator* except f is invoked in a separate thread. + Execute (f & args) in a separate thread and pass the result to onNext. + If an exception is thrown, onError is called with the exception. runner is a function that takes a no-arg function argument and returns a future representing the execution of that function. @@ -23,23 +20,27 @@ Returns an Observable. If the subscriber unsubscribes, the future will be canceled with clojure.core/future-cancel - See: - rx.lang.clojure.core/generator* - rx.lang.clojure.future/future-generator + Examples: + + (subscribe (rx/future future-call + #(slurp \"input.txt\")) + (fn [v] (println \"Got: \" v))) + ; eventually outputs content of input.txt " [runner f & args] {:pre [(ifn? runner) (ifn? f)]} (rx/observable* (fn [^rx.Subscriber observer] - (let [wrapped (-> (fn [o] - (apply f o args)) - rx/wrap-on-completed - rx/wrap-on-error) - fu (runner #(wrapped observer))] - (.add observer - (rx/subscription #(future-cancel fu))))))) + (let [wrapped (-> #(rx/on-next % (apply f args)) + rx/wrap-on-completed + rx/wrap-on-error) + fu (runner #(wrapped observer))] + (.add observer + (rx/subscription #(future-cancel fu))))))) -(defmacro future-generator - "Same as rx/generator macro except body is invoked in a separate thread. +(defn future-generator* + "Exerimental/Possibly a bad idea + + Same as rx/generator* except f is invoked in a separate thread. runner is a function that takes a no-arg function argument and returns a future representing the execution of that function. @@ -49,56 +50,22 @@ Example: - (future-generator default-runner - [o] - (rx/on-next o 1) - (Thread/sleep 1000) - (rx/on-next o 2)) + (future-generator* future-call + (fn [o] + (rx/on-next o 1) + (Thread/sleep 1000) + (rx/on-next o 2))) See: rx.lang.clojure.core/generator* - rx.lang.clojure.future/future-generator - " - [runner bindings & body] - `(future-generator* ~runner (fn ~bindings ~@body))) - -(defn future* - "Execute (f & args) in a separate thread and pass the result to onNext. - If an exception is thrown, onError is called with the exception. - - runner is a function that takes a no-arg function argument and returns a future - representing the execution of that function. - - Returns an Observable. If the subscriber unsubscribes, the future will be canceled - with clojure.core/future-cancel " [runner f & args] {:pre [(ifn? runner) (ifn? f)]} (rx/observable* (fn [^rx.Subscriber observer] - (let [wrapped (-> #(rx/on-next % (apply f args)) - rx/wrap-on-completed - rx/wrap-on-error) - fu (runner #(wrapped observer))] - (.add observer - (rx/subscription #(future-cancel fu))))))) - -(defmacro future - "Executes body in a separate thread and passes the single result to onNext. - If an exception occurs, onError is called. - - Returns an Observable. If the subscriber unsubscribes, the future will be canceled - with clojure.core/future-cancel - - runner is a function that takes a no-arg function argument and returns a future - representing the execution of that function. - - Examples: - - (subscribe (rx/future rx/default-runner - (slurp \"input.txt\")) - (fn [v] (println \"Got: \" v))) - ; eventually outputs content of input.txt - " - [runner & body] - `(future* ~runner (fn [] ~@body))) - + (let [wrapped (-> (fn [o] + (apply f o args)) + rx/wrap-on-completed + rx/wrap-on-error) + fu (runner #(wrapped observer))] + (.add observer + (rx/subscription #(future-cancel fu))))))) diff --git a/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/chunk_test.clj b/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/chunk_test.clj index c0d8974e8b..58ef044c9d 100644 --- a/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/chunk_test.clj +++ b/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/chunk_test.clj @@ -9,13 +9,16 @@ (deftest test-chunk (let [n 20 chunk-size 10 - factory (rx-future/future-generator rx-future/default-runner [o] - (doseq [i (range n)] - (Thread/sleep (rand-int 50)) - (rx/on-next o (rx-future/future rx-future/default-runner - (let [t (rand-int 500)] - (Thread/sleep t)) - i))))] + factory (rx-future/future-generator* + future-call + (fn[o] + (doseq [i (range n)] + (Thread/sleep (rand-int 50)) + (rx/on-next o (rx-future/future* + future-call + #(let [t (rand-int 500)] + (Thread/sleep t) + i))))))] (is (= (range n) (sort (rx-blocking/into [] (rx-chunk/chunk chunk-size {:debug true} factory))))))) @@ -24,14 +27,17 @@ (testing "error from source is propagated" (let [n 20 chunk-size 4 - factory (rx-future/future-generator rx-future/default-runner [o] - (doseq [i (range n)] - (Thread/sleep (rand-int 50)) - (rx/on-next o (rx-future/future rx-future/default-runner - (let [t (rand-int 1000)] - (Thread/sleep t)) - i))) - (throw (IllegalArgumentException. "hi")))] + factory (rx-future/future-generator* + future-call + (fn [o] + (doseq [i (range n)] + (Thread/sleep (rand-int 50)) + (rx/on-next o (rx-future/future* + future-call + #(let [t (rand-int 1000)] + (Thread/sleep t) + i)))) + (throw (IllegalArgumentException. "hi"))))] (is (thrown-with-msg? IllegalArgumentException #"hi" (rx-blocking/into [] (rx-chunk/chunk chunk-size {:debug true} factory)))))) @@ -39,14 +45,17 @@ (testing "error from single observable is propagated" (let [n 20 chunk-size 4 - factory (rx-future/future-generator rx-future/default-runner [o] - (doseq [i (range n)] - (Thread/sleep (rand-int 50)) - (rx/on-next o (rx-future/future rx-future/default-runner - (let [t (rand-int 1000)] - (throw (IllegalArgumentException. "byebye")) - (Thread/sleep t)) - i))))] + factory (rx-future/future-generator* + future-call + (fn [o] + (doseq [i (range n)] + (Thread/sleep (rand-int 50)) + (rx/on-next o (rx-future/future* + future-call + #(let [t (rand-int 1000)] + (throw (IllegalArgumentException. "byebye")) + (Thread/sleep t) + i))))))] (is (thrown? rx.exceptions.CompositeException (rx-blocking/into [] (rx-chunk/chunk chunk-size diff --git a/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/core_test.clj b/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/core_test.clj index f0c3242ce5..0ec806c6bd 100644 --- a/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/core_test.clj +++ b/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/core_test.clj @@ -112,10 +112,12 @@ (b/into [])))))) (let [expected-result [[1 3 5] [2 4 6]] - sleepy-o #(f/future-generator f/default-runner [o] - (doseq [x %] - (Thread/sleep 10) - (rx/on-next o x))) + sleepy-o #(f/future-generator* + future-call + (fn [o] + (doseq [x %] + (Thread/sleep 10) + (rx/on-next o x)))) make-inputs (fn [] (mapv sleepy-o expected-result)) make-output (fn [r] [(keep #{1 3 5} r) (keep #{2 4 6} r)])] diff --git a/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/future_test.clj b/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/future_test.clj index ae2bf4c81f..ba2344e4e2 100644 --- a/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/future_test.clj +++ b/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/future_test.clj @@ -7,27 +7,25 @@ (deftest test-future-generator (is (not= [(.getId (Thread/currentThread))] (b/into [] - (f/future-generator f/default-runner - [observer] - (rx/on-next observer (.getId (Thread/currentThread)))))))) + (f/future-generator* future-call + #(rx/on-next % (.getId (Thread/currentThread)))))))) (deftest test-future - (is (= [15] (b/into [] (f/future* f/default-runner + 1 2 3 4 5)))) - (is (= [15] (b/into [] (f/future f/default-runner (println "HI") (+ 1 2 3 4 5))))) ) + (is (= [15] (b/into [] (f/future* future-call + 1 2 3 4 5))))) (deftest test-future-exception (is (= "Caught: boo" - (->> (f/future f/default-runner (throw (java.io.FileNotFoundException. "boo"))) + (->> (f/future* future-call #(throw (java.io.FileNotFoundException. "boo"))) (rx/catch java.io.FileNotFoundException e (rx/return (str "Caught: " (.getMessage e)))) (b/single))))) (deftest test-future-cancel (let [exited? (atom nil) - o (f/future f/default-runner - (Thread/sleep 1000) - (reset! exited? true) - "WAT") + o (f/future* future-call + (fn [] (Thread/sleep 1000) + (reset! exited? true) + "WAT")) result (->> o (rx/take 0) (b/into []))] @@ -37,11 +35,11 @@ (deftest test-future-generator-cancel (let [exited? (atom nil) - o (f/future-generator f/default-runner - [o] - (rx/on-next o "FIRST") - (Thread/sleep 1000) - (reset! exited? true)) + o (f/future-generator* future-call + (fn [o] + (rx/on-next o "FIRST") + (Thread/sleep 1000) + (reset! exited? true))) result (->> o (rx/take 1) (b/into []))] @@ -52,12 +50,12 @@ (deftest test-future-generator-exception (let [e (java.io.FileNotFoundException. "snake")] (is (= [1 2 e] - (->> (f/future-generator - f/default-runner - [o] - (rx/on-next o 1) - (rx/on-next o 2) - (throw e)) + (->> (f/future-generator* + future-call + (fn [o] + (rx/on-next o 1) + (rx/on-next o 2) + (throw e))) (rx/catch java.io.FileNotFoundException e (rx/return e)) (b/into [])))))) diff --git a/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/graph_test.clj b/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/graph_test.clj index cd3cf59788..56ddfc9ff3 100644 --- a/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/graph_test.clj +++ b/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/graph_test.clj @@ -43,8 +43,8 @@ (rx-blocking/single (-> (let [z (rx/return "hi")] ; an observable from "somewhere else" (graph/let-o - [?a (rx-future/future rx-future/default-runner (Thread/sleep 50) 99) - ?b (rx-future/future rx-future/default-runner (Thread/sleep 500) 100) + [?a (rx-future/future* future-call #(do (Thread/sleep 50) 99)) + ?b (rx-future/future* future-call #(do (Thread/sleep 500) 100)) ?c (rx/map #(hash-map :a %1 :b %2 :z %3) ?a ?b ?z) ?z z] (rx/reduce merge {} ?c))))))))) @@ -52,21 +52,24 @@ (deftest test-complicated-graph ; These funcs model network requests for various stuff. They all return observable. (let [request-vhs (fn [] - (rx-future/future-generator rx-future/default-runner - [o] - (Thread/sleep 50) - (doseq [i (range 3)] - (rx/on-next o {:id i})))) + (rx-future/future-generator* + future-call + (fn [o] + (Thread/sleep 50) + (doseq [i (range 3)] + (rx/on-next o {:id i}))))) request-user (fn [id] - (rx-future/future rx-future/default-runner - (Thread/sleep (rand-int 250)) - {:id id - :name (str "friend" id) })) + (rx-future/future* + future-call + #(do (Thread/sleep (rand-int 250)) + {:id id + :name (str "friend" id) }))) request-ab (fn [u] - (rx-future/future rx-future/default-runner - (Thread/sleep (rand-int 250)) - {:user-id (:id u) - :cell (* 2 (:id u))})) + (rx-future/future* + future-call + #(do (Thread/sleep (rand-int 250)) + {:user-id (:id u) + :cell (* 2 (:id u))}))) request-video-md (fn [v] (rx/return {:video v @@ -74,11 +77,12 @@ ; Now we can stitch all these requests together into an rx graph to ; produce a response. - o (graph/let-o [?user-info (rx-future/future rx-future/default-runner - (Thread/sleep 20) - {:name "Bob" - :id 12345 - :friend-ids [1 2 3] }) + o (graph/let-o [?user-info (rx-future/future* + future-call + #(do (Thread/sleep 20) + {:name "Bob" + :id 12345 + :friend-ids [1 2 3] })) ?friends (->> ?user-info (rx/mapcat (fn [ui] From 9bc5a8578b52f6865ca0dcd038f348e34bc3d1f2 Mon Sep 17 00:00:00 2001 From: Dave Ray Date: Thu, 20 Feb 2014 23:20:31 -0800 Subject: [PATCH 047/422] Implemented multi-sequence mapcat --- .../src/main/clojure/rx/lang/clojure/core.clj | 21 +++++++++++++++++- .../clojure/rx/lang/clojure/core_test.clj | 22 ++++++++++++++++++- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj index 5f007abbe2..93926a786c 100644 --- a/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj +++ b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj @@ -597,16 +597,35 @@ (Observable/zip ^Iterable observables ^rx.functions.FuncN (iop/fnN* f))) +(defn ^Observable mapcat* + "Same as multi-arg mapcat, but input is an Observable of Observables. + + See: + mapcat + clojure.core/mapcat + " + [f ^Observable xs] + (->> xs + (map* f) + (concat*))) + (defn ^Observable mapcat "Returns an observable which, for each value x in xs, calls (f x), which must return an Observable. The resulting observables are concatentated together into one observable. + If multiple Observables are given, the arguments to f are the first item from + each observable, then the second item, etc. + See: clojure.core/mapcat rx.Observable/flatMap " - ([f ^Observable xs] (.flatMap xs (iop/fn* f)))) + [f & xs] + (if (clojure.core/next xs) + (mapcat* f (seq->o xs)) + ; use built-in flatMap for single-arg case + (.flatMap ^Observable (clojure.core/first xs) (iop/fn* f)))) (defn map-indexed "Returns an observable that invokes (f index value) for each value of the input diff --git a/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/core_test.clj b/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/core_test.clj index 0ec806c6bd..9dc7a98a3e 100644 --- a/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/core_test.clj +++ b/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/core_test.clj @@ -423,11 +423,31 @@ (catch java.io.FileNotFoundException e (is (= :b (-> e .getCause .getValue))))))) +(deftest test-mapcat* + (let [f (fn [a b c d e] + [(+ a b) (+ c d) e])] + (is (= (->> (range 5) + (map (fn [_] (range 5))) + (apply mapcat f)) + (->> (range 5) + (map (fn [_] (rx/seq->o (range 5)))) + (rx/seq->o) + (rx/mapcat* (fn [& args] (rx/seq->o (apply f args)))) + (b/into [])))))) + (deftest test-mapcat (let [f (fn [v] [v (* v v)]) xs (range 10)] (is (= (mapcat f xs) - (b/into [] (rx/mapcat (comp rx/seq->o f) (rx/seq->o xs))))))) + (b/into [] (rx/mapcat (comp rx/seq->o f) (rx/seq->o xs)))))) + + (let [f (fn [a b] [a b (* a b)]) + as (range 10) + bs (range 15)] + (is (= (mapcat f as bs) + (b/into [] (rx/mapcat (comp rx/seq->o f) + (rx/seq->o as) + (rx/seq->o bs))))))) (deftest test-next (let [in [:q :r :s :t :u]] From ea2fd75386fd034494be029bcd0cda9442c144ab Mon Sep 17 00:00:00 2001 From: Dave Ray Date: Fri, 21 Feb 2014 12:21:11 -0800 Subject: [PATCH 048/422] Remove val-fn version of group-by --- .../src/main/clojure/rx/lang/clojure/core.clj | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj index 93926a786c..7850d44501 100644 --- a/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj +++ b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj @@ -478,8 +478,7 @@ (defn ^Observable group-by "Returns an Observable of clojure.lang.MapEntry where the key is the result of - (key-fn x) and the val is an Observable of (val-fn x) for each key. If val-fn is - omitted, it defaults to identity. + (key-fn x) and the val is an Observable of x for each key. This returns a clojure.lang.MapEntry rather than rx.observables.GroupedObservable for some vague consistency with clojure.core/group-by and so that clojure.core/key, @@ -492,15 +491,6 @@ " ([key-fn ^Observable xs] (->> (.groupBy xs (iop/fn* key-fn)) - (map (fn [^GroupedObservable go] - (clojure.lang.MapEntry. (.getKey go) go))))) - ([key-fn val-fn ^Observable xs] - ; TODO reinstate once this is implemented - ; see https://github.com/Netflix/RxJava/commit/02ccc4d727a9297f14219549208757c6e0efce2a - (throw (UnsupportedOperationException. "groupBy with val-fn is currently unimplemented in RxJava")) - (->> (.groupBy xs - (iop/fn* key-fn) - (iop/fn* val-fn)) (map (fn [^GroupedObservable go] (clojure.lang.MapEntry. (.getKey go) go)))))) From 6bc098de7871abb7a7dd041d6a90ff088d68cc44 Mon Sep 17 00:00:00 2001 From: Dave Ray Date: Fri, 21 Feb 2014 13:41:03 -0800 Subject: [PATCH 049/422] Implement iterate --- .../src/main/clojure/rx/lang/clojure/core.clj | 15 ++++++++++++++- .../test/clojure/rx/lang/clojure/core_test.clj | 8 ++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj index 7850d44501..39e8cd1be1 100644 --- a/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj +++ b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj @@ -4,7 +4,7 @@ empty every? filter first future group-by - interleave interpose into + interleave interpose into iterate keep keep-indexed map mapcat map-indexed merge next nth partition-all reduce reductions @@ -550,6 +550,19 @@ ; same thread, so we can't do that here. (reduce conj to from)) +(defn iterate + "Returns an Observable of x, (f x), (f (f x)) etc. f must be free of side-effects + + See: + clojure.core/iterate + " + [f x] + (observable* (fn [s] + (loop [x x] + (when-not (unsubscribed? s) + (on-next s x) + (recur (f x))))))) + (defn keep [f xs] (filter (complement nil?) (map f xs))) diff --git a/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/core_test.clj b/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/core_test.clj index 9dc7a98a3e..ef4f7b458a 100644 --- a/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/core_test.clj +++ b/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/core_test.clj @@ -348,6 +348,14 @@ {} [] '() (range 50))) +(deftest test-iterate + (are [f x n] (= (->> (iterate f x) (take n)) + (->> (rx/iterate f x) (rx/take n) (b/into []))) + inc 0 10 + dec 20 100 + #(conj % (count %)) [] 5 + #(cons (count %) % ) nil 5)) + (deftest test-keep (is (= (into [] (keep identity [true true false])) (b/into [] (rx/keep identity (rx/seq->o [true true false]))))) From 7919c0451d4d3eb1dcec6055d1e5cd0105bc6fbe Mon Sep 17 00:00:00 2001 From: Dave Ray Date: Fri, 21 Feb 2014 14:22:09 -0800 Subject: [PATCH 050/422] Implement range --- .../src/main/clojure/rx/lang/clojure/core.clj | 27 ++++++++++++++++++- .../clojure/rx/lang/clojure/core_test.clj | 22 +++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj index 39e8cd1be1..39a164e6e9 100644 --- a/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj +++ b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj @@ -7,7 +7,8 @@ interleave interpose into iterate keep keep-indexed map mapcat map-indexed - merge next nth partition-all reduce reductions + merge next nth partition-all + range reduce reductions rest seq some sort sort-by split-with take take-while throw]) (:require [rx.lang.clojure.interop :as iop] @@ -678,6 +679,30 @@ ([n ^Observable xs] (.window xs (int n))) ([n step ^Observable xs] (.window xs (int n) (int step)))) +(defn range + "Returns an Observable nums from start (inclusive) to end + (exclusive), by step, where start defaults to 0, step to 1, and end + to infinity. + + Note: this is not implemented on rx.Observable/range + + See: + clojure.core/range + " + ([] (range 0 Double/POSITIVE_INFINITY 1)) + ([end] (range 0 end 1)) + ([start end] (range start end 1)) + ([start end step] + (observable* (fn [s] + (let [comp (if (pos? step) < >)] + (loop [i start] + (if-not (unsubscribed? s) + (if (comp i end) + (do + (on-next s i) + (recur (+ i step))) + (on-completed s))))))))) + (defn ^Observable reduce ([f ^Observable xs] (.reduce xs (iop/fn* f))) ([f val ^Observable xs] (.reduce xs val (iop/fn* f)))) diff --git a/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/core_test.clj b/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/core_test.clj index ef4f7b458a..e645db653c 100644 --- a/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/core_test.clj +++ b/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/core_test.clj @@ -498,6 +498,28 @@ 10 3 15 30)) +(deftest test-range + (are [start end step] (= (range start end step) + (->> (rx/range start end step) (b/into []))) + 0 10 2 + 0 -100 -1 + 5 100 9) + + (are [start end] (= (range start end) + (->> (rx/range start end) (b/into []))) + 0 10 + 0 -100 + 5 100) + + (are [start] (= (->> (range start) (take 100)) + (->> (rx/range start) (rx/take 100) (b/into []))) + 50 + 0 + 5 + -20) + (is (= (->> (range) (take 500)) + (->> (rx/range) (rx/take 500) (b/into []))))) + (deftest test-reduce (is (= (reduce + 0 (range 4)) (b/first (rx/reduce + 0 (rx/seq->o (range 4))))))) From d19bc4263165984e9354f01e119bbc6cfab84750 Mon Sep 17 00:00:00 2001 From: Dave Ray Date: Fri, 21 Feb 2014 16:54:07 -0800 Subject: [PATCH 051/422] seq->o should seq-ify arg --- .../rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj | 4 ++-- .../src/test/clojure/rx/lang/clojure/core_test.clj | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj index 39a164e6e9..d2768d1b37 100644 --- a/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj +++ b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj @@ -285,8 +285,8 @@ (defn ^Observable seq->o "Make an observable out of some seq-able thing. The rx equivalent of clojure.core/seq." [xs] - (if xs - (Observable/from ^Iterable xs) + (if-let [s (clojure.core/seq xs)] + (Observable/from ^Iterable s) (empty))) ;################################################################################ diff --git a/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/core_test.clj b/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/core_test.clj index e645db653c..d77b184bd8 100644 --- a/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/core_test.clj +++ b/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/core_test.clj @@ -166,6 +166,7 @@ (deftest test-seq->o (is (= [] (b/into [] (rx/seq->o [])))) (is (= [] (b/into [] (rx/seq->o nil)))) + (is (= [\a \b \c] (b/into [] (rx/seq->o "abc")))) (is (= [0 1 2 3] (b/first (rx/into [] (rx/seq->o (range 4)))))) (is (= #{0 1 2 3} (b/first (rx/into #{} (rx/seq->o (range 4)))))) (is (= {:a 1 :b 2 :c 3} (b/first (rx/into {} (rx/seq->o [[:a 1] [:b 2] [:c 3]])))))) From fefd35ba85905ab978f07880ed96f3c59e270e6c Mon Sep 17 00:00:00 2001 From: Bob T Builder Date: Tue, 25 Feb 2014 08:03:13 +0000 Subject: [PATCH 052/422] [Gradle Release Plugin] - pre tag commit: '0.17.0-RC5'. --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 2f793be8d8..fc88554485 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -version=0.17.0-RC5-SNAPSHOT +version=0.17.0-RC5 From 4a93b4aadcb9119104b231247e827d4703b14135 Mon Sep 17 00:00:00 2001 From: Bob T Builder Date: Tue, 25 Feb 2014 08:03:17 +0000 Subject: [PATCH 053/422] [Gradle Release Plugin] - new version commit: '0.17.0-RC6-SNAPSHOT'. --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index fc88554485..b78e3f0dae 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -version=0.17.0-RC5 +version=0.17.0-RC6-SNAPSHOT From 7cf40da0ac74c6f1f442c87e3e46fbda9eaac8b6 Mon Sep 17 00:00:00 2001 From: pron Date: Tue, 25 Feb 2014 23:06:48 +0200 Subject: [PATCH 054/422] Added BlockingObservable --- rxjava-contrib/rxjava-quasar/README.md | 1 + .../java/rx/quasar/BlockingObservable.java | 612 ++++++++++++++++++ .../java/rx/quasar/ChannelObservable.java | 2 +- .../rx/quasar/BlockingObservableTest.java | 382 +++++++++++ 4 files changed, 996 insertions(+), 1 deletion(-) create mode 100644 rxjava-contrib/rxjava-quasar/src/main/java/rx/quasar/BlockingObservable.java create mode 100644 rxjava-contrib/rxjava-quasar/src/test/java/rx/quasar/BlockingObservableTest.java diff --git a/rxjava-contrib/rxjava-quasar/README.md b/rxjava-contrib/rxjava-quasar/README.md index 5c67aaee2b..a89ab6eb9c 100644 --- a/rxjava-contrib/rxjava-quasar/README.md +++ b/rxjava-contrib/rxjava-quasar/README.md @@ -7,6 +7,7 @@ Main Classes: - [NewFiberScheduler](https://github.com/Netflix/RxJava/blob/master/rxjava-contrib/rxjava-quasar/src/main/java/rx/quasar/NewFiberScheduler.java) - [ChannelObservable](https://github.com/Netflix/RxJava/blob/master/rxjava-contrib/rxjava-quasar/src/main/java/rx/quasar/ChannelObservable.java) +- [BlockingObservable](https://github.com/Netflix/RxJava/blob/master/rxjava-contrib/rxjava-quasar/src/main/java/rx/quasar/BlockingObservable.java) # Binaries diff --git a/rxjava-contrib/rxjava-quasar/src/main/java/rx/quasar/BlockingObservable.java b/rxjava-contrib/rxjava-quasar/src/main/java/rx/quasar/BlockingObservable.java new file mode 100644 index 0000000000..a161e184d8 --- /dev/null +++ b/rxjava-contrib/rxjava-quasar/src/main/java/rx/quasar/BlockingObservable.java @@ -0,0 +1,612 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package rx.quasar; + +import co.paralleluniverse.fibers.SuspendExecution; +import co.paralleluniverse.fibers.Suspendable; +import co.paralleluniverse.strands.AbstractFuture; +import co.paralleluniverse.strands.ConditionSynchronizer; +import co.paralleluniverse.strands.SimpleConditionSynchronizer; +import co.paralleluniverse.strands.Strand; +import co.paralleluniverse.strands.channels.Channels; +import co.paralleluniverse.strands.channels.DelegatingReceivePort; +import co.paralleluniverse.strands.channels.ProducerException; +import co.paralleluniverse.strands.channels.ReceivePort; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +import rx.Observable; +import rx.Observer; +import rx.Subscriber; +import rx.Subscription; +import rx.observers.SafeSubscriber; +import rx.util.Exceptions; +import rx.util.functions.Action1; +import rx.util.functions.Func1; + +/** + * An extension of {@link Observable} that provides blocking operators, compatible with both threads and fibers. + *

+ * You construct a BlockingObservable from an + * Observable with {@link #from(Observable)}. + *

+ * The documentation for this interface makes use of a form of marble diagram + * that has been modified to illustrate blocking operators. The following legend + * explains these marble diagrams: + *

+ * + *

+ * For more information see the + * Blocking + * Observable Operators page at the RxJava Wiki. + * + * @param + */ +public class BlockingObservable { + private static final int BUFFER_SIZE = 10; + private final Observable o; + + private BlockingObservable(Observable o) { + this.o = o; + } + + /** + * Convert an Observable into a BlockingObservable. + */ + public static BlockingObservable from(final Observable o) { + return new BlockingObservable(o); + } + + /** + * Used for protecting against errors being thrown from {@link Subscriber} implementations and ensuring onNext/onError/onCompleted contract + * compliance. + *

+ * See https://github.com/Netflix/RxJava/issues/216 for discussion on + * "Guideline 6.4: Protect calls to user code from within an operator" + */ + private Subscription protectivelyWrapAndSubscribe(Subscriber observer) { + return o.subscribe(new SafeSubscriber(observer)); + } + + /** + * Invoke a method on each item emitted by the {@link Observable}; block + * until the Observable completes. + *

+ * NOTE: This will block even if the Observable is asynchronous. + *

+ * This is similar to {@link Observable#subscribe(Subscriber)}, but it blocks. + * Because it blocks it does not need the {@link Subscriber#onCompleted()} or {@link Subscriber#onError(Throwable)} methods. + *

+ * + * + * @param onNext + * the {@link Action1} to invoke for every item emitted by the {@link Observable} + * @throws RuntimeException + * if an error occurs + * @see RxJava Wiki: forEach() + */ + @Suspendable + public void forEach(final Action1 onNext) { + try { + final AtomicBoolean done = new AtomicBoolean(false); + final ConditionSynchronizer sync = new SimpleConditionSynchronizer(this); + final AtomicReference exceptionFromOnError = new AtomicReference(); + + /** + * Wrapping since raw functions provided by the user are being invoked. + * + * See https://github.com/Netflix/RxJava/issues/216 for discussion on + * "Guideline 6.4: Protect calls to user code from within an operator" + */ + protectivelyWrapAndSubscribe(new Subscriber() { + @Override + public void onCompleted() { + done.set(true); + sync.signalAll(); + } + + @Override + public void onError(Throwable e) { + /* + * If we receive an onError event we set the reference on the + * outer thread so we can git it and throw after the + * latch.await(). + * + * We do this instead of throwing directly since this may be on + * a different thread and the latch is still waiting. + */ + exceptionFromOnError.set(e); + done.set(true); + sync.signalAll(); + } + + @Override + public void onNext(T args) { + onNext.call(args); + } + }); + // block until the subscription completes and then return + try { + final Object token = sync.register(); + try { + for (int i = 0; !done.get(); i++) + sync.await(i); + } finally { + sync.unregister(token); + } + } catch (InterruptedException e) { + // set the interrupted flag again so callers can still get it + // for more information see https://github.com/Netflix/RxJava/pull/147#issuecomment-13624780 + Strand.currentStrand().interrupt(); + // using Runtime so it is not checked + throw new RuntimeException("Interrupted while waiting for subscription to complete.", e); + } + + if (exceptionFromOnError.get() != null) { + if (exceptionFromOnError.get() instanceof RuntimeException) { + throw (RuntimeException) exceptionFromOnError.get(); + } else { + throw new RuntimeException(exceptionFromOnError.get()); + } + } + } catch (SuspendExecution e) { + throw new AssertionError(e); + } + } + + /** + * Returns an {@link ReceivePort} that receives all items emitted by a + * specified {@link Observable}. + * + * @return an {@link ReceivePort} that receives all items emitted by a + * specified {@link Observable}. + */ + public ReceivePort toChannel() { + return ChannelObservable.subscribe(BUFFER_SIZE, Channels.OverflowPolicy.BLOCK, o); + } + + /** + * Returns the first item emitted by a specified {@link Observable}, or + * IllegalArgumentException if source contains no elements. + * + * @return the first item emitted by the source {@link Observable} + * @throws IllegalArgumentException + * if source contains no elements + * @see RxJava Wiki: first() + * @see MSDN: Observable.First + */ + @Suspendable + public T first() { + return from(o.first()).single(); + } + + /** + * Returns the first item emitted by a specified {@link Observable} that + * matches a predicate, or IllegalArgumentException if no such + * item is emitted. + * + * @param predicate + * a predicate function to evaluate items emitted by the {@link Observable} + * @return the first item emitted by the {@link Observable} that matches the + * predicate + * @throws IllegalArgumentException + * if no such items are emitted + * @see RxJava Wiki: first() + * @see MSDN: Observable.First + */ + @Suspendable + public T first(Func1 predicate) { + return from(o.first(predicate)).single(); + } + + /** + * Returns the first item emitted by a specified {@link Observable}, or a + * default value if no items are emitted. + * + * @param defaultValue + * a default value to return if the {@link Observable} emits no items + * @return the first item emitted by the {@link Observable}, or the default + * value if no items are emitted + * @see RxJava Wiki: firstOrDefault() + * @see MSDN: Observable.FirstOrDefault + */ + @Suspendable + public T firstOrDefault(T defaultValue) { + return from(o.take(1)).singleOrDefault(defaultValue); + } + + /** + * Returns the first item emitted by a specified {@link Observable} that + * matches a predicate, or a default value if no such items are emitted. + * + * @param defaultValue + * a default value to return if the {@link Observable} emits no matching items + * @param predicate + * a predicate function to evaluate items emitted by the {@link Observable} + * @return the first item emitted by the {@link Observable} that matches the + * predicate, or the default value if no matching items are emitted + * @see RxJava Wiki: firstOrDefault() + * @see MSDN: Observable.FirstOrDefault + */ + @Suspendable + public T firstOrDefault(T defaultValue, Func1 predicate) { + return from(o.filter(predicate)).firstOrDefault(defaultValue); + } + + /** + * Returns the last item emitted by a specified {@link Observable}, or + * throws IllegalArgumentException if it emits no items. + *

+ * + * + * @return the last item emitted by the source {@link Observable} + * @throws IllegalArgumentException + * if source contains no elements + * @see RxJava Wiki: last() + * @see MSDN: Observable.Last + */ + @Suspendable + public T last() { + return from(o.last()).single(); + } + + /** + * Returns the last item emitted by a specified {@link Observable} that + * matches a predicate, or throws IllegalArgumentException if + * it emits no such items. + *

+ * + * + * @param predicate + * a predicate function to evaluate items emitted by the {@link Observable} + * @return the last item emitted by the {@link Observable} that matches the + * predicate + * @throws IllegalArgumentException + * if no such items are emitted + * @see RxJava Wiki: last() + * @see MSDN: Observable.Last + */ + @Suspendable + public T last(final Func1 predicate) { + return from(o.last(predicate)).single(); + } + + /** + * Returns the last item emitted by a specified {@link Observable}, or a + * default value if no items are emitted. + *

+ * + * + * @param defaultValue + * a default value to return if the {@link Observable} emits no items + * @return the last item emitted by the {@link Observable}, or the default + * value if no items are emitted + * @see RxJava Wiki: lastOrDefault() + * @see MSDN: Observable.LastOrDefault + */ + @Suspendable + public T lastOrDefault(T defaultValue) { + return from(o.takeLast(1)).singleOrDefault(defaultValue); + } + + /** + * Returns the last item emitted by a specified {@link Observable} that + * matches a predicate, or a default value if no such items are emitted. + *

+ * + * + * @param defaultValue + * a default value to return if the {@link Observable} emits no matching items + * @param predicate + * a predicate function to evaluate items emitted by the {@link Observable} + * @return the last item emitted by the {@link Observable} that matches the + * predicate, or the default value if no matching items are emitted + * @see RxJava Wiki: lastOrDefault() + * @see MSDN: Observable.LastOrDefault + */ + @Suspendable + public T lastOrDefault(T defaultValue, Func1 predicate) { + return from(o.filter(predicate)).lastOrDefault(defaultValue); + } + + /** + * Returns an {@link Iterable} that always returns the item most recently + * emitted by an {@link Observable}. + *

+ * + * + * @param initialValue + * the initial value that will be yielded by the {@link Iterable} sequence if the {@link Observable} has not yet emitted an item + * @return an {@link Iterable} that on each iteration returns the item that + * the {@link Observable} has most recently emitted + * @see RxJava wiki: mostRecent() + * @see MSDN: Observable.MostRecent + */ + public ReceivePort mostRecent(T initialValue) { + return new RecentReceivePort(ChannelObservable.subscribe(1, Channels.OverflowPolicy.DISPLACE, o), initialValue); + } + + /** + * Returns an {@link Iterable} that blocks until the {@link Observable} emits another item, then returns that item. + *

+ * + * + * @return an {@link Iterable} that blocks upon each iteration until the {@link Observable} emits a new item, whereupon the Iterable returns that item + * @see RxJava Wiki: next() + * @see MSDN: Observable.Next + */ + public ReceivePort next() { + return ChannelObservable.subscribe(1, Channels.OverflowPolicy.DISPLACE, o); + } + + /** + * Returns the latest item emitted by the underlying Observable, waiting if + * necessary for one to become available. + *

+ * If the underlying Observable produces items faster than the + * Iterator.next() takes them, onNext events might + * be skipped, but onError or onCompleted events + * are not. + *

+ * Note also that an onNext() directly followed by + * onCompleted() might hide the onNext() event. + * + * @return the receive port + * @see RxJava wiki: latest() + * @see MSDN: Observable.Latest + */ + public ReceivePort latest() { + return new LatestReceivePort(ChannelObservable.subscribe(1, Channels.OverflowPolicy.DISPLACE, o)); + } + + /** + * If the {@link Observable} completes after emitting a single item, return + * that item, otherwise throw an IllegalArgumentException. + *

+ * + * + * @return the single item emitted by the {@link Observable} + * @see RxJava Wiki: single() + * @see MSDN: Observable.Single + */ + @Suspendable + public T single() { + try { + return from(o.single()).toChannel().receive(); + } catch (ProducerException e) { + throw Exceptions.propagate(e.getCause()); + } catch (InterruptedException e) { + Strand.currentStrand().interrupt(); + throw Exceptions.propagate(e); + } catch (SuspendExecution e) { + throw new AssertionError(e); + } + } + + /** + * If the {@link Observable} completes after emitting a single item that + * matches a given predicate, return that item, otherwise throw an + * IllegalArgumentException. + *

+ * + * + * @param predicate + * a predicate function to evaluate items emitted by the {@link Observable} + * @return the single item emitted by the source {@link Observable} that + * matches the predicate + * @see RxJava Wiki: single() + * @see MSDN: Observable.Single + */ + @Suspendable + public T single(Func1 predicate) { + try { + return from(o.single(predicate)).toChannel().receive(); + } catch (ProducerException e) { + throw Exceptions.propagate(e.getCause()); + } catch (InterruptedException e) { + Strand.currentStrand().interrupt(); + throw Exceptions.propagate(e); + } catch (SuspendExecution e) { + throw new AssertionError(e); + } + } + + /** + * If the {@link Observable} completes after emitting a single item, return + * that item; if it emits more than one item, throw an + * IllegalArgumentException; if it emits no items, return a + * default value. + *

+ * + * + * @param defaultValue + * a default value to return if the {@link Observable} emits no items + * @return the single item emitted by the {@link Observable}, or the default + * value if no items are emitted + * @see RxJava Wiki: singleOrDefault() + * @see MSDN: Observable.SingleOrDefault + */ + @Suspendable + public T singleOrDefault(T defaultValue) { + try { + ReceivePort c = this.toChannel(); + + T result = c.receive(); + if (result == null) + return defaultValue; + if (c.receive() != null) { + throw new IllegalArgumentException("Sequence contains too many elements"); + } + return result; + } catch (ProducerException e) { + throw Exceptions.propagate(e.getCause()); + } catch (InterruptedException e) { + Strand.currentStrand().interrupt(); + throw Exceptions.propagate(e); + } catch (SuspendExecution e) { + throw new AssertionError(e); + } + } + + /** + * If the {@link Observable} completes after emitting a single item that + * matches a predicate, return that item; if it emits more than one such + * item, throw an IllegalArgumentException; if it emits no + * items, return a default value. + *

+ * + * + * @param defaultValue + * a default value to return if the {@link Observable} emits no matching items + * @param predicate + * a predicate function to evaluate items emitted by the {@link Observable} + * @return the single item emitted by the {@link Observable} that matches + * the predicate, or the default value if no such items are emitted + * @see RxJava Wiki: singleOrDefault() + * @see MSDN: Observable.SingleOrDefault + */ + @Suspendable + public T singleOrDefault(T defaultValue, Func1 predicate) { + return from(o.filter(predicate)).singleOrDefault(defaultValue); + } + + /** + * Returns a {@link Future} representing the single value emitted by an {@link Observable}. + *

+ * toFuture() throws an exception if the Observable emits more + * than one item. If the Observable may emit more than item, use {@link Observable#toList toList()}.toFuture(). + *

+ * + * + * @return a {@link Future} that expects a single item to be emitted by the source {@link Observable} + * @see RxJava Wiki: toFuture() + */ + public Future toFuture() { + return new AbstractFuture() { + final AtomicReference val = new AtomicReference(); + + { + o.subscribe(new Observer() { + @Override + public void onCompleted() { + set(val.get()); + } + + @Override + public void onError(Throwable e) { + setException(e); + } + + @Override + public void onNext(T t) { + if (!val.compareAndSet(null, t)) + setException(new IllegalStateException("Observable.toFuture() only supports sequences with a single value.")); + } + }); + } + }; + } + + private static class RecentReceivePort extends DelegatingReceivePort { + private V value; + + public RecentReceivePort(ReceivePort target, V initialValue) { + super(target); + this.value = initialValue; + } + + @Override + public V receive(long timeout, TimeUnit unit) { + return getValue(); + } + + @Override + public V receive() { + return getValue(); + } + + @Override + public V tryReceive() { + return getValue(); + } + + private V getValue() { + V v = super.tryReceive(); + if (v == null) { + if (isClosed()) + return null; + return value; + } + this.value = v; + return v; + } + } + + private static class LatestReceivePort extends DelegatingReceivePort { + private V value; + + public LatestReceivePort(ReceivePort target) { + super(target); + this.value = null; + } + + @Override + public V receive() throws SuspendExecution, InterruptedException { + V v = getValue(); + if (v == null && !isClosed()) { + this.value = super.receive(); + return value; + } + return v; + } + + @Override + public V receive(long timeout, TimeUnit unit) throws SuspendExecution, InterruptedException { + V v = getValue(); + if (v == null && !isClosed()) { + this.value = super.receive(timeout, unit); + return value; + } + return v; + } + + @Override + public V tryReceive() { + return getValue(); + } + + @Override + public boolean isClosed() { + return super.isClosed() && value == null; + } + + private V getValue() { + V v = tryReceive(); + if (v == null) { + v = value; + if (isClosed()) { + this.value = null; + return null; + } + return v; + } + this.value = v; + return v; + } + } +} diff --git a/rxjava-contrib/rxjava-quasar/src/main/java/rx/quasar/ChannelObservable.java b/rxjava-contrib/rxjava-quasar/src/main/java/rx/quasar/ChannelObservable.java index 97e9af950b..a4cd824f9d 100644 --- a/rxjava-contrib/rxjava-quasar/src/main/java/rx/quasar/ChannelObservable.java +++ b/rxjava-contrib/rxjava-quasar/src/main/java/rx/quasar/ChannelObservable.java @@ -119,7 +119,7 @@ public void onError(Throwable e) { * @param o the observable * @return A new channel with the given buffer size and overflow policy that will receive all events emitted by the observable. */ - public static ReceivePort subscribe(int bufferSize, Channels.OverflowPolicy policy, Observable o) { + public static ReceivePort subscribe(int bufferSize, Channels.OverflowPolicy policy, Observable o) { final Channel channel = Channels.newChannel(bufferSize, policy); o.subscribe(new Observer() { diff --git a/rxjava-contrib/rxjava-quasar/src/test/java/rx/quasar/BlockingObservableTest.java b/rxjava-contrib/rxjava-quasar/src/test/java/rx/quasar/BlockingObservableTest.java new file mode 100644 index 0000000000..5c0afc9c31 --- /dev/null +++ b/rxjava-contrib/rxjava-quasar/src/test/java/rx/quasar/BlockingObservableTest.java @@ -0,0 +1,382 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package rx.quasar; + +import co.paralleluniverse.strands.channels.ProducerException; +import co.paralleluniverse.strands.channels.ReceivePort; +import static org.junit.Assert.*; + +import java.util.Iterator; +import java.util.NoSuchElementException; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import rx.Observable; +import rx.Observer; +import rx.Subscriber; +import rx.Subscription; +import rx.subscriptions.BooleanSubscription; +import rx.subscriptions.Subscriptions; +import rx.util.functions.Action1; +import rx.util.functions.Func1; + +public class BlockingObservableTest { + + @Mock + Subscriber w; + + @Before + public void before() { + MockitoAnnotations.initMocks(this); + } + + @Test + public void testLast() { + BlockingObservable obs = BlockingObservable.from(Observable.from("one", "two", "three")); + + assertEquals("three", obs.last()); + } + + @Test(expected = IllegalArgumentException.class) + public void testLastEmptyObservable() { + BlockingObservable obs = BlockingObservable.from(Observable.empty()); + obs.last(); + } + + @Test + public void testLastOrDefault() { + BlockingObservable observable = BlockingObservable.from(Observable.from(1, 0, -1)); + int last = observable.lastOrDefault(-100, new Func1() { + @Override + public Boolean call(Integer args) { + return args >= 0; + } + }); + assertEquals(0, last); + } + + @Test + public void testLastOrDefault1() { + BlockingObservable observable = BlockingObservable.from(Observable.from("one", "two", "three")); + assertEquals("three", observable.lastOrDefault("default")); + } + + @Test + public void testLastOrDefault2() { + BlockingObservable observable = BlockingObservable.from(Observable.empty()); + assertEquals("default", observable.lastOrDefault("default")); + } + + @Test + public void testLastOrDefaultWithPredicate() { + BlockingObservable observable = BlockingObservable.from(Observable.from(1, 0, -1)); + int last = observable.lastOrDefault(0, new Func1() { + @Override + public Boolean call(Integer args) { + return args < 0; + } + }); + + assertEquals(-1, last); + } + + @Test + public void testLastOrDefaultWrongPredicate() { + BlockingObservable observable = BlockingObservable.from(Observable.from(-1, -2, -3)); + int last = observable.lastOrDefault(0, new Func1() { + @Override + public Boolean call(Integer args) { + return args >= 0; + } + }); + assertEquals(0, last); + } + + @Test + public void testLastWithPredicate() { + BlockingObservable obs = BlockingObservable.from(Observable.from("one", "two", "three")); + + assertEquals("two", obs.last(new Func1() { + @Override + public Boolean call(String s) { + return s.length() == 3; + } + })); + } + + public void testSingle() { + BlockingObservable observable = BlockingObservable.from(Observable.from("one")); + assertEquals("one", observable.single()); + } + + @Test + public void testSingleDefault() { + BlockingObservable observable = BlockingObservable.from(Observable.empty()); + assertEquals("default", observable.singleOrDefault("default")); + } + + @Test(expected = IllegalArgumentException.class) + public void testSingleDefaultPredicateMatchesMoreThanOne() { + BlockingObservable.from(Observable.from("one", "two")).singleOrDefault("default", new Func1() { + @Override + public Boolean call(String args) { + return args.length() == 3; + } + }); + } + + @Test + public void testSingleDefaultPredicateMatchesNothing() { + BlockingObservable observable = BlockingObservable.from(Observable.from("one", "two")); + String result = observable.singleOrDefault("default", new Func1() { + @Override + public Boolean call(String args) { + return args.length() == 4; + } + }); + assertEquals("default", result); + } + + @Test(expected = IllegalArgumentException.class) + public void testSingleDefaultWithMoreThanOne() { + BlockingObservable observable = BlockingObservable.from(Observable.from("one", "two", "three")); + observable.singleOrDefault("default"); + } + + @Test + public void testSingleWithPredicateDefault() { + BlockingObservable observable = BlockingObservable.from(Observable.from("one", "two", "four")); + assertEquals("four", observable.single(new Func1() { + @Override + public Boolean call(String s) { + return s.length() == 4; + } + })); + } + + @Test(expected = IllegalArgumentException.class) + public void testSingleWrong() { + BlockingObservable observable = BlockingObservable.from(Observable.from(1, 2)); + observable.single(); + } + + @Test(expected = IllegalArgumentException.class) + public void testSingleWrongPredicate() { + BlockingObservable observable = BlockingObservable.from(Observable.from(-1)); + observable.single(new Func1() { + @Override + public Boolean call(Integer args) { + return args > 0; + } + }); + } + + @Test + public void testToChannel() throws Exception { + BlockingObservable obs = BlockingObservable.from(Observable.from("one", "two", "three")); + + ReceivePort c = obs.toChannel(); + + assertEquals(false, c.isClosed()); + assertEquals("one", c.receive()); + + assertEquals(false, c.isClosed()); + assertEquals("two", c.receive()); + + assertEquals(false, c.isClosed()); + assertEquals("three", c.receive()); + + assertEquals(true, c.isClosed()); + + } + + public void testToChannelNextOnly() throws Exception { + BlockingObservable obs = BlockingObservable.from(Observable.from(1, 2, 3)); + + ReceivePort c = obs.toChannel(); + + Assert.assertEquals((Integer) 1, c.receive()); + Assert.assertEquals((Integer) 2, c.receive()); + Assert.assertEquals((Integer) 3, c.receive()); + + Assert.assertEquals(c.receive(), null); + } + + public void testToChannelNextOnlyTwice() throws Exception { + BlockingObservable obs = BlockingObservable.from(Observable.from(1, 2, 3)); + + ReceivePort c = obs.toChannel(); + + Assert.assertEquals((Integer) 1, c.receive()); + Assert.assertEquals((Integer) 2, c.receive()); + Assert.assertEquals((Integer) 3, c.receive()); + + Assert.assertEquals(c.receive(), null); + Assert.assertEquals(c.receive(), null); + } + + @Test + public void testToChannelManyTimes() throws Exception { + BlockingObservable obs = BlockingObservable.from(Observable.from(1, 2, 3)); + + for (int j = 0; j < 3; j++) { + ReceivePort c = obs.toChannel(); + + Assert.assertFalse(c.isClosed()); + Assert.assertEquals((Integer) 1, c.receive()); + Assert.assertFalse(c.isClosed()); + Assert.assertEquals((Integer) 2, c.receive()); + Assert.assertFalse(c.isClosed()); + Assert.assertEquals((Integer) 3, c.receive()); + Assert.assertTrue(c.isClosed()); + } + } + + @Test(expected = TestException.class) + public void testToChannelWithException() throws Throwable { + BlockingObservable obs = BlockingObservable.from(Observable.create(new Observable.OnSubscribeFunc() { + + @Override + public Subscription onSubscribe(Observer observer) { + observer.onNext("one"); + observer.onError(new TestException()); + return Subscriptions.empty(); + } + })); + + ReceivePort c = obs.toChannel(); + + assertEquals(false, c.isClosed()); + assertEquals("one", c.receive()); + + try { + c.receive(); + } catch (ProducerException e) { + throw e.getCause(); + } + } + + @Test + public void testForEachWithError() { + try { + BlockingObservable.from(Observable.create(new Observable.OnSubscribeFunc() { + + @Override + public Subscription onSubscribe(final Observer observer) { + final BooleanSubscription subscription = new BooleanSubscription(); + new Thread(new Runnable() { + + @Override + public void run() { + observer.onNext("one"); + observer.onNext("two"); + observer.onNext("three"); + observer.onCompleted(); + } + }).start(); + return subscription; + } + })).forEach(new Action1() { + + @Override + public void call(String t1) { + throw new RuntimeException("fail"); + } + }); + fail("we expect an exception to be thrown"); + } catch (Throwable e) { + // do nothing as we expect this + } + } + + @Test + public void testFirst() { + BlockingObservable observable = BlockingObservable.from(Observable.from("one", "two", "three")); + assertEquals("one", observable.first()); + } + + @Test(expected = IllegalArgumentException.class) + public void testFirstWithEmpty() { + BlockingObservable.from(Observable.empty()).first(); + } + + @Test + public void testFirstWithPredicate() { + BlockingObservable observable = BlockingObservable.from(Observable.from("one", "two", "three")); + String first = observable.first(new Func1() { + @Override + public Boolean call(String args) { + return args.length() > 3; + } + }); + assertEquals("three", first); + } + + @Test(expected = IllegalArgumentException.class) + public void testFirstWithPredicateAndEmpty() { + BlockingObservable observable = BlockingObservable.from(Observable.from("one", "two", "three")); + observable.first(new Func1() { + @Override + public Boolean call(String args) { + return args.length() > 5; + } + }); + } + + @Test + public void testFirstOrDefault() { + BlockingObservable observable = BlockingObservable.from(Observable.from("one", "two", "three")); + assertEquals("one", observable.firstOrDefault("default")); + } + + @Test + public void testFirstOrDefaultWithEmpty() { + BlockingObservable observable = BlockingObservable.from(Observable.empty()); + assertEquals("default", observable.firstOrDefault("default")); + } + + @Test + public void testFirstOrDefaultWithPredicate() { + BlockingObservable observable = BlockingObservable.from(Observable.from("one", "two", "three")); + String first = observable.firstOrDefault("default", new Func1() { + @Override + public Boolean call(String args) { + return args.length() > 3; + } + }); + assertEquals("three", first); + } + + @Test + public void testFirstOrDefaultWithPredicateAndEmpty() { + BlockingObservable observable = BlockingObservable.from(Observable.from("one", "two", "three")); + String first = observable.firstOrDefault("default", new Func1() { + @Override + public Boolean call(String args) { + return args.length() > 5; + } + }); + assertEquals("default", first); + } + + private static class TestException extends RuntimeException { + private static final long serialVersionUID = 1L; + } +} From bdfd9fe1191ca9620c365e830b16ca3ad7414c6f Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Tue, 25 Feb 2014 14:11:29 -0800 Subject: [PATCH 055/422] TestSubscriber: Default onError and Terminal Latch Behavior --- .../java/rx/observers/TestSubscriber.java | 33 ++++++++++++++++--- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/rxjava-core/src/main/java/rx/observers/TestSubscriber.java b/rxjava-core/src/main/java/rx/observers/TestSubscriber.java index 50bfabf059..97966b8ebd 100644 --- a/rxjava-core/src/main/java/rx/observers/TestSubscriber.java +++ b/rxjava-core/src/main/java/rx/observers/TestSubscriber.java @@ -41,13 +41,33 @@ public TestSubscriber(Observer delegate) { } public TestSubscriber() { - this.testObserver = new TestObserver(Subscribers. empty()); + this.testObserver = new TestObserver(new Observer() { + + @Override + public void onCompleted() { + // do nothing + } + + @Override + public void onError(Throwable e) { + // do nothing + } + + @Override + public void onNext(T t) { + // do nothing + } + + }); } @Override public void onCompleted() { - testObserver.onCompleted(); - latch.countDown(); + try { + testObserver.onCompleted(); + } finally { + latch.countDown(); + } } public List> getOnCompletedEvents() { @@ -56,8 +76,11 @@ public List> getOnCompletedEvents() { @Override public void onError(Throwable e) { - testObserver.onError(e); - latch.countDown(); + try { + testObserver.onError(e); + } finally { + latch.countDown(); + } } public List getOnErrorEvents() { From a0c80095d5781c3566e4c61ace730e5f634088eb Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Tue, 25 Feb 2014 14:51:32 -0800 Subject: [PATCH 056/422] TestSubscriber lastSeenThread --- rxjava-core/src/main/java/rx/observers/TestSubscriber.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rxjava-core/src/main/java/rx/observers/TestSubscriber.java b/rxjava-core/src/main/java/rx/observers/TestSubscriber.java index 97966b8ebd..dd1f2b8a7e 100644 --- a/rxjava-core/src/main/java/rx/observers/TestSubscriber.java +++ b/rxjava-core/src/main/java/rx/observers/TestSubscriber.java @@ -64,6 +64,7 @@ public void onNext(T t) { @Override public void onCompleted() { try { + lastSeenThread = Thread.currentThread(); testObserver.onCompleted(); } finally { latch.countDown(); @@ -77,6 +78,7 @@ public List> getOnCompletedEvents() { @Override public void onError(Throwable e) { try { + lastSeenThread = Thread.currentThread(); testObserver.onError(e); } finally { latch.countDown(); From 9b63de2f4366172084b93386956cb19c04f09301 Mon Sep 17 00:00:00 2001 From: Bob T Builder Date: Tue, 25 Feb 2014 23:17:13 +0000 Subject: [PATCH 057/422] [Gradle Release Plugin] - pre tag commit: '0.17.0-RC6'. --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index b78e3f0dae..a8d27ebf4d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -version=0.17.0-RC6-SNAPSHOT +version=0.17.0-RC6 From cbbf5144e12966d4a58a64ae61726e9ad8fa9d80 Mon Sep 17 00:00:00 2001 From: Bob T Builder Date: Tue, 25 Feb 2014 23:17:17 +0000 Subject: [PATCH 058/422] [Gradle Release Plugin] - new version commit: '0.17.0-RC7-SNAPSHOT'. --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index a8d27ebf4d..f84383b779 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -version=0.17.0-RC6 +version=0.17.0-RC7-SNAPSHOT From c32209bff1fb346ddd70f8905843e58e1f07e56f Mon Sep 17 00:00:00 2001 From: akarnokd Date: Wed, 26 Feb 2014 08:30:50 +0100 Subject: [PATCH 059/422] Fix deadlock in SubscribeOnBounded --- .../rx/operators/OperatorSubscribeOnBounded.java | 3 +-- .../operators/OperatorSubscribeOnBoundedTest.java | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/rxjava-core/src/main/java/rx/operators/OperatorSubscribeOnBounded.java b/rxjava-core/src/main/java/rx/operators/OperatorSubscribeOnBounded.java index 969f0df8b9..f5a459ef5d 100644 --- a/rxjava-core/src/main/java/rx/operators/OperatorSubscribeOnBounded.java +++ b/rxjava-core/src/main/java/rx/operators/OperatorSubscribeOnBounded.java @@ -88,14 +88,13 @@ public void onNext(final Observable o) { if (checkNeedBuffer(o)) { // use buffering (possibly blocking) for a possibly synchronous subscribe final BufferUntilSubscriber bus = new BufferUntilSubscriber(bufferSize, subscriber); - o.subscribe(bus); subscriber.add(scheduler.schedule(new Action1() { @Override public void call(final Inner inner) { bus.enterPassthroughMode(); } })); - return; + o.subscribe(bus); } else { // no buffering (async subscribe) subscriber.add(scheduler.schedule(new Action1() { diff --git a/rxjava-core/src/test/java/rx/operators/OperatorSubscribeOnBoundedTest.java b/rxjava-core/src/test/java/rx/operators/OperatorSubscribeOnBoundedTest.java index 9b55256055..7a38bcd8da 100644 --- a/rxjava-core/src/test/java/rx/operators/OperatorSubscribeOnBoundedTest.java +++ b/rxjava-core/src/test/java/rx/operators/OperatorSubscribeOnBoundedTest.java @@ -399,5 +399,18 @@ public void call(Subscriber sub) { ts.assertReceivedOnNext(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)); assertEquals(10, count.get()); } - + @Test(timeout = 2000) + public void testNoDeadlock() { + List data = Arrays.asList(1, 2, 3, 4, 5); + Observable source = Observable.from(data); + + Observable result = source.nest().lift(new OperatorSubscribeOnBounded(Schedulers.newThread(), 1)); + + TestSubscriber ts = new TestSubscriber(); + + result.subscribe(ts); + + ts.awaitTerminalEvent(); + ts.assertReceivedOnNext(data); + } } From 9e4e180290b5c1d9093ead591701caf1c919cde7 Mon Sep 17 00:00:00 2001 From: Volker Leck Date: Wed, 26 Feb 2014 17:19:42 +0100 Subject: [PATCH 060/422] correct link to maven search --- rxjava-contrib/rxjava-android/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rxjava-contrib/rxjava-android/README.md b/rxjava-contrib/rxjava-android/README.md index 7a9e9edc20..8b7309b641 100644 --- a/rxjava-contrib/rxjava-android/README.md +++ b/rxjava-contrib/rxjava-android/README.md @@ -13,7 +13,7 @@ Android applications easy and hassle free. More specifically, it # Binaries -Binaries and dependency information for Maven, Ivy, Gradle and others can be found at [http://search.maven.org](http://search.maven.org/#search%7Cga%7C1%7Ca%3A%22hystrix-servo-metrics-publisher%22). +Binaries and dependency information for Maven, Ivy, Gradle and others can be found at [http://search.maven.org](http://search.maven.org/#search%7Cga%7C1%7Ca%3A%22rxjava-android%22). Example for [Maven](http://search.maven.org/#search%7Cga%7C1%7Ca%3A%22rxjava-android%22): From c298d5efb1a3b2d84183330358dded3cfcacfa38 Mon Sep 17 00:00:00 2001 From: Matthias Kaeppler Date: Wed, 26 Feb 2014 17:41:23 +0100 Subject: [PATCH 061/422] A number of improvements to OperatorObserveFromAndroidComponent - move the UI thread assert out of the operator and into the helpers; this way, we don't fail the observer anymore with an exception, but the caller. - do not loop unsubscribe through the main thread anymore. This unnecessarily defers releasing the references, and might in fact be processed only after Android creates the component after a rotation change. I had to make the references volatile for this to work. - immediately unsubscribe in case we detect the componentRef has become invalid. This solves the problem that dangling observers would continue to listen to notifications with no observer alive anymore. refs: https://github.com/Netflix/RxJava/issues/754 https://github.com/Netflix/RxJava/issues/899 --- .../observables/AndroidObservable.java | 2 + .../rx/android/observables/Assertions.java | 11 +++++ .../android/observables/ViewObservable.java | 13 ++---- .../OperatorCompoundButtonInput.java | 4 +- .../rx/operators/OperatorEditTextInput.java | 4 +- .../OperatorObserveFromAndroidComponent.java | 28 ++++++------- .../java/rx/operators/OperatorViewClick.java | 4 +- .../observables/AndroidObservableTest.java | 42 ++++++++++++++++++- ...eratorObserveFromAndroidComponentTest.java | 16 ------- 9 files changed, 75 insertions(+), 49 deletions(-) create mode 100644 rxjava-contrib/rxjava-android/src/main/java/rx/android/observables/Assertions.java diff --git a/rxjava-contrib/rxjava-android/src/main/java/rx/android/observables/AndroidObservable.java b/rxjava-contrib/rxjava-android/src/main/java/rx/android/observables/AndroidObservable.java index d55617ddea..a0236bfbd5 100644 --- a/rxjava-contrib/rxjava-android/src/main/java/rx/android/observables/AndroidObservable.java +++ b/rxjava-contrib/rxjava-android/src/main/java/rx/android/observables/AndroidObservable.java @@ -59,6 +59,7 @@ private AndroidObservable() {} * @return a new observable sequence that will emit notifications on the main UI thread */ public static Observable fromActivity(Activity activity, Observable sourceObservable) { + Assertions.assertUiThread(); return OperatorObserveFromAndroidComponent.observeFromAndroidComponent(sourceObservable, activity); } @@ -87,6 +88,7 @@ public static Observable fromActivity(Activity activity, Observable so * @return a new observable sequence that will emit notifications on the main UI thread */ public static Observable fromFragment(Object fragment, Observable sourceObservable) { + Assertions.assertUiThread(); if (USES_SUPPORT_FRAGMENTS && fragment instanceof android.support.v4.app.Fragment) { return OperatorObserveFromAndroidComponent.observeFromAndroidComponent(sourceObservable, (android.support.v4.app.Fragment) fragment); } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB && fragment instanceof Fragment) { diff --git a/rxjava-contrib/rxjava-android/src/main/java/rx/android/observables/Assertions.java b/rxjava-contrib/rxjava-android/src/main/java/rx/android/observables/Assertions.java new file mode 100644 index 0000000000..58ed484ef5 --- /dev/null +++ b/rxjava-contrib/rxjava-android/src/main/java/rx/android/observables/Assertions.java @@ -0,0 +1,11 @@ +package rx.android.observables; + +import android.os.Looper; + +public class Assertions { + public static void assertUiThread() { + if (Looper.getMainLooper() != Looper.myLooper()) { + throw new IllegalStateException("Observers must subscribe from the main UI thread, but was " + Thread.currentThread()); + } + } +} diff --git a/rxjava-contrib/rxjava-android/src/main/java/rx/android/observables/ViewObservable.java b/rxjava-contrib/rxjava-android/src/main/java/rx/android/observables/ViewObservable.java index 6e31af051e..205f25c944 100644 --- a/rxjava-contrib/rxjava-android/src/main/java/rx/android/observables/ViewObservable.java +++ b/rxjava-contrib/rxjava-android/src/main/java/rx/android/observables/ViewObservable.java @@ -15,15 +15,15 @@ */ package rx.android.observables; -import android.os.Looper; -import android.view.View; -import android.widget.CompoundButton; -import android.widget.EditText; import rx.Observable; import rx.operators.OperatorCompoundButtonInput; import rx.operators.OperatorEditTextInput; import rx.operators.OperatorViewClick; +import android.view.View; +import android.widget.CompoundButton; +import android.widget.EditText; + public class ViewObservable { public static Observable clicks(final View view, final boolean emitInitialValue) { @@ -38,10 +38,5 @@ public static Observable input(final CompoundButton button, final boole return Observable.create(new OperatorCompoundButtonInput(button, emitInitialValue)); } - public static void assertUiThread() { - if (Looper.getMainLooper() != Looper.myLooper()) { - throw new IllegalStateException("Observers must subscribe from the main UI thread, but was " + Thread.currentThread()); - } - } } diff --git a/rxjava-contrib/rxjava-android/src/main/java/rx/operators/OperatorCompoundButtonInput.java b/rxjava-contrib/rxjava-android/src/main/java/rx/operators/OperatorCompoundButtonInput.java index 9a61093699..f9973d5f54 100644 --- a/rxjava-contrib/rxjava-android/src/main/java/rx/operators/OperatorCompoundButtonInput.java +++ b/rxjava-contrib/rxjava-android/src/main/java/rx/operators/OperatorCompoundButtonInput.java @@ -23,7 +23,7 @@ import rx.Observable; import rx.Subscriber; import rx.Subscription; -import rx.android.observables.ViewObservable; +import rx.android.observables.Assertions; import rx.android.subscriptions.AndroidSubscriptions; import rx.functions.Action0; import android.view.View; @@ -40,7 +40,7 @@ public OperatorCompoundButtonInput(final CompoundButton button, final boolean em @Override public void call(final Subscriber observer) { - ViewObservable.assertUiThread(); + Assertions.assertUiThread(); final CompositeOnCheckedChangeListener composite = CachedListeners.getFromViewOrCreate(button); final CompoundButton.OnCheckedChangeListener listener = new CompoundButton.OnCheckedChangeListener() { diff --git a/rxjava-contrib/rxjava-android/src/main/java/rx/operators/OperatorEditTextInput.java b/rxjava-contrib/rxjava-android/src/main/java/rx/operators/OperatorEditTextInput.java index 1dcbac0014..563d80bf24 100644 --- a/rxjava-contrib/rxjava-android/src/main/java/rx/operators/OperatorEditTextInput.java +++ b/rxjava-contrib/rxjava-android/src/main/java/rx/operators/OperatorEditTextInput.java @@ -18,7 +18,7 @@ import rx.Observable; import rx.Subscriber; import rx.Subscription; -import rx.android.observables.ViewObservable; +import rx.android.observables.Assertions; import rx.android.subscriptions.AndroidSubscriptions; import rx.functions.Action0; import android.text.Editable; @@ -36,7 +36,7 @@ public OperatorEditTextInput(final EditText input, final boolean emitInitialValu @Override public void call(final Subscriber observer) { - ViewObservable.assertUiThread(); + Assertions.assertUiThread(); final TextWatcher watcher = new SimpleTextWatcher() { @Override public void afterTextChanged(final Editable editable) { diff --git a/rxjava-contrib/rxjava-android/src/main/java/rx/operators/OperatorObserveFromAndroidComponent.java b/rxjava-contrib/rxjava-android/src/main/java/rx/operators/OperatorObserveFromAndroidComponent.java index 3e656ccfbb..0ad16b4cc2 100644 --- a/rxjava-contrib/rxjava-android/src/main/java/rx/operators/OperatorObserveFromAndroidComponent.java +++ b/rxjava-contrib/rxjava-android/src/main/java/rx/operators/OperatorObserveFromAndroidComponent.java @@ -19,10 +19,10 @@ import rx.Observer; import rx.Subscriber; import rx.android.schedulers.AndroidSchedulers; -import rx.android.subscriptions.AndroidSubscriptions; import rx.functions.Action0; +import rx.subscriptions.Subscriptions; + import android.app.Activity; -import android.os.Looper; import android.util.Log; public class OperatorObserveFromAndroidComponent { @@ -44,8 +44,8 @@ private static class OnSubscribeBase implements Observable. private static final String LOG_TAG = "AndroidObserver"; private final Observable source; - private AndroidComponent componentRef; - private Observer observerRef; + private volatile AndroidComponent componentRef; + private volatile Observer observerRef; private OnSubscribeBase(Observable source, AndroidComponent component) { this.source = source; @@ -54,9 +54,9 @@ private OnSubscribeBase(Observable source, AndroidComponent component) { private void log(String message) { if (Log.isLoggable(LOG_TAG, Log.DEBUG)) { - Log.d(LOG_TAG, "componentRef = " + componentRef); - Log.d(LOG_TAG, "observerRef = " + observerRef); - Log.d(LOG_TAG, message); + String thread = Thread.currentThread().getName(); + Log.d(LOG_TAG, "[" + thread + "] componentRef = " + componentRef + "; observerRef = " + observerRef); + Log.d(LOG_TAG, "[" + thread + "]" + message); } } @@ -65,8 +65,7 @@ protected boolean isComponentValid(AndroidComponent component) { } @Override - public void call(Subscriber subscriber) { - assertUiThread(); + public void call(final Subscriber subscriber) { observerRef = subscriber; source.observeOn(AndroidSchedulers.mainThread()).subscribe(new Subscriber(subscriber) { @Override @@ -74,6 +73,7 @@ public void onCompleted() { if (componentRef != null && isComponentValid(componentRef)) { observerRef.onCompleted(); } else { + unsubscribe(); log("onComplete: target component released or detached; dropping message"); } } @@ -83,6 +83,7 @@ public void onError(Throwable e) { if (componentRef != null && isComponentValid(componentRef)) { observerRef.onError(e); } else { + unsubscribe(); log("onError: target component released or detached; dropping message"); } } @@ -92,11 +93,12 @@ public void onNext(T args) { if (componentRef != null && isComponentValid(componentRef)) { observerRef.onNext(args); } else { + unsubscribe(); log("onNext: target component released or detached; dropping message"); } } }); - subscriber.add(AndroidSubscriptions.unsubscribeInUiThread(new Action0() { + subscriber.add(Subscriptions.create(new Action0() { @Override public void call() { log("unsubscribing from source sequence"); @@ -109,12 +111,6 @@ private void releaseReferences() { observerRef = null; componentRef = null; } - - private void assertUiThread() { - if (Looper.getMainLooper() != Looper.myLooper()) { - throw new IllegalStateException("Observers must subscribe from the main UI thread, but was " + Thread.currentThread()); - } - } } private static final class OnSubscribeFragment extends OnSubscribeBase { diff --git a/rxjava-contrib/rxjava-android/src/main/java/rx/operators/OperatorViewClick.java b/rxjava-contrib/rxjava-android/src/main/java/rx/operators/OperatorViewClick.java index 1fb2bb8b91..7ee7149b72 100644 --- a/rxjava-contrib/rxjava-android/src/main/java/rx/operators/OperatorViewClick.java +++ b/rxjava-contrib/rxjava-android/src/main/java/rx/operators/OperatorViewClick.java @@ -23,7 +23,7 @@ import rx.Observable; import rx.Subscriber; import rx.Subscription; -import rx.android.observables.ViewObservable; +import rx.android.observables.Assertions; import rx.android.subscriptions.AndroidSubscriptions; import rx.functions.Action0; import android.view.View; @@ -39,7 +39,7 @@ public OperatorViewClick(final View view, final boolean emitInitialValue) { @Override public void call(final Subscriber observer) { - ViewObservable.assertUiThread(); + Assertions.assertUiThread(); final CompositeOnClickListener composite = CachedListeners.getFromViewOrCreate(view); final View.OnClickListener listener = new View.OnClickListener() { diff --git a/rxjava-contrib/rxjava-android/src/test/java/rx/android/observables/AndroidObservableTest.java b/rxjava-contrib/rxjava-android/src/test/java/rx/android/observables/AndroidObservableTest.java index a5006bdf91..0591c6454e 100644 --- a/rxjava-contrib/rxjava-android/src/test/java/rx/android/observables/AndroidObservableTest.java +++ b/rxjava-contrib/rxjava-android/src/test/java/rx/android/observables/AndroidObservableTest.java @@ -15,7 +15,7 @@ */ package rx.android.observables; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.verify; import org.junit.Before; import org.junit.Test; @@ -25,14 +25,20 @@ import org.robolectric.Robolectric; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; - import rx.Observable; import rx.Observer; import rx.observers.TestObserver; + import android.app.Activity; import android.app.Fragment; import android.support.v4.app.FragmentActivity; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + @RunWith(RobolectricTestRunner.class) @Config(manifest = Config.NONE) @@ -79,4 +85,36 @@ public void itSupportsNativeFragments() { public void itThrowsIfObjectPassedIsNotAFragment() { AndroidObservable.fromFragment("not a fragment", Observable.never()); } + + @Test(expected = IllegalStateException.class) + public void itThrowsIfObserverCallsFromFragmentFromBackgroundThread() throws Throwable { + final Future future = Executors.newSingleThreadExecutor().submit(new Callable() { + @Override + public Object call() throws Exception { + AndroidObservable.fromFragment(fragment, Observable.empty()); + return null; + } + }); + try { + future.get(1, TimeUnit.SECONDS); + } catch (ExecutionException e) { + throw e.getCause(); + } + } + + @Test(expected = IllegalStateException.class) + public void itThrowsIfObserverCallsFromActivityFromBackgroundThread() throws Throwable { + final Future future = Executors.newSingleThreadExecutor().submit(new Callable() { + @Override + public Object call() throws Exception { + AndroidObservable.fromActivity(activity, Observable.empty()); + return null; + } + }); + try { + future.get(1, TimeUnit.SECONDS); + } catch (ExecutionException e) { + throw e.getCause(); + } + } } diff --git a/rxjava-contrib/rxjava-android/src/test/java/rx/android/operators/OperatorObserveFromAndroidComponentTest.java b/rxjava-contrib/rxjava-android/src/test/java/rx/android/operators/OperatorObserveFromAndroidComponentTest.java index 70a9c881fb..443901a1f7 100644 --- a/rxjava-contrib/rxjava-android/src/test/java/rx/android/operators/OperatorObserveFromAndroidComponentTest.java +++ b/rxjava-contrib/rxjava-android/src/test/java/rx/android/operators/OperatorObserveFromAndroidComponentTest.java @@ -70,22 +70,6 @@ public void setupMocks() { when(mockFragment.isAdded()).thenReturn(true); } - @Test - public void itThrowsIfObserverSubscribesFromBackgroundThread() throws Exception { - final Observable testObservable = Observable.from(1); - final Future future = Executors.newSingleThreadExecutor().submit(new Callable() { - @Override - public Object call() throws Exception { - OperatorObserveFromAndroidComponent.observeFromAndroidComponent( - testObservable, mockFragment).subscribe(mockObserver); - return null; - } - }); - future.get(1, TimeUnit.SECONDS); - verify(mockObserver).onError(any(IllegalStateException.class)); - verifyNoMoreInteractions(mockObserver); - } - // TODO needs to be fixed, see comments inline below @Ignore public void itObservesTheSourceSequenceOnTheMainUIThread() { From 096f44322e83095a260bf4b58dfe24f5565157e9 Mon Sep 17 00:00:00 2001 From: Matthias Kaeppler Date: Wed, 26 Feb 2014 18:36:58 +0100 Subject: [PATCH 062/422] Initial commit of the Android samples module refs https://github.com/soundcloud/RxJava/issues/3 --- .gitignore | 1 + .../build.gradle | 19 ++ .../rxjava-android-samples/.gitignore | 4 + .../rxjava-android-samples/build.gradle | 17 ++ .../rxjava-android-samples/gradle.properties | 18 ++ .../gradle/wrapper/gradle-wrapper.properties | 6 + rxjava-contrib/rxjava-android-samples/gradlew | 164 ++++++++++++++++++ .../rxjava-android-samples/gradlew.bat | 90 ++++++++++ .../rxjava-android-samples/samples/.gitignore | 1 + .../samples/build.gradle | 24 +++ .../samples/proguard-rules.txt | 17 ++ .../samples/src/main/AndroidManifest.xml | 21 +++ .../android/samples/RetainedFragment.java | 65 +++++++ .../samples/SampleFragmentActivity.java | 17 ++ .../android/samples/SampleObservables.java | 29 ++++ .../main/res/drawable-hdpi/ic_launcher.png | Bin 0 -> 9397 bytes .../main/res/drawable-mdpi/ic_launcher.png | Bin 0 -> 5237 bytes .../main/res/drawable-xhdpi/ic_launcher.png | Bin 0 -> 14383 bytes .../main/res/drawable-xxhdpi/ic_launcher.png | Bin 0 -> 19388 bytes .../src/main/res/layout/retained_fragment.xml | 13 ++ .../src/main/res/layout/sample_activity.xml | 14 ++ .../src/main/res/values-w820dp/dimens.xml | 6 + .../samples/src/main/res/values/dimens.xml | 6 + .../samples/src/main/res/values/strings.xml | 9 + .../samples/src/main/res/values/styles.xml | 12 ++ .../rxjava-android-samples/settings.gradle | 1 + settings.gradle | 1 + 27 files changed, 555 insertions(+) create mode 100644 rxjava-contrib/rxjava-android-samples-build-wrapper/build.gradle create mode 100644 rxjava-contrib/rxjava-android-samples/.gitignore create mode 100644 rxjava-contrib/rxjava-android-samples/build.gradle create mode 100644 rxjava-contrib/rxjava-android-samples/gradle.properties create mode 100644 rxjava-contrib/rxjava-android-samples/gradle/wrapper/gradle-wrapper.properties create mode 100755 rxjava-contrib/rxjava-android-samples/gradlew create mode 100644 rxjava-contrib/rxjava-android-samples/gradlew.bat create mode 100644 rxjava-contrib/rxjava-android-samples/samples/.gitignore create mode 100644 rxjava-contrib/rxjava-android-samples/samples/build.gradle create mode 100644 rxjava-contrib/rxjava-android-samples/samples/proguard-rules.txt create mode 100644 rxjava-contrib/rxjava-android-samples/samples/src/main/AndroidManifest.xml create mode 100644 rxjava-contrib/rxjava-android-samples/samples/src/main/java/com/netflix/rxjava/android/samples/RetainedFragment.java create mode 100644 rxjava-contrib/rxjava-android-samples/samples/src/main/java/com/netflix/rxjava/android/samples/SampleFragmentActivity.java create mode 100644 rxjava-contrib/rxjava-android-samples/samples/src/main/java/com/netflix/rxjava/android/samples/SampleObservables.java create mode 100644 rxjava-contrib/rxjava-android-samples/samples/src/main/res/drawable-hdpi/ic_launcher.png create mode 100644 rxjava-contrib/rxjava-android-samples/samples/src/main/res/drawable-mdpi/ic_launcher.png create mode 100644 rxjava-contrib/rxjava-android-samples/samples/src/main/res/drawable-xhdpi/ic_launcher.png create mode 100644 rxjava-contrib/rxjava-android-samples/samples/src/main/res/drawable-xxhdpi/ic_launcher.png create mode 100644 rxjava-contrib/rxjava-android-samples/samples/src/main/res/layout/retained_fragment.xml create mode 100644 rxjava-contrib/rxjava-android-samples/samples/src/main/res/layout/sample_activity.xml create mode 100644 rxjava-contrib/rxjava-android-samples/samples/src/main/res/values-w820dp/dimens.xml create mode 100644 rxjava-contrib/rxjava-android-samples/samples/src/main/res/values/dimens.xml create mode 100644 rxjava-contrib/rxjava-android-samples/samples/src/main/res/values/strings.xml create mode 100644 rxjava-contrib/rxjava-android-samples/samples/src/main/res/values/styles.xml create mode 100644 rxjava-contrib/rxjava-android-samples/settings.gradle diff --git a/.gitignore b/.gitignore index f69630c23c..52b0ae57e2 100644 --- a/.gitignore +++ b/.gitignore @@ -39,6 +39,7 @@ Thumbs.db # Gradle Files # ################ .gradle +.gradletasknamecache .m2 # Build output directies diff --git a/rxjava-contrib/rxjava-android-samples-build-wrapper/build.gradle b/rxjava-contrib/rxjava-android-samples-build-wrapper/build.gradle new file mode 100644 index 0000000000..87f0f5bbf3 --- /dev/null +++ b/rxjava-contrib/rxjava-android-samples-build-wrapper/build.gradle @@ -0,0 +1,19 @@ +def androidHome = System.getenv("ANDROID_HOME") +tasks.build.doLast { + if (androidHome.isEmpty()) { + println("No Android SDK detected; skipping Android samples build") + } else { + println("Android SDK detected at $androidHome, running samples build") + project.exec { + workingDir '../rxjava-android-samples' + + def props = new Properties() + file("$rootDir/gradle.properties").withReader { reader -> + props.load(reader) + properties.putAll(props) + } + + commandLine "./gradlew", "clean", "packageDebug", "-PrxjVersion=${props.getProperty("version")}" + } + } +} diff --git a/rxjava-contrib/rxjava-android-samples/.gitignore b/rxjava-contrib/rxjava-android-samples/.gitignore new file mode 100644 index 0000000000..d6bfc95b18 --- /dev/null +++ b/rxjava-contrib/rxjava-android-samples/.gitignore @@ -0,0 +1,4 @@ +.gradle +/local.properties +/.idea/workspace.xml +.DS_Store diff --git a/rxjava-contrib/rxjava-android-samples/build.gradle b/rxjava-contrib/rxjava-android-samples/build.gradle new file mode 100644 index 0000000000..db342a5e2a --- /dev/null +++ b/rxjava-contrib/rxjava-android-samples/build.gradle @@ -0,0 +1,17 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath 'com.android.tools.build:gradle:0.8.+' + } +} + +allprojects { + repositories { + mavenLocal() + mavenCentral() + } +} diff --git a/rxjava-contrib/rxjava-android-samples/gradle.properties b/rxjava-contrib/rxjava-android-samples/gradle.properties new file mode 100644 index 0000000000..5d08ba75bb --- /dev/null +++ b/rxjava-contrib/rxjava-android-samples/gradle.properties @@ -0,0 +1,18 @@ +# Project-wide Gradle settings. + +# IDE (e.g. Android Studio) users: +# Settings specified in this file will override any Gradle settings +# configured through the IDE. + +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html + +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +# Default value: -Xmx10248m -XX:MaxPermSize=256m +# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 + +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true \ No newline at end of file diff --git a/rxjava-contrib/rxjava-android-samples/gradle/wrapper/gradle-wrapper.properties b/rxjava-contrib/rxjava-android-samples/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..5de946b072 --- /dev/null +++ b/rxjava-contrib/rxjava-android-samples/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Wed Apr 10 15:27:10 PDT 2013 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=http\://services.gradle.org/distributions/gradle-1.10-all.zip diff --git a/rxjava-contrib/rxjava-android-samples/gradlew b/rxjava-contrib/rxjava-android-samples/gradlew new file mode 100755 index 0000000000..91a7e269e1 --- /dev/null +++ b/rxjava-contrib/rxjava-android-samples/gradlew @@ -0,0 +1,164 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# For Cygwin, ensure paths are in UNIX format before anything is touched. +if $cygwin ; then + [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` +fi + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >&- +APP_HOME="`pwd -P`" +cd "$SAVED" >&- + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/rxjava-contrib/rxjava-android-samples/gradlew.bat b/rxjava-contrib/rxjava-android-samples/gradlew.bat new file mode 100644 index 0000000000..8a0b282aa6 --- /dev/null +++ b/rxjava-contrib/rxjava-android-samples/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/rxjava-contrib/rxjava-android-samples/samples/.gitignore b/rxjava-contrib/rxjava-android-samples/samples/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/rxjava-contrib/rxjava-android-samples/samples/.gitignore @@ -0,0 +1 @@ +/build diff --git a/rxjava-contrib/rxjava-android-samples/samples/build.gradle b/rxjava-contrib/rxjava-android-samples/samples/build.gradle new file mode 100644 index 0000000000..1b89f83fdc --- /dev/null +++ b/rxjava-contrib/rxjava-android-samples/samples/build.gradle @@ -0,0 +1,24 @@ +apply plugin: 'android' + +android { + compileSdkVersion 19 + buildToolsVersion "19.0.0" + + defaultConfig { + minSdkVersion 14 + targetSdkVersion 19 + versionCode 1 + versionName "1.0" + } + buildTypes { + release { + runProguard false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' + } + } +} + +dependencies { + compile "com.netflix.rxjava:rxjava-android:$rxjVersion" + compile fileTree(dir: 'libs', include: ['*.jar']) +} diff --git a/rxjava-contrib/rxjava-android-samples/samples/proguard-rules.txt b/rxjava-contrib/rxjava-android-samples/samples/proguard-rules.txt new file mode 100644 index 0000000000..c423b03cb2 --- /dev/null +++ b/rxjava-contrib/rxjava-android-samples/samples/proguard-rules.txt @@ -0,0 +1,17 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /usr/local/opt/android-sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the ProGuard +# include property in project.properties. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} \ No newline at end of file diff --git a/rxjava-contrib/rxjava-android-samples/samples/src/main/AndroidManifest.xml b/rxjava-contrib/rxjava-android-samples/samples/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..6296812264 --- /dev/null +++ b/rxjava-contrib/rxjava-android-samples/samples/src/main/AndroidManifest.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + diff --git a/rxjava-contrib/rxjava-android-samples/samples/src/main/java/com/netflix/rxjava/android/samples/RetainedFragment.java b/rxjava-contrib/rxjava-android-samples/samples/src/main/java/com/netflix/rxjava/android/samples/RetainedFragment.java new file mode 100644 index 0000000000..a3a0c73d72 --- /dev/null +++ b/rxjava-contrib/rxjava-android-samples/samples/src/main/java/com/netflix/rxjava/android/samples/RetainedFragment.java @@ -0,0 +1,65 @@ +package com.netflix.rxjava.android.samples; + +import android.app.Fragment; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import rx.Observable; +import rx.Subscription; +import rx.android.observables.AndroidObservable; +import rx.functions.Action1; +import rx.subscriptions.Subscriptions; + +/** + * This fragment shows one of the most common use cases: listening for data emitted from a background + * task on the UI. + *

+ * We achieve this by retaining the fragment, creating the observable sequence in onCreate (thus + * also retaining the sequence), and connecting to it in onViewCreated. Note how we use the cache + * operator to replay items already emitted. This ensure that if we should go through a configuration + * change, and the activity gets destroyed, we do not lose any data, but simply cache and re-emit + * it when the views get recreated. + */ +public class RetainedFragment extends Fragment { + + private Observable strings; + private Subscription subscription = Subscriptions.empty(); + + public RetainedFragment() { + setRetainInstance(true); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + strings = SampleObservables.numberStrings2().cache(); + } + + @Override + public void onDestroyView() { + subscription.unsubscribe(); + super.onDestroyView(); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + return inflater.inflate(R.layout.retained_fragment, container, false); + } + + @Override + public void onViewCreated(final View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + subscription = AndroidObservable.fromFragment(this, strings).subscribe(new Action1() { + @Override + public void call(String s) { + final TextView textView = (TextView) view.findViewById(android.R.id.text1); + textView.setText(s); + } + }); + } +} diff --git a/rxjava-contrib/rxjava-android-samples/samples/src/main/java/com/netflix/rxjava/android/samples/SampleFragmentActivity.java b/rxjava-contrib/rxjava-android-samples/samples/src/main/java/com/netflix/rxjava/android/samples/SampleFragmentActivity.java new file mode 100644 index 0000000000..5da3b3b7e6 --- /dev/null +++ b/rxjava-contrib/rxjava-android-samples/samples/src/main/java/com/netflix/rxjava/android/samples/SampleFragmentActivity.java @@ -0,0 +1,17 @@ +package com.netflix.rxjava.android.samples; + +import android.app.Activity; +import android.os.Bundle; +import android.os.StrictMode; + +public class SampleFragmentActivity extends Activity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + StrictMode.enableDefaults(); + + super.onCreate(savedInstanceState); + setContentView(R.layout.sample_activity); + } + +} diff --git a/rxjava-contrib/rxjava-android-samples/samples/src/main/java/com/netflix/rxjava/android/samples/SampleObservables.java b/rxjava-contrib/rxjava-android-samples/samples/src/main/java/com/netflix/rxjava/android/samples/SampleObservables.java new file mode 100644 index 0000000000..e2b67b104a --- /dev/null +++ b/rxjava-contrib/rxjava-android-samples/samples/src/main/java/com/netflix/rxjava/android/samples/SampleObservables.java @@ -0,0 +1,29 @@ +package com.netflix.rxjava.android.samples; + +import android.os.SystemClock; + +import rx.Observable; +import rx.functions.Action1; +import rx.functions.Func1; +import rx.schedulers.Schedulers; + +public class SampleObservables { + + /** + * Emits numbers as strings, where these numbers a generated on a background thread. + */ + public static Observable numberStrings() { + return Observable.range(1, 10).map(new Func1() { + @Override + public String call(Integer integer) { + return integer.toString(); + } + }).doOnNext(new Action1() { + @Override + public void call(String s) { + SystemClock.sleep(1000); + } + }).subscribeOn(Schedulers.newThread()); + } + +} diff --git a/rxjava-contrib/rxjava-android-samples/samples/src/main/res/drawable-hdpi/ic_launcher.png b/rxjava-contrib/rxjava-android-samples/samples/src/main/res/drawable-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..96a442e5b8e9394ccf50bab9988cb2316026245d GIT binary patch literal 9397 zcmV;mBud+fP)L`9r|n3#ts(U@pVoQ)(ZPc(6i z8k}N`MvWQ78F(rhG(?6FnFXYo>28{yZ}%O}TvdDT_5P?j=iW=V`8=UNc_}`JbG!ST zs@lK(TWkH+P**sB$A`cEY%Y53cQ}1&6`x-M$Cz&{o9bLU^M-%^mY?+vedlvt$RT-^ zu|w7}IaWaljBq#|I%Mpo!Wc2bbZF3KF9|D%wZe{YFM=hJAv$>j>nhx`=Wis#KG!cJA5x!4)f) zezMz1?Vn$GnZNjbFXH(pK83nn!^3=+^*kTTs5rV9Dq^XS(IKO!mKt5!dSmb3IVCxZ z8TTk5IE)F1V29$G7v#j9d-hy&_pdg8?kT4)zqr>?`}I%W>(?GO%*C&}?Fp|bI*~2&KZ$%^B6R&1~2kA{`CWy+>F-x=z-f{_&vyu_3yp{jtw(*syi% zu3t2|4{c~LJXRt2m>rMg2V_kLltCZ<`m>qcI?BPP?6hf``|e!rZEFszeYQ3f-*nAS zZ+h1$mFwy+7156lkB(k6)!1fUbJCxgIBK38$jj5cC$r&YXN)nr#PY=tJaLc?C_o?j+8H3Q>891JJ9&$l-r+-SG#q)*;r52% z@nlKflb65o%s*Jt)!pw1k{vIoQIvoJ0Y&Msiw0X!qJ)_47G*?aJ6bJFLh_4b$5&1k5wN>du*>6#i7R9T8; z7>EHOV=ue7mo77SJPwER4(A+s?n0JjYK)b}Om6n>ke?0JR=jTI+RFBg_iwb7k%n*2 zR_M0DJ9x+0zxba4(B1y^JQ_Nj6dlP5PGXvSq8fF#mxrFYj3d9(V#jJwt+IqU9+8+D z6C6Us1OI$d8OF!3+Hm1 zW5in zXV^%U35HooOpSmeqlG6e0kUMYNonKp1vr|My9}4-WO+uOxe_c-o&}%voNYHkqtle% z5yQ_^oozSUUNu30EQSAl!Q%(%3G1NXENSMjCL*Vx-Td2~rk(}d z8pT!HZe>1r5EGuz`pgsg@^yQEi=BIa#meLq0!?{TZ}q#}=7UC9_l=w|wv+pP!g4#! zRys6EN$Jv}#U47$k&)pDzvks}LGfPku6P9p!56Py)~1)W(11n7n}`Wx!=;_JTiu#d zpCqx=hEk@t4sp?!j{W}wP@V-=Pd=T^>6IKBy;#mLA7hCe{V7B3@I7Ipa}L`MbF|YQ z)$BNWsiEnoNHrtJli|n8cOnn4NyF=8MbVxgof0>Uv%wM_j94a;8(LMjlL~E(99gJ*2%JtNtAkD@j;^ za~Y~&j6uY{=Rv5S4joH*RW_m9N{ZSN0HhAwFyJNok zS9kx$>wMf%tUi&Eb`6u0lWJ|k?A-42(lp2UmS(PrAc(24wexRiHUieMwf$o%m6$xs zp#-SdBUu2D5`v;(9-sm&kN2M74c&AvKe_v@tQ|dzJ2qSgQHpnUP(iQ?J%Il;Jdyp# z7}cpq6Kdm+FS~zS4Eo;fuO=DFP*UlpO|_CNt5&NUqBvQWxmg7#ARvMf=%#H@p%RZ` zjK$hMbNb+vVP3UlkfIt&ptJ<00Ic{Ka+lF+&w;OEs1O2#V8~O|R*Gq9TIgM&UqM&bZOXBwnbC? zDr))NR&g>lwVgcmnx`K1$)PTTw3m}-T11^ZkY{}jQ@lGD$XzJIcVFkYBBW=o_}TUU zt@yd{Jz;@~72x#!RG(#ira6}v-*J#<{@@^OI-Q2T^}=IKLubsa&V-%WwlF1s7fz~u zMdQTV7SnRet#^`VO0V7H(?59X{uy+S`(sorO@2-+qioUdo9+6r4#|jb=?t50oh42R z{}I>Krut|YKkOc|O|M>y#(3YA;I(i+MiHSfwbJA$jIUr$Y2i|u)*>@2eUYk`j4C5r z>61dKu!AqM_E7#DoDzbd-bfT%AYXUUB{SS|{b{`5^?wz1{PVQgTlvyqOX8(#GTz(U zNPhnj>$lC`xaD56`TjW&uW8p~qikP*F8kHFM0frzdk%UNGjb1O$%uLK`0-)2UsZ3L z#+j+CI_8k4VslL%$aVR@joX>M-@odbX!os$xY$HDIOCokY?{Q0v2kQErf|ZlN>D9w zC+2}E&?rDdi#%))$p%P4C_xGXu=@U~_<|V4L|{>TP$XBp$5pCPXLzK3!;gP>7=QNi zkNOur`>xY=@VSpB#LsN9JKpOz({ANcdv>?K+D_*_HZ<;9>kplj^Ph5!e&&a#?(3vK z_Q@}D_M5kGcx^AuaI~qKYUnb1Mj-n;MURXa)+x7~e2gbMW|gw?5Rg zTOMlo>6zIJ$VNVgn(@kTSL0eP)nR35IHpoHM2W#h6cNmTm@-9`dFJ$;k(S`7Lg@RY zp!hNmb9un!O4Wt05ANDGirv(B14gW| zwjP}C9bK{J`qZ_S2o)b`RonR-b8~y8)$H0`+gg6>#^wu8eCp9xA9B>>8(KRizI?+^ zAJ#i>*({qM-c4gBB~5dzg(wj!HA`hkh!aDl5>u&J;>2K#Ax2)2wt|L!9X;(=*jy!`r4_FhCBoRxNjXNv(~jGQ|%<}%K6RimaBJcP0v}oCgRN3B;oiM)opj? zXm;;tv3q-yy}NqMOr^~3&1lW$w3}UK_IT2sCrkYx5$&6e2A%g;QZUX~A&L!2rFd0p z5%men@^zN_Xw2|v%*c2|wQfkN4r6u&k;LxYY+w3{KY#cie)!iz>(yAgt=&-+Sy2V& z9BJxI+VMKQ%dvY~x>gmEijj3ss_*NAT(8d1@DQ6e&#Ln&6Qk>wHrh>;V2nvomC`8& z(w?`?*_^3u-TJrMzv2~7dH(XLJvUOXk4U8oW6Ol)YsawhIB{GdvIzu1hzMTrE)cvB z%2GxMpaF89<9uF(?cfN(BNR?wwWvCZ6e62+G_{$+;`yjgLj{(^z*zzwd;K3RElb*%=??P zm+lLY0@Y}^kVdMYX5M)YJ~8h=i(S{q#NfU0xPTao4WPDQL=Y_;vg=p%iay1_`<0Ga zMG&<(pOU+bI2u9_g8IJBTqGX*3@G$Zc`pj0f@)vd2?Aj`ms>DHg>;w~p}HXV(*VJX zphd;fht9qL3E)D8h$$A;SGl22Ygv>`iU=A)z=1ZYN$|2`*$`R)?KD>$tw_e9h_x~eX_udS~Q%yz?48i*aIa+_wx|j{B zsG7mwZ)6M3dmvgMC3K-66;ML(9o2xU!F8+qF)>v{1;ip)6v_I)6law|rd_Dx2oV|n z(Qm_PUnTTuKFG)w%s|)lS!w~Lm$k|Al=0djocyHU;>1H=!N}0E0lSV^b2^6~^lUco zyoH+|_!li3#euHd4TJS8=CLaHG9H8g&h3Xm z#>BkpUBAmae(#)qO3)ZMG3irM=5IzA^s+)w86=tIMT{&?Awux<(k2>U#n`c&@Z?u= z%=#BoO-9Nc^?)hz*YW~~tU8rLR-MZBJsY_7fp2r~mY>q-O;L%5Fp?}V6CK=F(18U3 znxB8ZR0TT{)T64RDt!+yFgp!JXGP0|It0Hz2Em#YfRv>O>8A?J=Sz!nq<|{&mW=?~ zDQT{S6PH0|jwy37t+0Ob6izz)JdRlNEUbyk>-K?}FOT=Dj9SuS_0nTFd+A^D?Bo83 zTkicXcW=IuZoZd(Dl;&#`LI;_s?e;OH9quf?*XuV0O$Qh0j~HWKpA|PXV4&b2zs z@W5<)dtovIRZ@gvsi$^s;v05(XwF3$lJ;wzYfE`46fnT7>!qt|hWHRE>yQP)i8= zVbC|O{Ud6%kwGcch>>|pE-=?cW;TDR0lE5Nw7l66lr-zIYT3bj^ujCn$b0{ZO;gwK z#}}W(*T3~in$6ZCpbB98pftPTo;!K>U;H*7_}t4m;;4i9#^2t`pS<=jsnx198);d3 z-M6Mx{7-c0A-jhJQ`5mBy8TBnfbr2~sER5E5oz}=so34cg)GYarRWi8w#W$%G{?Z*4xDb#LX1B1 zg!4G{m~*)H_J8J^SNt`XU-fxjea`>p_$Qyn*Dn18*WdPCp8oWw^XU)%kfRQHMgfQh z1j_ua@O4G%QK;&YH3Y9(q!hkgOUCkcVH5N0Ug(EPX%H6qCfPqg))qrd#ec^47dBu- z=sRkmjGS>3K(tfRTo;zCXO-74hV;y1!vCN}v|w?AWR$YpYXs@Dr?iNLKD9s|2)0aHY!TKTYhwMI z7b#54h!H6rUU9+xnL$g6h?t?Li5guXPY1g)$bI$~rHWP%QkYJ6Y-U^0C(@*$ruN2*zn0QRBOeVpgMFbT%k!Dn1*u#%J^y)enX1K;0~ z%3Q zP(b%}P!Loj6M{v96(Qa~K!bq-V-P89U_K)0zHC_F#L==3IPh2hHG6&?rxvQ%|EljR zfGIDyu=rIrl1dyjuMfwuh?pXZmARwNZ?GbW;5BH5D#nN|WbGm+UGAh7_AcG>4&|{0 zrg?k@h8zm!0A|5Zo%X%g|2tBPKHHB6`~4h?I@bepDe6?^f8w zBnzfOf|j{kR5m6BLRr0$!RZ$PHSk*)tyjkws*DpyHIiiL*8o(Smx(OKT7@D&Y3OI^ zEUMtKa2*SLjt(eJsZsLsrgV`A+xL(~JN#JU6+L)gCe%VuSNbCzTr09w>eZ#779SKV z)m)@#TNVy|q3Tz_U`^7MY`l}`GU~OlQi|*cprX?tm@tIV+8kOGkaa=9Y<{N|RZ)ns zHlgnz2S%qwK9wXjest~Ux$YNNA{0?6Xpv{_mqYt8D`g&7Yb~>lX+HP&AK<=+Zl_kO z6a2g`^4=9W92GQ3e9Mk6?DlzlkIM`iOzwk*5L81TcuyYkI-<3^@49_+^XC7&N}SL1 zh$kIBxb`9+v}acfV?FQ zN#04eHe0*j{pz=zOj3#EHLrT3e)O;3xqpCWrl$e)PcD9jQ4P-8_zyZg^M7i|*kOuj znsvlwNUsy5+01^P_sqMOjXjxKwHn4)$87t-MWZZ*5Dbit4|D9vL+spsJ0JPd?{Ms) zFW^<@yqjZ=IvG%$ck_Cu9|b8CvoV%5P5IZWzs>i4`~`N+-p`7a6RbLHJ;nxtSB#Mb z`1I552=9DrYWFNZ{-=Mt;SVo5@3cmv`IZT@@>#~zCe-=qENxsn+uHfL`e?SbT3IQ_ zt~e)Lcirs_S5^X#?hDYmgV%8QQDe+?>*1&0e^BnaeZz(&D~3<)#QuUL8h*NlXgtr| z&a{_Z)o9FK_U5<0!E3N|yY1P2g%J9s*?!zF78+NSb%!ix)tbQ09oO&|U$~Bwk35^- zec9VN^xz{043e^xD}WEmzh8d^-~Pd8**bEfd+I?HuO~n4SksoN8LRPUy={E<@BjRMUh?X71Xaey>t^$&Eq2B7)u_r$ z|IQwpG52G!F$J5fRo1LqLB7iKz_!bI@27skX~+Eze|Y}IBuRp?hR7z|eA~7B<99#7 zrX4r2a_tCDUb_}Cg)g!OEVeJ5AEVRyb!9~f4OL68qhZZRP0l*>MdkxvxXeGWx$T>+ zI^X!wnYQDnwK9?i)j)eLXJU2Cw>~>R?72@MecvT7;h~2gATow_cbc)$Ws+xNSB{++ zo^tTp^y*(-Y-XF=$XyoBJnMN9+p!Qrep1)%ym_v7zZH{;u~L>T=4XP!f^?uC4ULUR zdl`>x+DVkHVd;|9#N*oubBFQEyRT#UK^0c7T}l)eEEFS)qvZl%f>#I;iCwAWb=kW0 z(e#lm51o?d>D|kgtTscVQCNDAXMAjxSX&{_Qf)T((wMHWWLbz6WpPXP0(3_SBWwI19Vx?$i6WUqP$4O|wjNbYzst$z{58`cBhm z&F(N-KeXFzo#aC|6BbC($As#B8X=}ggpDyQUp|Q>9cG$47#>TQn%T(eHA`5se7KnZ zF_dj_6NN0xS-oZ%Nj%PTpK=MC zw*4IMGls_v)mokI)Dph*pD<)7prEF|j6I$2=XF=Ua3z;BN^yt&H@G%7& zWnL7*e0S9svjSP>kuc;VCbZXUN3G7D8`G@!Qnjt=p=7yC?QH0tsa@RsuPMLj@wf-c z|LV)H$Auga+MTAU#>)eeuh_L`!qC=Ls|{m}Cy)|w6#aP}w6_-ya~9LF z{dQAPa-|&ME858gIK=}lVK7MLT~Oye&UM9y?0X=8Qmvb*)=X}iv%Me)Gqav+FWdGT zuk&#ak~?2Kzf}w)xZuKGx%+`1?Ecoq?*H@EjFm%C6OT577vWKoJB z$A^sIasm!5TGOFFGmHkKNTE7KW3nveUq1bt4Uj)!1_6BJ zU6=EoPrjVdk+pQX+j-GTpQS&&^43tT43kuRlvE8fGdYc!1|m)3WCuwlqB>NeQc0** zYE&wTj*QpuPLfJ)j2$(`sI@k@oR!^9d(3&Kd6r3*<)pooPNzq=)1%#NQ;nAsF*5VR zOYXQC;B^4*Sik--jy?J`uDj-! zSep}9YT4*SOrT2I6MF4H+EZFRPh+}^b4@i8OYk9Y&86o*Y4(`Ax1W4#tX^5m6LjZPb61LF2?qBy?B_?1YE!nej)R5c8qG`2s_uF`Cu+ z`X_$#2Ur#!Pw0WVd60fYG8A#y55LDyJ!Yt$5G6Efb<6Nr%-BTC_|llMB?%*A5%rOX z`fyBbD5g@4Ns^)P;F7zjv{t6u?k1J0kR*v#Dhair3iXjH^^qz=!xd`vm`W`oN-Wj_ zNML7~t!rRbc|9I0mUjpEgOJ9XGg2;vjDZ;b~V638P!uVuejytg~ci-I(n9#M6AR=mQG0YjoLKGPgFp(jS4Pn7UJR)Et z-8ZsqWsRLXri#f_BSeWIat3P+Q3Td1#ws={2CLGpDdvrgP#KD7 z&SnaR^#_Bsq;Xt;kyI^}iX~1WYzdHamc$tH1#Mz6f<2(WuH^s%^yXK78Gyg}{;LNA zoW%$)#R!a0wv&q%qj%+~i3^k&1jY!ljfi82Vr$~W5G6u&$Wp0VqR3*bDIWLE4Y64K ze08)CmeFrq2>QGFSDAk%Rhs}$r*rJVNuoO(~AJ!PG{T~d_i(dQ;OsQc+q&twwlJV|`Bv$N}R$K=uxCPyc!RBBXfRjRcZi5yAQk|YKj*>d`|Xw~ckP!!SW%^gsH z4oDR1AJt?S?}B;<&e0TPFsNAMQwxCt69o{uA>=K^qd1+MST3tptj8GHnN(upgb*ji zq`i%b+{{=o7ByB78@8!x_Gs&uqLOKv_6{gO2b4jbc8YT@EEzqBp!v_c?XXFx9Dq zb{!I|Nu<;4kZbyl3*LDg#$f7`nKwT9p9|2|t&fmAe64Of^c3TKI%Q?_^+uxaj|?xL zw5U4G#YlpQDngbfM)q85qt=DJt|y5nG){VqE;V8I&WBCAH+|pe@QT+};^BWB8(lGB zqe!DD7GqI`0pj%h;hm z;n?F&(5YS1X4{T?Hf24&;~ic?rDC*Zgk;*ga9b~Je`?R%gBQy3U5$!cEi-#s>T+d# zWH}Mbv|6p1R<`wiiPB32Gn*u}EQxC^LGJIR?H}~g*|#s5IQY`pJzcYP=0El5RWIen z8*k;5(^qldFJ}(enhxl1pnB_vPi5uu!@1|-9|Owd=%J>WPwQ>dkLW|!5WV<$<73Xb z{0CRJT1OpP567)vYea*J7*!3_M-nC`C)l*@dKzsw^5El5v)K$c-nf?sZ)?i>Gc=yt zg{xL=urnv{!j}h=hh{KFAjIS@=h9C!xJWW@nmR0Ns^Wrk)72_X;&VM@qLNZyn;-h1m-)j4PH{!#b7fObo=TF+Xw z)_t{JRqgNW{e9m)=MZ*rJl6A%IHK!gcqM)U)>TjF8ytMTRLpN39jns9J?@oOe47l4 z1dw7d06;*nuu_+V$6Qs4K>#PCRHVFExV^duw#+4>?(j) z*AHP%*L5@qEpM#j?*@5nOq@HlBR^5M@^_J9)U!&MV7N?QAAfFbdJaGWPgRws)6~+R z-NrZmx0V*7Od$!{dkY1w*wll3j_1b``)C%NHS6N>yBU998+?y%)4SU2YA} zA%$NKSGVi)4!sVH=l1lla~XcBLKrfnO2~CXCa>$GlX_p?dYsM`3%)hidhs()bzlDL zr7zEG>kK#SwpW`1YyR;!pa1&-`0t?)V)3FnK7V~pCo%hYIQUj+f?7Oh#@-(|a?XKA zr;?n->{Mx?{fOYn3n4;UD5a5kBx9Z>DQ1SETOzUjjZ`HF0&e`i-6T<17qM|ec7?fBc z;0k&%hz+o?+KMG>1)PSqUSqTR@!luCa_YiGo3TkPUp^w8T}r$YFf$gPyy|ZYU`={9 z3c4MNG|FgE6ETxVuw_~St-lefEMgF+NTdzZD8wWJ0s<69@frs3IxH*_A4`(dIZhJT z)TwApTxD36oOSS>-?;UKV^n{)k!mFpfWRL3*Rxl@V_bS?f`4@I!*C2lX%(H}L=`CT z0BxGtLQ@`yX#0U)3`bO@9NHBjM^*Gw64K=(1QdKEK*p+u<&qTSoUzKhfO`4Wz>@z)uK^Aw6m!k{QPq@f~bd?t)6?} z1bJ=k7!E&fDxUmP-(QVQ?F@i8a-dv4%Gg64haX`yNv^E%Ea<=YJ4SdqH4e{1~Sk?qbu|M;*f zbqpYh(szvQ9ev=Amrj8q0@9+|SbxTQw)=Lr&Hm@e_hY2mXXchai5dBmusvCYf%>!X zK>#8PKtTjx&+y*EIR|SkT*`=|2>VPq0kb=fM~F#u|GG<9sj?zc-#-8BqmC*-%N5t% z3v1um65bJjO9}`JV*qzjs9O-*vCma1qq%z0=Thg*sPtm8u4CiyU5H^JCTU0mH2?_M zGn{jci{Y)p`kvomV&MR6*th{{opqpyh3Ux4m)!GykUSWKMk@t>>SyNTwj2L%XZ{Nn z>Xv_j0zm+HA-wSFCJ4n;tqux{Z<*M!+ghP`mh}};q{({$d;y{&M#518E{~{H2e(KJ+~I! z(QA0${wLzt8F#!r1DoX%bYVIIT!6Y1 zJctN_2;>9AahjEz5Cm@p&;a2*ykj`$0UrSH$QJ^n3By@S!UCJh5jS2|HIuruyXF34 zRDv0v?9yEOYVFWR0jftU~yzAQIFKu_~N!vxLSpD zIxEmBpAwnRC3gEyg%Yon(xeEA2t*11fhfB~8i^HvMIcQOp5dF9V>l7DZ+tS31TC`?6B2!P-{Ai`NS%8sfWFCh_# z2!sJ<26G0;dxnUBNT3Wrj-j+52u(2zc*4ieoxAxfi_hFMD8$Dt*t4hHU+Z6a>y4`) z-dgRJ&wT2GICjQeJ24|X4P=?_kA+q7QY|L{F) z>E#!CslTU!sFuPzhBSJAZ4?NAGFdr600O~tQ;`JDd9Vkv#1X>KptUV8Q)hHgp)4=n zf7k1aF8a|v_e`5zKCDz~Nuz3ARYohScS~Kpws!0=fL0XBO0`T-YycqYn}yY@ZV?g2 zlnDnM86|@t(hM=mC6W&G)j}8N_Fwtr#>s`2R4qD9xuZ_o&BU=o5&`up5LX5DnnxN7 z(!|510_PdtJ9u$`Fq8(A0!#>KLogu_1c1^6@0sdRitRngzWe^er2PiAMIqpkE7Xj4 zqSD0i@PNn2cHaUJ;)tnGEM^?Y2OX%5fOPNhi#0IY;la!zy_Gm@B#Lw#(Mo_^%= znu44{7-|HeMy{k$Y%?&%Kq&>KG_*4CK85oRio&-@sE4y2Y3h;2*%j9ragC&24JaC` z`!uzlS%RjYWaMg=C2{s!Ax`QU03w3c0Yn(2{;azYNJdU3mn!CrxI&4*JCC^T#}y}2 zA`QzFa=EsmQ0RGvftbU zQ>{c90A|-98)Xj4nT0b0yyJf8t%xIraRd)QQ&z*I6o?d@PmrXe$eT_q-0f@}wCCAq zEl$Ss8*j&&jkjWZGSHg|Kx;aNPWFa9~0$jGSbWOU>XjH6xDc0w(iTEtcE6dO3#5TC{ScvW=I(b=Nv*)M5VtC-7j0@OiMO};u|K_aA+ua&Wy|G z0O?p6>sL7#>4bE^@$`cedW&;pHYGbq)cE=gVUygN~?!_hF|0teV`9}~ml+s!M!x_o7(s*;* zCVc-VU&If8em*{M)JJgGyiZ}QGSUDFC<*}~u!v@1)yzPXBMKoDa!^zNBmjHLN~pCo z86Fi-BjwE?n=_NmIA?K7liV3M;v_;xTNl23?ow=ga}EA*-%{NFA9)Ej6(HYiJs85m`CL9ANNz_7Wfw>}W{H&o zhy)^>0cdZXg2B-WvL1};5P}FJQvqpeDFK{}*W_F4Q?l}yJ$-+C<-Fxs|HfnZ?SC!9 z1CQT|j+S@fx%Cg={YRgO&z2Z>i~diz*O?*BnAkIbU{QcAP}Z33z=$xNR5+KgfMs35xDG&i*Vb0Kg44zZ^zZ& zc>uXE4-p1))`B-&1MC}R(r5-n0MAaC)!S!3D{E#4D+*c5&ME_7bO-`vnhuJ0%rG^y z*MSI{U{o_J!WqGvFVAW?BdzlmMhBQRZ2?B+Z$U21!?_gN1W=^F4PGQ^jHW1{`Cb9o zLx~8DXBkZ|AhymqMH-oHxQxU~>&7f9WD8o#QYOvxW(yKUdVH3~XXbxdwyFjxt+lAv zZaWSag=@ z=8P$&K}1lbY?iX@ee4?s0wKUBJ964=H$0STaA3T?n~R$9CTTo$W*+}*eEXdRL>ghx z0ulvhz0Z>9A)>e;5?WE{3wn~(Mxl@k5Z8vY60)g)Z7AM`NMj7L0~nqG?*MV$0cj#* zg?t%+Zb&IZs~iSLH{&P2T8vGbH$W*3fW~XQxiirODk4xy!&-;m-f<)T^zbbx6J$2bI!+g&Q(Tb>mTpfw(MhPbbX*24YD+xC~pjzlg4B?I0>ZG1eo;$GZ-@3q)Ayc(TT%9uB8CcO9K>t$rJ4+!Ga!{2blb3*{mJ?rAx;e_@g zW=}sb8SURhsg02gkr06Qo;))H{@ois2J0*E-a_ku;$#FwS}J2z^z{y5!Tf{u-m?$! zW7XmPw~xK}Y|U*DV-zVxM2Z?xn6(ROnxdy?JIXW%Qzy=WHv^~-wPRiPJ(xPPjP?m_ zU@!3AH)Mt2y@NuFGk%)cvT4gxH~;vV!~gKarE2vv&(f8P@Ag++xft8kE4o&xvN3^V zhgKTPzIFc&iMV*lvDmVC6ReMr3kzh>qKs;xT2uwI^KCQwiCuxGcI>;nX1mYH6|D_I zV?e$kJ`M5;L7M=zY84}cF$$#|Dx-Bwp4xT+U;&*D<@0j8tMo%x5%Tg?~5R?T=3cv%@lt|5rbf!U~$$KWHR3?Xk zu&I|c5%P}XIIb@4XrJ=aC`y!W*}^Y88R7A}hVa+MJ05U+?`P+M8rvjM6j3edroqA2 zxm4Kuj7oLnm$`fxbar$}K3^bGfWT*$Wd5R*hEfJ52%w-LATTp*YNZ}ksTNg7J=bnd z-Pkqa!RO=D(kYB&|Wjqg0rvF8kum{NfucTYqrP z`5U%u**G!G6{S=zQMp`3K3_yWUyzoz^2Q(tmC>3+s5Oq`4(BY=)S@2MFgiNo;u?&k zg`0}`37-~9P0%vHiA@+H2!cEy8o#>wuOImB)G_Pj7yce!TXGVt#ORn z(=jFB*q2Zp6$}lGp?}+$um^#4QjKaSEI75c$z6AAYL348>#uKEccl>fFbuUZ0R$d} zZ~}6sT!$|qC`YPurgrtQ76=RC$YS~T-}$t1r_YJ6x+vSq`|xwOl@gGLU>BhcFBv~FMie-ahi$Rz-LINpu0Hu~Za`}LYEdk2y0hQVU6k7}mB|~9e!x(}I6ii4k;VvE0 z?|KG+Oj%0Bi3m(dlp;$c5Cu`1CM@ypLV(%bX9 zr_WVSKiJ10x1!vdPr`gLXF?@f1r%~#N8UkH?XgO1p%e>?-DLnfb z=86?7j~f~sKElT8lSw^&-{|PJ_Z)D@o-cw6^yvN1aY@hS38meM!r|M7s_XW%93Aak za$IUh=gpcu=jzR`4$^18^F8_11#h4-#Jd^}{s&{CB`(>qac=+s03~!qSaf7zbY(hY za%Ew3WdJfTF)=MLIW00WR4_R@Gcr0eGA%GSIxsM(l48sN001R)MObuXVRU6WZEs|0 vW_bWIFflPLFgYzTHdHV-Ix;spGd3+SH##sdcWUue00000NkvXXu0mjfB?gph literal 0 HcmV?d00001 diff --git a/rxjava-contrib/rxjava-android-samples/samples/src/main/res/drawable-xhdpi/ic_launcher.png b/rxjava-contrib/rxjava-android-samples/samples/src/main/res/drawable-xhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..71c6d760f05183ef8a47c614d8d13380c8528499 GIT binary patch literal 14383 zcmV+~IMBz5P)>IR{Zx9EA~4K?jU8DyU!%BVu|c#=(H1 zIAFva(2=Yn8AKWhO=@Vm>As!A%_mpwu-+fLs?Ir051^0kZ=Q9(`cB=t=bYMm<@H-@ z?@QQC#}7(lHuiOKOg-hI-&yJQ@X z>38Dx`mgcs{{O@!m2+^EdNUPDF+a6!8!8*d@!BI^jeED=gH;btqEI5d{e*jVDP7bq z{q~MSBE(fsoQg6}7k95+Ji!s3$poDp-qlOkXAwnM{3JB1P1P!!MLkm@C24>Si7~v(J@mNzG-t<6(_#~IP~Z}QN`;~#%u^^ zBv=E1KsZ>EXwWhEA%MjWSj+&p1YiKMScFGKjPH_0g9QS9!hVpahud$BNHq6km8f&$y)VmTQ`qJPd+?0zVd*nDN_N;fDC>PCKgkkd- zF&a`~zS4LCy*S)Om}M0r157c%Vz&|}g=6?|;XWKwAQT*MxQ#H?lrYWC!I5q;pTUZZ zoF|S^mMxt;_qPCIXf(txX5a0Ww;uk~=vd{jwJXPI%UbvK`FqRT9{O`bUiO)BJM_2% z(XOY!tbcIB+EHv;)4J*BV9|&y5&#Sa0{{$SB&foHK?p!lAcP=9mJn^Q zEdF4f`u+CiwmYVjr%WuN^Du#n`yU&B^3IJzBL_Zu-$?zTyBfz|`{R*^-t)z|a`kd+ z3q1~f(k6y5Nm3x1Yb_kKdg+KYV*sjIe!V z{5>Bz^<6`n@li*u;}T2+4lyJ`2oxNk906cBFdVfoiU|zCpa} z1i&zeF@X)3#Clk0*p&E|Ev$2}*1}l_W2{Z$7(q~!&ar*`feE?ciQuhsm(q`Gl}fN+ z@eJbtu1z-J9Kjlg^G?2Vm(yjpIN`_LzXAXv^r3($xF(p5y?b9P1*F-Cr~YXsj=g)| zS$n>$x7f>y=ZgXCM@>wqVLVI>hXL%1sn{O{%!kA@0KEW80E%#MFwm*p_a{B zD)9ll)VtgP1B?cSF@g0+Q1@mB1{Ma^85pZ!tc5iO#u!-ZV6}xY4oPBJCzg_?K&wta zn%L5Rj?vAeG*Bm!j&+Mc0?>)WhhMvFm(gdJCt~yENoevA*5h{EDh@*#(_{(r%m&=? zu|e$lr34M$iU-{w?Joo(Y{qhgD4~QIkSM}}!O$?MLZbI-s18e=OF&ai&7-M0rh0zYyI+(=47^@pK8?@?t)yRhO zzs%pSswcJ+l9+kcqH%0n*9V;dpM3NE&pVBFsSjxAt=MWGLVz-sxL2ty_6bwL*y%l( z^9>+yo3UI7lth3j7{MAa0$2!WSj1?ejxkiQ4K<7-K?@ef2cKYAaNFUg(T{h&499@8 zfO7ildBY909A~mi5d(n62vetXrh7` z4HzV;U3Zyv?>JqX@EIcrL17PGz;pl_gtaW`qV2(}?K z7!zhaTCssiN~pzE)ZG|bt^v&&Iw!VCuMKp5YG@e$;~cE9-qBhIYucx?3~Lx{30fye zS{fl{!|4FcxRUz?fTWbfM0}x+#ep9=eVP@JqE)w;wWx(pTzXQP1!_hCDgS-E@^?9S!F42HJ_S_#uc_5Su zs5YV8=8;EdD(d~XBf)i7k@eOjOu}f!6L8G}mPQ{ykK7Z1=*K{C7^dQQG~*hqW*BXt zwShMNOtkjDYl9@w(22=Uqtnw^7;U{qm`pPmt+!FL;E8XQ{Y&G*#ZExj-eADv1EkRiA9p=HbW9mXn&pE zx6s<=(T*{$-anb}*Q^f2@NW}!Ypi#4-44eZ5;wFGR z2l-#ffa_PC34p;4_~V9Ch1H=Mop@k2T=ZsZ95ER2~w$V2Qwf@K~R83 zvJIQ6w*fXxCEOy(CETXcuAvj1GDN3@H|;ZhZ>JU*V<1q%=E-}pVf-!#5kQI%P6I0* zTLpFk*7~tCJ3&MYqC=<6ZM^c6Z@7>dv20Zp<}9uM?_~fH0U)$$1VND)+d76o^q=A^ zEr^rEHJg*7*_`x*)CPi!7_L8n$2VUEYYnzlmg6rQKZCm73TFhg)~N(r7^9)J_GT#Y z=E!J+L>qrUGe4>H>r4xD=7=p^O5i)6{5&4r@Eg=yoNE;R%JeoxjiXN3-XX0XM8Z3x+2kseod+K#}a>@yV^%M}^*#iQp1F zAst%zV+r1|H5(QIra@x@LRv&YFN9=BDFGr7sAH&E#DX-22b|;do=c^e;n;zlgR|aA zyY$*QZ{k|5CRq1iVqyY?LIkChclb`g8G$6Wu3oE&%0x0;uh6maSl?4UGb=(U=b9CT zAAD)W^Fp)dRRgSbAYouM5g5E}`|w<2-3dk;YPD)2(M=f5sbl0cDunQcOk3Ku&N5x^1FSJ=M3mZon=-*VILENo0tgU=eUPES)PX*zAoL7o z=^+bdICcU=mYo}9XOEjc^IkZoMNjft0EE-uvH$-*2E<7n^$EZlD+Y?kfE~ZUXxp14 zEf*&Z@EgTT(Y7k=$iK(SA|BR=ybI5Z(;@VwCMZ!$sa_=8wT7h@fN5QG4U zvlvfCab)odtTZ3MLn~IoCYzzuBK6l5SDPdEd-X-eRX!@EFbu5#2NG>lLPR;HL-}yh z`_wi&MC5}HqLgS1BLC{41#goav%lv!HA~s6mwsoR&nay7yEk7xf5)QejjzT(&AaOVO#?>xa{z!6%4qPn@N-<8|7}ThG@fYqze_s}1$89iq|O`10Jds> zYaEiem4=mV>361M;_0g=f=i>8)OmJ>lG;J1CPwF4k%DWP#OL>1TN^ShV9rgEXOi~~ zo@v>AmuiBAwT9R;XvwTawOIhrs)H{7(gpbBM@FC!BA{L{Kms92D$+oBAOK+VhGBg7 zc3)5U{+-ADeGFL39|7~7nBW-O`9f^QpHak8ybYhG0{W>$Q)!!B3u9_nx2~CC?^LgC zw{LpU1qHTp&{+jz9CbniodoVWt?PyotcB^iXFaoWV!JN0<83{suyab>OdC2+=C-z^ z*N%~DOvW?==a`rY)^SNHJ^KfD&w!Ai3aa?hC9_FWO<7cBACBb`&gR+lG2YO;P7w)N z$40Dvd?O~u8W0k=P_IuBrh5qCR6NJtRo;Uu{YcZwM}hWjy#XVYoCUvLpd zn?q7ah~9Dw)-ffue$<-Vr!$MGYy)F7V6=nL-sT&_xx^dO37}>6x)aZ_usS8a%cMPf zzwKh0F>OY;)b6|VyE8_(G-_&JBaQvN3G>W?H+4=hAT(PCWA*%fj=K_LBQ@Gqt;@M| z0ZT|@FlvE~(|`wNGT+_rM8!xctgZCX?71^U5PB0x1YCU0kH~j9c;9A zYgg6?07kd90N`nW-cG@|S^K;O3l@!{FPe@H@;ShX>*$mw_$j6^H?+9E=;4JzVe!A@_?7{ll9hUq1mbgaVweTVAJ>>5RxDy zfyg`1+@W^8a!MHF63fmz-L`Zicf>A}NqK&zoP2oG6*0z51&Nt7Xq#*6oY5hmlvF>Uo>Ti(<_Xtp)F~;ksPsCeiHJgq7 zn$5=R4m)V>q0WihPCt1@ef7GAsEk=IlmzNki#xB|p40kiCCT4D^jduClFfL-Sv@e^ zq6;hk={{Bbz?2dOzty0|8!a3{^g%#iL_dXUZG5(F%43_g;A~0i{de7X?|+~1_Lqu} z|7ndFoN~|&f4=+SEz(T;R$MDCC9*6F4U%CCGKx{`Arwmi!h%2$3aF4ga|D3|00Km= zqm;J_I=921Ib{Opzk;3UNYv8Prgq*kOu|TFhq%dTH7uHSz{U}59Kkd~#0`PT>R4;r z*3qB6=(O->fBDloG%$^<-m+w9!-M}_oKl}V(7!?8r*DX#7%u# zqiRa;J8#t~r@W!xW`h%=JMerO17z636 z>Mb-fJc&3q&`AQ4jHsXxMuey+Q78!%N`#<5P)Z>xNCcroSP&p$2q6&!5-MaMt^Vc| zPeWE~7&-y0wP4542_uOu;-<%xlGq|?IJ|60S##{G0sLlSv?cqe2e#FWpP2z*0cQeKM=O$hoZYsudfZqvbY?RiHsquN31R{S z0>CNg*igOhM72^+CdV655EMRErtjZ%@l}86Iq1lP-m}kvi!p0H>ql3u3HDgW*t#yn z)(sXTTY<6dEliBY7#@kytXt?9ND{yq_^zwxbnKYQFtUpAP7eV{38;XeLZDCx5EUhQ z`T~@D6^gwAJ^dOzQ=dY)M{-|ZKNTkJ85`G@zCy6ewr-p}R9j}CAtu5EK^OvzHZ~P& zv|0v9lWAf^^R`XRg8}?z+r}m>+`HE&c+bRu=EMLn8`!d8f@lwkiS6ouM!Z2XVnZZ} zg!InY5u5{zwn$nAjYgtc4ab!+w-}&k-kf6x*RNUKSE+8n)c*Nu!QvU%V{eOMG!^U^ z^=1XFra|0vXw`w*q(;4(pjowO)HLd~1dUpPxMh*F99k`pjQY$u%^949O_Q+9JP83v zMUYBBDFGFD^A;5(!h-Z#6%nF>M4==R6@+I-Kv03VcSd^?Rj)d7Y^-%mlES^`(fP~X z`^AHcjk>1VWK1eFkTUTo1_RDGXzjddYd9n=qGp}>?Ju|ouQ_`GKKQD?;zM6O@R=Fl zbO;b5X+)SoAHa`qeOsYf6CCRVQYe6QZgVrcYP3V#vZz-yRmNighLdVfZ>5UU7AU}H@0rcd5CEg?Gc!Pt!ZA}W!(}(TI#qBn!3=VaL7hz@xpV7?oe3bJ zdJa5tR(}-sRpORy7`8oOBALjM3)zi_o|!!u`^Dj6v?Eq9p-V)oXiw-F^3s( zGX_Y(8W2ebDg9`PDDC6-s_6;lnFH5NW$#Km9BhYhfe8eO#59oT7@;ad$pDTmIw`?u z19cu|KzBaC$g^SR+Cs(-IW&>YlaNb@;PybeXpvLjKQB`Nk&PJuv}<(Jc}K$MQ>Gn| z$j(4JpIye)lw2u7sf`AlXgf>mCCs`G>9a1yW_B=TopzMlh^Axq!)1v$X<=+~8x#*> z-jo->B!r2|b{Jy-R_(+sBeLrzen!~LbaDsrokMPDIlX2NOL%&ue{6q$N8;E;CZA#w zaXtGW05mJzGXFnoKn@VMO;}oV$|Z`snBY<(k#9wosn*!G84wn5zQ5Mn^z?hY4@jTm z+FIb!=Tn-Mwc{J2UW1DA?tu3mx$H*`L^tI?Z91X>{FLJiu_yR&#Cwa5{Qs25|buw&r+a zojE^m|EX=`vJ8(D3BP!vJblLWa-a&W_FxFPjn3@1OY0pXv$fncA!a}d1?L=MU4hmH z1LeJN+<~vh{tHh=Pia~%2s5VciBpgLERGs~6PB<3Z#=sGT1+;!BMM6hgJMd2(`B1G zCAU+_^WY|py4pS^P4t{`%*u!2sbEo;eeC!O-<3yz@6H1}2KFo(&|%a3@0C;vsQnCX zzb};*4=WJ>mMS1Aq-4&K#Y{ajtx0_W5yE!VDZ{PF;$ZANesHv+rAR|EeqT*t+X5T3LfYMTmlO%4pjaGG=pN&O+S| zMsyICJZwfp6nV*ZkR4H2Zk*HWP9M^FIM;pe=}?3SQi=9Bog~@tlSH0yWISNUd4!S) z2{Tyhn4Pu649X_!Z6KweNkh-{b0j3?N1!?Da?|o37v?^|T#kh>!=~ zUj1WZoFtOH{yC1AWgdBTa-i*yI|7N!S>st4(B@EHIuvcKXb&N-H!g^JRGvOpLO^F|o(F{~cf1z(-Y(%2 zIFgPtZS5lWj)P}*sTax1NZK z6_m6>1a0l;kd}PHOh`-<{iOw1IQT+b^!>Ns%y%A!>;Lc@z)46U(~gGc42^aj)>#k{ zq*SO^8~DLbzkyTE+zXfe_>0(Q?kSKc!dQdOfFf;8L=g0#RG6NVh#>LU(5>X0>7I92 zMvR=HnWJ{8>B(MgHx#t9k|bmL)J0xB0T3t#$Z?KMba1{SBkYj6Ac$1ZzS*5McNWBv zI^7xl2jC4SeG?a5a4qI7nTpSU`*k?yBQM2Wci-$WAt6#mSUlU20dUL=DJ1Ik27YtZ z6?oHm$KaAHK7gZ+J_J50^Tlr|C9HAy{Y_Wm zSJz&Qr#9b%Lk>I!A9>$ZIPS1hA%wtWWgPXYfeYFhaCd@5I}DR}-Npw)A_}u`)@SBf zCeUFOoC6R*$*?2(Nyp3G<9-?g-uR-+ap6y2;E_lGBs!em4){nH@zV)p4N&L`gR?9& zjhHe%r0_yBo&*3`XAr0eFFxu`IO@QE#!bt9u>+An5<56z-;4V+ z3C)tn6uTmcdOXoX5arHbvK_{DV2IPJub;JAZdhnw&H4z9oLyZGouSK;XW z-+;HA@nI}kvZw#7wZ4fLz+aZ#fh&IXpLlfbAF#(>3-G~rei<)1;*A*SpOrI>h;pE@ zv$&r})|o>S?SV3bo#j|c(FO&&61G&xkY&~kcs+I6#Ib+2;SSn7GXwg2r)496ps>M= zI)J{6xw$lVG9pt{-(^4mEC8FosUyiD+3mnOQBNO9wHYxubs^4t`4@4*p>M)X_kIW0 z-E;-s@$sMIWk;WbH=KSh7A{w#>;o zN+}=20uVx2fUFPAkcVM;5u`%}DXmsXNdiCuxOz6X9A4QWjN3`Jz5^qCb~|^*zIf{^ zFUE<7zZKWtekrcH;hVT^*_Bv4=TQ9h;Tth9vw#nr_bI&mgnz}%X^XogUW)&DJ$jCa zb_hSa)S|$*!XWiIl;xzkx8|JaT|&mlg{a+%p9M9~;sg94+Tj$7E=07WD$^DFrbJ@^ zLQ$!dt3y|I$UePy+>!P0(_-UpMx@zo%7}%t55c)-eiyGe;a&LNl^?^hzg~;ePk$rM zKI@AZoH{QhssWMABf0`z++;^%uafT zm}kV@W7=tFoDd?X4~aCx$`Gbbsofz=aE_UX5EY^V5rI2805Ubrq^%3YdJcIOrP;7! z3u85w%sm`0I^th2cX0`?dBr&xoH`H2Bw%(BLOm_xeERpbr8PgSc0 zr0O1Mra4`5n1OlOrSlwXW4=3LzdM_x5RhpK9)&%1BGf4j>pN?qS?2+zgUudntxx-; z2)ca*x79vpBA$~1>~JuMgl~&63@NEyxqA+u1%Otofkva|%@lX~HqL!nXVFPW!Oo>E z8qYB9_MAM(Xmr*vmc4e9e5VZPTpWQk3T~I&IOlYyA8l6$JpKQBskgK1zm0pelY8Fa2xLiE_7`ioC6%Bo zLCq`xfE~cb6q;iJfOQh3~E(;W$QhLqV%s3Q#Pd=|I0WrxYP z{m9>^18IQ$_kEnuZjVWCWOEWE(V?pVV488gW)ddnI+4hoJf5?%E5TXT8qyPXR6fXP4Cm>~aQT~4j z8T^cv|JtYelpFKR-nQA^q8;*?1Gx4Y8y>s7AOR5*)4CvSmvGFs)m^mjC_2 z(^0QKOGy#{nstk!801$Rf4EeYqKzB0-dRD;S!bQi2;DJ5z%e_c8F7>AI;QmiP>6aM zP{Dw2}f>-}+^|?~^CtC%^tW>h&t5^x5olDZ)IH8OjJRrNZ`+E%^H7pTOB4 zd>L-N`!^^Si@t^+(BX_TEXQM8k?IE=u~JgC^q7X}`E;Wy!Dc{(G*b)iw{X1QFST{U2Bp$xAj>lInhY-&J4ZZj7hcNxrSt!yX_njL)g!;Jp z>g0s@X9!sigGg)J63+QGw8juyExB0>s5)t7qvpPS)G;$3zWJ(ED3zw#vY7_s>hL=q zrZ@@OOS8egIcv$%`Pj5>3_rg56ZqrpKfxLQ{9e5L#s7k0v6xoT9Au8|WKMYJqMt1{ zl~O`Vh0(F?xcc`$!f&ttE+*@nF=N&M=Jw7(5F$lqvj*f8OUN-Sh7vun7E~w%4Anr= zto=$BsaTuTUo3}n=9Ef)Pq`#XP}3FY=A^WVS=WpwKODw;-F)t+PY{>?$6a=^au67d zD0&VWaLq68#@+YbjHm~0*#mbHK=(E)!CB+m-L~3jIdJv)GM*R|wb6c2AMKOX;j*et zkZ4rRw>Phz_>>b<6#yuyxWBvrf&yf%dU@1}4!a3PSYXUuI2DH;y#%U%8!r3R`|!R` zy#jx_?YACb71F~U&UK0W4l!1WfcmOfv(>=QfBS8md;ZDz@$Wu|zCn!x4q1qqb9+$g zZ!gH$5tO1GmOruMdZXE>UGVV_!3igw!xi=B@QK4?YtEmn4FA5>sy(W8^ATfOH&|Ey z=t%v+7dk_~?U`8<{pFbs0M32Wr6?9kxb5l<&#nRQIsbJ0||h!8Pz&|T}y%N2P2E8mafjyef|-+GMNnIb?L7UiI1 zfFy}=Q$4R`fm%d zeLdXL!=wW9DnY&f`RQ}6x@e!*Lrw1o?)omw`!76^ozqYe$-Va8!*1HR38%h&0bY3Q z3wNrmJJoNat{I(=7_D2kO@LaNTG1co!8*pkG&FK`~JDG;YJ*A=mN}`-3J*m zWI%rTQa}g-0j2!91V(2Ucsn`+$aisrw<2F zz(N2Z3n47#FPee<4w;4Z{yQXJ7XL(^U#w+TVe)CAma7wwnA&` zNEq|A-|fw(op>-#J7IrRDn~F0ZP*45>`>~nSTg+}%$dFiuDo<;r*wYCH0J#OJQcSt zy8(MI+7HD-8A53M*B9=`8RyO=Ye51bw22vE%&s;S);TO$v?mtru~68!=z`E3;AH*& zYP?n%H!6h827}nA{zB3uKmd>TzJ`AaMa-k;?_UkDrOJvbK_zCGqG zS_LkU%CBS;J1kY&ktmtD%F}%AScAn1!`rH8H4Wx0=*Pr(4Xvs`-_#<6wCM`TZ0%Xc zGcvoL<}P`1$bR{h)*8e`L~=G@3Z`1Es%^t-Rwx;~xY`;XE(e1!PIGm#g`0n~>A8^Z zS&zRHO5FLeeB0%??zeX$Dg6~Lp5Mj_)1LKZ3X`Rw+)CR1vh9DUz34tQm3ct0m>)7j`{o*_J`~IhWHtD(n@@Liu zIJfs&uKV^1Yquf(mfpYqG4sR>4^bYXo%SD_(3%E{zF1W8SQ#SnDmYJ(pMhr_w6?cnyrMj9+v}s zdu(OaS81acCULxf94EpU$AU`~1yd2KUJyrMr@*WL4&ZD`C|1a`X_f#Kh!uzeND4s| zK!^~6B1joRsRATLkTQax2!sL%5r`rXhX99Qr{J7|(*o8guu~3BS#4X=*qQ+8$AU0? z%kc2J-wEmyM;vj2tJfdHjVmfR<&b~DPcOaYd866$zIE{}*FTIGzIX zSQwP#o{JW_&%XCsocNlB*mrOaEXMKhJS=J!VWPSbjxDB7St7QL zuB38tx;^Q*vuECT>rYp09eupF+#7IM2&owLAPW0Y2>PH@(RW6BY|`UFWWjJCB1Z&H zyY$mMK&0y#gdk*#yJbgdwG)G~a8AS67>TZPyTsKTCFNtdIGT-hjvvsZUMqUN&zJUgsK2R0ZCC1 zp(;?IN))ORML~%IRiHvtLaA6rp-@B=MF^t+Dj*2u;JAf2nMAcViqX-n*tBs2#Cmj8MC|07kNe(W+0 z$d2>B{7TH3GaqB46PPl!k3R6`%lVJXzB~Q)yRLm=<*NIqwHlV2bwf$)7i*C4n`{J; zL=Z`Yp@32fg<=s>f%~VH?+-#XDM(EbLKcM}_Bn-O9lIrsMy+IxL!y&>3*#g+3ui(IzkR{wpI^Sq=(EfJ zhs>8gdL6#`%d_!+-uDZ9``70J0KzDAK_s|XR#1u%MgltBpTQ)))uh#MXjVDhhMo}x z7Ol8pbwj>u`8}KOKmH7arD@<0ply@je?RlTrd)mfFK>SA$p;T4NGAjdAMPrTiYf^y zebf|20x}?k5s_d{65FZ|&KR&O?p=+s%~NpjOCnS^7ZAtIT}pglH~kwcsnS&bTbS2@EKBEdP1Bn0PBgumxA@4T2xe)}9)BAIuB z`>yAoU4F-Iqsea3fD8i2@b^|SPErX{fj|_c8z~hf3h7zuktp^kL`5&LA_dWe^hEsn z$Nmbf8IB9+EzII`PP&GcF4?yZLL&v*Sf&}V3R3hl5(o|k;nk!v?nz)7gBm@m5MkF0!SIyT4SR6 z+ViGBn--t;wncE%0#EU+9-Y~5?gPSQ2=9tbG}TKf6@A2H8% z>^2`zES69#^kHb|N%;0vvVw?h+QdlA;B5aOmu_urvpO*#IYJ;E*ITP%1OTH9KtU?v z*PgPEWOhzU)d~W|5RQXTLInaUkRG&{{iLudV|?5HV-I`rAPkF$qB07F9z=z*D@46$ z#^V&*;ct_`q_IY9cqHcj8M~GKyEhZ=Db7bweU05~;Tkbz8g3t6MgPu>i~DmseyDp`}_M6@#}p zXMfV)Gjmp{)C=okM?$bv3W5}@WzneDMI{*#QpBGh-n{vHhaI+`KtbF6j_*gSx_c9W z-KGIj5=JH-!%=)57S4Ey+p=XuY#)2#8;yGF)x*PEme(qpgc(o)&r$);PznPIt{}8d zwiw%Ze^OlW?nYeT-o65yW$q~~M%-$`I*lZ0V%4fgU92aBl;S24Brj?tTYeNL6SXib zik{Md>?ux@g|Jr=gt4x5j}xuaO{4tjB}?}cebXhMwDcWVH#C7;ezj${GGLd((VfRt zk9-#Q-SPlV*!Ln_bI+U5)Z1lTW81Xb3Xz(2VlkR}Tp{XTq+}==Zd0OL_f1xZZYqaM z$80m8n72X(f|FK)sZ-~pS{cEdh5fK@9HXNXsMa@O!Mwwz3}Rcbi!oxB&F?QSIIdWj zx>(6VaVGmk*5<(bg6N3tnEv$EiVjmlm zKuU#5Wh;L1&Bp-%AN|S+IN+dtu>8SW;MiEQQXoi>G#VR3kNlOA0hCa%=}ubL{Rw#g z8>O^z*aor(V1b*ij4|}&n%zkb0KoqRbb1&ct<2Ko0000bbVXQnWMOn=I%9HWVRU5x zGB7bQEigGPGBQ*!IXW{kIx{jYFgH3dFsPDZ%m4rYC3HntbYx+4WjbwdWNBu305UK! pF)c7TEipD!FgH3fH###mEigAaFfey&@l*f+002ovPDHLkV1iQC3p)S+ literal 0 HcmV?d00001 diff --git a/rxjava-contrib/rxjava-android-samples/samples/src/main/res/drawable-xxhdpi/ic_launcher.png b/rxjava-contrib/rxjava-android-samples/samples/src/main/res/drawable-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..4df18946442ed763bd52cf3adca31617848656fa GIT binary patch literal 19388 zcmV)wK$O3UP)Px#AY({UO#lFTCIA3{ga82g0001h=l}q9FaQARU;qF* zm;eA5aGbhPJOBUy24YJ`L;wH)0002_L%V+f000SaNLh0L02dMf02dMgXP?qi002pU zNkl|h&1u(8czNZ4@#f$#wV0)!Ag z0v`kdaZJA80Etb`em&5Y!E zUqa2Vr|;XhZ+9(EpYxohs)2tf|4`1N(7CR_lTdd#*A@G}sSVM&uD}@-3icHIEogT9 zb{>Rw-DkC7JJ-J|`dnAwG>h+a4T1&`?>~PbW?^0Atb+3d+gG~!HYm6UI6D8r#W>H6 zwno(1UHZ#kb`pT9jweMCgp$4I_j^Yl9Tqx59L1_@ipE2`9YIt*07QrZBrAJ*y<Z$tDT`3MX%djE2uvg_2DFw!uERrrpiu}Kng&7(Pi`f z%{4psj+%BfOWY=!RJ}WRO`2o z1*lMUb-KNH?&zVBdgsT!`NuFndHUV=K5Xy1^CUJ_i+==wl8z4RzOBnn0#H>3{Umz- zJ8!?|-doh)PR40G9!>P(O27BZe{#*QZ=5VJw-_$~=%T3#W&y^7A}+TCP6c*@eYkbX zEh#tuyAV{f0OeIzB7&}!V(yLqg{i5VYjyy87Tbm<1bYOzN_?=_Fp<^suwJ*73eyMxn(;qx~m)0aA@M^#l zYA-dSa!UZjq^Q&D$K91({r>LVgZ{2vbN!{I{$OFD*X#E>z4^IbZ`aD8x3X){UtZ~T z=NCHNI8iZ+#B9Y&C55I`YJ(>R(A&MQw>;c1o&RzDE8e~}87-YSxp^L`r1ToZlp9B7s?t=6zSdt7cTYYmXc19TWt(`$<{E}iO}u#@-KBz)6%` zL?%f`XV<^)z~5c{yk~##nJ=5XO6y1lb3OWrw_f$@Kla+2{^{Ieygb|}2tW=1y?zw! z+qcj;`sgqkZRK{fRm98Zsq=pBS6=+|7ro$V*Is(b1y5UET)J@3n_EfZ?tG-1N=WLa8FhMS||@e^yS2k(C1;k!O^!|k{I{%?K$P9Ce{EF3M&_w@WqQXD%xOpDx_ zvc8cBdU;mNecPL#f6bN8kH7Dcht}=p#t0AGInnR?{bRonCE#pgHvwb-40Zr`fE_^6 zX4KbPGJODxy@B308AS^}|9j8)(+jUuOLOz{h!fD?{`t}W{I-Ah#XnG*iuw6YL8545 zb6kj^`-bnh{F)#7!LRw+Yp%ZPWxJR5U#h4Fz(BB$9Gl3oCI*?XWWo>-6bLaibxEN^ zG3H34iv)8J5GFR`M^79(aMNvfe)K>5^7}q;+YPIC12DVy4)l1O7vo`}mUeX()=y^9 z$4`9wyN8p_3ywazE{7i2qWAyd+S@<={)4}(6m2ofNdQAQ31qPYK(rG9R1s1D0|3ha z_B`jsmp$)We|+ITt?cdaU~W#bEY-jK=DWW0k^9yUrxUw=`P1k2zU8;x@Vb{=_w3g% z&t0$w&@ecHq1x!q8tBa z^MQB#=X<^<>F9Bu*<%1g_2s$Swk|sjK)%kN2zLR@N3q&t3ZDNbKXUDlKJQiP^>Yh- z=?}Ve|D78T{_Zb4@N4h-tMB;EXFv6sFNoAGvN$T6@&zvFq>8afJv;?nTmWDm07Ec_ z#RwJ?Fmf1dVhfKV!#cQx58y{vz$Kh43<@a(hCe(c-d`DZV9 z>D7CF_IIB88xP;V#;Yecap1FC>JNV9(Dw{SoA;U=#{jGW7{RIA)AeJW)4|wjB_yX_ z3axZ{`uuDn3;*gjzv91LaE0uPlO8U(RLiTcdOh`V1yZ@kZs2yMNYOm5Mi-X>h+uFG zV?2Zu$6+uo8FvJNE(wV0(>w-PYml3q6?d`Fy+mb``QrG=`_r}6&H43{ zLpgkKNbmdo)wh4} zSO4XLU;e6>@8?SfD=Lu-ctR(XhQczQg%}rsv4$<&g%KVFK5BM1suuZ{64z>zJqk&)^&X3U8@H^{H{lSK2Fp| zk@F(}Jom}4L%5GGJIx9U!wHoWaBd;#4L1vZ){FP;`{O_Rz8}3{ZwDvjCPmVRp^;j` zRp{X=Sghd$K7t8Opo1kW;pymMHwfLTFu?2p#DGFX zDpoYfPhxp@f~P-s3Cf(G+;aWu^47-WWYW=bp4rfkv}2?Xu(SL?K+~_10O;@D*I!;= zP1SGy{;U7#+uriszqq%5MURowkRC;sc4Gz4LW12`!{=}Up9dkqA}+%sE=7VRxS+Uq z5B1<^RS(YL90RaOv4s?yurO5>1PW3LLxIDM2*4I#harf#dqv&sM{qFzp?XQ02cWB;a zH`EvOQThy4@HDL8D^OsB!}ugJjL^sVn8W$#VgU<|<+K`;Shj0v`oVgm+wHL?P#J~K*5QvpUwFiCYxMC!jq z009W3jLq!+r$ohkbt>Xdg!ZldLMHu23PT($du?q?@I#?*dlORS91PzNE1``y>U{O@I zl)I@5X&L0mF@i0vFwcoBZ2gHXm@TZeu-1TWdCW4bwGg%?x%O&I%5w!pX1ORtJ$#q? z_|JXkr+#p8B{3VT`6_@hoJqf}z0%uV0)>vl4uJmN^9H+)9Uk>QclZbX_?mssxC%(* z1RbE0xCaZk4D+}EW31yi?m~iP5Hu7z(C9+EzXmB%Y+{5pq}V`?F$$zG$YIOPATNQH zS9VtY55bW@!m!j*h^16x0u~AOfC!h;NdOSB5$-LROP=$R3!d>e?|k^L=a=G6o;Enq zwgeBby#drV*L%D6_Et_D9Y;6Z`(1B)*2UL8i=-nP^e7$29q3>e=5Zkm3{K!4D0HCE zg@r|g9t46MDRPXEVOUC)6butM2y1YJ=DGy77DF1~VG)S+rn>`A1)x*yDfOP7ytJ{F#eedN*Ztf}pZV<9Kzf|g zP#wb;V8IyR0w^Td#1UlJLX1TeNXy)N4TAy(DGVkhpRo;z0-%DB1aN9Q4#Q(CTuL1& zEiVrcZUV-Z-v$1miW>>Q%oT_h_sBK7_pWT+a>LOtM6puLVo>{rwq4n-0II_kgpSfQ zpQm>4uitvzYrp-QUi@QP7A%v|C-DGAIEDl(C15fPaRh`e1O$s5ga`tLK?aKy7N&%N zqkpwU*ZRx{ciyCycB-s`CK-P%ed!c^m#?j@|4UjHtffM4;UtDQ3Wf%uQ&Qax z6zl>I6WKx`1_lNhCde^CfdUp>ZtgrAP-0Vla^Km;cU+#!!VWwffTskAlQbSgD8C1+ z6)+PDW0B?~M7umaqHn<+lh&b90N)5}MhS+p26w2^0oPdyBg| zOPgz1{LUL+_tr~xUwR=EsT?_mIEt}Zbsl2s!hkU@P9o1z%*(Ton2V4VTbS@MfCyF$ zga9e+&V~K|GG3ddUxq$8!h2073+xh<@CE~CJCo!20?7s3<<#<26z7=|?#wy-e9 zI^T?Sdt)rDamP*J&as6%=C=A=Hg$NyZ)}~^G1f^HYb@sD%W>Yq3t%O8^%H@J#cQ7a zHpH|HVX8=V)d@seYmJwEgWm7VRzo=Abn9lL7p8!*X+U`v&04*^6BwCeNR3Sa%o zH(vJ2@s>%5s6ErQ90G6-&N9TVJ+n5dKloc7WY=kr&q9_VCXhvX+ zMNeHkeYNt5UQZu@ur8%V0EQMw!oO?j6iT1+`%sGceZ_g4>SF6a1<_a=KLEp7tD$cE zyK*s#qJRjMTUm9drIb<{&v;?-LjdCboF1T_Mzk%Y&~^e)MV_Nrb=Qt(`e*%L(y z*Pk=FL7wHvvI!>XCh~k#4w|=ufX&IHjf)8wL>iB5-GEVcq#Ed20yR}u8%V}F@R-6@ zD$AYE4K?OBwzUeYEwM6W!6|NiJ%rDXd81|jC&ynV_G zUViZlM@|a)sP8!k53qdzXQK7izTFW>!b)^J=ynz$!eCZ_wa({4j(xaA7+lUzT?Lfpd-<^@B;Yb~>$5kq#_AVlLoIQ{N&;Vr^0;Qz#e+viFD~N-M)O<()7KTy@<_Ejc zPXvWA5DS0^B#!$yKa_&7^D()5lL7>LFV?RH@QzMbbtfYpp{c^oi6q(%00II6y}6#o z&-=Nul~RFAT=_xqt5Pvo6a?0N2Xe6kp;k3e zTS6W*Wy+yQ02zi;0k~wBv6W+$BL!0z#RBYCE+|qM2M4~y+&hh zx5%hKlLwtMHMXq)q$3rZobj@6IR7~;1~3J&wXl+wGk7exS7#YuAYB>QEWg_p@;yM0uTm~0*C`CziYzj!y08*7?Uy}dO>+E7|rESIm z;3~2YhzN;T?7KL5?(Lt!^;)aAT*%@7Y5;{uP;p1a06GiH$rYv$5M@w`N-iTVc2)ku z0l|TXLvmX7VGH^L(TkOAkqUc|Rv@ecm+JMnOrWMR+&RABdzwG#9l(>u;qL zDIy{f5oW1pL%PkUhA>*q{&EAT0fJ!PemZ=&acf_lHyK%Z%2mrtAO*07KtserNFY>$ z#!Dfm#<-MDts1chTN^N?G%7`uv(lvcT{xH(j>7m<%e?ohtupJq^(1Hji9^ohe*-Te zQSmH6kXJ1Z6Ar8j5E2oSEH3osN0ae!)XVgt+(*kR{bbj!x#ZZ9Ew#Bdso31yd`!Fd z&&k@!Nw%??=5Q;3gxQW~1fsJAP?$YftvMLSI^Ml^E}k27G=!8m2_Tb6W=?FpaxTr z3Rsl~9HHuRr|}Gl#2iSgN~fU#uBIyVjS-NjQeQe5D@^G2BZ%Z!+SQrgcmRTW>AYla zp_3$0)LUI0nYGpN+}FJ3+NZqYYo2!DVt=u}F&<7n`k{Ls{?G?L^AHhXu%HJJH5qLc z6Vy|O{8*e8h|UH;jr0ouajzeDckP<%J@W9H96q!ms28dvxP+(_K(c$^oKDBZWVn_2 z)wonCBRC&xBSjBUvc^TGh*`*ig{nEBrTB4vA#!TVapC{@4#*cID!$yB*8}1x7fE0t#>X@n>Um^335~cdUK*H-6%?zkTx!58gdk zh`XcBVzV3geVF_B-G8n(JPC;j5N+B~OhKT4DgE zh=yxx=DyE<{?PS5^#kwxi^Go`Jv_hIQJd@8u&j98>BNg!RxJF`PrdOcE`Ij$Z(Z0^ z2y;eJq@c6{DKAAz$wFS*1fSc-Q4{N`>Mg5Z{5f8;p$V2ICkmuT03ez1+0hw4)!AEK z^_~T8N|2up&9(oB4Nw$>B4bQO1|kKram;t!#Q*jB_kZyZv{oZ)Ih|kZBwHJqyyF8u z@WWsK>Z|`HV_hr?um}@~PU2pSv4Mh(6q!-hD2z6QZv5cZ@BY8v|CwK#Ta0$zvn>)4%*@-}{=czv3sf&SQfDIdWJqPq2mKe1Meckg^L> zq$_gsM>gO7FTd%3{>O#o4sWhy!}8iat<@e8USaNCdg+ym&-v;%?0VJW9(!Tj0R{^| zZ=lib#fTG)IF6unZHf^As)}(T@c9Jbn$hejS{+D(rguOZ0oj=V0&3udJcyg*x*g25 zMo{F8G-ae?gLKT8Yysn;!TM2k&lhf5{qV#0uiZ+-2LW0ak&RwIQIm1bfAaAk`1db( z${_&QqiByt#P)FMj{${-6GQ zRE)RGI?iByqB8|hwc`59?*8)XiE;AT`+w$bmtER<*;rC*P*6hiY7XZiLKnwyKORj# zk32OPjYd3~j79Ohe&j%M;D=xP;cx5DaXKEF34mBfYS|iIdd2H5ef9HRcEOuC8=Rl5 zt-$6HAPh@GSlWU_Bj`?s-n?LbF+q0_q0?1}6GD^#Q3Q|@DCPDJP_<)-9;@{&M1}sJ zT9t($sR38>8mbppV3#$(7BB@+i=7QFeVUizBX{&Hf#*VfMed7nRUwp?~@A|_iQbS{S3yu>#ZYgxS94I8s@xoGP zuzF%l@4fANe|g`f(aR3Uxg+v(|fwvZyX{BM8zWncf2mp}JM4t^o#!}n&A78|s&wuU?J{v7fQC^Gl7 z7KO{jQJN4%geX=>x)C}(jc#9|Kd+EvizdE1rq@{tEUiUqqz%vi-Xs{QvIy;ypio?_GyJ*6T-u@u;wuUaNli@S#U! zW%q*KqyqWm5k!%OQW4lPilRW4WyrG}X=;$A1+vs&GB$cL6yE<7`WFEHyf>$KYn>;7 z1PY&>Ck#LyM4E__&GoGNb#J=rIp3No@}XR zl2%fw4txeeOc-$Uyr9ZiAWExJ3Nn<^u5U^+(&b45Ac2m6G>dS{7e9!>0%2uuLKk0h zAz(J`rPtzT?!7CziN(gdckf%=+T6GxSu>VsqO(-c=@ig91`(C2(V!>{ilRV~7sxY< zB4cDJA)9C!Zf)+q;Nsm^9yxsCwh|BRJeMa2K)penjEA|r{PpL*;o!l$F-cc7mDW6w zqenyr1Pu`aTR~A+~ok>jYO^)BDEj--}O9Mn(T6ue|sv$BrF^S-DZ2 zKYuk|_lh^-(91p!lUt0oa%`N;apK4j#~z*F=%F!=KRUtj!zngS=Ga=d7;OTRQI0$n z*sNSj%&Qg#zO0MC3t&ZH1yCB$0z?rZ?hra1Mt_dbo70$Iim|k-gT-A5<`*N(FUJ_n zN9gt=DD8Mqk*BzFu$S(+ZGAC`l6}UEC-aNl<>A%@(MbTJk&Z0lB!||jjsuERS(2tO zC<;cNS)>z-@g}gf#t_&AYY?uu|G3K;tFS22F@QLtrHdXt_#jAus;3zmZn-~Q`ZcJU zwP13KJTEXA8x%RPxt`+WiR?T818b06a`}0et({oMaC8_OOUEJH1z@1GLDK2s@=LD7 zGp_0(qg6l^5EwU51}IWsJW4SdW*84MOoj%dVUFQehS64rt*s1`VS#jFkfmIymprv7 za=(gLU=bNdh`od&I4J@Es#JARtPm#(QRMbsRd%`>oqmK~U!ymOkaRRUJ&j)9t5A(7 zcIwmmNr~3Y5J^*uY+{h73|j!;4tjl!&Gjwh#TdKx4K6r*XnasdG+-+*1*pgwN-2m~ zC|w7ft6;7b7~}ehErG29M7!)qHv>3)*T<6vpbAJLr4!5cR65o$CarR8h}=?e|%7+Px(ZQ>Y?xxrHrl+w^D zLKG#4q8LfsLpNE(+};H7`7vT0Bhejb9YK+*Cj0n*PDs=<;j7#mpj-wfgB1f7H=o{c z2Fp3P%zyTAF(Psa^yO3@V{8QoYo(krWKa|qMPaHbMR{sVHC(60I&P)FrUNiw4Wr0Y zWLbtRwO|H1-Dm~Cqfw-~PMwzhT&<8s4hoe87)W6WLNc|I3L^)=X@KZVRTzo$)M*Hj zh|{;!KC6uDK)f~L=aUEdzi!<8+i%o(XzgTVA>#tp0Hh4GBItl@qrI|(KL9I&vqYD0Zd!>|kPW6gPBRXS^!=2|A3g+3r} zzE|riT2$aF%5@csj8Ww7{32uIDT6I309r>X3DZPE@3zkw_u-RSaX#;xGKJWBO753O z0#!f)6oq~f3cYjH0F;NS*iq?Z^G^gr1Ec{VVIpCI6{o8q3Zwv~7)mQBWudf!RyEmm z#1~LXRgOfT|D!4Zc?rV~TvA8*oB7aE*V{+$%Te*kUR4|nfr^+)<3QuMC-hZXhtHKR z=Z{rRL~q>{1U3=C1hEVjTP|2dCpKl0YcWWSOZwNC)2t4eN2hLL?CNn;H?(aAfhr| zwd5;x;57hC%OtNHLbJjcje!U~&_Nt4a2P_+h<{a5p|SX8ur?6;6c#Eb5}I1B zJ=Zd=DQcvMln?8ytjb2aygN)PMZtm9`J~0d>PRIZzTzxmE3OkFjRGOm_@a&}21WZ& zX;Fw}12DO#6OeN1fy*KG^ALo}m3_SGp>oY1@^UzcRX~ELEO-v6RX1rKtWuI^3`iq? z$nV>dsRBXSS5g*aEQ==EuI|Lpx_)LRZ zXRN|X$w6#U=qk&&eyTmnsZs|BdJdI-E}N@dJk^S@2wMeK?g{lRS1zL&ssx5xWy60T z0L4o;@{+5Tc2#t9mei@;%~KuUNb#T<9_e6^+dy)9Cpb6QDli4N^^0Fsp!AwIh@<&7 zDFxL?{15NpheF6ny(uu&DvVj|<97T!Q2_E)p?YzzI*}_7Jp$EuIuJ;SVBl0Kf!Gw* zFay>lK@q`q0EnQtw3WQt5+{-TeVuCZ63BzPM7mc4b)*zQjRKHO1FO;f9DMBu-%6E( z6sqe`D$6Xgizcw@-wAx)v;@EPI+@vt9UZBtQIFu7VVi=y$A*NgbG92f0$&~gRZGHI z7){~g+`&hoN>qhu4K1&&5J9za4IP(|;DKVN))XjkbqUJp7G*C6mQKPzhHdE6Ab)B@x=pLCTG~+E zNhPQn^ro&l8i{1oXj`?LBGUe{p=liMy}Ae_O+z9Dk$SK+c~6+V0hVj@IqN#-`|V-Mprckwnn>Dl0>Qj#bbddtW=01 z)ao;=O!L9Q^x#&yyD3$|z9&UxJ~UDLI`!loN<8gtVy&8xXKW0w9*es z5R+-EHs2_Klp=x!Y{3>11!S|u3`43@iS#npC(xkO?)Bhi(neo9_a|h@GwK^23nkB# zs%xDe8lkfi*rx8`8{0exE+vpwq^B|gLg{`Au!n&5&-(wrBGXKR32fpq*YkKkVVfBGBcfWZMB5v4J7=3>gLn^ z*QkHkPhnkx8#?fnff@ycDa&{II#ZGo%|2oyXUu_47eJvV5&&ck7jEiF^OR|Q+x$E9 z>xnph4gf`N43$$^+G4)hJ?GyotKrD+rh5PYKmNQA`X!fHB6Ez8F z=qhhMShXiMJinZEQH8PUaSw@f(6L@e1@WwqIEKk!66n@2alYB1{>ZetkW>Bb8`*gB zn;>X_Gn5Ga@33>4&g1}O^?b6aYLa-rYJHDZ-%dFyTlMw$KNl)Y0KhGPO;s%$BELdV z-54Mk;IiXb039jiuIJ475Ph{}681#c3GF94s7LGmvv}C4q-R6PRDh6X9opatpM2j0 zZeAw@LUn2o>#BHFL(_ULNv@9oXiX8dAL+0u;ZqFMk{WgU+`0~I0~K~!Qs`{_KmY(! zNZ}Vcs3mW0K{XUao2QhY6;+aljAcfUM^p(NFWG7fzPgqV+E$YX;UjCaD_s-&;G6cN z->7yt;(=VLIEueU^Si0bg_3v*%r$tc2dtE`u5D7czpArPbGB@YTQwf2#*sobvBVtAzKR#R+Ce zvMFxDEjR@veinF|Kxwk8@L_13*eH!*oElDdfZ0U}b?N#DFIB6@n)mtagIVYhcmSOl zi9YMO@oY;DR62pHRkh@?Ya~^7l}|YN>(x=osZ}qejDOWXoxW~^CjsqYlg6me7^t?2 zdrThGJhy?#5M+%A{|qUGdf=sXeCki(H5sm;AI7~kR}?RM9L-SBZWyR?C)c1S`g0+(hy3pW~iO0zu#ZVSO8 zQcfLc_srufXS2|_<3N@zh2})nl7KW<0mEq`;FVYv$`Gl-pKYK`0k0w90-YZYR9KxE z&XJ}DXvz2LI!#p6q%`mW&C*Ma-_96SG(mG}H6no_QJwT?uWZ*OU}OQvoS(uo>SWmcWQHu%J8 zN})53#`_ON&IOSQdab3hS~}Q!f17z*0V3buT?8-ewZ&h9+nMs{wSc+oT1eGEYZl47k5$4Pu1)xboW)NQIKOO~PkVfS_)r zVKQrhsmBeXv$4Vi0E*0*+UoMpi5q10?|cXw77)ZnHN6#9t%DL0Psd*>e%Tm%K@eRn zuUn^W)bgZ07W&?*-=C_Htvb&39o6@4fTtmSLbWOt>!1oqp=1qi86?EPcafWw0i~eB zNhOVdc8eD^)oh~;ej$Y~Gl?$mR~Tyu%>k=2|ETp;1f3d^PXLI@^vohRE=j-9BVmJU z-_a~7)cOhy+2b9E;q|Eb-OQHCV;pNsuId9-Dz?t^X`gdy?o?HIT5VPn8c0Ef-Po3{ zjl{j+e$`M2AbfVO(L5UtBmj`5rXW(a>TMIaHka||1lOYKztSV^vztyCGN=zs4P?(rA&BCLPMZYh3V@Azyq2_K^f(%dQ>YFHGVf6bpb!D@fJMHXZ5z9 zv$4Vi1mu~u&XL%1@Xi8E_(#ht?5(h(Fx(LT{&~ZD&O`!LH&cp`XU5d4!pn3&w#0f( zjP)HxryA+@ghB*>X{n#K3I^b&=mbBk9+2vpk*U6zImj|=G^=Y909z%?&};#~Qm>mF z*2mw>k3p%Ti{S9AaemBlR?&E+71A`fp$$JpPTM>pRAJ4U5&#srwP8Y7WuAv8PpQFr zK?nb&lb=u3N(U91Q32oUG`nJcP(vTo%qP1=mS+Mothh{rsr>^98d3SUyn^ztMVQey z%}|CkfLTku%8__R1R6L?4x|)GmKJtuFdoahS|cB`ds|#I-dk=#Cs4_CDpD%$QLFTQ z`I0$5MpF`}&Gm7LN>(Sg2IDb$V=60hMw=T}8n?jMQ1fjf-q3H>|5Ak{nu4vZQ(F&$ z>r?XeC}s@8<1S|;BFU6lq_Li3~UW#ve;6os8RQ(H>u5x$KFfO{u~ zs!tM7ouSz75#M_au@-c6ICq{}bqu8}!u!>it}fRCOL*A*Os3Rg%B|ao@1Lec5G;Gt><2Ve ze^>`^)q4rleq0`JIjeLIMTE&XH;&FyBZ}Ib0^FS4*#t#Jb_f8hu`-pQ)@t5N-XOub z!KFiIWnF{WKR#8Qt0@FzCYYKksJgUq6XAFASax(}oDdOtWm93L6+n^|g(Xn^a=@CcwmP=ywdFw2h)5L+v+UR9m>$GRfCtuA zm{8yL-Asd_<~OrJG~xRU`)XtmSOo zO;bvwrE=c?SwL#J7 zl$Nw_XoLEE;qpyA=Y#{fakc>2>glZ-@8eT$&y`hGPNzM^s1~_#Z__Kk5B)(7Y_0pW zF45?0ZVqJCZxR5r%}dZ!Pu1S%^t8vQHFhBns?=F%!-|U9~M1gjwU=rpH zg(5lpjenZLfp4@vcrs`Dr%u&Vfs|-SqVV@KdV2b0ENIcDJK;$ zivh#{FeFse+@`#hUn#bdK+Wk*zMj4hY=JG;t>H3MkH4Jh@-B|Vxm17xLV2Zs!%8YwFn(wVRRrW#+KWPBZtI~QPX8byU?v%&2MX`Va^Hp`BOc@Dtbf5+y>#B@;PR@iX;+G<;Nx`YdEmy2r~L7rKRhX(m5 z*}DI(V|R9v!~!s#WFT61pi~SO?wL~PGdW+V0vcO`yR=S1>!jAL+L8u9Wh1xOFKSDj zPK~Vpb3oU?v8T3)5(0c>KhJx2s>vMzJm?Ju}z2Od{Hch;}2QUC`JC zO)CH|gY$XhlP<FE#*(J1)<0Zqb)*_C3ZZ@_3EMM_bkR+BAo<466p>P zy31h7L8Kdo0?!ys+aTF(y)ymDbz2Ar(@DyW&f$A6qbup7O2iXLu& z9&Q2h;noC19Rv3!8>^J!Pki*YzlDA(p7z4w&vug`_V2lZRRk~!VzDqq0g)WJNyTPE zkciR|+gm<7{P6>~AG(8xh9cr$cX`@8NI%{aTV3h9Ua^Hrv$5iI;r8Wy`Wr@DDbIJV z6mXxi5il7u(ve_16ih~h$xtI3CSr@2N5i4sJkovlXFl=3A1bYE6l-e=tH1u6ulwe1 zcRpekGCTsv)T`0MN9*eplJH$$;oo(2AFC;k=hzI%;ISsthu!&YebxTHMRh`}t^DlY zpTWkx1|c11$S2Xshwk3^-#SvMH9XW>@k95YIQYoj@}ZUevWugQOQIyw-OhkI$$%oA zkcg1s38m@K9DZ=~1MmLb2Y>d_hfm%^pbZh(05C@VzSPqyXC;9Eu!^vAe_vr`zLPx5w zh9`=s2SAIkQ7Y>C+0M1kv5a;30V1jltyyaWIXw80qK3=A+6M<3nUO)N$t>_Rq)7mR z5Ij>>RZC3~WO_c0G_N=9Z<3-M>=eMrS{^B-`l~0`%sYPTj!TAi~)< zCPSn)t>qEi6QC7Q7eL0AGab`3%PB>XlQi|T8B$He_(2b)QiC`(_|FufngWMB&hJj; zYx0PvveQBfwH>9ONumWIr}Ko@z)7OKJf0T09Ro;+5G$o3rAd{(Bes@{bZq_kdHLJ$ zHQ%Q#eSouH-X#PP11R#$rbN_>6Ws%)leLZUNnUj+K9MF)IyyInOiaNkAZghc0g#9w z2asi{SsQd|pUatXZ#-61r)so^Jsb#6hU+1le!|-(H4rRRITI<8kUq z^TK#pE!tc>%t!CTx%VV2LTu<5+~mR#L|pDO09pjvT2|IJl18`$OSqkp_c<(QJ2TZk zRNe%%aJ*=eXC^AIuK|!)NMVKDOBWGt`y^fGvCJ;ek-~V{7ww3^#5aKjU&HR@h?!$~VM=BZqq`(qPL_i_p;f zN!D_tBbq;XWW4_D7hLv+wAkXp43$U@ke`uCe)eId%7S_04eW%+rpv6E8mF4Q5wvjT zblGy(5@9nuRSoB1!@KQNP3dB)-z8=ZU<$!xT!=7bpM2lyuc{;;StFaM`AcYi`*8@j z@SHPV%4JqL>lMmcl?fYQ(0mGJofj78VU6STz!x95_sGK=H+Pqk=NFlVC25C^$AtZME$5TG#|lZ=3L_`HwKe8g`D> zoROsl>6nGZsA9bE7r8yS9+4iGk~}28;r>+lj!y_^!tz8)pmrq%vqk5r#3lhy##luP z{gX$=4_@=!i@$L^9$8~k#cWZ}4Xe3L6(*qIGd#%-u|l(JIo0L0t>4U&XeGJLGVvR( zpR%3}^S-v~d`@)r>Ps%8<3>>Aj4WkjsYQ{yKvxnEM(_W_M}JNy#n2SI4rfJ$&cAa~ zo(urB%j0GE9vMn26&*XeI@-T)-(+Qf?}ek$mKCsaZ~P+&tMc8U?y61&xWB7Z2@iy_ z2GWpBUZylT4Sfl9Hxj4lk(*N(BmmhlU;<8PTcwYXYRZA>Ze_?yE7+O zk4BpoP!2>wAS6)Kae+ft<$#o%Ex}Z7Tv~HADGdfyYQ9-T@Wlbp4Zf=WM)_JZ|K3;k zGCdXiUYFVXgg62ZNw#YLoDs)HLmumW2rz1XS}bRqD{0WbG{&>^b6j%WzGznP=ze&7fq?*1e( zAaPlr7$h---DgPT>cvqN9cM!&pj_14XO}B&rQ1*ReV@Z`eB`eV{O>4IBWrjSzz9v} z-#gi#GPAjyzlb~_S|>jWxKA+&1R>_En6cng(Yx=SkIMPABqwvByIo^ zLm>i@OKSi$2o7Kn_cuzZ0ns362Ld<`1W^Q(*8yS>#ZK}efl>*G{1&@o6oab!M^zmN zC74+|5S9RCt4gp%AkrW(3l$P4qQrELeDr^<{_D?u^1)9;ks2BYo*|qi>s8k|1y6g> zwnOpyL<2|w^Z?Aov0i53#Ypv5UjJRs`u_R7Td&T{h7tx8MM#nX<_5bl-(Nw}>4HeW zV!%>>#X!VD;5N@W!zfhf3h+d{3f7XU_oW+wyeomc#3)sqx89^qyKSbiFs$W9wkESd zVN$l7lF|iPLz4&Z$p7?xfAYmYixZl##hSIejv72|@9{Ywun2BKJFak+<;Jp(K(5mY>eQ4?(DM}SO$5JOW3TK5p^E~2P|SSQd?1g#?>r9~-4 zq4ZJ8gT)_HwWHhT8bcuBBpQn?rCbgx87xv1oFXeG7;X-+zBa__u`xDI7LtxEJ$hep z!$bESzrEAdn|a13^3?hiix4S0U->0>E09 z+T#G$P^&A?bfBwYdW#kVEBj>X*3}YijW%hfEHlL-3YVh*a<~C%@imdWk8nGHR_I-HWlp;NjAiJxEU~R*$5~f7;^P(2J z72b@QANR5V=#{f!=b_SFf~F3Jjl8 z>>iEoQEU_6IQlwMm70IIpSz#?ICq7Mi*3o-6eTaa2v;Y6ef`3mXcw>iSN_$v!i^>f zpsL?kbuwTpFt$?&$6s}AR8^@REY7xyEjUJeWtOz*|9vDNm z{Q$c-it&%!+zf)bdF(mga=(rojI1Laa`FW#c+i}JYL*#Ue{rRrebA#AmU`w7V_HUW zeN>Mmf5X;i!NG94^)@UjPES^zExk#!0ZYx-;YH%7j02=FcDe`QHtgIl4D$Y#%u_H( zAc>@N*eRvOD8V?Hyi5u}sXQFi>QK5ifxthsc4^6ajU)dF;ksgC;iB?Quh$up+Yyip zvuC*TR7`x>O6*y)e?q?H&Qo7!V0h>dZuHT)(GWmY=rKc~6m%|Q#{B5Hs(LS}Gg$2z z17e`{N@^vMHeWk%Zs+mQ@N9HG^zwO8b_?6Yl#f_}iGx5?j&pGK$%dO`e#Fcdb;^P_Jy7SJK2jiU!knKJEj{j^=?{gvP|zIJvmGaJ0LZxHyP_fX1pA@O9_3lbg=%Mk^K zW*p@fimf@VROqZ(D_=gb4Id%i6Fg;-h)7T6mU1_)&D2B7&D9VNZopQ2NCT5QwHT;v z(|G4<%4!!2@%?=y=P}Wm000|`nxU4M`&!TBn=dk|<5;I9j~_J0C(jyyo5qQ=?kDFY z?R9vtJv!p~7U`|c3OyEFmML*0LCpx0P_3e}2%+5UZSy-AdCMLrXP}LDDyha>85a4R%Z4u&ADo&S|{Y(7wNXbcJw`pQjTlrHaca&@UB^Bs`VjrX{C|5*}BN9Jp zZAZA}kbQq7nJE-~e?5wKtYlFGu(OrxJ#VExD94{4ul(-kqD`uCg?LX(>cN6}#}i(0 z^aZ_4UgZ_v(nsVErq|eaTwqyN^<*4ZItNalbe>-g*ib~oT$G;R@oHaeKc*bBZ)ea} zYW}yA{RL*1?S>FbkSlfQU{e~ipSzPZRf6#r5QQdj6ghheMs(`d4dn+EaarHhjxqaf zgTK#U`KZ!o<{xeyk1?^-5sn!T8EV{d*Cf}6>wMLch)9nG5@2#ok2Iw;3&#?;-$`a+ zS57={KkD>xZ%Gj?X2eFvXQEL@&RbxuI4exUv~R+`pG^&mZO*qT z)>9F+qV z?dP36KYkDx;wZ@4QXZn9Y+aL}Nwh*& z+(Z2&YR!csV*&aP*q?uWdZ=g>YvAI>hetp3$+>swRcesoi$dOwviQ?`FAo%}*Yjg7 z6PNUZr-W|nXHsi#n!jEzU&>Srh!{S++~lu!Qvbc|8ntLF1s3-}A=U4b^xY$P6}FPH z|A;e=k<0Jg)n^q2ixV*sz&$GbsjwXnc!Vg8`4o08Fu!S3%$ue7d@8Li*L67)wE7db zd~GOpeQ)-aAFZid2BtVSPZT&IqJedXbwIyhtPW$(Bv9p8Z4#r1$7pi$uM$X?rVJQM zV_oa1LfxV<`^LlT5BP@NNd<#Dy9Q>i|J>q5s_Z;evts}~i4tr?65cmC?;$c?u}>QAdT zGBl2LncX;1kXfE^TF_4+azantNH~Mna^QB74AjNb*g7ro>E7xVJnVPjZT%8);ytsc zA>M5jp<;l$&|IhEu~69d=3sAnXhC0oQ_z;+<+RBg+Dn%GQaQs}xXSuSlD|yW8$I_4 zKGWOpecVh3KXvcc8AQCKXPY;s%}G_}UiKv6=zJqiK*q`dLxe~q&Iw1*^@FEB-YAN% z#%(08A%}IcAuTTyxnQqMv4LU>Ix&M7aTDfYh0*a#y1y5MrT4nW3|7AvG3|{#op5JB zZI&qN>r<4>f!N;berv<2ms@HsBoR_^iGPn@fxq7P^G8not6xh=Ye_t&x%!FL9>GS> zr@MC_UbJZb<3X42quWNGPSke#Ud{_<9+s`?1JLBvPKmrU`#Y>;-|WyIGzYzl z;bzz6w(l5Tms|MrlW3O)Q&#VcK^Fqn(D{_wZ&wHb#@$ zCbd+T$M~v5g4Xbf?>C!;f?T)T9V(l@?3&GAu71)SY}jfbs~m7x9)s>yDpS^6YMoyv zXoY=t*$C?!neh<+TJvI2HBycBQ9gCPk^Pixp?98{Pw@sOP}kfO$DZ<2#eX`eH-s&< z7qqCaL#PJo-Zexx~6xkH{GZw zCc!5lphQbH2*&madGEpUZ|CTwUK>rjR96lPv&e-DaW<|`ZT@urL0eCP-AWd80b26& zcAyI%rM_P2Msh+;9WHW$A)Z|y|6q_iYn(pql!xBlIKSIcYd?`+))d(>R4u{5w9Y;4 z&Bt2fIA@#Y2*7aTLFjCb4jC7^TU4m2} zv>h1UNRQ)v7kg>x-1p5lBi+X@nfG(4jPESBs~Apa(7&aNT%}Bkyik2o34dHIUH{YL z**g{8V;Hxi7PUs+j-F~we5@_#o5rAEz21K|$-6koV00aV*BgQynhM)C;qCV0UO0|P;7pn4D+rcyuzmRw(k`H+26EglR%2C_dcS5K7~}*L_rV_*p^v<@IGuq07)S5&#aC>Abr0Kbg?0k fedym91iL@%p^iY2K86jjF~HQs0{hVDO4NS<0ONux literal 0 HcmV?d00001 diff --git a/rxjava-contrib/rxjava-android-samples/samples/src/main/res/layout/retained_fragment.xml b/rxjava-contrib/rxjava-android-samples/samples/src/main/res/layout/retained_fragment.xml new file mode 100644 index 0000000000..0fc012b481 --- /dev/null +++ b/rxjava-contrib/rxjava-android-samples/samples/src/main/res/layout/retained_fragment.xml @@ -0,0 +1,13 @@ + + + + + + + \ No newline at end of file diff --git a/rxjava-contrib/rxjava-android-samples/samples/src/main/res/layout/sample_activity.xml b/rxjava-contrib/rxjava-android-samples/samples/src/main/res/layout/sample_activity.xml new file mode 100644 index 0000000000..5900d1d74e --- /dev/null +++ b/rxjava-contrib/rxjava-android-samples/samples/src/main/res/layout/sample_activity.xml @@ -0,0 +1,14 @@ + + + + + diff --git a/rxjava-contrib/rxjava-android-samples/samples/src/main/res/values-w820dp/dimens.xml b/rxjava-contrib/rxjava-android-samples/samples/src/main/res/values-w820dp/dimens.xml new file mode 100644 index 0000000000..63fc816444 --- /dev/null +++ b/rxjava-contrib/rxjava-android-samples/samples/src/main/res/values-w820dp/dimens.xml @@ -0,0 +1,6 @@ + + + 64dp + diff --git a/rxjava-contrib/rxjava-android-samples/samples/src/main/res/values/dimens.xml b/rxjava-contrib/rxjava-android-samples/samples/src/main/res/values/dimens.xml new file mode 100644 index 0000000000..a0171a705d --- /dev/null +++ b/rxjava-contrib/rxjava-android-samples/samples/src/main/res/values/dimens.xml @@ -0,0 +1,6 @@ + + + 16dp + 16dp + + diff --git a/rxjava-contrib/rxjava-android-samples/samples/src/main/res/values/strings.xml b/rxjava-contrib/rxjava-android-samples/samples/src/main/res/values/strings.xml new file mode 100644 index 0000000000..79ce6bb997 --- /dev/null +++ b/rxjava-contrib/rxjava-android-samples/samples/src/main/res/values/strings.xml @@ -0,0 +1,9 @@ + + + + RxJava Android Samples + RetainedFragmentActivity + Hello world! + Settings + + diff --git a/rxjava-contrib/rxjava-android-samples/samples/src/main/res/values/styles.xml b/rxjava-contrib/rxjava-android-samples/samples/src/main/res/values/styles.xml new file mode 100644 index 0000000000..b07e679474 --- /dev/null +++ b/rxjava-contrib/rxjava-android-samples/samples/src/main/res/values/styles.xml @@ -0,0 +1,12 @@ + + + + + + + diff --git a/rxjava-contrib/rxjava-android-samples/settings.gradle b/rxjava-contrib/rxjava-android-samples/settings.gradle new file mode 100644 index 0000000000..f3a182b826 --- /dev/null +++ b/rxjava-contrib/rxjava-android-samples/settings.gradle @@ -0,0 +1 @@ +include ':samples' diff --git a/settings.gradle b/settings.gradle index 5be4134cef..3a1052e1b1 100644 --- a/settings.gradle +++ b/settings.gradle @@ -7,6 +7,7 @@ include 'rxjava-core', \ 'language-adaptors:rxjava-kotlin', \ 'rxjava-contrib:rxjava-swing', \ 'rxjava-contrib:rxjava-android', \ +'rxjava-contrib:rxjava-android-samples-build-wrapper', \ 'rxjava-contrib:rxjava-apache-http', \ 'rxjava-contrib:rxjava-string', \ 'rxjava-contrib:rxjava-debug', \ From de3184475b48efabb99aea51b4b015b9ecab2f2f Mon Sep 17 00:00:00 2001 From: headinthebox Date: Wed, 26 Feb 2014 18:53:19 -0800 Subject: [PATCH 063/422] Implemented Skip using Lift in Observable and OperatorSkip Deleted non-time part from OperationSkip Moved tests to OperatorSkipTest --- rxjava-core/src/main/java/rx/Observable.java | 84 +------------------ .../main/java/rx/operators/OperationSkip.java | 75 ----------------- .../main/java/rx/operators/OperatorMap.java | 1 + .../main/java/rx/operators/OperatorSkip.java | 49 +++++++++++ .../java/rx/operators/OperationSkipTest.java | 31 ------- .../java/rx/operators/OperatorSkipTest.java | 40 +++++++++ 6 files changed, 92 insertions(+), 188 deletions(-) create mode 100644 rxjava-core/src/main/java/rx/operators/OperatorSkip.java create mode 100644 rxjava-core/src/test/java/rx/operators/OperatorSkipTest.java diff --git a/rxjava-core/src/main/java/rx/Observable.java b/rxjava-core/src/main/java/rx/Observable.java index 893ec455eb..c244fb5001 100644 --- a/rxjava-core/src/main/java/rx/Observable.java +++ b/rxjava-core/src/main/java/rx/Observable.java @@ -49,87 +49,7 @@ import rx.observables.ConnectableObservable; import rx.observables.GroupedObservable; import rx.observers.SafeSubscriber; -import rx.operators.OnSubscribeFromIterable; -import rx.operators.OnSubscribeRange; -import rx.operators.OperationAll; -import rx.operators.OperationAmb; -import rx.operators.OperationAny; -import rx.operators.OperationAsObservable; -import rx.operators.OperationAverage; -import rx.operators.OperationBuffer; -import rx.operators.OperationCache; -import rx.operators.OperationCombineLatest; -import rx.operators.OperationConcat; -import rx.operators.OperationDebounce; -import rx.operators.OperationDefaultIfEmpty; -import rx.operators.OperationDefer; -import rx.operators.OperationDelay; -import rx.operators.OperationDematerialize; -import rx.operators.OperationDistinct; -import rx.operators.OperationDistinctUntilChanged; -import rx.operators.OperationElementAt; -import rx.operators.OperationFinally; -import rx.operators.OperationFlatMap; -import rx.operators.OperationGroupByUntil; -import rx.operators.OperationGroupJoin; -import rx.operators.OperationInterval; -import rx.operators.OperationJoin; -import rx.operators.OperationJoinPatterns; -import rx.operators.OperationMaterialize; -import rx.operators.OperationMergeDelayError; -import rx.operators.OperationMergeMaxConcurrent; -import rx.operators.OperationMinMax; -import rx.operators.OperationMulticast; -import rx.operators.OperationOnErrorResumeNextViaObservable; -import rx.operators.OperationOnErrorReturn; -import rx.operators.OperationOnExceptionResumeNextViaObservable; -import rx.operators.OperationParallelMerge; -import rx.operators.OperationReplay; -import rx.operators.OperationRetry; -import rx.operators.OperationSample; -import rx.operators.OperationSequenceEqual; -import rx.operators.OperationSingle; -import rx.operators.OperationSkip; -import rx.operators.OperationSkipLast; -import rx.operators.OperationSkipUntil; -import rx.operators.OperationSkipWhile; -import rx.operators.OperationSum; -import rx.operators.OperationSwitch; -import rx.operators.OperationSynchronize; -import rx.operators.OperationTakeLast; -import rx.operators.OperationTakeTimed; -import rx.operators.OperationTakeUntil; -import rx.operators.OperationTakeWhile; -import rx.operators.OperationThrottleFirst; -import rx.operators.OperationTimeInterval; -import rx.operators.OperationTimer; -import rx.operators.OperationToMap; -import rx.operators.OperationToMultimap; -import rx.operators.OperationToObservableFuture; -import rx.operators.OperationUsing; -import rx.operators.OperationWindow; -import rx.operators.OperatorCast; -import rx.operators.OperatorDoOnEach; -import rx.operators.OperatorFilter; -import rx.operators.OperatorGroupBy; -import rx.operators.OperatorMap; -import rx.operators.OperatorMerge; -import rx.operators.OperatorObserveOn; -import rx.operators.OperatorOnErrorResumeNextViaFunction; -import rx.operators.OperatorOnErrorFlatMap; -import rx.operators.OperatorParallel; -import rx.operators.OperatorRepeat; -import rx.operators.OperatorScan; -import rx.operators.OperatorSubscribeOn; -import rx.operators.OperatorTake; -import rx.operators.OperatorTimeout; -import rx.operators.OperatorTimeoutWithSelector; -import rx.operators.OperatorTimestamp; -import rx.operators.OperatorToObservableList; -import rx.operators.OperatorToObservableSortedList; -import rx.operators.OperatorUnsubscribeOn; -import rx.operators.OperatorZip; -import rx.operators.OperatorZipIterable; +import rx.operators.*; import rx.plugins.RxJavaObservableExecutionHook; import rx.plugins.RxJavaPlugins; import rx.schedulers.Schedulers; @@ -6274,7 +6194,7 @@ public final Observable singleOrDefault(T defaultValue, Func1RxJava Wiki: skip() */ public final Observable skip(int num) { - return create(OperationSkip.skip(this, num)); + return lift(new OperatorSkip(num)); } /** diff --git a/rxjava-core/src/main/java/rx/operators/OperationSkip.java b/rxjava-core/src/main/java/rx/operators/OperationSkip.java index 5267322e96..31ffa1c90f 100644 --- a/rxjava-core/src/main/java/rx/operators/OperationSkip.java +++ b/rxjava-core/src/main/java/rx/operators/OperationSkip.java @@ -39,81 +39,6 @@ */ public final class OperationSkip { - /** - * Skips a specified number of contiguous values from the start of a Observable sequence and then returns the remaining values. - * - * @param items - * @param num - * @return the observable sequence starting after a number of skipped values - * - * @see Observable.Skip(TSource) Method - */ - public static OnSubscribeFunc skip(final Observable items, final int num) { - // wrap in a Observable so that if a chain is built up, then asynchronously subscribed to twice we will have 2 instances of Take rather than 1 handing both, which is not thread-safe. - return new OnSubscribeFunc() { - - @Override - public Subscription onSubscribe(Observer observer) { - return new Skip(items, num).onSubscribe(observer); - } - - }; - } - - /** - * This class is NOT thread-safe if invoked and referenced multiple times. In other words, don't subscribe to it multiple times from different threads. - *

- * It IS thread-safe from within it while receiving onNext events from multiple threads. - * - * @param - */ - private static class Skip implements OnSubscribeFunc { - private final int num; - private final Observable items; - - Skip(final Observable items, final int num) { - this.num = num; - this.items = items; - } - - public Subscription onSubscribe(Observer observer) { - return items.subscribe(new ItemObserver(observer)); - } - - /** - * Used to subscribe to the 'items' Observable sequence and forward to the actualObserver up to 'num' count. - */ - private class ItemObserver implements Observer { - - private AtomicInteger counter = new AtomicInteger(); - private final Observer observer; - - public ItemObserver(Observer observer) { - this.observer = observer; - } - - @Override - public void onCompleted() { - observer.onCompleted(); - } - - @Override - public void onError(Throwable e) { - observer.onError(e); - } - - @Override - public void onNext(T args) { - // skip them until we reach the 'num' value - if (counter.incrementAndGet() > num) { - observer.onNext(args); - } - } - - } - - } - /** * Skip the items after subscription for the given duration. * diff --git a/rxjava-core/src/main/java/rx/operators/OperatorMap.java b/rxjava-core/src/main/java/rx/operators/OperatorMap.java index 6418005521..233e2fcb70 100644 --- a/rxjava-core/src/main/java/rx/operators/OperatorMap.java +++ b/rxjava-core/src/main/java/rx/operators/OperatorMap.java @@ -61,3 +61,4 @@ public void onNext(T t) { } } + diff --git a/rxjava-core/src/main/java/rx/operators/OperatorSkip.java b/rxjava-core/src/main/java/rx/operators/OperatorSkip.java new file mode 100644 index 0000000000..020c488fa6 --- /dev/null +++ b/rxjava-core/src/main/java/rx/operators/OperatorSkip.java @@ -0,0 +1,49 @@ +package rx.operators; + +import rx.Observable; +import rx.Subscriber; + +/** + * Returns an Observable that skips the first num items emitted by the source + * Observable. + *

+ * + *

+ * You can ignore the first num items emitted by an Observable and attend only to + * those items that come after, by modifying the Observable with the skip operation. + */ +public final class OperatorSkip implements Observable.Operator { + + int n; + + public OperatorSkip(int n) { + this.n = n; + } + + @Override + public Subscriber call(final Subscriber child) { + return new Subscriber(child) { + + @Override + public void onCompleted() { + child.onCompleted(); + } + + @Override + public void onError(Throwable e) { + child.onError(e); + } + + @Override + public void onNext(T t) { + if(n <= 0) { + child.onNext(t); + } else { + n -= 1; + } + } + + }; + } + +} diff --git a/rxjava-core/src/test/java/rx/operators/OperationSkipTest.java b/rxjava-core/src/test/java/rx/operators/OperationSkipTest.java index 5999e07fee..c7dad11ce1 100644 --- a/rxjava-core/src/test/java/rx/operators/OperationSkipTest.java +++ b/rxjava-core/src/test/java/rx/operators/OperationSkipTest.java @@ -17,7 +17,6 @@ import static org.mockito.Matchers.*; import static org.mockito.Mockito.*; -import static rx.operators.OperationSkip.*; import java.util.concurrent.TimeUnit; @@ -31,36 +30,6 @@ public class OperationSkipTest { - @Test - public void testSkip1() { - Observable w = Observable.from("one", "two", "three"); - Observable skip = Observable.create(skip(w, 2)); - - @SuppressWarnings("unchecked") - Observer observer = mock(Observer.class); - skip.subscribe(observer); - verify(observer, never()).onNext("one"); - verify(observer, never()).onNext("two"); - verify(observer, times(1)).onNext("three"); - verify(observer, never()).onError(any(Throwable.class)); - verify(observer, times(1)).onCompleted(); - } - - @Test - public void testSkip2() { - Observable w = Observable.from("one", "two", "three"); - Observable skip = Observable.create(skip(w, 1)); - - @SuppressWarnings("unchecked") - Observer observer = mock(Observer.class); - skip.subscribe(observer); - verify(observer, never()).onNext("one"); - verify(observer, times(1)).onNext("two"); - verify(observer, times(1)).onNext("three"); - verify(observer, never()).onError(any(Throwable.class)); - verify(observer, times(1)).onCompleted(); - } - @Test public void testSkipTimed() { TestScheduler scheduler = new TestScheduler(); diff --git a/rxjava-core/src/test/java/rx/operators/OperatorSkipTest.java b/rxjava-core/src/test/java/rx/operators/OperatorSkipTest.java new file mode 100644 index 0000000000..155faf21eb --- /dev/null +++ b/rxjava-core/src/test/java/rx/operators/OperatorSkipTest.java @@ -0,0 +1,40 @@ +package rx.operators; + +import org.junit.Test; +import rx.Observable; +import rx.Observer; + +import static org.mockito.Mockito.*; + +public class OperatorSkipTest { + + @Test + public void testSkip1() { + Observable w = Observable.from("one", "two", "three"); + Observable skip = w.lift(new OperatorSkip(2)); + + @SuppressWarnings("unchecked") + Observer observer = mock(Observer.class); + skip.subscribe(observer); + verify(observer, never()).onNext("one"); + verify(observer, never()).onNext("two"); + verify(observer, times(1)).onNext("three"); + verify(observer, never()).onError(any(Throwable.class)); + verify(observer, times(1)).onCompleted(); + } + + @Test + public void testSkip2() { + Observable w = Observable.from("one", "two", "three"); + Observable skip = w.lift(new OperatorSkip(1)); + + @SuppressWarnings("unchecked") + Observer observer = mock(Observer.class); + skip.subscribe(observer); + verify(observer, never()).onNext("one"); + verify(observer, times(1)).onNext("two"); + verify(observer, times(1)).onNext("three"); + verify(observer, never()).onError(any(Throwable.class)); + verify(observer, times(1)).onCompleted(); + } +} From dbb08348f9928000c883a9157840b3b3d5ba4ce1 Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Thu, 27 Feb 2014 15:17:24 +1100 Subject: [PATCH 064/422] add Observable.startWith(Observable) method and unit test to avoid breaking method chaining when want to startWith an Observable rather than just an Iterator or specific values. --- rxjava-core/src/main/java/rx/Observable.java | 16 ++++++++++++++++ rxjava-core/src/test/java/rx/StartWithTests.java | 13 +++++++++++++ 2 files changed, 29 insertions(+) diff --git a/rxjava-core/src/main/java/rx/Observable.java b/rxjava-core/src/main/java/rx/Observable.java index 893ec455eb..61d0e1d180 100644 --- a/rxjava-core/src/main/java/rx/Observable.java +++ b/rxjava-core/src/main/java/rx/Observable.java @@ -6431,6 +6431,22 @@ public final Observable skipWhileWithIndex(Func2 return create(OperationSkipWhile.skipWhileWithIndex(this, predicate)); } + /** + * Returns an Observable that emits the items in a specified {@link Observable} before it begins to emit items + * emitted by the source Observable. + *

+ * + * + * @param values + * an Observable that contains the items you want the modified Observable to emit first + * @return an Observable that emits the items in the specified {@link Observable} and then emits the items + * emitted by the source Observable + * @see RxJava Wiki: startWith() + */ + public final Observable startWith(Observable values) { + return concat(values, this); + } + /** * Returns an Observable that emits the items in a specified {@link Iterable} before it begins to emit items * emitted by the source Observable. diff --git a/rxjava-core/src/test/java/rx/StartWithTests.java b/rxjava-core/src/test/java/rx/StartWithTests.java index e2a429723d..4ea4c7cde0 100644 --- a/rxjava-core/src/test/java/rx/StartWithTests.java +++ b/rxjava-core/src/test/java/rx/StartWithTests.java @@ -45,4 +45,17 @@ public void startWithIterable() { assertEquals("two", values.get(3)); } + @Test + public void startWithObservable() { + List li = new ArrayList(); + li.add("alpha"); + li.add("beta"); + List values = Observable.from("one", "two").startWith(Observable.from(li)).toList().toBlockingObservable().single(); + + assertEquals("alpha", values.get(0)); + assertEquals("beta", values.get(1)); + assertEquals("one", values.get(2)); + assertEquals("two", values.get(3)); + } + } From 0b6b4650105aa148ec3f69740c09a53b2631888f Mon Sep 17 00:00:00 2001 From: Dave Moten Date: Thu, 27 Feb 2014 17:26:41 +1100 Subject: [PATCH 065/422] add space to a comment to force ci rebuild to see if suspected non-deterministic test fails again --- rxjava-core/src/main/java/rx/Observable.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rxjava-core/src/main/java/rx/Observable.java b/rxjava-core/src/main/java/rx/Observable.java index 61d0e1d180..8a3bfa95de 100644 --- a/rxjava-core/src/main/java/rx/Observable.java +++ b/rxjava-core/src/main/java/rx/Observable.java @@ -6433,7 +6433,7 @@ public final Observable skipWhileWithIndex(Func2 /** * Returns an Observable that emits the items in a specified {@link Observable} before it begins to emit items - * emitted by the source Observable. + * emitted by the source Observable. *

* * From 9f8e3e8226cfd845b9c46cf2b2c7ba4e296b5bc4 Mon Sep 17 00:00:00 2001 From: Matthias Kaeppler Date: Thu, 27 Feb 2014 09:59:55 +0100 Subject: [PATCH 066/422] Simplify version property forwarding --- .../rxjava-android-samples-build-wrapper/build.gradle | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/rxjava-contrib/rxjava-android-samples-build-wrapper/build.gradle b/rxjava-contrib/rxjava-android-samples-build-wrapper/build.gradle index 87f0f5bbf3..af1f33cec5 100644 --- a/rxjava-contrib/rxjava-android-samples-build-wrapper/build.gradle +++ b/rxjava-contrib/rxjava-android-samples-build-wrapper/build.gradle @@ -7,13 +7,7 @@ tasks.build.doLast { project.exec { workingDir '../rxjava-android-samples' - def props = new Properties() - file("$rootDir/gradle.properties").withReader { reader -> - props.load(reader) - properties.putAll(props) - } - - commandLine "./gradlew", "clean", "packageDebug", "-PrxjVersion=${props.getProperty("version")}" + commandLine "./gradlew", "clean", "packageDebug", "-PrxjVersion=${version}" } } } From b780143add1c649fb6b594adb94e247157bd3b84 Mon Sep 17 00:00:00 2001 From: Matthias Kaeppler Date: Thu, 27 Feb 2014 10:10:51 +0100 Subject: [PATCH 067/422] Fix project import issue in Android Studio and a compilation failure --- .../rxjava-android-samples-build-wrapper/build.gradle | 2 +- .../rxjava-android-samples/samples/build.gradle | 8 ++++++++ .../netflix/rxjava/android/samples/RetainedFragment.java | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/rxjava-contrib/rxjava-android-samples-build-wrapper/build.gradle b/rxjava-contrib/rxjava-android-samples-build-wrapper/build.gradle index af1f33cec5..de7ef0162c 100644 --- a/rxjava-contrib/rxjava-android-samples-build-wrapper/build.gradle +++ b/rxjava-contrib/rxjava-android-samples-build-wrapper/build.gradle @@ -7,7 +7,7 @@ tasks.build.doLast { project.exec { workingDir '../rxjava-android-samples' - commandLine "./gradlew", "clean", "packageDebug", "-PrxjVersion=${version}" + commandLine "./gradlew", "clean", "packageDebug" } } } diff --git a/rxjava-contrib/rxjava-android-samples/samples/build.gradle b/rxjava-contrib/rxjava-android-samples/samples/build.gradle index 1b89f83fdc..9a080128a8 100644 --- a/rxjava-contrib/rxjava-android-samples/samples/build.gradle +++ b/rxjava-contrib/rxjava-android-samples/samples/build.gradle @@ -18,7 +18,15 @@ android { } } +// make sure we always compile against the latest version of RxJava +def rootProjectProperties = new Properties() +file("../../../gradle.properties").withReader { reader -> + rootProjectProperties.load(reader) + properties.putAll(rootProjectProperties) +} + dependencies { + def rxjVersion = rootProjectProperties.get("version") compile "com.netflix.rxjava:rxjava-android:$rxjVersion" compile fileTree(dir: 'libs', include: ['*.jar']) } diff --git a/rxjava-contrib/rxjava-android-samples/samples/src/main/java/com/netflix/rxjava/android/samples/RetainedFragment.java b/rxjava-contrib/rxjava-android-samples/samples/src/main/java/com/netflix/rxjava/android/samples/RetainedFragment.java index a3a0c73d72..0db00e3ac8 100644 --- a/rxjava-contrib/rxjava-android-samples/samples/src/main/java/com/netflix/rxjava/android/samples/RetainedFragment.java +++ b/rxjava-contrib/rxjava-android-samples/samples/src/main/java/com/netflix/rxjava/android/samples/RetainedFragment.java @@ -36,7 +36,7 @@ public RetainedFragment() { public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - strings = SampleObservables.numberStrings2().cache(); + strings = SampleObservables.numberStrings().cache(); } @Override From 6b25b2304b08dbaf9f4d52054f06d743a9fb4a22 Mon Sep 17 00:00:00 2001 From: headinthebox Date: Thu, 27 Feb 2014 11:31:40 -0800 Subject: [PATCH 068/422] Fixed state capture bug. Added some additional tests --- .../main/java/rx/operators/OperatorSkip.java | 8 +- .../java/rx/operators/OperatorSkipTest.java | 102 ++++++++++++++++-- 2 files changed, 99 insertions(+), 11 deletions(-) diff --git a/rxjava-core/src/main/java/rx/operators/OperatorSkip.java b/rxjava-core/src/main/java/rx/operators/OperatorSkip.java index 020c488fa6..cc0ec068c2 100644 --- a/rxjava-core/src/main/java/rx/operators/OperatorSkip.java +++ b/rxjava-core/src/main/java/rx/operators/OperatorSkip.java @@ -14,7 +14,7 @@ */ public final class OperatorSkip implements Observable.Operator { - int n; + final int n; public OperatorSkip(int n) { this.n = n; @@ -24,6 +24,8 @@ public OperatorSkip(int n) { public Subscriber call(final Subscriber child) { return new Subscriber(child) { + int skipped = 0; + @Override public void onCompleted() { child.onCompleted(); @@ -36,10 +38,10 @@ public void onError(Throwable e) { @Override public void onNext(T t) { - if(n <= 0) { + if(skipped >= n) { child.onNext(t); } else { - n -= 1; + skipped += 1; } } diff --git a/rxjava-core/src/test/java/rx/operators/OperatorSkipTest.java b/rxjava-core/src/test/java/rx/operators/OperatorSkipTest.java index 155faf21eb..f0a9706786 100644 --- a/rxjava-core/src/test/java/rx/operators/OperatorSkipTest.java +++ b/rxjava-core/src/test/java/rx/operators/OperatorSkipTest.java @@ -9,24 +9,39 @@ public class OperatorSkipTest { @Test - public void testSkip1() { - Observable w = Observable.from("one", "two", "three"); - Observable skip = w.lift(new OperatorSkip(2)); + public void testSkipNegativeElements() { + + Observable skip = Observable.from("one", "two", "three").lift(new OperatorSkip(-99)); @SuppressWarnings("unchecked") Observer observer = mock(Observer.class); skip.subscribe(observer); - verify(observer, never()).onNext("one"); - verify(observer, never()).onNext("two"); + verify(observer, times(1)).onNext("one"); + verify(observer, times(1)).onNext("two"); verify(observer, times(1)).onNext("three"); verify(observer, never()).onError(any(Throwable.class)); verify(observer, times(1)).onCompleted(); } @Test - public void testSkip2() { - Observable w = Observable.from("one", "two", "three"); - Observable skip = w.lift(new OperatorSkip(1)); + public void testSkipZeroElements() { + + Observable skip = Observable.from("one", "two", "three").lift(new OperatorSkip(0)); + + @SuppressWarnings("unchecked") + Observer observer = mock(Observer.class); + skip.subscribe(observer); + verify(observer, times(1)).onNext("one"); + verify(observer, times(1)).onNext("two"); + verify(observer, times(1)).onNext("three"); + verify(observer, never()).onError(any(Throwable.class)); + verify(observer, times(1)).onCompleted(); + } + + @Test + public void testSkipOneElement() { + + Observable skip = Observable.from("one", "two", "three").lift(new OperatorSkip(1)); @SuppressWarnings("unchecked") Observer observer = mock(Observer.class); @@ -37,4 +52,75 @@ public void testSkip2() { verify(observer, never()).onError(any(Throwable.class)); verify(observer, times(1)).onCompleted(); } + + @Test + public void testSkipTwoElements() { + + Observable skip = Observable.from("one", "two", "three").lift(new OperatorSkip(2)); + + @SuppressWarnings("unchecked") + Observer observer = mock(Observer.class); + skip.subscribe(observer); + verify(observer, never()).onNext("one"); + verify(observer, never()).onNext("two"); + verify(observer, times(1)).onNext("three"); + verify(observer, never()).onError(any(Throwable.class)); + verify(observer, times(1)).onCompleted(); + } + + @Test + public void testSkipEmptyStream() { + + Observable w = Observable.empty(); + Observable skip = w.lift(new OperatorSkip(1)); + + @SuppressWarnings("unchecked") + Observer observer = mock(Observer.class); + skip.subscribe(observer); + verify(observer, never()).onNext(any(String.class)); + verify(observer, never()).onError(any(Throwable.class)); + verify(observer, times(1)).onCompleted(); + } + + @Test + public void testSkipMultipleObservers() { + + Observable skip = Observable.from("one", "two", "three").lift(new OperatorSkip(2)); + + @SuppressWarnings("unchecked") + Observer observer1 = mock(Observer.class); + skip.subscribe(observer1); + + @SuppressWarnings("unchecked") + Observer observer2 = mock(Observer.class); + skip.subscribe(observer2); + + verify(observer1, times(1)).onNext(any(String.class)); + verify(observer1, never()).onError(any(Throwable.class)); + verify(observer1, times(1)).onCompleted(); + + verify(observer2, times(1)).onNext(any(String.class)); + verify(observer2, never()).onError(any(Throwable.class)); + verify(observer2, times(1)).onCompleted(); + } + + @Test + public void testSkipError() { + + Exception e = new Exception(); + + Observable ok = Observable.from("one"); + Observable error = Observable.error(e); + + Observable skip = Observable.concat(ok, error).lift(new OperatorSkip(100)); + + @SuppressWarnings("unchecked") + Observer observer = mock(Observer.class); + skip.subscribe(observer); + + verify(observer, never()).onNext(any(String.class)); + verify(observer, times(1)).onError(e); + verify(observer, never()).onCompleted(); + + } } From 955bc509d6ced282c249d5714673ec35f0061380 Mon Sep 17 00:00:00 2001 From: Matthias Kaeppler Date: Sun, 2 Mar 2014 12:40:50 +0100 Subject: [PATCH 069/422] First implementation of OperatorWeakBinding --- .../observables/AndroidObservable.java | 23 ++++++ .../OperatorObserveFromAndroidComponent.java | 1 + .../rx/operators/OperatorWeakBinding.java | 75 +++++++++++++++++++ 3 files changed, 99 insertions(+) create mode 100644 rxjava-contrib/rxjava-android/src/main/java/rx/operators/OperatorWeakBinding.java diff --git a/rxjava-contrib/rxjava-android/src/main/java/rx/android/observables/AndroidObservable.java b/rxjava-contrib/rxjava-android/src/main/java/rx/android/observables/AndroidObservable.java index d55617ddea..723ebf40b0 100644 --- a/rxjava-contrib/rxjava-android/src/main/java/rx/android/observables/AndroidObservable.java +++ b/rxjava-contrib/rxjava-android/src/main/java/rx/android/observables/AndroidObservable.java @@ -15,8 +15,12 @@ */ package rx.android.observables; +import static rx.android.schedulers.AndroidSchedulers.mainThread; + import rx.Observable; import rx.operators.OperatorObserveFromAndroidComponent; +import rx.operators.OperatorWeakBinding; + import android.app.Activity; import android.app.Fragment; import android.os.Build; @@ -58,6 +62,7 @@ private AndroidObservable() {} * @param * @return a new observable sequence that will emit notifications on the main UI thread */ + @Deprecated public static Observable fromActivity(Activity activity, Observable sourceObservable) { return OperatorObserveFromAndroidComponent.observeFromAndroidComponent(sourceObservable, activity); } @@ -86,6 +91,7 @@ public static Observable fromActivity(Activity activity, Observable so * @param * @return a new observable sequence that will emit notifications on the main UI thread */ + @Deprecated public static Observable fromFragment(Object fragment, Observable sourceObservable) { if (USES_SUPPORT_FRAGMENTS && fragment instanceof android.support.v4.app.Fragment) { return OperatorObserveFromAndroidComponent.observeFromAndroidComponent(sourceObservable, (android.support.v4.app.Fragment) fragment); @@ -95,4 +101,21 @@ public static Observable fromFragment(Object fragment, Observable sour throw new IllegalArgumentException("Target fragment is neither a native nor support library Fragment"); } } + + public static Observable bindActivity(Activity activity, Observable cachedSequence) { + return cachedSequence.observeOn(mainThread()).lift(new OperatorWeakBinding(activity)); + } + + public static Observable bindFragment(Object fragment, Observable cachedSequence) { + Observable source = cachedSequence.observeOn(mainThread()); + if (USES_SUPPORT_FRAGMENTS && fragment instanceof android.support.v4.app.Fragment) { + android.support.v4.app.Fragment f = (android.support.v4.app.Fragment) fragment; + return source.lift(new OperatorWeakBinding(f)); + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB && fragment instanceof Fragment) { + Fragment f = (Fragment) fragment; + return source.lift(new OperatorWeakBinding(f)); + } else { + throw new IllegalArgumentException("Target fragment is neither a native nor support library Fragment"); + } + } } diff --git a/rxjava-contrib/rxjava-android/src/main/java/rx/operators/OperatorObserveFromAndroidComponent.java b/rxjava-contrib/rxjava-android/src/main/java/rx/operators/OperatorObserveFromAndroidComponent.java index 3e656ccfbb..754ebb2e4e 100644 --- a/rxjava-contrib/rxjava-android/src/main/java/rx/operators/OperatorObserveFromAndroidComponent.java +++ b/rxjava-contrib/rxjava-android/src/main/java/rx/operators/OperatorObserveFromAndroidComponent.java @@ -25,6 +25,7 @@ import android.os.Looper; import android.util.Log; +@Deprecated public class OperatorObserveFromAndroidComponent { public static Observable observeFromAndroidComponent(Observable source, android.app.Fragment fragment) { diff --git a/rxjava-contrib/rxjava-android/src/main/java/rx/operators/OperatorWeakBinding.java b/rxjava-contrib/rxjava-android/src/main/java/rx/operators/OperatorWeakBinding.java new file mode 100644 index 0000000000..3392cc887a --- /dev/null +++ b/rxjava-contrib/rxjava-android/src/main/java/rx/operators/OperatorWeakBinding.java @@ -0,0 +1,75 @@ +package rx.operators; + +import rx.Observable; +import rx.Subscriber; + +import android.util.Log; + +import java.lang.ref.WeakReference; + +public final class OperatorWeakBinding implements Observable.Operator { + + private static final String LOG_TAG = "WeakBinding"; + + private final WeakReference boundRef; + + public OperatorWeakBinding(R bound) { + boundRef = new WeakReference(bound); + } + + @Override + public Subscriber call(final Subscriber child) { + return new WeakSubscriber(child, boundRef); + } + + private static final class WeakSubscriber extends Subscriber { + + private final WeakReference> subscriberRef; + private final WeakReference boundRef; + + private WeakSubscriber(Subscriber op, WeakReference boundRef) { + subscriberRef = new WeakReference>(op); + this.boundRef = boundRef; + } + + @Override + public void onCompleted() { + Subscriber sub = subscriberRef.get(); + if (sub != null && boundRef.get() != null) { + sub.onCompleted(); + } else { + handleLostBinding(sub, "onCompleted"); + } + } + + @Override + public void onError(Throwable e) { + Subscriber sub = subscriberRef.get(); + if (sub != null && boundRef.get() != null) { + sub.onError(e); + } else { + handleLostBinding(sub, "onError"); + } + } + + @Override + public void onNext(T t) { + Subscriber sub = subscriberRef.get(); + if (sub != null && boundRef.get() != null) { + sub.onNext(t); + } else { + handleLostBinding(sub, "onNext"); + } + } + + private void handleLostBinding(Subscriber sub, String context) { + if (sub == null) { + Log.d(LOG_TAG, "subscriber gone; skipping " + context); + } else { + Log.d(LOG_TAG, "bound component gone; skipping " + context); + } + unsubscribe(); + } + + } +} From eb92efdbcbe49bfced1509012860345ee8729488 Mon Sep 17 00:00:00 2001 From: George Campbell Date: Sat, 1 Mar 2014 16:11:46 -0800 Subject: [PATCH 070/422] refactor the debug hooks before they become a breaking change. --- .../java/rx/operators/DebugSubscriber.java | 49 ++-- .../java/rx/operators/DebugSubscription.java | 22 +- .../src/main/java/rx/plugins/DebugHook.java | 73 +++--- .../java/rx/plugins/DebugNotification.java | 111 +++++++-- .../rx/plugins/DebugNotificationListener.java | 70 ++++++ .../src/test/java/rx/debug/DebugHookTest.java | 233 +++++++++++++----- rxjava-core/src/main/java/rx/Observable.java | 16 +- .../src/main/java/rx/functions/Actions.java | 36 ++- .../src/main/java/rx/functions/Functions.java | 75 ++++++ .../rx/operators/OnSubscribeFromIterable.java | 3 + .../RxJavaObservableExecutionHook.java | 93 ++++--- 11 files changed, 557 insertions(+), 224 deletions(-) create mode 100644 rxjava-contrib/rxjava-debug/src/main/java/rx/plugins/DebugNotificationListener.java diff --git a/rxjava-contrib/rxjava-debug/src/main/java/rx/operators/DebugSubscriber.java b/rxjava-contrib/rxjava-debug/src/main/java/rx/operators/DebugSubscriber.java index f75c6b5662..f8ef22e9a5 100644 --- a/rxjava-contrib/rxjava-debug/src/main/java/rx/operators/DebugSubscriber.java +++ b/rxjava-contrib/rxjava-debug/src/main/java/rx/operators/DebugSubscriber.java @@ -3,72 +3,63 @@ import rx.Observable.Operator; import rx.Observer; import rx.Subscriber; -import rx.functions.Action1; -import rx.functions.Action2; -import rx.functions.Func1; import rx.plugins.DebugNotification; +import rx.plugins.DebugNotificationListener; public final class DebugSubscriber extends Subscriber { - private final Func1 onNextHook; - private final Func1 start; - private final Action1 complete; - private final Action2 error; + private DebugNotificationListener listener; private final Observer o; private Operator from = null; private Operator to = null; public DebugSubscriber( - Func1 onNextHook, - Func1 start, - Action1 complete, - Action2 error, + DebugNotificationListener listener, Subscriber _o, Operator _out, Operator _in) { super(_o); - this.start = start; - this.complete = complete; - this.error = error; + this.listener = listener; this.o = _o; - this.onNextHook = onNextHook; this.from = _out; this.to = _in; - this.add(new DebugSubscription(this, start, complete, error)); + this.add(new DebugSubscription(this, listener)); } @Override public void onCompleted() { - final DebugNotification n = DebugNotification.createOnCompleted(o, from, to); - C context = start.call(n); + final DebugNotification n = DebugNotification.createOnCompleted(o, from, to); + C context = listener.start(n); try { o.onCompleted(); - complete.call(context); + listener.complete(context); } catch (Throwable e) { - error.call(context, e); + listener.error(context, e); } } @Override public void onError(Throwable e) { - final DebugNotification n = DebugNotification.createOnError(o, from, e, to); - C context = start.call(n); + final DebugNotification n = DebugNotification.createOnError(o, from, e, to); + C context = listener.start(n); try { o.onError(e); - complete.call(context); + listener.complete(context); } catch (Throwable e2) { - error.call(context, e2); + listener.error(context, e2); } } @Override public void onNext(T t) { - final DebugNotification n = DebugNotification.createOnNext(o, from, t, to); - C context = start.call(n); + final DebugNotification n = DebugNotification.createOnNext(o, from, t, to); + t = (T) listener.onNext(n); + + C context = listener.start(n); try { - o.onNext(onNextHook.call(t)); - complete.call(context); + o.onNext(t); + listener.complete(context); } catch (Throwable e) { - error.call(context, e); + listener.error(context, e); } } diff --git a/rxjava-contrib/rxjava-debug/src/main/java/rx/operators/DebugSubscription.java b/rxjava-contrib/rxjava-debug/src/main/java/rx/operators/DebugSubscription.java index 256fd8798a..0a3ce9d5ef 100644 --- a/rxjava-contrib/rxjava-debug/src/main/java/rx/operators/DebugSubscription.java +++ b/rxjava-contrib/rxjava-debug/src/main/java/rx/operators/DebugSubscription.java @@ -1,33 +1,27 @@ package rx.operators; import rx.Subscription; -import rx.functions.Action1; -import rx.functions.Action2; -import rx.functions.Func1; import rx.plugins.DebugNotification; +import rx.plugins.DebugNotificationListener; final class DebugSubscription implements Subscription { private final DebugSubscriber debugObserver; - private final Func1 start; - private final Action1 complete; - private final Action2 error; + private DebugNotificationListener listener; - DebugSubscription(DebugSubscriber debugObserver, Func1 start, Action1 complete, Action2 error) { + DebugSubscription(DebugSubscriber debugObserver, DebugNotificationListener listener) { this.debugObserver = debugObserver; - this.start = start; - this.complete = complete; - this.error = error; + this.listener = listener; } @Override public void unsubscribe() { - final DebugNotification n = DebugNotification. createUnsubscribe(debugObserver.getActual(), debugObserver.getFrom(), debugObserver.getTo()); - C context = start.call(n); + final DebugNotification n = DebugNotification. createUnsubscribe(debugObserver.getActual(), debugObserver.getFrom(), debugObserver.getTo()); + C context = listener.start(n); try { debugObserver.unsubscribe(); - complete.call(context); + listener.complete(context); } catch (Throwable e) { - error.call(context, e); + listener.error(context, e); } } diff --git a/rxjava-contrib/rxjava-debug/src/main/java/rx/plugins/DebugHook.java b/rxjava-contrib/rxjava-debug/src/main/java/rx/plugins/DebugHook.java index 3e5d5eba9a..9e48267333 100644 --- a/rxjava-contrib/rxjava-debug/src/main/java/rx/plugins/DebugHook.java +++ b/rxjava-contrib/rxjava-debug/src/main/java/rx/plugins/DebugHook.java @@ -19,10 +19,7 @@ * @author gscampbell */ public class DebugHook extends RxJavaObservableExecutionHook { - private final Func1 onNextHook; - private final Func1 start; - private final Action1 complete; - private final Action2 error; + private DebugNotificationListener listener; /** * Creates a new instance of the DebugHook RxJava plug-in that can be passed into @@ -34,11 +31,10 @@ public class DebugHook extends RxJavaObservableExecutionHook { * @param events * This action is invoked as each notification is generated */ - public DebugHook(Func1 onNextDataHook, Func1 start, Action1 complete, Action2 error) { - this.complete = complete; - this.error = error; - this.onNextHook = onNextDataHook == null ? Functions.identity() : onNextDataHook; - this.start = (Func1) (start == null ? Actions.empty() : start); + public DebugHook(DebugNotificationListener listener) { + if (listener == null) + throw new IllegalArgumentException("The debug listener must not be null"); + this.listener = listener; } @Override @@ -46,27 +42,48 @@ public OnSubscribe onSubscribeStart(final Observable observa return new OnSubscribe() { @Override public void call(Subscriber o) { - C context = start.call(DebugNotification.createSubscribe(o, observableInstance, f)); + final DebugNotification n = DebugNotification.createSubscribe(o, observableInstance, f); + o = wrapOutbound(null, o); + + C context = listener.start(n); try { - f.call(wrapOutbound(null, o)); - complete.call(context); + f.call(o); + listener.complete(context); } catch(Throwable e) { - error.call(context, e); + listener.error(context, e); } } }; } @Override - public Subscription onSubscribeReturn(Observable observableInstance, Subscription subscription) { + public Subscription onSubscribeReturn(Subscription subscription) { return subscription; } @Override public OnSubscribe onCreate(final OnSubscribe f) { - return new OnCreateWrapper(f); + return new DebugOnSubscribe(f); } + + public final class DebugOnSubscribe implements OnSubscribe { + private final OnSubscribe f; + + private DebugOnSubscribe(OnSubscribe f) { + this.f = f; + } + + @Override + public void call(Subscriber o) { + f.call(wrapInbound(null, o)); + } + + public OnSubscribe getActual() { + return f; + } + } + @Override public Operator onLift(final Operator bind) { @@ -78,11 +95,6 @@ public Subscriber call(final Subscriber o) { }; } - @Override - public Subscription onAdd(Subscriber subscriber, Subscription s) { - return s; - } - @SuppressWarnings("unchecked") private Subscriber wrapOutbound(Operator bind, Subscriber o) { if (o instanceof DebugSubscriber) { @@ -90,7 +102,7 @@ private Subscriber wrapOutbound(Operator bind, Su ((DebugSubscriber) o).setFrom(bind); return o; } - return new DebugSubscriber(onNextHook, start, complete, error, o, bind, null); + return new DebugSubscriber(listener, o, bind, null); } @SuppressWarnings("unchecked") @@ -100,23 +112,6 @@ private Subscriber wrapInbound(Operator bind, Subsc ((DebugSubscriber) o).setTo(bind); return o; } - return new DebugSubscriber(onNextHook, start, complete, error, o, null, bind); - } - - public final class OnCreateWrapper implements OnSubscribe { - private final OnSubscribe f; - - private OnCreateWrapper(OnSubscribe f) { - this.f = f; - } - - @Override - public void call(Subscriber o) { - f.call(wrapInbound(null, o)); - } - - public OnSubscribe getActual() { - return f; - } + return new DebugSubscriber(listener, o, null, bind); } } diff --git a/rxjava-contrib/rxjava-debug/src/main/java/rx/plugins/DebugNotification.java b/rxjava-contrib/rxjava-debug/src/main/java/rx/plugins/DebugNotification.java index f22d4e31ef..1cc8eab752 100644 --- a/rxjava-contrib/rxjava-debug/src/main/java/rx/plugins/DebugNotification.java +++ b/rxjava-contrib/rxjava-debug/src/main/java/rx/plugins/DebugNotification.java @@ -7,7 +7,7 @@ import rx.observers.SafeSubscriber; import rx.operators.DebugSubscriber; -public class DebugNotification { +public class DebugNotification { public static enum Kind { OnNext, OnError, OnCompleted, Subscribe, Unsubscribe } @@ -21,37 +21,39 @@ public static enum Kind { private final T value; private final Observer observer; - public static DebugNotification createSubscribe(Observer o, Observable source, OnSubscribe sourceFunc) { + @SuppressWarnings("unchecked") + public static DebugNotification createSubscribe(Observer o, Observable source, OnSubscribe sourceFunc) { Operator to = null; Operator from = null; if (o instanceof SafeSubscriber) { - o = ((SafeSubscriber) o).getActual(); + o = ((SafeSubscriber) o).getActual(); } if (o instanceof DebugSubscriber) { - to = ((DebugSubscriber) o).getTo(); - from = ((DebugSubscriber) o).getFrom(); - o = ((DebugSubscriber) o).getActual(); + final DebugSubscriber ds = (DebugSubscriber) o; + to = ds.getTo(); + from = ds.getFrom(); + o = ds.getActual(); } - if (sourceFunc instanceof DebugHook.OnCreateWrapper) { - sourceFunc = ((DebugHook.OnCreateWrapper) sourceFunc).getActual(); + if (sourceFunc instanceof DebugHook.DebugOnSubscribe) { + sourceFunc = ((DebugHook.DebugOnSubscribe) sourceFunc).getActual(); } - return new DebugNotification(o, from, Kind.Subscribe, null, null, to, source, sourceFunc); + return new DebugNotification(o, from, Kind.Subscribe, null, null, to, source, sourceFunc); } - public static DebugNotification createOnNext(Observer o, Operator from, T t, Operator to) { - return new DebugNotification(o, from, Kind.OnNext, t, null, to, null, null); + public static DebugNotification createOnNext(Observer o, Operator from, T t, Operator to) { + return new DebugNotification(o, from, Kind.OnNext, t, null, to, null, null); } - public static DebugNotification createOnError(Observer o, Operator from, Throwable e, Operator to) { - return new DebugNotification(o, from, Kind.OnError, null, e, to, null, null); + public static DebugNotification createOnError(Observer o, Operator from, Throwable e, Operator to) { + return new DebugNotification(o, from, Kind.OnError, null, e, to, null, null); } - public static DebugNotification createOnCompleted(Observer o, Operator from, Operator to) { - return new DebugNotification(o, from, Kind.OnCompleted, null, null, to, null, null); + public static DebugNotification createOnCompleted(Observer o, Operator from, Operator to) { + return new DebugNotification(o, from, Kind.OnCompleted, null, null, to, null, null); } - public static DebugNotification createUnsubscribe(Observer o, Operator from, Operator to) { - return new DebugNotification(o, from, Kind.Unsubscribe, null, null, to, null, null); + public static DebugNotification createUnsubscribe(Observer o, Operator from, Operator to) { + return new DebugNotification(o, from, Kind.Unsubscribe, null, null, to, null, null); } private DebugNotification(Observer o, Operator from, Kind kind, T value, Throwable throwable, Operator to, Observable source, OnSubscribe sourceFunc) { @@ -88,11 +90,11 @@ public Throwable getThrowable() { public Kind getKind() { return kind; } - + public Observable getSource() { return source; } - + public OnSubscribe getSourceFunc() { return sourceFunc; } @@ -103,11 +105,14 @@ public OnSubscribe getSourceFunc() { */ public String toString() { final StringBuilder s = new StringBuilder("{"); - s.append("\"observer\": \"").append(observer.getClass().getName()).append("@").append(Integer.toHexString(observer.hashCode())).append("\""); + s.append("\"observer\": "); + if (observer != null) + s.append("\"").append(observer.getClass().getName()).append("@").append(Integer.toHexString(observer.hashCode())).append("\""); + else + s.append("null"); s.append(", \"type\": \"").append(kind).append("\""); if (kind == Kind.OnNext) - // not json safe - s.append(", \"value\": \"").append(value).append("\""); + s.append(", \"value\": ").append(quote(value)).append(""); if (kind == Kind.OnError) s.append(", \"exception\": \"").append(throwable.getMessage().replace("\\", "\\\\").replace("\"", "\\\"")).append("\""); if (source != null) @@ -121,4 +126,66 @@ public String toString() { s.append("}"); return s.toString(); } + + public static String quote(Object obj) { + if (obj == null) { + return "null"; + } + + String string; + try { + string = obj.toString(); + } catch (Throwable e) { + return "\"\""; + } + if (string == null || string.length() == 0) { + return "\"\""; + } + + char c = 0; + int i; + int len = string.length(); + StringBuilder sb = new StringBuilder(len + 4); + String t; + + sb.append('"'); + for (i = 0; i < len; i += 1) { + c = string.charAt(i); + switch (c) { + case '\\': + case '"': + sb.append('\\'); + sb.append(c); + break; + case '/': + sb.append('\\'); + sb.append(c); + break; + case '\b': + sb.append("\\b"); + break; + case '\t': + sb.append("\\t"); + break; + case '\n': + sb.append("\\n"); + break; + case '\f': + sb.append("\\f"); + break; + case '\r': + sb.append("\\r"); + break; + default: + if (c < ' ') { + t = "000" + Integer.toHexString(c); + sb.append("\\u" + t.substring(t.length() - 4)); + } else { + sb.append(c); + } + } + } + sb.append('"'); + return sb.toString(); + } } diff --git a/rxjava-contrib/rxjava-debug/src/main/java/rx/plugins/DebugNotificationListener.java b/rxjava-contrib/rxjava-debug/src/main/java/rx/plugins/DebugNotificationListener.java new file mode 100644 index 0000000000..f34e25159e --- /dev/null +++ b/rxjava-contrib/rxjava-debug/src/main/java/rx/plugins/DebugNotificationListener.java @@ -0,0 +1,70 @@ +package rx.plugins; + +import rx.Observable; +import rx.Observable.Operator; +import rx.Observer; + +/** + * Subclasses of this are passed into the constructor of {@link DebugHook} to receive notification + * about all activity in Rx. + * + * @author gscampbell + * + * @param + * Context type that is returned from start and passed to either complete or error. + * @see DebugHook + */ +@SuppressWarnings("unused") +public abstract class DebugNotificationListener { + /** + * Override this to change the default behavior of returning the encapsulated value. This will + * only be invoked when the {@link DebugNotification#getKind()} is + * {@link DebugNotification.Kind#OnNext} and the value (null or not) is just about to be sent to + * next {@link Observer}. This can end up being called multiple times for + * the same value as it passes from {@link Operator} to {@link Operator} in the + * {@link Observable} chain. + *

+ * This can be used to decorate or replace the values passed into any onNext function or just + * perform extra logging, metrics and other such things and pass-thru the function. + * + * @param n + * {@link DebugNotification} containing the data and context about what is happening. + * @return + */ + public T onNext(DebugNotification n) { + return n.getValue(); + } + + /** + * For each {@link DebugNotification.Kind} start is invoked before the actual method is invoked. + *

+ * This can be used to perform extra logging, metrics and other such things. + * + * @param n + * {@link DebugNotification} containing the data and context about what is happening. + * @return + * A contextual object that the listener can use in the {@link #complete(C)} or + * {@link #error(C, Throwable)} after the actual operation has ended. + */ + public C start(DebugNotification n) { + return null; + } + + /** + * After the actual operations has completed from {@link #start(DebugNotification)} this is + * invoked + * + * @param context + */ + public void complete(C context) { + } + + /** + * After the actual operations has thrown an exception from {@link #start(DebugNotification)} + * this is invoked + * + * @param context + */ + public void error(C context, Throwable e) { + } +} diff --git a/rxjava-contrib/rxjava-debug/src/test/java/rx/debug/DebugHookTest.java b/rxjava-contrib/rxjava-debug/src/test/java/rx/debug/DebugHookTest.java index ea69258259..c3f4f2d426 100644 --- a/rxjava-contrib/rxjava-debug/src/test/java/rx/debug/DebugHookTest.java +++ b/rxjava-contrib/rxjava-debug/src/test/java/rx/debug/DebugHookTest.java @@ -1,27 +1,28 @@ package rx.debug; +import static org.junit.Assert.*; import static org.mockito.Matchers.*; import static org.mockito.Mockito.*; import java.util.Arrays; +import java.util.Map.Entry; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; import org.hamcrest.BaseMatcher; import org.hamcrest.Description; import org.junit.After; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; +import org.mockito.InOrder; import rx.Observable; -import rx.Subscriber; -import rx.functions.Action1; -import rx.functions.Action2; import rx.functions.Func1; +import rx.observers.Subscribers; import rx.plugins.DebugHook; import rx.plugins.DebugNotification; import rx.plugins.DebugNotification.Kind; +import rx.plugins.DebugNotificationListener; import rx.plugins.PlugReset; import rx.plugins.RxJavaPlugins; @@ -32,112 +33,212 @@ public void reset() { PlugReset.reset(); } + private static class TestDebugNotificationListener extends DebugNotificationListener { + ConcurrentHashMap allThreadDepths = new ConcurrentHashMap(1); + ThreadLocal currentThreadDepth = new ThreadLocal() { + protected AtomicInteger initialValue() { + AtomicInteger depth = new AtomicInteger(); + allThreadDepths.put(Thread.currentThread(), depth); + return depth; + }; + }; + + @Override + public T onNext(DebugNotification n) { + if (n == null) + return null; // because we are verifying on a spied object. + System.err.println("next: " + n.getValue()); + return super.onNext(n); + } + + @Override + public Object start(DebugNotification n) { + if (n == null) + return null; // because we are verifying on a spied object. + currentThreadDepth.get().incrementAndGet(); + Object context = new Object(); + System.err.println("start: " + Integer.toHexString(context.hashCode()) + " " + n); + return context; + } + + @Override + public void complete(Object context) { + if (context == null) + return; // because we are verifying on a spied object. + currentThreadDepth.get().decrementAndGet(); + System.err.println("complete: " + Integer.toHexString(context.hashCode())); + } + + @Override + public void error(Object context, Throwable e) { + if (context == null) + return; // because we are verifying on a spied object. + currentThreadDepth.get().decrementAndGet(); + System.err.println("error: " + Integer.toHexString(context.hashCode())); + } + + public void assertValidState() { + for (Entry threadDepth : allThreadDepths.entrySet()) { + assertEquals(0, threadDepth.getValue().get()); + } + } + } + + @SuppressWarnings("unchecked") @Test - @Ignore public void testSimple() { - Func1 start = mock(Func1.class); - Action1 complete = mock(Action1.class); - Action2 error = mock(Action2.class); - final DebugHook hook = new DebugHook(null, start, complete, error); + TestDebugNotificationListener listener = new TestDebugNotificationListener(); + listener = spy(listener); + final DebugHook hook = new DebugHook(listener); RxJavaPlugins.getInstance().registerObservableExecutionHook(hook); - Observable.empty().subscribe(); - verify(start, times(1)).call(subscribe()); - verify(start, times(1)).call(onCompleted()); - verify(complete, times(2)).call(any()); + Observable.from(1).subscribe(Subscribers.empty()); + + final InOrder inOrder = inOrder(listener); + inOrder.verify(listener).start(subscribe()); + inOrder.verify(listener).onNext(onNext(1)); + inOrder.verify(listener).start(onNext(1)); + inOrder.verify(listener).complete(any()); + inOrder.verify(listener).start(onCompleted()); + inOrder.verify(listener, times(2)).complete(any()); + inOrder.verifyNoMoreInteractions(); - verify(error, never()).call(any(), any()); + listener.assertValidState(); } + @SuppressWarnings("unchecked") @Test public void testOneOp() { - Func1, Object> start = mock(Func1.class); - doAnswer(new Answer() { - public Object answer(InvocationOnMock invocation) throws Throwable { - Object context = new Object(); - System.out.println("start: " + context.hashCode() + " " + invocation.getArguments()[0]); - return context; - } - }).when(start).call(any(DebugNotification.class)); - Action1 complete = mock(Action1.class); - doAnswer(new Answer() { - public Object answer(InvocationOnMock invocation) throws Throwable { - System.out.println("complete: " + invocation.getArguments()[0].hashCode()); - return null; - } - }).when(complete).call(any()); - Action2 error = mock(Action2.class); - doAnswer(new Answer() { - public Object answer(InvocationOnMock invocation) throws Throwable { - System.out.println("error: " + invocation.getArguments()[1].hashCode()); - return null; - } - }).when(error).call(any(), any(Throwable.class)); - final DebugHook hook = new DebugHook(null, start, complete, error); + TestDebugNotificationListener listener = new TestDebugNotificationListener(); + listener = spy(listener); + + // create and register the hooks. + final DebugHook hook = new DebugHook(listener); RxJavaPlugins.getInstance().registerObservableExecutionHook(hook); - Observable.from(Arrays.asList(1, 3)).flatMap(new Func1>() { + + // do the operation + Observable + .from(Arrays.asList(1, 3)) + .flatMap(new Func1>() { + @Override + public Observable call(Integer it) { + return Observable.from(Arrays.asList(it * 10, (it + 1) * 10)); + } + }) + .take(3) + .subscribe(Subscribers. empty()); + + InOrder calls = inOrder(listener); + + calls.verify(listener).start(subscribe()); + calls.verify(listener).start(onNext(1)); // from to map + calls.verify(listener).start(onNext(Observable.class)); // map to merge + calls.verify(listener).start(subscribe()); // merge inner + calls.verify(listener).start(onNext(10)); // from to merge inner + calls.verify(listener).start(onNext(10)); // merge inner to take + calls.verify(listener).start(onNext(10)); // take to empty subscriber + calls.verify(listener, times(3)).complete(any()); + calls.verify(listener).start(onNext(20)); // next from to merge inner + calls.verify(listener).start(onNext(20)); // merge inner to take + calls.verify(listener).start(onNext(20)); // take to output + calls.verify(listener, times(3)).complete(any()); + calls.verify(listener).start(onCompleted()); // sub from completes + // calls.verify(listener).start(unsubscribe()); // merge's composite subscription + // unnecessarily calls unsubscribe during the removing the subscription from the array. + // + // i didn't include it because it could cause a test failure if the internals change. + calls.verify(listener, times(5)).complete(any()); // pop the call stack up to onNext(1) + calls.verify(listener).start(onNext(3)); // from to map + calls.verify(listener).start(onNext(Observable.class)); // map to merge + calls.verify(listener).start(subscribe()); + calls.verify(listener).start(onNext(30)); // next from to merge inner + calls.verify(listener).start(onNext(30)); // merge inner to take + calls.verify(listener).start(onNext(30)); // take to output + calls.verify(listener).complete(any()); + calls.verify(listener).start(onCompleted()); // take to output + calls.verify(listener).start(unsubscribe()); // take unsubscribes + calls.verify(listener).complete(any()); + calls.verify(listener).start(unsubscribe()); // merge inner unsubscribes + calls.verify(listener).complete(any()); + calls.verify(listener).start(unsubscribe()); // merge outer unsubscribes + calls.verify(listener).complete(any()); + calls.verify(listener).start(unsubscribe()); // map unsubscribe + calls.verify(listener, times(7)).complete(any()); + calls.verifyNoMoreInteractions(); + + listener.assertValidState(); + } + + private static DebugNotification onNext(final T value) { + return argThat(new BaseMatcher>() { @Override - public Observable call(Integer it) { - return Observable.from(Arrays.asList(it, it + 1)); + public boolean matches(Object item) { + if (item instanceof DebugNotification) { + @SuppressWarnings("unchecked") + DebugNotification dn = (DebugNotification) item; + return dn.getKind() == Kind.OnNext && dn.getValue().equals(value); + } + return false; } - }).take(3).subscribe(new Subscriber() { + @Override - public void onCompleted() { + public void describeTo(Description description) { + description.appendText("OnNext " + value); } + }); + } + private static DebugNotification onNext(final Class type) { + return argThat(new BaseMatcher>() { @Override - public void onError(Throwable e) { + public boolean matches(Object item) { + if (item instanceof DebugNotification) { + @SuppressWarnings("unchecked") + DebugNotification dn = (DebugNotification) item; + return dn.getKind() == Kind.OnNext && type.isAssignableFrom(dn.getValue().getClass()); + } + return false; } @Override - public void onNext(Integer t) { + public void describeTo(Description description) { + description.appendText("OnNext " + type); } }); - verify(start, atLeast(3)).call(subscribe()); - verify(start, times(4)).call(onNext(1)); - // one less because it originates from the inner observable sent to merge - verify(start, times(3)).call(onNext(2)); - verify(start, times(4)).call(onNext(3)); - // because the take unsubscribes - verify(start, never()).call(onNext(4)); - - verify(complete, atLeast(14)).call(any()); - - verify(error, never()).call(any(), any(Throwable.class)); } - private static DebugNotification onNext(final T value) { - return argThat(new BaseMatcher>() { + private static DebugNotification subscribe() { + return argThat(new BaseMatcher>() { @Override public boolean matches(Object item) { if (item instanceof DebugNotification) { - DebugNotification dn = (DebugNotification) item; - return dn.getKind() == Kind.OnNext && dn.getValue().equals(value); + DebugNotification dn = (DebugNotification) item; + return dn.getKind() == DebugNotification.Kind.Subscribe; } return false; } @Override public void describeTo(Description description) { - description.appendText("OnNext " + value); + description.appendText("Subscribe"); } }); } - private static DebugNotification subscribe() { - return argThat(new BaseMatcher() { + private static DebugNotification unsubscribe() { + return argThat(new BaseMatcher>() { @Override public boolean matches(Object item) { if (item instanceof DebugNotification) { DebugNotification dn = (DebugNotification) item; - return dn.getKind() == DebugNotification.Kind.Subscribe; + return dn.getKind() == DebugNotification.Kind.Unsubscribe; } return false; } @Override public void describeTo(Description description) { - description.appendText("Subscribe"); + description.appendText("Unsubscribe"); } }); } diff --git a/rxjava-core/src/main/java/rx/Observable.java b/rxjava-core/src/main/java/rx/Observable.java index 893ec455eb..8e29da1e8f 100644 --- a/rxjava-core/src/main/java/rx/Observable.java +++ b/rxjava-core/src/main/java/rx/Observable.java @@ -25,8 +25,8 @@ import java.util.concurrent.TimeUnit; import rx.exceptions.Exceptions; -import rx.exceptions.OnErrorThrowable; import rx.exceptions.OnErrorNotImplementedException; +import rx.exceptions.OnErrorThrowable; import rx.functions.Action0; import rx.functions.Action1; import rx.functions.Action2; @@ -115,8 +115,8 @@ import rx.operators.OperatorMap; import rx.operators.OperatorMerge; import rx.operators.OperatorObserveOn; -import rx.operators.OperatorOnErrorResumeNextViaFunction; import rx.operators.OperatorOnErrorFlatMap; +import rx.operators.OperatorOnErrorResumeNextViaFunction; import rx.operators.OperatorParallel; import rx.operators.OperatorRepeat; import rx.operators.OperatorScan; @@ -171,10 +171,10 @@ public class Observable { * {@link OnSubscribe} to be executed when {@link #subscribe(Subscriber)} is called */ protected Observable(OnSubscribe f) { - this.f = hook.onCreate(f); + this.f = f; } - private final static RxJavaObservableExecutionHook hook = RxJavaPlugins.getInstance().getObservableExecutionHook(); + private final RxJavaObservableExecutionHook hook = RxJavaPlugins.getInstance().getObservableExecutionHook(); /** * Returns an Observable that will execute the specified function when a {@link Subscriber} subscribes to @@ -7041,7 +7041,7 @@ public final Subscription subscribe(Subscriber observer) { observer = new SafeSubscriber(observer); onSubscribeFunction.call(observer); } - final Subscription returnSubscription = hook.onSubscribeReturn(this, observer); + final Subscription returnSubscription = hook.onSubscribeReturn(observer); // we return it inside a Subscription so it can't be cast back to Subscriber return Subscriptions.create(new Action0() { @@ -7056,7 +7056,7 @@ public void call() { Exceptions.throwIfFatal(e); // if an unhandled error occurs executing the onSubscribe we will propagate it try { - observer.onError(hook.onSubscribeError(this, e)); + observer.onError(hook.onSubscribeError(e)); } catch (OnErrorNotImplementedException e2) { // special handling when onError is not implemented ... we just rethrow throw e2; @@ -7064,7 +7064,9 @@ public void call() { // if this happens it means the onError itself failed (perhaps an invalid function implementation) // so we are unable to propagate the error correctly and will just throw RuntimeException r = new RuntimeException("Error occurred attempting to subscribe [" + e.getMessage() + "] and then again while trying to pass to onError.", e2); - hook.onSubscribeError(this, r); + // TODO could the hook be the cause of the error in the on error handling. + hook.onSubscribeError(r); + // TODO why aren't we throwing the hook's return value. throw r; } return Subscriptions.empty(); diff --git a/rxjava-core/src/main/java/rx/functions/Actions.java b/rxjava-core/src/main/java/rx/functions/Actions.java index 9954ce5119..0a9f79726e 100644 --- a/rxjava-core/src/main/java/rx/functions/Actions.java +++ b/rxjava-core/src/main/java/rx/functions/Actions.java @@ -25,51 +25,63 @@ private Actions() { throw new IllegalStateException("No instances!"); } - public static final EmptyAction empty() { - return EMPTY_ACTION; + @SuppressWarnings("unchecked") + public static final EmptyAction empty() { + return (EmptyAction) EMPTY_ACTION; } private static final EmptyAction EMPTY_ACTION = new EmptyAction(); - private static final class EmptyAction implements Action0, Action1, Action2, Action3, Action4, Action5, Action6, Action7, Action8, Action9, ActionN { + private static final class EmptyAction implements + Action0, + Action1, + Action2, + Action3, + Action4, + Action5, + Action6, + Action7, + Action8, + Action9, + ActionN { @Override public void call() { } @Override - public void call(Object t1) { + public void call(T0 t1) { } @Override - public void call(Object t1, Object t2) { + public void call(T0 t1, T1 t2) { } @Override - public void call(Object t1, Object t2, Object t3) { + public void call(T0 t1, T1 t2, T2 t3) { } @Override - public void call(Object t1, Object t2, Object t3, Object t4) { + public void call(T0 t1, T1 t2, T2 t3, T3 t4) { } @Override - public void call(Object t1, Object t2, Object t3, Object t4, Object t5) { + public void call(T0 t1, T1 t2, T2 t3, T3 t4, T4 t5) { } @Override - public void call(Object t1, Object t2, Object t3, Object t4, Object t5, Object t6) { + public void call(T0 t1, T1 t2, T2 t3, T3 t4, T4 t5, T5 t6) { } @Override - public void call(Object t1, Object t2, Object t3, Object t4, Object t5, Object t6, Object t7) { + public void call(T0 t1, T1 t2, T2 t3, T3 t4, T4 t5, T5 t6, T6 t7) { } @Override - public void call(Object t1, Object t2, Object t3, Object t4, Object t5, Object t6, Object t7, Object t8) { + public void call(T0 t1, T1 t2, T2 t3, T3 t4, T4 t5, T5 t6, T6 t7, T7 t8) { } @Override - public void call(Object t1, Object t2, Object t3, Object t4, Object t5, Object t6, Object t7, Object t8, Object t9) { + public void call(T0 t1, T1 t2, T2 t3, T3 t4, T4 t5, T5 t6, T6 t7, T7 t8, T8 t9) { } @Override diff --git a/rxjava-core/src/main/java/rx/functions/Functions.java b/rxjava-core/src/main/java/rx/functions/Functions.java index b30ad4e4d2..becfe31c43 100644 --- a/rxjava-core/src/main/java/rx/functions/Functions.java +++ b/rxjava-core/src/main/java/rx/functions/Functions.java @@ -358,4 +358,79 @@ public Boolean call(Object o) { return false; } } + + @SuppressWarnings("unchecked") + public static NullFunction returnNull() { + return (NullFunction) NULL_FUNCTION; + } + + private static final NullFunction NULL_FUNCTION = new NullFunction(); + + private static final class NullFunction implements + Func0, + Func1, + Func2, + Func3, + Func4, + Func5, + Func6, + Func7, + Func8, + Func9, + FuncN { + @Override + public R call() { + return null; + } + + @Override + public R call(T0 t1) { + return null; + } + + @Override + public R call(T0 t1, T1 t2) { + return null; + } + + @Override + public R call(T0 t1, T1 t2, T2 t3) { + return null; + } + + @Override + public R call(T0 t1, T1 t2, T2 t3, T3 t4) { + return null; + } + + @Override + public R call(T0 t1, T1 t2, T2 t3, T3 t4, T4 t5) { + return null; + } + + @Override + public R call(T0 t1, T1 t2, T2 t3, T3 t4, T4 t5, T5 t6) { + return null; + } + + @Override + public R call(T0 t1, T1 t2, T2 t3, T3 t4, T4 t5, T5 t6, T6 t7) { + return null; + } + + @Override + public R call(T0 t1, T1 t2, T2 t3, T3 t4, T4 t5, T5 t6, T6 t7, T7 t8) { + return null; + } + + @Override + public R call(T0 t1, T1 t2, T2 t3, T3 t4, T4 t5, T5 t6, T6 t7, T7 t8, T8 t9) { + return null; + } + + @Override + public R call(Object... args) { + return null; + } + } } diff --git a/rxjava-core/src/main/java/rx/operators/OnSubscribeFromIterable.java b/rxjava-core/src/main/java/rx/operators/OnSubscribeFromIterable.java index 45dbde9004..38766395f4 100644 --- a/rxjava-core/src/main/java/rx/operators/OnSubscribeFromIterable.java +++ b/rxjava-core/src/main/java/rx/operators/OnSubscribeFromIterable.java @@ -42,6 +42,9 @@ public void call(Subscriber o) { } o.onNext(i); } + if (o.isUnsubscribed()) { + return; + } o.onCompleted(); } diff --git a/rxjava-core/src/main/java/rx/plugins/RxJavaObservableExecutionHook.java b/rxjava-core/src/main/java/rx/plugins/RxJavaObservableExecutionHook.java index 01e6c97507..64402fe0de 100644 --- a/rxjava-core/src/main/java/rx/plugins/RxJavaObservableExecutionHook.java +++ b/rxjava-core/src/main/java/rx/plugins/RxJavaObservableExecutionHook.java @@ -17,85 +17,108 @@ import rx.Observable; import rx.Observable.OnSubscribe; -import rx.Observable.OnSubscribeFunc; import rx.Observable.Operator; import rx.Subscriber; import rx.Subscription; import rx.functions.Func1; /** - * Abstract ExecutionHook with invocations at different lifecycle points of {@link Observable} execution with a default no-op implementation. + * Abstract ExecutionHook with invocations at different lifecycle points of {@link Observable} + * execution with a default no-op implementation. *

* See {@link RxJavaPlugins} or the RxJava GitHub Wiki for information on configuring plugins: https://github.com/Netflix/RxJava/wiki/Plugins. + * href ="https://github.com/Netflix/RxJava/wiki/Plugins">https://github.com/Netflix/RxJava/wiki/ + * Plugins . *

* Note on thread-safety and performance *

- * A single implementation of this class will be used globally so methods on this class will be invoked concurrently from multiple threads so all functionality must be thread-safe. + * A single implementation of this class will be used globally so methods on this class will be + * invoked concurrently from multiple threads so all functionality must be thread-safe. *

- * Methods are also invoked synchronously and will add to execution time of the observable so all behavior should be fast. If anything time-consuming is to be done it should be spawned asynchronously - * onto separate worker threads. + * Methods are also invoked synchronously and will add to execution time of the observable so all + * behavior should be fast. If anything time-consuming is to be done it should be spawned + * asynchronously onto separate worker threads. * - * */ + */ public abstract class RxJavaObservableExecutionHook { + /** + * Invoked during the construction by {@link Observable#create(OnSubscribe)} + *

+ * This can be used to decorate or replace the onSubscribe function or just perform + * extra logging, metrics and other such things and pass-thru the function. + * + * @param onSubscribe + * original {@link OnSubscribe}<{@code T}> to be executed + * @return {@link OnSubscribe}<{@code T}> function that can be modified, decorated, replaced or + * just returned as a pass-thru. + */ + public OnSubscribe onCreate(OnSubscribe f) { + return f; + } + /** * Invoked before {@link Observable#subscribe(rx.Subscriber)} is about to be executed. *

- * This can be used to decorate or replace the onSubscribe function or just perform extra logging, metrics and other such things and pass-thru the function. + * This can be used to decorate or replace the onSubscribe function or just perform + * extra logging, metrics and other such things and pass-thru the function. * - * @param observableInstance - * The executing {@link Observable} instance. * @param onSubscribe - * original {@link Func1}<{@link Subscriber}{@code }, {@link Subscription}> to be executed - * @return {@link Func1}<{@link Subscriber}{@code }, {@link Subscription}> function that can be modified, decorated, replaced or just returned as a pass-thru. + * original {@link OnSubscribe}<{@code T}> to be executed + * @return {@link OnSubscribe}<{@code T}> function that can be modified, decorated, replaced or + * just returned as a pass-thru. */ - public OnSubscribe onSubscribeStart(Observable observableInstance, final OnSubscribe onSubscribe) { + public OnSubscribe onSubscribeStart(Observable observableInsance, final OnSubscribe onSubscribe) { // pass-thru by default return onSubscribe; } /** - * Invoked after successful execution of {@link Observable#subscribe(rx.Subscriber)} with returned {@link Subscription}. + * Invoked after successful execution of {@link Observable#subscribe(rx.Subscriber)} with + * returned {@link Subscription}. *

- * This can be used to decorate or replace the {@link Subscription} instance or just perform extra logging, metrics and other such things and pass-thru the subscription. + * This can be used to decorate or replace the {@link Subscription} instance or just perform + * extra logging, metrics and other such things and pass-thru the subscription. * - * @param observableInstance - * The executing {@link Observable} instance. * @param subscription * original {@link Subscription} - * @return {@link Subscription} subscription that can be modified, decorated, replaced or just returned as a pass-thru. + * @return {@link Subscription} subscription that can be modified, decorated, replaced or just + * returned as a pass-thru. */ - public Subscription onSubscribeReturn(Observable observableInstance, Subscription subscription) { + public Subscription onSubscribeReturn(Subscription subscription) { // pass-thru by default return subscription; } /** - * Invoked after failed execution of {@link Observable#subscribe(Subscriber)} with thrown Throwable. + * Invoked after failed execution of {@link Observable#subscribe(Subscriber)} with thrown + * Throwable. *

- * This is NOT errors emitted via {@link Subscriber#onError(Throwable)} but exceptions thrown when attempting - * to subscribe to a {@link Func1}<{@link Subscriber}{@code }, {@link Subscription}>. + * This is NOT errors emitted via {@link Subscriber#onError(Throwable)} but exceptions thrown + * when attempting to subscribe to a {@link Func1}<{@link Subscriber}{@code }, + * {@link Subscription}>. * - * @param observableInstance - * The executing {@link Observable} instance. * @param e * Throwable thrown by {@link Observable#subscribe(Subscriber)} * @return Throwable that can be decorated, replaced or just returned as a pass-thru. */ - public Throwable onSubscribeError(Observable observableInstance, Throwable e) { + public Throwable onSubscribeError(Throwable e) { // pass-thru by default return e; } - public OnSubscribe onCreate(OnSubscribe f) { - return f; - } - - public Operator onLift(final Operator bind) { - return bind; - } - - public Subscription onAdd(Subscriber subscriber, Subscription s) { - return s; + /** + * Invoked just as the operator functions is called to bind two operations together into a new + * {@link Observable} and the return value is used as the lifted function + *

+ * This can be used to decorate or replace the {@link Operator} instance or just perform extra + * logging, metrics and other such things and pass-thru the onSubscribe. + * + * @param lift + * original {@link Operator}{@code} ' * @return {@link Operator}{@code } + * function that can be modified, decorated, replaced or + * just returned as a pass-thru. + */ + public Operator onLift(final Operator lift) { + return lift; } } From 68d40628a78db18ecb49d880798f1a03551ccd59 Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Tue, 4 Mar 2014 22:30:18 -0800 Subject: [PATCH 071/422] math-module Moving the average/sum/min/max functionality to the MathObservable similar to StringObservable. --- rxjava-contrib/rxjava-math/build.gradle | 31 ++ .../rx/math/operators/OperationAverage.java | 348 ++++++++++++++++ .../rx/math/operators/OperationMinMax.java | 147 +++++++ .../java/rx/math/operators/OperationSum.java | 89 +++++ .../java/rx/observables/MathObservable.java | 378 ++++++++++++++++++ .../math/operators/OperationAverageTest.java | 342 ++++++++++++++++ .../math/operators/OperationMinMaxTest.java | 354 ++++++++++++++++ .../rx/math/operators/OperationSumTest.java | 346 ++++++++++++++++ rxjava-core/src/main/java/rx/Observable.java | 22 +- settings.gradle | 1 + 10 files changed, 2057 insertions(+), 1 deletion(-) create mode 100644 rxjava-contrib/rxjava-math/build.gradle create mode 100644 rxjava-contrib/rxjava-math/src/main/java/rx/math/operators/OperationAverage.java create mode 100644 rxjava-contrib/rxjava-math/src/main/java/rx/math/operators/OperationMinMax.java create mode 100644 rxjava-contrib/rxjava-math/src/main/java/rx/math/operators/OperationSum.java create mode 100644 rxjava-contrib/rxjava-math/src/main/java/rx/observables/MathObservable.java create mode 100644 rxjava-contrib/rxjava-math/src/test/java/rx/math/operators/OperationAverageTest.java create mode 100644 rxjava-contrib/rxjava-math/src/test/java/rx/math/operators/OperationMinMaxTest.java create mode 100644 rxjava-contrib/rxjava-math/src/test/java/rx/math/operators/OperationSumTest.java diff --git a/rxjava-contrib/rxjava-math/build.gradle b/rxjava-contrib/rxjava-math/build.gradle new file mode 100644 index 0000000000..cbc8cca804 --- /dev/null +++ b/rxjava-contrib/rxjava-math/build.gradle @@ -0,0 +1,31 @@ +apply plugin: 'osgi' + +sourceCompatibility = JavaVersion.VERSION_1_6 +targetCompatibility = JavaVersion.VERSION_1_6 + +dependencies { + compile project(':rxjava-core') + testCompile project(":rxjava-core").sourceSets.test.output + provided 'junit:junit-dep:4.10' + provided 'org.mockito:mockito-core:1.8.5' +} + +javadoc { + options { + doclet = "org.benjchristensen.doclet.DocletExclude" + docletpath = [rootProject.file('./gradle/doclet-exclude.jar')] + stylesheetFile = rootProject.file('./gradle/javadocStyleSheet.css') + windowTitle = "RxJava Javadoc ${project.version}" + } + options.addStringOption('top').value = '

RxJava

' +} + +jar { + manifest { + name = 'rxjava-math' + instruction 'Bundle-Vendor', 'Netflix' + instruction 'Bundle-DocURL', 'https://github.com/Netflix/RxJava' + instruction 'Import-Package', '!org.junit,!junit.framework,!org.mockito.*,*' + instruction 'Fragment-Host', 'com.netflix.rxjava.core' + } +} diff --git a/rxjava-contrib/rxjava-math/src/main/java/rx/math/operators/OperationAverage.java b/rxjava-contrib/rxjava-math/src/main/java/rx/math/operators/OperationAverage.java new file mode 100644 index 0000000000..25d6bc7376 --- /dev/null +++ b/rxjava-contrib/rxjava-math/src/main/java/rx/math/operators/OperationAverage.java @@ -0,0 +1,348 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package rx.math.operators; + +import rx.Observable; +import rx.Observable.OnSubscribeFunc; +import rx.Observer; +import rx.Subscription; +import rx.functions.Func1; +import rx.functions.Func2; + +/** + * A few operators for implementing the averaging operation. + * + * @see MSDN: Observable.Average + */ +public final class OperationAverage { + private static final class Tuple2 { + private final T current; + private final Integer count; + + private Tuple2(T v1, Integer v2) { + current = v1; + count = v2; + } + } + + public static Observable average(Observable source) { + return source.reduce(new Tuple2(0, 0), new Func2, Integer, Tuple2>() { + @Override + public Tuple2 call(Tuple2 accu, Integer next) { + return new Tuple2(accu.current + next, accu.count + 1); + } + }).map(new Func1, Integer>() { + @Override + public Integer call(Tuple2 result) { + if (result.count == 0) { + throw new IllegalArgumentException("Sequence contains no elements"); + } + return result.current / result.count; + } + }); + } + + public static Observable averageLongs(Observable source) { + return source.reduce(new Tuple2(0L, 0), new Func2, Long, Tuple2>() { + @Override + public Tuple2 call(Tuple2 accu, Long next) { + return new Tuple2(accu.current + next, accu.count + 1); + } + }).map(new Func1, Long>() { + @Override + public Long call(Tuple2 result) { + if (result.count == 0) { + throw new IllegalArgumentException("Sequence contains no elements"); + } + return result.current / result.count; + } + }); + } + + public static Observable averageFloats(Observable source) { + return source.reduce(new Tuple2(0.0f, 0), new Func2, Float, Tuple2>() { + @Override + public Tuple2 call(Tuple2 accu, Float next) { + return new Tuple2(accu.current + next, accu.count + 1); + } + }).map(new Func1, Float>() { + @Override + public Float call(Tuple2 result) { + if (result.count == 0) { + throw new IllegalArgumentException("Sequence contains no elements"); + } + return result.current / result.count; + } + }); + } + + public static Observable averageDoubles(Observable source) { + return source.reduce(new Tuple2(0.0d, 0), new Func2, Double, Tuple2>() { + @Override + public Tuple2 call(Tuple2 accu, Double next) { + return new Tuple2(accu.current + next, accu.count + 1); + } + }).map(new Func1, Double>() { + @Override + public Double call(Tuple2 result) { + if (result.count == 0) { + throw new IllegalArgumentException("Sequence contains no elements"); + } + return result.current / result.count; + } + }); + } + + /** + * Compute the average by extracting integer values from the source via an + * extractor function. + * + * @param + * the source value type + */ + public static final class AverageIntegerExtractor implements OnSubscribeFunc { + final Observable source; + final Func1 valueExtractor; + + public AverageIntegerExtractor(Observable source, Func1 valueExtractor) { + this.source = source; + this.valueExtractor = valueExtractor; + } + + @Override + public Subscription onSubscribe(Observer t1) { + return source.subscribe(new AverageObserver(t1)); + } + + /** Computes the average. */ + private final class AverageObserver implements Observer { + final Observer observer; + int sum; + int count; + + public AverageObserver(Observer observer) { + this.observer = observer; + } + + @Override + public void onNext(T args) { + sum += valueExtractor.call(args); + count++; + } + + @Override + public void onError(Throwable e) { + observer.onError(e); + } + + @Override + public void onCompleted() { + if (count > 0) { + try { + observer.onNext(sum / count); + } catch (Throwable t) { + observer.onError(t); + return; + } + observer.onCompleted(); + } else { + observer.onError(new IllegalArgumentException("Sequence contains no elements")); + } + } + + } + } + + /** + * Compute the average by extracting long values from the source via an + * extractor function. + * + * @param + * the source value type + */ + public static final class AverageLongExtractor implements OnSubscribeFunc { + final Observable source; + final Func1 valueExtractor; + + public AverageLongExtractor(Observable source, Func1 valueExtractor) { + this.source = source; + this.valueExtractor = valueExtractor; + } + + @Override + public Subscription onSubscribe(Observer t1) { + return source.subscribe(new AverageObserver(t1)); + } + + /** Computes the average. */ + private final class AverageObserver implements Observer { + final Observer observer; + long sum; + int count; + + public AverageObserver(Observer observer) { + this.observer = observer; + } + + @Override + public void onNext(T args) { + sum += valueExtractor.call(args); + count++; + } + + @Override + public void onError(Throwable e) { + observer.onError(e); + } + + @Override + public void onCompleted() { + if (count > 0) { + try { + observer.onNext(sum / count); + } catch (Throwable t) { + observer.onError(t); + return; + } + observer.onCompleted(); + } else { + observer.onError(new IllegalArgumentException("Sequence contains no elements")); + } + } + + } + } + + /** + * Compute the average by extracting float values from the source via an + * extractor function. + * + * @param + * the source value type + */ + public static final class AverageFloatExtractor implements OnSubscribeFunc { + final Observable source; + final Func1 valueExtractor; + + public AverageFloatExtractor(Observable source, Func1 valueExtractor) { + this.source = source; + this.valueExtractor = valueExtractor; + } + + @Override + public Subscription onSubscribe(Observer t1) { + return source.subscribe(new AverageObserver(t1)); + } + + /** Computes the average. */ + private final class AverageObserver implements Observer { + final Observer observer; + float sum; + int count; + + public AverageObserver(Observer observer) { + this.observer = observer; + } + + @Override + public void onNext(T args) { + sum += valueExtractor.call(args); + count++; + } + + @Override + public void onError(Throwable e) { + observer.onError(e); + } + + @Override + public void onCompleted() { + if (count > 0) { + try { + observer.onNext(sum / count); + } catch (Throwable t) { + observer.onError(t); + return; + } + observer.onCompleted(); + } else { + observer.onError(new IllegalArgumentException("Sequence contains no elements")); + } + } + + } + } + + /** + * Compute the average by extracting double values from the source via an + * extractor function. + * + * @param + * the source value type + */ + public static final class AverageDoubleExtractor implements OnSubscribeFunc { + final Observable source; + final Func1 valueExtractor; + + public AverageDoubleExtractor(Observable source, Func1 valueExtractor) { + this.source = source; + this.valueExtractor = valueExtractor; + } + + @Override + public Subscription onSubscribe(Observer t1) { + return source.subscribe(new AverageObserver(t1)); + } + + /** Computes the average. */ + private final class AverageObserver implements Observer { + final Observer observer; + double sum; + int count; + + public AverageObserver(Observer observer) { + this.observer = observer; + } + + @Override + public void onNext(T args) { + sum += valueExtractor.call(args); + count++; + } + + @Override + public void onError(Throwable e) { + observer.onError(e); + } + + @Override + public void onCompleted() { + if (count > 0) { + try { + observer.onNext(sum / count); + } catch (Throwable t) { + observer.onError(t); + return; + } + observer.onCompleted(); + } else { + observer.onError(new IllegalArgumentException("Sequence contains no elements")); + } + } + + } + } +} diff --git a/rxjava-contrib/rxjava-math/src/main/java/rx/math/operators/OperationMinMax.java b/rxjava-contrib/rxjava-math/src/main/java/rx/math/operators/OperationMinMax.java new file mode 100644 index 0000000000..cfa92d4fce --- /dev/null +++ b/rxjava-contrib/rxjava-math/src/main/java/rx/math/operators/OperationMinMax.java @@ -0,0 +1,147 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package rx.math.operators; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +import rx.Observable; +import rx.functions.Func1; +import rx.functions.Func2; + +/** + * Returns the minimum element in an observable sequence. + */ +public class OperationMinMax { + + public static > Observable min( + Observable source) { + return minMax(source, -1L); + } + + public static Observable min(Observable source, + final Comparator comparator) { + return minMax(source, comparator, -1L); + } + + public static > Observable> minBy( + Observable source, final Func1 selector) { + return minMaxBy(source, selector, -1L); + } + + public static Observable> minBy(Observable source, + final Func1 selector, final Comparator comparator) { + return minMaxBy(source, selector, comparator, -1L); + } + + public static > Observable max( + Observable source) { + return minMax(source, 1L); + } + + public static Observable max(Observable source, + final Comparator comparator) { + return minMax(source, comparator, 1L); + } + + public static > Observable> maxBy( + Observable source, final Func1 selector) { + return minMaxBy(source, selector, 1L); + } + + public static Observable> maxBy(Observable source, + final Func1 selector, final Comparator comparator) { + return minMaxBy(source, selector, comparator, 1L); + } + + private static > Observable minMax( + Observable source, final long flag) { + return source.reduce(new Func2() { + @Override + public T call(T acc, T value) { + if (flag * acc.compareTo(value) > 0) { + return acc; + } + return value; + } + }); + } + + private static Observable minMax(Observable source, + final Comparator comparator, final long flag) { + return source.reduce(new Func2() { + @Override + public T call(T acc, T value) { + if (flag * comparator.compare(acc, value) > 0) { + return acc; + } + return value; + } + }); + } + + private static > Observable> minMaxBy( + Observable source, final Func1 selector, final long flag) { + return source.reduce(new ArrayList(), + new Func2, T, List>() { + + @Override + public List call(List acc, T value) { + if (acc.isEmpty()) { + acc.add(value); + } else { + int compareResult = selector.call(acc.get(0)) + .compareTo(selector.call(value)); + if (compareResult == 0) { + acc.add(value); + } else if (flag * compareResult < 0) { + acc.clear(); + acc.add(value); + } + } + return acc; + } + }); + } + + private static Observable> minMaxBy(Observable source, + final Func1 selector, final Comparator comparator, + final long flag) { + return source.reduce(new ArrayList(), + new Func2, T, List>() { + + @Override + public List call(List acc, T value) { + if (acc.isEmpty()) { + acc.add(value); + } else { + int compareResult = comparator.compare( + selector.call(acc.get(0)), + selector.call(value)); + if (compareResult == 0) { + acc.add(value); + } else if (flag * compareResult < 0) { + acc.clear(); + acc.add(value); + } + } + return acc; + } + }); + } + +} diff --git a/rxjava-contrib/rxjava-math/src/main/java/rx/math/operators/OperationSum.java b/rxjava-contrib/rxjava-math/src/main/java/rx/math/operators/OperationSum.java new file mode 100644 index 0000000000..3d519e4220 --- /dev/null +++ b/rxjava-contrib/rxjava-math/src/main/java/rx/math/operators/OperationSum.java @@ -0,0 +1,89 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package rx.math.operators; + +import rx.Observable; +import rx.functions.Func2; + +/** + * A few operators for implementing the sum operation. + * + * @see MSDN: + * Observable.Sum + */ +public final class OperationSum { + + public static Observable sumIntegers(Observable source) { + return source.reduce(0, ACCUM_INT); + } + + public static Observable sumLongs(Observable source) { + return source.reduce(0l, ACCUM_LONG); + } + + public static Observable sumFloats(Observable source) { + return source.reduce(0.0f, ACCUM_FLOAT); + } + + public static Observable sumDoubles(Observable source) { + return source.reduce(0.0d, ACCUM_DOUBLE); + } + + public static Observable sumAtLeastOneIntegers(Observable source) { + return source.reduce(ACCUM_INT); + } + + public static Observable sumAtLeastOneLongs(Observable source) { + return source.reduce(ACCUM_LONG); + } + + public static Observable sumAtLeastOneFloats(Observable source) { + return source.reduce(ACCUM_FLOAT); + } + + public static Observable sumAtLeastOneDoubles(Observable source) { + return source.reduce(ACCUM_DOUBLE); + } + + private static final Func2 ACCUM_INT = new Func2() { + @Override + public Integer call(Integer accu, Integer next) { + return accu + next; + } + }; + + private static final Func2 ACCUM_LONG = new Func2() { + @Override + public Long call(Long accu, Long next) { + return accu + next; + } + }; + + private static final Func2 ACCUM_FLOAT = new Func2() { + @Override + public Float call(Float accu, Float next) { + return accu + next; + } + }; + + private static final Func2 ACCUM_DOUBLE = new Func2() { + @Override + public Double call(Double accu, Double next) { + return accu + next; + } + }; +} diff --git a/rxjava-contrib/rxjava-math/src/main/java/rx/observables/MathObservable.java b/rxjava-contrib/rxjava-math/src/main/java/rx/observables/MathObservable.java new file mode 100644 index 0000000000..2614983bf8 --- /dev/null +++ b/rxjava-contrib/rxjava-math/src/main/java/rx/observables/MathObservable.java @@ -0,0 +1,378 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package rx.observables; + +import java.util.Comparator; + +import rx.Observable; +import rx.functions.Func1; +import rx.math.operators.OperationAverage; +import rx.math.operators.OperationMinMax; +import rx.math.operators.OperationSum; + +public class MathObservable { + + private final Observable o; + + private MathObservable(Observable o) { + this.o = o; + } + + public static MathObservable from(Observable o) { + return new MathObservable(o); + } + + /** + * Returns an Observable that emits the average of the Doubles emitted by the source Observable. + *

+ * + * + * @param source + * source Observable to compute the average of + * @return an Observable that emits a single item: the average of all the Doubles emitted by the source + * Observable + * @see RxJava Wiki: averageDouble() + * @see MSDN: Observable.Average + */ + public final static Observable averageDouble(Observable source) { + return OperationAverage.averageDoubles(source); + } + + /** + * Returns an Observable that emits the average of the Floats emitted by the source Observable. + *

+ * + * + * @param source + * source Observable to compute the average of + * @return an Observable that emits a single item: the average of all the Floats emitted by the source + * Observable + * @see RxJava Wiki: averageFloat() + * @see MSDN: Observable.Average + */ + public final static Observable averageFloat(Observable source) { + return OperationAverage.averageFloats(source); + } + + /** + * Returns an Observable that emits the average of the Integers emitted by the source Observable. + *

+ * + * + * @param source + * source Observable to compute the average of + * @return an Observable that emits a single item: the average of all the Integers emitted by the source + * Observable + * @throws IllegalArgumentException + * if the source Observable emits no items + * @see RxJava Wiki: averageInteger() + * @see MSDN: Observable.Average + */ + public final static Observable averageInteger(Observable source) { + return OperationAverage.average(source); + } + + /** + * Returns an Observable that emits the average of the Longs emitted by the source Observable. + *

+ * + * + * @param source + * source Observable to compute the average of + * @return an Observable that emits a single item: the average of all the Longs emitted by the source + * Observable + * @see RxJava Wiki: averageLong() + * @see MSDN: Observable.Average + */ + public final static Observable averageLong(Observable source) { + return OperationAverage.averageLongs(source); + } + + /** + * Returns an Observable that emits the single item emitted by the source Observable with the maximum + * numeric value. If there is more than one item with the same maximum value, it emits the last-emitted of + * these. + *

+ * + * + * @param source + * an Observable to scan for the maximum emitted item + * @return an Observable that emits this maximum item + * @throws IllegalArgumentException + * if the source is empty + * @see RxJava Wiki: max() + * @see MSDN: Observable.Max + */ + public final static > Observable max(Observable source) { + return OperationMinMax.max(source); + } + + /** + * Returns an Observable that emits the single numerically minimum item emitted by the source Observable. + * If there is more than one such item, it returns the last-emitted one. + *

+ * + * + * @param source + * an Observable to determine the minimum item of + * @return an Observable that emits the minimum item emitted by the source Observable + * @throws IllegalArgumentException + * if the source is empty + * @see MSDN: Observable.Min + */ + public final static > Observable min(Observable source) { + return OperationMinMax.min(source); + } + + /** + * Returns an Observable that emits the sum of all the Doubles emitted by the source Observable. + *

+ * + * + * @param source + * the source Observable to compute the sum of + * @return an Observable that emits a single item: the sum of all the Doubles emitted by the source + * Observable + * @see RxJava Wiki: sumDouble() + * @see MSDN: Observable.Sum + */ + public final static Observable sumDouble(Observable source) { + return OperationSum.sumDoubles(source); + } + + /** + * Returns an Observable that emits the sum of all the Floats emitted by the source Observable. + *

+ * + * + * @param source + * the source Observable to compute the sum of + * @return an Observable that emits a single item: the sum of all the Floats emitted by the source + * Observable + * @see RxJava Wiki: sumFloat() + * @see MSDN: Observable.Sum + */ + public final static Observable sumFloat(Observable source) { + return OperationSum.sumFloats(source); + } + + /** + * Returns an Observable that emits the sum of all the Integers emitted by the source Observable. + *

+ * + * + * @param source + * source Observable to compute the sum of + * @return an Observable that emits a single item: the sum of all the Integers emitted by the source + * Observable + * @see RxJava Wiki: sumInteger() + * @see MSDN: Observable.Sum + */ + public final static Observable sumInteger(Observable source) { + return OperationSum.sumIntegers(source); + } + + /** + * Returns an Observable that emits the sum of all the Longs emitted by the source Observable. + *

+ * + * + * @param source + * source Observable to compute the sum of + * @return an Observable that emits a single item: the sum of all the Longs emitted by the + * source Observable + * @see RxJava Wiki: sumLong() + * @see MSDN: Observable.Sum + */ + public final static Observable sumLong(Observable source) { + return OperationSum.sumLongs(source); + } + + /** + * Returns an Observable that transforms items emitted by the source Observable into Doubles by using a + * function you provide and then emits the Double average of the complete sequence of transformed values. + *

+ * + * + * @param valueExtractor + * the function to transform an item emitted by the source Observable into a Double + * @return an Observable that emits a single item: the Double average of the complete sequence of items + * emitted by the source Observable when transformed into Doubles by the specified function + * @see RxJava Wiki: averageDouble() + * @see MSDN: Observable.Average + */ + public final Observable averageDouble(Func1 valueExtractor) { + return Observable.create(new OperationAverage.AverageDoubleExtractor(o, valueExtractor)); + } + + /** + * Returns an Observable that transforms items emitted by the source Observable into Floats by using a + * function you provide and then emits the Float average of the complete sequence of transformed values. + *

+ * + * + * @param valueExtractor + * the function to transform an item emitted by the source Observable into a Float + * @return an Observable that emits a single item: the Float average of the complete sequence of items + * emitted by the source Observable when transformed into Floats by the specified function + * @see RxJava Wiki: averageFloat() + * @see MSDN: Observable.Average + */ + public final Observable averageFloat(Func1 valueExtractor) { + return Observable.create(new OperationAverage.AverageFloatExtractor(o, valueExtractor)); + } + + /** + * Returns an Observable that transforms items emitted by the source Observable into Integers by using a + * function you provide and then emits the Integer average of the complete sequence of transformed values. + *

+ * + * + * @param valueExtractor + * the function to transform an item emitted by the source Observable into an Integer + * @return an Observable that emits a single item: the Integer average of the complete sequence of items + * emitted by the source Observable when transformed into Integers by the specified function + * @see RxJava Wiki: averageInteger() + * @see MSDN: Observable.Average + */ + public final Observable averageInteger(Func1 valueExtractor) { + return Observable.create(new OperationAverage.AverageIntegerExtractor(o, valueExtractor)); + } + + /** + * Returns an Observable that transforms items emitted by the source Observable into Longs by using a + * function you provide and then emits the Long average of the complete sequence of transformed values. + *

+ * + * + * @param valueExtractor + * the function to transform an item emitted by the source Observable into a Long + * @return an Observable that emits a single item: the Long average of the complete sequence of items + * emitted by the source Observable when transformed into Longs by the specified function + * @see RxJava Wiki: averageLong() + * @see MSDN: Observable.Average + */ + public final Observable averageLong(Func1 valueExtractor) { + return Observable.create(new OperationAverage.AverageLongExtractor(o, valueExtractor)); + } + + /** + * Returns an Observable that emits the maximum item emitted by the source Observable, according to the + * specified comparator. If there is more than one item with the same maximum value, it emits the + * last-emitted of these. + *

+ * + * + * @param comparator + * the comparer used to compare items + * @return an Observable that emits the maximum item emitted by the source Observable, according to the + * specified comparator + * @throws IllegalArgumentException + * if the source is empty + * @see RxJava Wiki: max() + * @see MSDN: Observable.Max + */ + public final Observable max(Comparator comparator) { + return OperationMinMax.max(o, comparator); + } + + /** + * Returns an Observable that emits the minimum item emitted by the source Observable, according to a + * specified comparator. If there is more than one such item, it returns the last-emitted one. + *

+ * + * + * @param comparator + * the comparer used to compare elements + * @return an Observable that emits the minimum item emitted by the source Observable according to the + * specified comparator + * @throws IllegalArgumentException + * if the source is empty + * @see RxJava Wiki: min() + * @see MSDN: Observable.Min + */ + public final Observable min(Comparator comparator) { + return OperationMinMax.min(o, comparator); + } + + /** + * Returns an Observable that extracts a Double from each of the items emitted by the source Observable via + * a function you specify, and then emits the sum of these Doubles. + *

+ * + * + * @param valueExtractor + * the function to extract a Double from each item emitted by the source Observable + * @return an Observable that emits the Double sum of the Double values corresponding to the items emitted + * by the source Observable as transformed by the provided function + * @see RxJava Wiki: sumDouble() + * @see MSDN: Observable.Sum + */ + public final Observable sumDouble(Func1 valueExtractor) { + return OperationSum.sumAtLeastOneDoubles(o.map(valueExtractor)); + } + + /** + * Returns an Observable that extracts a Float from each of the items emitted by the source Observable via + * a function you specify, and then emits the sum of these Floats. + *

+ * + * + * @param valueExtractor + * the function to extract a Float from each item emitted by the source Observable + * @return an Observable that emits the Float sum of the Float values corresponding to the items emitted by + * the source Observable as transformed by the provided function + * @see RxJava Wiki: sumFloat() + * @see MSDN: Observable.Sum + */ + public final Observable sumFloat(Func1 valueExtractor) { + return OperationSum.sumAtLeastOneFloats(o.map(valueExtractor)); + } + + /** + * Returns an Observable that extracts an Integer from each of the items emitted by the source Observable + * via a function you specify, and then emits the sum of these Integers. + *

+ * + * + * @param valueExtractor + * the function to extract an Integer from each item emitted by the source Observable + * @return an Observable that emits the Integer sum of the Integer values corresponding to the items emitted + * by the source Observable as transformed by the provided function + * @see RxJava Wiki: sumInteger() + * @see MSDN: Observable.Sum + */ + public final Observable sumInteger(Func1 valueExtractor) { + return OperationSum.sumAtLeastOneIntegers(o.map(valueExtractor)); + } + + /** + * Returns an Observable that extracts a Long from each of the items emitted by the source Observable via a + * function you specify, and then emits the sum of these Longs. + *

+ * + * + * @param valueExtractor + * the function to extract a Long from each item emitted by the source Observable + * @return an Observable that emits the Long sum of the Long values corresponding to the items emitted by + * the source Observable as transformed by the provided function + * @see RxJava Wiki: sumLong() + * @see MSDN: Observable.Sum + */ + public final Observable sumLong(Func1 valueExtractor) { + return OperationSum.sumAtLeastOneLongs(o.map(valueExtractor)); + } +} diff --git a/rxjava-contrib/rxjava-math/src/test/java/rx/math/operators/OperationAverageTest.java b/rxjava-contrib/rxjava-math/src/test/java/rx/math/operators/OperationAverageTest.java new file mode 100644 index 0000000000..a77868dc7a --- /dev/null +++ b/rxjava-contrib/rxjava-math/src/test/java/rx/math/operators/OperationAverageTest.java @@ -0,0 +1,342 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package rx.math.operators; + +import static org.mockito.Matchers.*; +import static org.mockito.Mockito.*; +import static rx.math.operators.OperationAverage.*; + +import org.junit.Test; + +import rx.Observable; +import rx.Observer; +import rx.functions.Func1; +import rx.observables.MathObservable; + +public class OperationAverageTest { + + @SuppressWarnings("unchecked") + Observer w = mock(Observer.class); + @SuppressWarnings("unchecked") + Observer wl = mock(Observer.class); + @SuppressWarnings("unchecked") + Observer wf = mock(Observer.class); + @SuppressWarnings("unchecked") + Observer wd = mock(Observer.class); + + @Test + public void testAverageOfAFewInts() throws Throwable { + Observable src = Observable.from(1, 2, 3, 4, 6); + average(src).subscribe(w); + + verify(w, times(1)).onNext(anyInt()); + verify(w).onNext(3); + verify(w, never()).onError(any(Throwable.class)); + verify(w, times(1)).onCompleted(); + } + + @Test + public void testEmptyAverage() throws Throwable { + Observable src = Observable.empty(); + average(src).subscribe(w); + + verify(w, never()).onNext(anyInt()); + verify(w, times(1)).onError(isA(IllegalArgumentException.class)); + verify(w, never()).onCompleted(); + } + + @Test + public void testAverageOfAFewLongs() throws Throwable { + Observable src = Observable.from(1L, 2L, 3L, 4L, 6L); + averageLongs(src).subscribe(wl); + + verify(wl, times(1)).onNext(anyLong()); + verify(wl).onNext(3L); + verify(wl, never()).onError(any(Throwable.class)); + verify(wl, times(1)).onCompleted(); + } + + @Test + public void testEmptyAverageLongs() throws Throwable { + Observable src = Observable.empty(); + averageLongs(src).subscribe(wl); + + verify(wl, never()).onNext(anyLong()); + verify(wl, times(1)).onError(isA(IllegalArgumentException.class)); + verify(wl, never()).onCompleted(); + } + + @Test + public void testAverageOfAFewFloats() throws Throwable { + Observable src = Observable.from(1.0f, 2.0f); + averageFloats(src).subscribe(wf); + + verify(wf, times(1)).onNext(anyFloat()); + verify(wf).onNext(1.5f); + verify(wf, never()).onError(any(Throwable.class)); + verify(wf, times(1)).onCompleted(); + } + + @Test + public void testEmptyAverageFloats() throws Throwable { + Observable src = Observable.empty(); + averageFloats(src).subscribe(wf); + + verify(wf, never()).onNext(anyFloat()); + verify(wf, times(1)).onError(isA(IllegalArgumentException.class)); + verify(wf, never()).onCompleted(); + } + + @Test + public void testAverageOfAFewDoubles() throws Throwable { + Observable src = Observable.from(1.0d, 2.0d); + averageDoubles(src).subscribe(wd); + + verify(wd, times(1)).onNext(anyDouble()); + verify(wd).onNext(1.5d); + verify(wd, never()).onError(any(Throwable.class)); + verify(wd, times(1)).onCompleted(); + } + + @Test + public void testEmptyAverageDoubles() throws Throwable { + Observable src = Observable.empty(); + averageDoubles(src).subscribe(wd); + + verify(wd, never()).onNext(anyDouble()); + verify(wd, times(1)).onError(isA(IllegalArgumentException.class)); + verify(wd, never()).onCompleted(); + } + + void testThrows(Observer o, Class errorClass) { + verify(o, never()).onNext(any()); + verify(o, never()).onCompleted(); + verify(o, times(1)).onError(any(errorClass)); + } + + void testValue(Observer o, N value) { + verify(o, times(1)).onNext(value); + verify(o, times(1)).onCompleted(); + verify(o, never()).onError(any(Throwable.class)); + } + + @Test + public void testIntegerAverageSelector() { + Observable source = Observable.from("a", "bb", "ccc", "dddd"); + Func1 length = new Func1() { + @Override + public Integer call(String t1) { + return t1.length(); + } + }; + + Observable result = MathObservable.from(source).averageInteger(length); + Observer o = mock(Observer.class); + result.subscribe(o); + + testValue(o, 2); + } + + @Test + public void testLongAverageSelector() { + Observable source = Observable.from("a", "bb", "ccc", "dddd"); + Func1 length = new Func1() { + @Override + public Long call(String t1) { + return (long) t1.length(); + } + }; + + Observable result = MathObservable.from(source).averageLong(length); + Observer o = mock(Observer.class); + result.subscribe(o); + + testValue(o, 2L); + } + + @Test + public void testFloatAverageSelector() { + Observable source = Observable.from("a", "bb", "ccc", "dddd"); + Func1 length = new Func1() { + @Override + public Float call(String t1) { + return (float) t1.length(); + } + }; + + Observable result = MathObservable.from(source).averageFloat(length); + Observer o = mock(Observer.class); + result.subscribe(o); + + testValue(o, 2.5f); + } + + @Test + public void testDoubleAverageSelector() { + Observable source = Observable.from("a", "bb", "ccc", "dddd"); + Func1 length = new Func1() { + @Override + public Double call(String t1) { + return (double) t1.length(); + } + }; + + Observable result = MathObservable.from(source).averageDouble(length); + Observer o = mock(Observer.class); + result.subscribe(o); + + testValue(o, 2.5d); + } + + @Test + public void testIntegerAverageSelectorEmpty() { + Observable source = Observable.empty(); + Func1 length = new Func1() { + @Override + public Integer call(String t1) { + return t1.length(); + } + }; + + Observable result = MathObservable.from(source).averageInteger(length); + Observer o = mock(Observer.class); + result.subscribe(o); + + testThrows(o, IllegalArgumentException.class); + } + + @Test + public void testLongAverageSelectorEmpty() { + Observable source = Observable.empty(); + Func1 length = new Func1() { + @Override + public Long call(String t1) { + return (long) t1.length(); + } + }; + + Observable result = MathObservable.from(source).averageLong(length); + Observer o = mock(Observer.class); + result.subscribe(o); + + testThrows(o, IllegalArgumentException.class); + } + + @Test + public void testFloatAverageSelectorEmpty() { + Observable source = Observable.empty(); + Func1 length = new Func1() { + @Override + public Float call(String t1) { + return (float) t1.length(); + } + }; + + Observable result = MathObservable.from(source).averageFloat(length); + Observer o = mock(Observer.class); + result.subscribe(o); + + testThrows(o, IllegalArgumentException.class); + } + + @Test + public void testDoubleAverageSelectorEmpty() { + Observable source = Observable.empty(); + Func1 length = new Func1() { + @Override + public Double call(String t1) { + return (double) t1.length(); + } + }; + + Observable result = MathObservable.from(source).averageDouble(length); + Observer o = mock(Observer.class); + result.subscribe(o); + + testThrows(o, IllegalArgumentException.class); + } + + @Test + public void testIntegerAverageSelectorThrows() { + Observable source = Observable.from("a"); + Func1 length = new Func1() { + @Override + public Integer call(String t1) { + throw new CustomException(); + } + }; + + Observable result = MathObservable.from(source).averageInteger(length); + Observer o = mock(Observer.class); + result.subscribe(o); + + testThrows(o, CustomException.class); + } + + @Test + public void testLongAverageSelectorThrows() { + Observable source = Observable.from("a"); + Func1 length = new Func1() { + @Override + public Long call(String t1) { + throw new CustomException(); + } + }; + + Observable result = MathObservable.from(source).averageLong(length); + Observer o = mock(Observer.class); + result.subscribe(o); + + testThrows(o, CustomException.class); + } + + @Test + public void testFloatAverageSelectorThrows() { + Observable source = Observable.from("a"); + Func1 length = new Func1() { + @Override + public Float call(String t1) { + throw new CustomException(); + } + }; + + Observable result = MathObservable.from(source).averageFloat(length); + Observer o = mock(Observer.class); + result.subscribe(o); + + testThrows(o, CustomException.class); + } + + @Test + public void testDoubleAverageSelectorThrows() { + Observable source = Observable.from("a"); + Func1 length = new Func1() { + @Override + public Double call(String t1) { + throw new CustomException(); + } + }; + + Observable result = MathObservable.from(source).averageDouble(length); + Observer o = mock(Observer.class); + result.subscribe(o); + + testThrows(o, CustomException.class); + } + + static class CustomException extends RuntimeException { + } +} diff --git a/rxjava-contrib/rxjava-math/src/test/java/rx/math/operators/OperationMinMaxTest.java b/rxjava-contrib/rxjava-math/src/test/java/rx/math/operators/OperationMinMaxTest.java new file mode 100644 index 0000000000..63eee7b28f --- /dev/null +++ b/rxjava-contrib/rxjava-math/src/test/java/rx/math/operators/OperationMinMaxTest.java @@ -0,0 +1,354 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package rx.math.operators; + +import static org.mockito.Matchers.*; +import static org.mockito.Mockito.*; +import static rx.math.operators.OperationMinMax.*; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; + +import org.junit.Test; +import org.mockito.InOrder; + +import rx.Observable; +import rx.Observer; +import rx.functions.Func1; + +public class OperationMinMaxTest { + @Test + public void testMin() { + Observable observable = min(Observable.from(2, 3, 1, 4)); + + @SuppressWarnings("unchecked") + Observer observer = (Observer) mock(Observer.class); + + observable.subscribe(observer); + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onNext(1); + inOrder.verify(observer, times(1)).onCompleted(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void testMinWithEmpty() { + Observable observable = min(Observable. empty()); + + @SuppressWarnings("unchecked") + Observer observer = (Observer) mock(Observer.class); + + observable.subscribe(observer); + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onError( + isA(IllegalArgumentException.class)); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void testMinWithComparator() { + Observable observable = min(Observable.from(2, 3, 1, 4), + new Comparator() { + @Override + public int compare(Integer o1, Integer o2) { + return o2 - o1; + } + }); + + @SuppressWarnings("unchecked") + Observer observer = (Observer) mock(Observer.class); + + observable.subscribe(observer); + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onNext(4); + inOrder.verify(observer, times(1)).onCompleted(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void testMinWithComparatorAndEmpty() { + Observable observable = min(Observable. empty(), + new Comparator() { + @Override + public int compare(Integer o1, Integer o2) { + return o2 - o1; + } + }); + + @SuppressWarnings("unchecked") + Observer observer = (Observer) mock(Observer.class); + + observable.subscribe(observer); + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onError( + isA(IllegalArgumentException.class)); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void testMinBy() { + Observable> observable = minBy( + Observable.from("1", "2", "3", "4", "5", "6"), + new Func1() { + @Override + public Integer call(String t1) { + return Integer.parseInt(t1) % 2; + } + }); + + @SuppressWarnings("unchecked") + Observer> observer = (Observer>) mock(Observer.class); + + observable.subscribe(observer); + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onNext(Arrays.asList("2", "4", "6")); + inOrder.verify(observer, times(1)).onCompleted(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void testMinByWithEmpty() { + Observable> observable = minBy( + Observable. empty(), new Func1() { + @Override + public Integer call(String t1) { + return Integer.parseInt(t1) % 2; + } + }); + + @SuppressWarnings("unchecked") + Observer> observer = (Observer>) mock(Observer.class); + + observable.subscribe(observer); + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onNext(new ArrayList()); + inOrder.verify(observer, times(1)).onCompleted(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void testMinByWithComparator() { + Observable> observable = minBy( + Observable.from("1", "2", "3", "4", "5", "6"), + new Func1() { + @Override + public Integer call(String t1) { + return Integer.parseInt(t1) % 2; + } + }, new Comparator() { + @Override + public int compare(Integer o1, Integer o2) { + return o2 - o1; + } + }); + + @SuppressWarnings("unchecked") + Observer> observer = (Observer>) mock(Observer.class); + + observable.subscribe(observer); + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onNext(Arrays.asList("1", "3", "5")); + inOrder.verify(observer, times(1)).onCompleted(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void testMinByWithComparatorAndEmpty() { + Observable> observable = minBy( + Observable. empty(), new Func1() { + @Override + public Integer call(String t1) { + return Integer.parseInt(t1) % 2; + } + }, new Comparator() { + @Override + public int compare(Integer o1, Integer o2) { + return o2 - o1; + } + }); + + @SuppressWarnings("unchecked") + Observer> observer = (Observer>) mock(Observer.class); + + observable.subscribe(observer); + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onNext(new ArrayList()); + inOrder.verify(observer, times(1)).onCompleted(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void testMax() { + Observable observable = max(Observable.from(2, 3, 1, 4)); + + @SuppressWarnings("unchecked") + Observer observer = (Observer) mock(Observer.class); + + observable.subscribe(observer); + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onNext(4); + inOrder.verify(observer, times(1)).onCompleted(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void testMaxWithEmpty() { + Observable observable = max(Observable. empty()); + + @SuppressWarnings("unchecked") + Observer observer = (Observer) mock(Observer.class); + + observable.subscribe(observer); + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onError( + isA(IllegalArgumentException.class)); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void testMaxWithComparator() { + Observable observable = max(Observable.from(2, 3, 1, 4), + new Comparator() { + @Override + public int compare(Integer o1, Integer o2) { + return o2 - o1; + } + }); + + @SuppressWarnings("unchecked") + Observer observer = (Observer) mock(Observer.class); + + observable.subscribe(observer); + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onNext(1); + inOrder.verify(observer, times(1)).onCompleted(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void testMaxWithComparatorAndEmpty() { + Observable observable = max(Observable. empty(), + new Comparator() { + @Override + public int compare(Integer o1, Integer o2) { + return o2 - o1; + } + }); + + @SuppressWarnings("unchecked") + Observer observer = (Observer) mock(Observer.class); + + observable.subscribe(observer); + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onError( + isA(IllegalArgumentException.class)); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void testMaxBy() { + Observable> observable = maxBy( + Observable.from("1", "2", "3", "4", "5", "6"), + new Func1() { + @Override + public Integer call(String t1) { + return Integer.parseInt(t1) % 2; + } + }); + + @SuppressWarnings("unchecked") + Observer> observer = (Observer>) mock(Observer.class); + + observable.subscribe(observer); + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onNext(Arrays.asList("1", "3", "5")); + inOrder.verify(observer, times(1)).onCompleted(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void testMaxByWithEmpty() { + Observable> observable = maxBy( + Observable. empty(), new Func1() { + @Override + public Integer call(String t1) { + return Integer.parseInt(t1) % 2; + } + }); + + @SuppressWarnings("unchecked") + Observer> observer = (Observer>) mock(Observer.class); + + observable.subscribe(observer); + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onNext(new ArrayList()); + inOrder.verify(observer, times(1)).onCompleted(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void testMaxByWithComparator() { + Observable> observable = maxBy( + Observable.from("1", "2", "3", "4", "5", "6"), + new Func1() { + @Override + public Integer call(String t1) { + return Integer.parseInt(t1) % 2; + } + }, new Comparator() { + @Override + public int compare(Integer o1, Integer o2) { + return o2 - o1; + } + }); + + @SuppressWarnings("unchecked") + Observer> observer = (Observer>) mock(Observer.class); + + observable.subscribe(observer); + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onNext(Arrays.asList("2", "4", "6")); + inOrder.verify(observer, times(1)).onCompleted(); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void testMaxByWithComparatorAndEmpty() { + Observable> observable = maxBy( + Observable. empty(), new Func1() { + @Override + public Integer call(String t1) { + return Integer.parseInt(t1) % 2; + } + }, new Comparator() { + @Override + public int compare(Integer o1, Integer o2) { + return o2 - o1; + } + }); + + @SuppressWarnings("unchecked") + Observer> observer = (Observer>) mock(Observer.class); + + observable.subscribe(observer); + InOrder inOrder = inOrder(observer); + inOrder.verify(observer, times(1)).onNext(new ArrayList()); + inOrder.verify(observer, times(1)).onCompleted(); + inOrder.verifyNoMoreInteractions(); + } +} diff --git a/rxjava-contrib/rxjava-math/src/test/java/rx/math/operators/OperationSumTest.java b/rxjava-contrib/rxjava-math/src/test/java/rx/math/operators/OperationSumTest.java new file mode 100644 index 0000000000..43e2805d43 --- /dev/null +++ b/rxjava-contrib/rxjava-math/src/test/java/rx/math/operators/OperationSumTest.java @@ -0,0 +1,346 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package rx.math.operators; + +import static org.mockito.Matchers.*; +import static org.mockito.Mockito.*; +import static rx.math.operators.OperationSum.*; + +import org.junit.Test; + +import rx.Observable; +import rx.Observer; +import rx.functions.Func1; +import rx.observables.MathObservable; + +public class OperationSumTest { + + @SuppressWarnings("unchecked") + Observer w = mock(Observer.class); + @SuppressWarnings("unchecked") + Observer wl = mock(Observer.class); + @SuppressWarnings("unchecked") + Observer wf = mock(Observer.class); + @SuppressWarnings("unchecked") + Observer wd = mock(Observer.class); + + @Test + public void testSumOfAFewInts() throws Throwable { + Observable src = Observable.from(1, 2, 3, 4, 5); + sumIntegers(src).subscribe(w); + + verify(w, times(1)).onNext(anyInt()); + verify(w).onNext(15); + verify(w, never()).onError(any(Throwable.class)); + verify(w, times(1)).onCompleted(); + } + + @Test + public void testEmptySum() throws Throwable { + Observable src = Observable.empty(); + sumIntegers(src).subscribe(w); + + verify(w, times(1)).onNext(anyInt()); + verify(w).onNext(0); + verify(w, never()).onError(any(Throwable.class)); + verify(w, times(1)).onCompleted(); + } + + @Test + public void testSumOfAFewLongs() throws Throwable { + Observable src = Observable.from(1L, 2L, 3L, 4L, 5L); + sumLongs(src).subscribe(wl); + + verify(wl, times(1)).onNext(anyLong()); + verify(wl).onNext(15L); + verify(wl, never()).onError(any(Throwable.class)); + verify(wl, times(1)).onCompleted(); + } + + @Test + public void testEmptySumLongs() throws Throwable { + Observable src = Observable.empty(); + sumLongs(src).subscribe(wl); + + verify(wl, times(1)).onNext(anyLong()); + verify(wl).onNext(0L); + verify(wl, never()).onError(any(Throwable.class)); + verify(wl, times(1)).onCompleted(); + } + + @Test + public void testSumOfAFewFloats() throws Throwable { + Observable src = Observable.from(1.0f); + sumFloats(src).subscribe(wf); + + verify(wf, times(1)).onNext(anyFloat()); + verify(wf).onNext(1.0f); + verify(wf, never()).onError(any(Throwable.class)); + verify(wf, times(1)).onCompleted(); + } + + @Test + public void testEmptySumFloats() throws Throwable { + Observable src = Observable.empty(); + sumFloats(src).subscribe(wf); + + verify(wf, times(1)).onNext(anyFloat()); + verify(wf).onNext(0.0f); + verify(wf, never()).onError(any(Throwable.class)); + verify(wf, times(1)).onCompleted(); + } + + @Test + public void testSumOfAFewDoubles() throws Throwable { + Observable src = Observable.from(0.0d, 1.0d, 0.5d); + sumDoubles(src).subscribe(wd); + + verify(wd, times(1)).onNext(anyDouble()); + verify(wd).onNext(1.5d); + verify(wd, never()).onError(any(Throwable.class)); + verify(wd, times(1)).onCompleted(); + } + + @Test + public void testEmptySumDoubles() throws Throwable { + Observable src = Observable.empty(); + sumDoubles(src).subscribe(wd); + + verify(wd, times(1)).onNext(anyDouble()); + verify(wd).onNext(0.0d); + verify(wd, never()).onError(any(Throwable.class)); + verify(wd, times(1)).onCompleted(); + } + + void testThrows(Observer o, Class errorClass) { + verify(o, never()).onNext(any()); + verify(o, never()).onCompleted(); + verify(o, times(1)).onError(any(errorClass)); + } + + void testValue(Observer o, N value) { + verify(o, times(1)).onNext(value); + verify(o, times(1)).onCompleted(); + verify(o, never()).onError(any(Throwable.class)); + } + + @Test + public void testIntegerSumSelector() { + Observable source = Observable.from("a", "bb", "ccc", "dddd"); + Func1 length = new Func1() { + @Override + public Integer call(String t1) { + return t1.length(); + } + }; + + Observable result = MathObservable.from(source).sumInteger(length); + Observer o = mock(Observer.class); + result.subscribe(o); + + testValue(o, 10); + } + + @Test + public void testLongSumSelector() { + Observable source = Observable.from("a", "bb", "ccc", "dddd"); + Func1 length = new Func1() { + @Override + public Long call(String t1) { + return (long) t1.length(); + } + }; + + Observable result = MathObservable.from(source).sumLong(length); + Observer o = mock(Observer.class); + result.subscribe(o); + + testValue(o, 10L); + } + + @Test + public void testFloatSumSelector() { + Observable source = Observable.from("a", "bb", "ccc", "dddd"); + Func1 length = new Func1() { + @Override + public Float call(String t1) { + return (float) t1.length(); + } + }; + + Observable result = MathObservable.from(source).sumFloat(length); + Observer o = mock(Observer.class); + result.subscribe(o); + + testValue(o, 10f); + } + + @Test + public void testDoubleSumSelector() { + Observable source = Observable.from("a", "bb", "ccc", "dddd"); + Func1 length = new Func1() { + @Override + public Double call(String t1) { + return (double) t1.length(); + } + }; + + Observable result = MathObservable.from(source).sumDouble(length); + Observer o = mock(Observer.class); + result.subscribe(o); + + testValue(o, 10d); + } + + @Test + public void testIntegerSumSelectorEmpty() { + Observable source = Observable.empty(); + Func1 length = new Func1() { + @Override + public Integer call(String t1) { + return t1.length(); + } + }; + + Observable result = MathObservable.from(source).sumInteger(length); + Observer o = mock(Observer.class); + result.subscribe(o); + + testThrows(o, IllegalArgumentException.class); + } + + @Test + public void testLongSumSelectorEmpty() { + Observable source = Observable.empty(); + Func1 length = new Func1() { + @Override + public Long call(String t1) { + return (long) t1.length(); + } + }; + + Observable result = MathObservable.from(source).sumLong(length); + Observer o = mock(Observer.class); + result.subscribe(o); + + testThrows(o, IllegalArgumentException.class); + } + + @Test + public void testFloatSumSelectorEmpty() { + Observable source = Observable.empty(); + Func1 length = new Func1() { + @Override + public Float call(String t1) { + return (float) t1.length(); + } + }; + + Observable result = MathObservable.from(source).sumFloat(length); + Observer o = mock(Observer.class); + result.subscribe(o); + + testThrows(o, IllegalArgumentException.class); + } + + @Test + public void testDoubleSumSelectorEmpty() { + Observable source = Observable.empty(); + Func1 length = new Func1() { + @Override + public Double call(String t1) { + return (double) t1.length(); + } + }; + + Observable result = MathObservable.from(source).sumDouble(length); + Observer o = mock(Observer.class); + result.subscribe(o); + + testThrows(o, IllegalArgumentException.class); + } + + @Test + public void testIntegerSumSelectorThrows() { + Observable source = Observable.from("a"); + Func1 length = new Func1() { + @Override + public Integer call(String t1) { + throw new CustomException(); + } + }; + + Observable result = MathObservable.from(source).sumInteger(length); + Observer o = mock(Observer.class); + result.subscribe(o); + + testThrows(o, CustomException.class); + } + + @Test + public void testLongSumSelectorThrows() { + Observable source = Observable.from("a"); + Func1 length = new Func1() { + @Override + public Long call(String t1) { + throw new CustomException(); + } + }; + + Observable result = MathObservable.from(source).sumLong(length); + Observer o = mock(Observer.class); + result.subscribe(o); + + testThrows(o, CustomException.class); + } + + @Test + public void testFloatSumSelectorThrows() { + Observable source = Observable.from("a"); + Func1 length = new Func1() { + @Override + public Float call(String t1) { + throw new CustomException(); + } + }; + + Observable result = MathObservable.from(source).sumFloat(length); + Observer o = mock(Observer.class); + result.subscribe(o); + + testThrows(o, CustomException.class); + } + + @Test + public void testDoubleSumSelectorThrows() { + Observable source = Observable.from("a"); + Func1 length = new Func1() { + @Override + public Double call(String t1) { + throw new CustomException(); + } + }; + + Observable result = MathObservable.from(source).sumDouble(length); + Observer o = mock(Observer.class); + result.subscribe(o); + + testThrows(o, CustomException.class); + } + + static class CustomException extends RuntimeException { + } +} diff --git a/rxjava-core/src/main/java/rx/Observable.java b/rxjava-core/src/main/java/rx/Observable.java index c244fb5001..92ae1c7c40 100644 --- a/rxjava-core/src/main/java/rx/Observable.java +++ b/rxjava-core/src/main/java/rx/Observable.java @@ -441,6 +441,7 @@ public final static Observable average(Observable source) { * Observable * @see RxJava Wiki: averageDouble() * @see MSDN: Observable.Average + * @deprecated Use rxjava-math module instead */ public final static Observable averageDouble(Observable source) { return OperationAverage.averageDoubles(source); @@ -457,6 +458,7 @@ public final static Observable averageDouble(Observable source) * Observable * @see RxJava Wiki: averageFloat() * @see MSDN: Observable.Average + * @deprecated Use rxjava-math module instead */ public final static Observable averageFloat(Observable source) { return OperationAverage.averageFloats(source); @@ -475,6 +477,7 @@ public final static Observable averageFloat(Observable source) { * if the source Observable emits no items * @see RxJava Wiki: averageInteger() * @see MSDN: Observable.Average + * @deprecated Use rxjava-math module instead */ public final static Observable averageInteger(Observable source) { return OperationAverage.average(source); @@ -491,6 +494,7 @@ public final static Observable averageInteger(Observable sourc * Observable * @see RxJava Wiki: averageLong() * @see MSDN: Observable.Average + * @deprecated Use rxjava-math module instead */ public final static Observable averageLong(Observable source) { return OperationAverage.averageLongs(source); @@ -1595,6 +1599,7 @@ public final static Observable just(T value, Scheduler scheduler) { * if the source is empty * @see RxJava Wiki: max() * @see MSDN: Observable.Max + * @deprecated Use rxjava-math module instead */ public final static > Observable max(Observable source) { return OperationMinMax.max(source); @@ -2309,6 +2314,7 @@ public final static Observable mergeDelayError(Observable t1 * @throws IllegalArgumentException * if the source is empty * @see MSDN: Observable.Min + * @deprecated Use rxjava-math module instead */ public final static > Observable min(Observable source) { return OperationMinMax.min(source); @@ -2485,7 +2491,7 @@ public final Boolean call(T first, T second) { public final static Observable sequenceEqual(Observable first, Observable second, Func2 equality) { return OperationSequenceEqual.sequenceEqual(first, second, equality); } - + /** * Returns an Observable that emits the sum of all the Doubles emitted by the source Observable. *

@@ -2497,6 +2503,7 @@ public final static Observable sequenceEqual(ObservableRxJava Wiki: sumDouble() * @see MSDN: Observable.Sum + * @deprecated Use rxjava-math module instead */ public final static Observable sumDouble(Observable source) { return OperationSum.sumDoubles(source); @@ -2513,6 +2520,7 @@ public final static Observable sumDouble(Observable source) { * Observable * @see RxJava Wiki: sumFloat() * @see MSDN: Observable.Sum + * @deprecated Use rxjava-math module instead */ public final static Observable sumFloat(Observable source) { return OperationSum.sumFloats(source); @@ -2529,6 +2537,7 @@ public final static Observable sumFloat(Observable source) { * Observable * @see RxJava Wiki: sumInteger() * @see MSDN: Observable.Sum + * @deprecated Use rxjava-math module instead */ public final static Observable sumInteger(Observable source) { return OperationSum.sumIntegers(source); @@ -2545,6 +2554,7 @@ public final static Observable sumInteger(Observable source) { * source Observable * @see RxJava Wiki: sumLong() * @see MSDN: Observable.Sum + * @deprecated Use rxjava-math module instead */ public final static Observable sumLong(Observable source) { return OperationSum.sumLongs(source); @@ -3413,6 +3423,7 @@ public final Observable asObservable() { * emitted by the source Observable when transformed into Doubles by the specified function * @see RxJava Wiki: averageDouble() * @see MSDN: Observable.Average + * @deprecated Use rxjava-math module instead */ public final Observable averageDouble(Func1 valueExtractor) { return create(new OperationAverage.AverageDoubleExtractor(this, valueExtractor)); @@ -3430,6 +3441,7 @@ public final Observable averageDouble(Func1 valueExtr * emitted by the source Observable when transformed into Floats by the specified function * @see RxJava Wiki: averageFloat() * @see MSDN: Observable.Average + * @deprecated Use rxjava-math module instead */ public final Observable averageFloat(Func1 valueExtractor) { return create(new OperationAverage.AverageFloatExtractor(this, valueExtractor)); @@ -3447,6 +3459,7 @@ public final Observable averageFloat(Func1 valueExtract * emitted by the source Observable when transformed into Integers by the specified function * @see RxJava Wiki: averageInteger() * @see MSDN: Observable.Average + * @deprecated Use rxjava-math module instead */ public final Observable averageInteger(Func1 valueExtractor) { return create(new OperationAverage.AverageIntegerExtractor(this, valueExtractor)); @@ -3464,6 +3477,7 @@ public final Observable averageInteger(Func1 valueE * emitted by the source Observable when transformed into Longs by the specified function * @see RxJava Wiki: averageLong() * @see MSDN: Observable.Average + * @deprecated Use rxjava-math module instead */ public final Observable averageLong(Func1 valueExtractor) { return create(new OperationAverage.AverageLongExtractor(this, valueExtractor)); @@ -4843,6 +4857,7 @@ public final Observable> materialize() { * if the source is empty * @see RxJava Wiki: max() * @see MSDN: Observable.Max + * @deprecated Use rxjava-math module instead */ public final Observable max(Comparator comparator) { return OperationMinMax.max(this, comparator); @@ -5011,6 +5026,7 @@ public final Observable mergeMapIterable(Func1RxJava Wiki: min() * @see MSDN: Observable.Min + * @deprecated Use rxjava-math module instead */ public final Observable min(Comparator comparator) { return OperationMinMax.min(this, comparator); @@ -7053,6 +7069,7 @@ public final Observable subscribeOn(Scheduler scheduler) { * by the source Observable as transformed by the provided function * @see RxJava Wiki: sumDouble() * @see MSDN: Observable.Sum + * @deprecated Use rxjava-math module instead */ public final Observable sumDouble(Func1 valueExtractor) { return OperationSum.sumAtLeastOneDoubles(map(valueExtractor)); @@ -7070,6 +7087,7 @@ public final Observable sumDouble(Func1 valueExtracto * the source Observable as transformed by the provided function * @see RxJava Wiki: sumFloat() * @see MSDN: Observable.Sum + * @deprecated Use rxjava-math module instead */ public final Observable sumFloat(Func1 valueExtractor) { return OperationSum.sumAtLeastOneFloats(map(valueExtractor)); @@ -7087,6 +7105,7 @@ public final Observable sumFloat(Func1 valueExtractor) * by the source Observable as transformed by the provided function * @see RxJava Wiki: sumInteger() * @see MSDN: Observable.Sum + * @deprecated Use rxjava-math module instead */ public final Observable sumInteger(Func1 valueExtractor) { return OperationSum.sumAtLeastOneIntegers(map(valueExtractor)); @@ -7104,6 +7123,7 @@ public final Observable sumInteger(Func1 valueExtra * the source Observable as transformed by the provided function * @see RxJava Wiki: sumLong() * @see MSDN: Observable.Sum + * @deprecated Use rxjava-math module instead */ public final Observable sumLong(Func1 valueExtractor) { return OperationSum.sumAtLeastOneLongs(map(valueExtractor)); diff --git a/settings.gradle b/settings.gradle index 5be4134cef..5a9e3e8cb0 100644 --- a/settings.gradle +++ b/settings.gradle @@ -9,6 +9,7 @@ include 'rxjava-core', \ 'rxjava-contrib:rxjava-android', \ 'rxjava-contrib:rxjava-apache-http', \ 'rxjava-contrib:rxjava-string', \ +'rxjava-contrib:rxjava-math', \ 'rxjava-contrib:rxjava-debug', \ 'rxjava-contrib:rxjava-async-util', \ 'rxjava-contrib:rxjava-computation-expressions' From d08df0bfd1983c56eac0f90593de92e890c452a3 Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Wed, 5 Mar 2014 21:53:31 -0800 Subject: [PATCH 072/422] OperationRetry -> OperatorRetry Updated to use "lift" and Subscriber. Fixes https://github.com/Netflix/RxJava/issues/943 Observable.retry() does not unsubscribe from source --- rxjava-core/src/main/java/rx/Observable.java | 4 +- .../java/rx/operators/OperationRetry.java | 104 ----------------- .../main/java/rx/operators/OperatorRetry.java | 110 ++++++++++++++++++ ...nRetryTest.java => OperatorRetryTest.java} | 28 ++++- 4 files changed, 135 insertions(+), 111 deletions(-) delete mode 100644 rxjava-core/src/main/java/rx/operators/OperationRetry.java create mode 100644 rxjava-core/src/main/java/rx/operators/OperatorRetry.java rename rxjava-core/src/test/java/rx/operators/{OperationRetryTest.java => OperatorRetryTest.java} (83%) diff --git a/rxjava-core/src/main/java/rx/Observable.java b/rxjava-core/src/main/java/rx/Observable.java index 92ae1c7c40..0db0910764 100644 --- a/rxjava-core/src/main/java/rx/Observable.java +++ b/rxjava-core/src/main/java/rx/Observable.java @@ -5985,7 +5985,7 @@ public final ConnectableObservable replay(Scheduler scheduler) { * @see RxJava Wiki: retry() */ public final Observable retry() { - return create(OperationRetry.retry(this)); + return nest().lift(new OperatorRetry()); } /** @@ -6009,7 +6009,7 @@ public final Observable retry() { * @see RxJava Wiki: retry() */ public final Observable retry(int retryCount) { - return create(OperationRetry.retry(this, retryCount)); + return nest().lift(new OperatorRetry(retryCount)); } /** diff --git a/rxjava-core/src/main/java/rx/operators/OperationRetry.java b/rxjava-core/src/main/java/rx/operators/OperationRetry.java deleted file mode 100644 index ece2358a6c..0000000000 --- a/rxjava-core/src/main/java/rx/operators/OperationRetry.java +++ /dev/null @@ -1,104 +0,0 @@ -/** - * Copyright 2014 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package rx.operators; - -/** - * Copyright 2014 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import java.util.concurrent.atomic.AtomicInteger; - -import rx.Observable; -import rx.Observable.OnSubscribeFunc; -import rx.Observer; -import rx.Scheduler.Inner; -import rx.Subscription; -import rx.functions.Action1; -import rx.schedulers.Schedulers; - -public class OperationRetry { - - private static final int INFINITE_RETRY = -1; - - public static OnSubscribeFunc retry(final Observable observable, final int retryCount) { - return new Retry(observable, retryCount); - } - - public static OnSubscribeFunc retry(final Observable observable) { - return new Retry(observable, INFINITE_RETRY); - } - - private static class Retry implements OnSubscribeFunc { - - private final Observable source; - private final int retryCount; - private final AtomicInteger attempts = new AtomicInteger(0); - - public Retry(Observable source, int retryCount) { - this.source = source; - this.retryCount = retryCount; - } - - @Override - public Subscription onSubscribe(final Observer observer) { - return Schedulers.trampoline().schedule(new Action1() { - - @Override - public void call(final Inner inner) { - final Action1 _self = this; - attempts.incrementAndGet(); - source.subscribe(new Observer() { - - @Override - public void onCompleted() { - observer.onCompleted(); - } - - @Override - public void onError(Throwable e) { - if ((retryCount == INFINITE_RETRY || attempts.get() <= retryCount) && !inner.isUnsubscribed()) { - // retry again - inner.schedule(_self); - } else { - // give up and pass the failure - observer.onError(e); - } - } - - @Override - public void onNext(T v) { - observer.onNext(v); - } - - }); - } - }); - } - - } -} diff --git a/rxjava-core/src/main/java/rx/operators/OperatorRetry.java b/rxjava-core/src/main/java/rx/operators/OperatorRetry.java new file mode 100644 index 0000000000..9f3ea2f5cc --- /dev/null +++ b/rxjava-core/src/main/java/rx/operators/OperatorRetry.java @@ -0,0 +1,110 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package rx.operators; + +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import java.util.concurrent.atomic.AtomicInteger; + +import rx.Observable; +import rx.Observable.Operator; +import rx.Scheduler.Inner; +import rx.Subscriber; +import rx.functions.Action1; +import rx.schedulers.Schedulers; + +public class OperatorRetry implements Operator> { + + private static final int INFINITE_RETRY = -1; + + private final int retryCount; + private final AtomicInteger attempts = new AtomicInteger(0); + + public OperatorRetry(int retryCount) { + this.retryCount = retryCount; + } + + public OperatorRetry() { + this(INFINITE_RETRY); + } + + @Override + public Subscriber> call(final Subscriber s) { + return new Subscriber>(s) { + + @Override + public void onCompleted() { + // ignore as we expect a single nested Observable + } + + @Override + public void onError(Throwable e) { + s.onError(e); + } + + @Override + public void onNext(final Observable o) { + Schedulers.trampoline().schedule(new Action1() { + + @Override + public void call(final Inner inner) { + final Action1 _self = this; + attempts.incrementAndGet(); + o.subscribe(new Subscriber(s) { + + @Override + public void onCompleted() { + s.onCompleted(); + } + + @Override + public void onError(Throwable e) { + if ((retryCount == INFINITE_RETRY || attempts.get() <= retryCount) && !inner.isUnsubscribed()) { + // retry again + inner.schedule(_self); + } else { + // give up and pass the failure + s.onError(e); + } + } + + @Override + public void onNext(T v) { + s.onNext(v); + } + + }); + } + }); + } + + }; + } +} diff --git a/rxjava-core/src/test/java/rx/operators/OperationRetryTest.java b/rxjava-core/src/test/java/rx/operators/OperatorRetryTest.java similarity index 83% rename from rxjava-core/src/test/java/rx/operators/OperationRetryTest.java rename to rxjava-core/src/test/java/rx/operators/OperatorRetryTest.java index f49c2ac572..6fbcf19678 100644 --- a/rxjava-core/src/test/java/rx/operators/OperationRetryTest.java +++ b/rxjava-core/src/test/java/rx/operators/OperatorRetryTest.java @@ -15,9 +15,10 @@ */ package rx.operators; +import static org.junit.Assert.*; import static org.mockito.Matchers.*; import static org.mockito.Mockito.*; -import static rx.operators.OperationRetry.*; +import static rx.operators.OperatorRetry.*; import java.util.concurrent.atomic.AtomicInteger; @@ -27,9 +28,11 @@ import rx.Observable; import rx.Observer; import rx.Subscription; +import rx.functions.Action1; +import rx.subjects.PublishSubject; import rx.subscriptions.Subscriptions; -public class OperationRetryTest { +public class OperatorRetryTest { @Test public void testOriginFails() { @@ -52,7 +55,7 @@ public void testRetryFail() { @SuppressWarnings("unchecked") Observer observer = mock(Observer.class); Observable origin = Observable.create(new FuncWithErrors(NUM_FAILURES)); - Observable.create(retry(origin, NUM_RETRIES)).subscribe(observer); + origin.nest().lift(new OperatorRetry(NUM_RETRIES)).subscribe(observer); InOrder inOrder = inOrder(observer); // should show 2 attempts (first time fail, second time (1st retry) fail) @@ -72,7 +75,7 @@ public void testRetrySuccess() { @SuppressWarnings("unchecked") Observer observer = mock(Observer.class); Observable origin = Observable.create(new FuncWithErrors(NUM_FAILURES)); - Observable.create(retry(origin, NUM_RETRIES)).subscribe(observer); + origin.nest().lift(new OperatorRetry(NUM_RETRIES)).subscribe(observer); InOrder inOrder = inOrder(observer); // should show 3 attempts @@ -92,7 +95,7 @@ public void testInfiniteRetry() { @SuppressWarnings("unchecked") Observer observer = mock(Observer.class); Observable origin = Observable.create(new FuncWithErrors(NUM_FAILURES)); - Observable.create(retry(origin)).subscribe(observer); + origin.nest().lift(new OperatorRetry()).subscribe(observer); InOrder inOrder = inOrder(observer); // should show 3 attempts @@ -127,4 +130,19 @@ public Subscription onSubscribe(Observer o) { return Subscriptions.empty(); } } + + @Test + public void testUnsubscribeFromRetry() { + PublishSubject subject = PublishSubject.create(); + final AtomicInteger count = new AtomicInteger(0); + Subscription sub = subject.retry().subscribe(new Action1() { + @Override + public void call(Integer n) { + count.incrementAndGet(); + }}); + subject.onNext(1); + sub.unsubscribe(); + subject.onNext(2); + assertEquals(1,count.get()); + } } From c54b3220701735d3db7fb2f42559117413678c88 Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Wed, 5 Mar 2014 22:08:45 -0800 Subject: [PATCH 073/422] Move state inside Subscriber --- rxjava-core/src/main/java/rx/operators/OperatorRetry.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rxjava-core/src/main/java/rx/operators/OperatorRetry.java b/rxjava-core/src/main/java/rx/operators/OperatorRetry.java index 9f3ea2f5cc..1ee809aa47 100644 --- a/rxjava-core/src/main/java/rx/operators/OperatorRetry.java +++ b/rxjava-core/src/main/java/rx/operators/OperatorRetry.java @@ -45,7 +45,6 @@ public class OperatorRetry implements Operator> { private static final int INFINITE_RETRY = -1; private final int retryCount; - private final AtomicInteger attempts = new AtomicInteger(0); public OperatorRetry(int retryCount) { this.retryCount = retryCount; @@ -58,7 +57,8 @@ public OperatorRetry() { @Override public Subscriber> call(final Subscriber s) { return new Subscriber>(s) { - + final AtomicInteger attempts = new AtomicInteger(0); + @Override public void onCompleted() { // ignore as we expect a single nested Observable From d3cb033ba02b524863cc44956e5ce8b88cc7cb17 Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Wed, 5 Mar 2014 22:21:19 -0800 Subject: [PATCH 074/422] Ignoring broken unit tests until fixed /cc @abersnaze --- .../rxjava-debug/src/test/java/rx/debug/DebugHookTest.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/rxjava-contrib/rxjava-debug/src/test/java/rx/debug/DebugHookTest.java b/rxjava-contrib/rxjava-debug/src/test/java/rx/debug/DebugHookTest.java index c3f4f2d426..5fdba559a6 100644 --- a/rxjava-contrib/rxjava-debug/src/test/java/rx/debug/DebugHookTest.java +++ b/rxjava-contrib/rxjava-debug/src/test/java/rx/debug/DebugHookTest.java @@ -13,6 +13,7 @@ import org.hamcrest.Description; import org.junit.After; import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; import org.mockito.InOrder; @@ -85,6 +86,7 @@ public void assertValidState() { } @SuppressWarnings("unchecked") + @Ignore @Test public void testSimple() { TestDebugNotificationListener listener = new TestDebugNotificationListener(); @@ -107,6 +109,7 @@ public void testSimple() { } @SuppressWarnings("unchecked") + @Ignore @Test public void testOneOp() { TestDebugNotificationListener listener = new TestDebugNotificationListener(); From bb73bbc7440ae09ef0974560185dc0258f74360f Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Wed, 5 Mar 2014 22:45:27 -0800 Subject: [PATCH 075/422] Changing line 38 to try and fix build Odd Jenkins build failures even though command-line build works fine: ``` :rxjava:rxjava-contrib:rxjava-debug:compileJava/mnt/builds/slave/workspace/OSS-RxJava-release/rxjava-contrib/rxjava-debug/src/main/java/rx/plugins/DebugNotification.java:38: ')' expected sourceFunc = ((DebugHook.DebugOnSubscribe) sourceFunc).getActual(); ^ /mnt/builds/slave/workspace/OSS-RxJava-release/rxjava-contrib/rxjava-debug/src/main/java/rx/plugins/DebugNotification.java:38: ')' expected sourceFunc = ((DebugHook.DebugOnSubscribe) sourceFunc).getActual(); ^ /mnt/builds/slave/workspace/OSS-RxJava-release/rxjava-contrib/rxjava-debug/src/main/java/rx/plugins/DebugNotification.java:38: illegal start of expression sourceFunc = ((DebugHook.DebugOnSubscribe) sourceFunc).getActual(); ^ /mnt/builds/slave/workspace/OSS-RxJava-release/rxjava-contrib/rxjava-debug/src/main/java/rx/plugins/DebugNotification.java:38: not a statement sourceFunc = ((DebugHook.DebugOnSubscribe) sourceFunc).getActual(); ^ /mnt/builds/slave/workspace/OSS-RxJava-release/rxjava-contrib/rxjava-debug/src/main/java/rx/plugins/DebugNotification.java:38: ';' expected sourceFunc = ((DebugHook.DebugOnSubscribe) sourceFunc).getActual(); ^ 5 errors FAILED ``` --- .../src/main/java/rx/plugins/DebugNotification.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rxjava-contrib/rxjava-debug/src/main/java/rx/plugins/DebugNotification.java b/rxjava-contrib/rxjava-debug/src/main/java/rx/plugins/DebugNotification.java index 1cc8eab752..f7e065b50c 100644 --- a/rxjava-contrib/rxjava-debug/src/main/java/rx/plugins/DebugNotification.java +++ b/rxjava-contrib/rxjava-debug/src/main/java/rx/plugins/DebugNotification.java @@ -35,7 +35,7 @@ public static DebugNotification createSubscribe(Observer o, o = ds.getActual(); } if (sourceFunc instanceof DebugHook.DebugOnSubscribe) { - sourceFunc = ((DebugHook.DebugOnSubscribe) sourceFunc).getActual(); + sourceFunc = (OnSubscribe) ((SafeSubscriber) sourceFunc).getActual(); } return new DebugNotification(o, from, Kind.Subscribe, null, null, to, source, sourceFunc); } From 509357b01009735aaaccda7fe29d08d3a21d4ff0 Mon Sep 17 00:00:00 2001 From: Bob T Builder Date: Thu, 6 Mar 2014 06:55:07 +0000 Subject: [PATCH 076/422] [Gradle Release Plugin] - pre tag commit: '0.17.0-RC7'. --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index f84383b779..75dc0f4000 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -version=0.17.0-RC7-SNAPSHOT +version=0.17.0-RC7 From 18916d14c32d6db33be372259aba5c3e799696d5 Mon Sep 17 00:00:00 2001 From: Bob T Builder Date: Thu, 6 Mar 2014 06:55:11 +0000 Subject: [PATCH 077/422] [Gradle Release Plugin] - new version commit: '0.17.0-RC8-SNAPSHOT'. --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 75dc0f4000..2e8a8bdd53 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -version=0.17.0-RC7 +version=0.17.0-RC8-SNAPSHOT From 1f47d1180fcda01ecbcc53084d523658c5651793 Mon Sep 17 00:00:00 2001 From: David Gross Date: Thu, 6 Mar 2014 09:59:40 -0800 Subject: [PATCH 078/422] startWith(observable) marble diagram --- rxjava-core/src/main/java/rx/Observable.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rxjava-core/src/main/java/rx/Observable.java b/rxjava-core/src/main/java/rx/Observable.java index ed0f8c1bb7..33d64089fa 100644 --- a/rxjava-core/src/main/java/rx/Observable.java +++ b/rxjava-core/src/main/java/rx/Observable.java @@ -6449,10 +6449,10 @@ public final Observable skipWhileWithIndex(Func2 } /** - * Returns an Observable that emits the items in a specified {@link Observable} before it begins to emit items - * emitted by the source Observable. + * Returns an Observable that emits the items in a specified {@link Observable} before it begins to emit + * items emitted by the source Observable. *

- * + * * * @param values * an Observable that contains the items you want the modified Observable to emit first From 940c26eb63d76f9cbedaaf89cd951b0a4d29347e Mon Sep 17 00:00:00 2001 From: David Gross Date: Fri, 7 Mar 2014 18:46:17 -0800 Subject: [PATCH 079/422] correcting markup error in javadoc comments --- rxjava-core/src/main/java/rx/Observable.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rxjava-core/src/main/java/rx/Observable.java b/rxjava-core/src/main/java/rx/Observable.java index 33d64089fa..42da02f62b 100644 --- a/rxjava-core/src/main/java/rx/Observable.java +++ b/rxjava-core/src/main/java/rx/Observable.java @@ -4101,7 +4101,7 @@ public final Observable delay( *

* *

- * Note: the resulting Observable will immediately propagate any {@code onError} notification + * Note: the resulting Observable will immediately propagate any {@code onError} notification * from the source Observable. * * @param From 0f19eb2225e9bb26abde3fb6d2378b69bb3e792c Mon Sep 17 00:00:00 2001 From: MarkVanDerVoort Date: Sat, 8 Mar 2014 22:17:59 +0100 Subject: [PATCH 080/422] OperationAll to OperatorAll --- rxjava-core/src/main/java/rx/Observable.java | 85 +---------------- .../main/java/rx/operators/OperationAll.java | 91 ------------------- .../main/java/rx/operators/OperatorAll.java | 58 ++++++++++++ ...ationAllTest.java => OperatorAllTest.java} | 50 ++++------ 4 files changed, 79 insertions(+), 205 deletions(-) delete mode 100644 rxjava-core/src/main/java/rx/operators/OperationAll.java create mode 100644 rxjava-core/src/main/java/rx/operators/OperatorAll.java rename rxjava-core/src/test/java/rx/operators/{OperationAllTest.java => OperatorAllTest.java} (60%) diff --git a/rxjava-core/src/main/java/rx/Observable.java b/rxjava-core/src/main/java/rx/Observable.java index 42da02f62b..06cf28cc9b 100644 --- a/rxjava-core/src/main/java/rx/Observable.java +++ b/rxjava-core/src/main/java/rx/Observable.java @@ -49,88 +49,7 @@ import rx.observables.ConnectableObservable; import rx.observables.GroupedObservable; import rx.observers.SafeSubscriber; -import rx.operators.OnSubscribeFromIterable; -import rx.operators.OnSubscribeRange; -import rx.operators.OperationAll; -import rx.operators.OperationAmb; -import rx.operators.OperationAny; -import rx.operators.OperationAsObservable; -import rx.operators.OperationAverage; -import rx.operators.OperationBuffer; -import rx.operators.OperationCache; -import rx.operators.OperationCombineLatest; -import rx.operators.OperationConcat; -import rx.operators.OperationDebounce; -import rx.operators.OperationDefaultIfEmpty; -import rx.operators.OperationDefer; -import rx.operators.OperationDelay; -import rx.operators.OperationDematerialize; -import rx.operators.OperationDistinct; -import rx.operators.OperationDistinctUntilChanged; -import rx.operators.OperationElementAt; -import rx.operators.OperationFinally; -import rx.operators.OperationFlatMap; -import rx.operators.OperationGroupByUntil; -import rx.operators.OperationGroupJoin; -import rx.operators.OperationInterval; -import rx.operators.OperationJoin; -import rx.operators.OperationJoinPatterns; -import rx.operators.OperationMaterialize; -import rx.operators.OperationMergeDelayError; -import rx.operators.OperationMergeMaxConcurrent; -import rx.operators.OperationMinMax; -import rx.operators.OperationMulticast; -import rx.operators.OperationOnErrorResumeNextViaObservable; -import rx.operators.OperationOnErrorReturn; -import rx.operators.OperationOnExceptionResumeNextViaObservable; -import rx.operators.OperationParallelMerge; -import rx.operators.OperationReplay; -import rx.operators.OperationSample; -import rx.operators.OperationSequenceEqual; -import rx.operators.OperationSingle; -import rx.operators.OperationSkip; -import rx.operators.OperationSkipLast; -import rx.operators.OperationSkipUntil; -import rx.operators.OperationSkipWhile; -import rx.operators.OperationSum; -import rx.operators.OperationSwitch; -import rx.operators.OperationSynchronize; -import rx.operators.OperationTakeLast; -import rx.operators.OperationTakeTimed; -import rx.operators.OperationTakeUntil; -import rx.operators.OperationTakeWhile; -import rx.operators.OperationThrottleFirst; -import rx.operators.OperationTimeInterval; -import rx.operators.OperationTimer; -import rx.operators.OperationToMap; -import rx.operators.OperationToMultimap; -import rx.operators.OperationToObservableFuture; -import rx.operators.OperationUsing; -import rx.operators.OperationWindow; -import rx.operators.OperatorCast; -import rx.operators.OperatorDoOnEach; -import rx.operators.OperatorFilter; -import rx.operators.OperatorGroupBy; -import rx.operators.OperatorMap; -import rx.operators.OperatorMerge; -import rx.operators.OperatorObserveOn; -import rx.operators.OperatorOnErrorFlatMap; -import rx.operators.OperatorOnErrorResumeNextViaFunction; -import rx.operators.OperatorParallel; -import rx.operators.OperatorRepeat; -import rx.operators.OperatorRetry; -import rx.operators.OperatorScan; -import rx.operators.OperatorSkip; -import rx.operators.OperatorSubscribeOn; -import rx.operators.OperatorTake; -import rx.operators.OperatorTimeout; -import rx.operators.OperatorTimeoutWithSelector; -import rx.operators.OperatorTimestamp; -import rx.operators.OperatorToObservableList; -import rx.operators.OperatorToObservableSortedList; -import rx.operators.OperatorUnsubscribeOn; -import rx.operators.OperatorZip; -import rx.operators.OperatorZipIterable; +import rx.operators.*; import rx.plugins.RxJavaObservableExecutionHook; import rx.plugins.RxJavaPlugins; import rx.schedulers.Schedulers; @@ -3461,7 +3380,7 @@ public final Observable aggregate(R initialValue, Func2 * @see RxJava Wiki: all() */ public final Observable all(Func1 predicate) { - return create(OperationAll.all(this, predicate)); + return lift(new OperatorAll(predicate)); } /** diff --git a/rxjava-core/src/main/java/rx/operators/OperationAll.java b/rxjava-core/src/main/java/rx/operators/OperationAll.java deleted file mode 100644 index 681486f13c..0000000000 --- a/rxjava-core/src/main/java/rx/operators/OperationAll.java +++ /dev/null @@ -1,91 +0,0 @@ -/** - * Copyright 2014 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package rx.operators; - -import java.util.concurrent.atomic.AtomicBoolean; - -import rx.Observable; -import rx.Observable.OnSubscribeFunc; -import rx.Observer; -import rx.Subscription; -import rx.functions.Func1; - -/** - * Returns an Observable that emits a Boolean that indicates whether all items emitted by an - * Observable satisfy a condition. - *

- * - */ -public class OperationAll { - - public static OnSubscribeFunc all(Observable sequence, Func1 predicate) { - return new AllObservable(sequence, predicate); - } - - private static class AllObservable implements OnSubscribeFunc { - private final Observable sequence; - private final Func1 predicate; - - private final SafeObservableSubscription subscription = new SafeObservableSubscription(); - - private AllObservable(Observable sequence, Func1 predicate) { - this.sequence = sequence; - this.predicate = predicate; - } - - @Override - public Subscription onSubscribe(final Observer observer) { - return subscription.wrap(sequence.subscribe(new AllObserver(observer))); - - } - - private class AllObserver implements Observer { - private final Observer underlying; - - private final AtomicBoolean status = new AtomicBoolean(true); - - public AllObserver(Observer underlying) { - this.underlying = underlying; - } - - @Override - public void onCompleted() { - if (status.get()) { - underlying.onNext(true); - underlying.onCompleted(); - } - } - - @Override - public void onError(Throwable e) { - underlying.onError(e); - } - - @Override - public void onNext(T args) { - boolean result = predicate.call(args); - boolean changed = status.compareAndSet(true, result); - - if (changed && !result) { - underlying.onNext(false); - underlying.onCompleted(); - subscription.unsubscribe(); - } - } - } - - } -} diff --git a/rxjava-core/src/main/java/rx/operators/OperatorAll.java b/rxjava-core/src/main/java/rx/operators/OperatorAll.java new file mode 100644 index 0000000000..b1b0535127 --- /dev/null +++ b/rxjava-core/src/main/java/rx/operators/OperatorAll.java @@ -0,0 +1,58 @@ +package rx.operators; + +import rx.Observable; +import rx.Subscriber; +import rx.exceptions.OnErrorThrowable; +import rx.functions.Func1; + +import java.util.concurrent.atomic.AtomicBoolean; + +import static rx.Observable.Operator; + +/** + * Returns an Observable that emits a Boolean that indicates whether all items emitted by an + * Observable satisfy a condition. + *

+ * + */ +public class OperatorAll implements Operator{ + + private final Func1 predicate; + + public OperatorAll(Func1 predicate) { + this.predicate = predicate; + } + + @Override + public Subscriber call(final Subscriber child) { + return new Subscriber() { + private AtomicBoolean status = new AtomicBoolean(true); + + @Override + public void onCompleted() { + child.onNext(status.get()); + child.onCompleted(); + } + + @Override + public void onError(Throwable e) { + child.onError(e); + } + + @Override + public void onNext(T t) { + try { + final Boolean result = predicate.call(t); + boolean changed = status.compareAndSet(true, result); + + if (changed && !result) { + child.onNext(false); + child.onCompleted(); + } + } catch (Throwable e) { + child.onError(OnErrorThrowable.addValueAsLastCause(e,t)); + } + } + }; + } +} diff --git a/rxjava-core/src/test/java/rx/operators/OperationAllTest.java b/rxjava-core/src/test/java/rx/operators/OperatorAllTest.java similarity index 60% rename from rxjava-core/src/test/java/rx/operators/OperationAllTest.java rename to rxjava-core/src/test/java/rx/operators/OperatorAllTest.java index e7940a0583..f97504f884 100644 --- a/rxjava-core/src/test/java/rx/operators/OperationAllTest.java +++ b/rxjava-core/src/test/java/rx/operators/OperatorAllTest.java @@ -16,28 +16,31 @@ package rx.operators; import static org.mockito.Mockito.*; -import static rx.operators.OperationAll.*; - import org.junit.Test; import rx.Observable; import rx.Observer; import rx.functions.Func1; +import rx.observers.TestSubscriber; + +import java.util.Arrays; + +public class OperatorAllTest { -public class OperationAllTest { + final Func1 hasLength3 = new Func1() { + @Override + public Boolean call(String s) { + return s.length() == 3; + } + }; @Test @SuppressWarnings("unchecked") public void testAll() { - Observable obs = Observable.from("one", "two", "six"); + Observable obs = Observable.from(Arrays.asList("one", "two", "six")).all(hasLength3); Observer observer = mock(Observer.class); - Observable.create(all(obs, new Func1() { - @Override - public Boolean call(String s) { - return s.length() == 3; - } - })).subscribe(observer); + obs.subscribe(new TestSubscriber(observer)); verify(observer).onNext(true); verify(observer).onCompleted(); @@ -47,15 +50,10 @@ public Boolean call(String s) { @Test @SuppressWarnings("unchecked") public void testNotAll() { - Observable obs = Observable.from("one", "two", "three", "six"); + Observable obs = Observable.from(Arrays.asList("one", "two", "three", "six")).all(hasLength3); Observer observer = mock(Observer.class); - Observable.create(all(obs, new Func1() { - @Override - public Boolean call(String s) { - return s.length() == 3; - } - })).subscribe(observer); + obs.subscribe(new TestSubscriber(observer)); verify(observer).onNext(false); verify(observer).onCompleted(); @@ -65,15 +63,10 @@ public Boolean call(String s) { @Test @SuppressWarnings("unchecked") public void testEmpty() { - Observable obs = Observable.empty(); + Observable obs = Observable.empty().all(hasLength3); Observer observer = mock(Observer.class); - Observable.create(all(obs, new Func1() { - @Override - public Boolean call(String s) { - return s.length() == 3; - } - })).subscribe(observer); + obs.subscribe(new TestSubscriber(observer)); verify(observer).onNext(true); verify(observer).onCompleted(); @@ -84,15 +77,10 @@ public Boolean call(String s) { @SuppressWarnings("unchecked") public void testError() { Throwable error = new Throwable(); - Observable obs = Observable.error(error); + Observable obs = Observable.error(error).all(hasLength3); Observer observer = mock(Observer.class); - Observable.create(all(obs, new Func1() { - @Override - public Boolean call(String s) { - return s.length() == 3; - } - })).subscribe(observer); + obs.subscribe(new TestSubscriber(observer)); verify(observer).onError(error); verifyNoMoreInteractions(observer); From a455b693ab2a34f9d74558582d02d78e582998a0 Mon Sep 17 00:00:00 2001 From: Acardiac Date: Sun, 9 Mar 2014 11:00:46 +0100 Subject: [PATCH 081/422] add support for eclipse pde --- rxjava-core/build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/rxjava-core/build.gradle b/rxjava-core/build.gradle index 8cd94496a6..00420d0f74 100644 --- a/rxjava-core/build.gradle +++ b/rxjava-core/build.gradle @@ -29,6 +29,7 @@ jar { instruction 'Bundle-Vendor', 'Netflix' instruction 'Bundle-DocURL', 'https://github.com/Netflix/RxJava' instruction 'Import-Package', '!org.junit,!junit.framework,!org.mockito.*,*' + instruction 'Eclipse-ExtensibleAPI', 'true' } } From a0ebbc77c22db31e7bada84dd6983be8ab3697ea Mon Sep 17 00:00:00 2001 From: David Gross Date: Sun, 9 Mar 2014 13:48:55 -0700 Subject: [PATCH 082/422] fixing a variety of errors in javadoc generation (syntax errors, misnamed params, etc.) --- rxjava-core/src/main/java/rx/Observable.java | 52 ++- rxjava-core/src/main/java/rx/Observer.java | 18 +- rxjava-core/src/main/java/rx/Scheduler.java | 53 +-- .../rx/observers/SynchronizedSubscriber.java | 2 +- .../java/rx/operators/OperationBuffer.java | 250 +++++++------- .../java/rx/operators/OperationWindow.java | 315 ++++++++++-------- .../main/java/rx/operators/OperatorScan.java | 40 +-- .../main/java/rx/operators/SafeObserver.java | 9 +- .../rx/plugins/RxJavaDefaultSchedulers.java | 13 +- .../RxJavaObservableExecutionHook.java | 67 ++-- .../java/rx/schedulers/ExecutorScheduler.java | 8 +- .../main/java/rx/schedulers/Schedulers.java | 17 +- .../java/rx/subjects/BehaviorSubject.java | 20 +- 13 files changed, 453 insertions(+), 411 deletions(-) diff --git a/rxjava-core/src/main/java/rx/Observable.java b/rxjava-core/src/main/java/rx/Observable.java index 42da02f62b..5bfb5ad278 100644 --- a/rxjava-core/src/main/java/rx/Observable.java +++ b/rxjava-core/src/main/java/rx/Observable.java @@ -259,7 +259,7 @@ public static interface OnSubscribeFunc extends Function { * observable.map(...).filter(...).take(5).lift(new ObserverA()).lift(new ObserverB(...)).subscribe() * } * - * @param bind + * @param lift * @return an Observable that emits values that are the result of applying the bind function to the values * of the current Observable */ @@ -1552,7 +1552,7 @@ public final static Observable from(T t1, T t2, T t3, T t4, T t5, T t6, T *

* * - * @param items + * @param t1 * the source Array * @param * the type of items in the Array and the type of items to be emitted by the resulting Observable @@ -1680,7 +1680,7 @@ public final static Observable just(T value, Scheduler scheduler) { * if the source is empty * @see RxJava Wiki: max() * @see MSDN: Observable.Max - * @deprecated Use rxjava-math module instead + * @deprecated use rxjava-math module instead */ public final static > Observable max(Observable source) { return OperationMinMax.max(source); @@ -2680,7 +2680,7 @@ public final static Observable switchDo(ObservableRxJava Wiki: switchOnNext() - * @see {@link #switchOnNext(Observable)} + * @see #switchOnNext(Observable) */ public final static Observable switchLatest(Observable> sequenceOfSequences) { return create(OperationSwitch.switchDo(sequenceOfSequences)); @@ -4308,7 +4308,7 @@ public final void onNext(T args) { *

* * - * @param observer + * @param onNotification * the action to invoke for each item emitted by the source Observable * @return the source Observable with the side-effecting behavior applied * @see RxJava Wiki: doOnEach() @@ -4503,7 +4503,7 @@ public final Observable elementAtOrDefault(int index, T defaultValue) { * @return an Observable that emits a Boolean that indicates whether any item emitted by the source * Observable satisfies the {@code predicate} * @see RxJava Wiki: exists() - * @see MSDN: Observable.Any Note: the description in this page was wrong at the time of this writing. + * @see MSDN: Observable.Any (Note: the description in this page was wrong at the time of this writing) */ public final Observable exists(Func1 predicate) { return create(OperationAny.exists(this, predicate)); @@ -4551,7 +4551,7 @@ public final Observable finallyDo(Action0 action) { * @return an Observable that emits only the very first item emitted by the source Observable, or raises an * {@code IllegalArgumentException} if the source Observable is empty * @see RxJava Wiki: first() - * @see MSDN: {@code Observable.firstAsync()} + * @see "MSDN: Observable.firstAsync()" */ public final Observable first() { return take(1).single(); @@ -4568,7 +4568,7 @@ public final Observable first() { * @return an Observable that emits only the very first item emitted by the source Observable that satisfies * the {@code predicate}, or raises an {@code IllegalArgumentException} if no such items are emitted * @see RxJava Wiki: first() - * @see MSDN: {@code Observable.firstAsync()} + * @see "MSDN: Observable.firstAsync()" */ public final Observable first(Func1 predicate) { return takeFirst(predicate).single(); @@ -4585,7 +4585,7 @@ public final Observable first(Func1 predicate) { * @return an Observable that emits only the very first item from the source, or a default item if the * source Observable completes without emitting any items * @see RxJava Wiki: firstOrDefault() - * @see MSDN: {@code Observable.firstOrDefaultAsync()} + * @see "MSDN: Observable.firstOrDefaultAsync()" */ public final Observable firstOrDefault(T defaultValue) { return take(1).singleOrDefault(defaultValue); @@ -4605,7 +4605,7 @@ public final Observable firstOrDefault(T defaultValue) { * @return an Observable that emits only the very first item emitted by the source Observable that satisfies * the {@code predicate}, or a default item if the source Observable emits no such items * @see RxJava Wiki: firstOrDefault() - * @see MSDN: {@code Observable.firstOrDefaultAsync()} + * @see "MSDN: Observable.firstOrDefaultAsync()" */ public final Observable firstOrDefault(T defaultValue, Func1 predicate) { return takeFirst(predicate).singleOrDefault(defaultValue); @@ -4784,7 +4784,7 @@ public final Observable join(Obser * @return an Observable that emits the last item from the source Observable or notifies observers of an * error * @see RxJava Wiki: last() - * @see MSDN: {@code Observable.lastAsync()} + * @see "MSDN: Observable.lastAsync()" */ public final Observable last() { return takeLast(1).single(); @@ -4803,7 +4803,7 @@ public final Observable last() { * @throws IllegalArgumentException * if no items that match the predicate are emitted by the source Observable * @see RxJava Wiki: last() - * @see MSDN: {@code Observable.lastAsync()} + * @see "MSDN: Observable.lastAsync()" */ public final Observable last(Func1 predicate) { return filter(predicate).takeLast(1).single(); @@ -4820,7 +4820,7 @@ public final Observable last(Func1 predicate) { * @return an Observable that emits only the last item emitted by the source Observable, or a default item * if the source Observable is empty * @see RxJava Wiki: lastOrDefault() - * @see MSDN: {@code Observable.lastOrDefaultAsync()} + * @see "MSDN: Observable.lastOrDefaultAsync()" */ public final Observable lastOrDefault(T defaultValue) { return takeLast(1).singleOrDefault(defaultValue); @@ -4840,7 +4840,7 @@ public final Observable lastOrDefault(T defaultValue) { * @return an Observable that emits only the last item emitted by the source Observable that satisfies the * given condition, or a default item if no such item is emitted by the source Observable * @see RxJava Wiki: lastOrDefault() - * @see MSDN: {@code Observable.lastOrDefaultAsync()} + * @see "MSDN: Observable.lastOrDefaultAsync()" */ public final Observable lastOrDefault(T defaultValue, Func1 predicate) { return filter(predicate).takeLast(1).singleOrDefault(defaultValue); @@ -5355,7 +5355,7 @@ public final Observable onExceptionResumeNext(final Observable r * @param f * a {@link Func1} that applies Observable Observers to {@code Observable} in parallel and * returns an {@code Observable} - * @return an Observable that emits the results of applying {@link f} to the items emitted by the source + * @return an Observable that emits the results of applying {@code f} to the items emitted by the source * Observable * @see RxJava Wiki: parallel() */ @@ -5376,7 +5376,7 @@ public final Observable parallel(Func1, Observable> f) { * returns an {@code Observable} * @param s * a {@link Scheduler} to perform the work on - * @return an Observable that emits the results of applying {@link f} to the items emitted by the source + * @return an Observable that emits the results of applying {@code f} to the items emitted by the source * Observable * @see RxJava Wiki: parallel() */ @@ -5450,7 +5450,7 @@ public final Subject call() { * @param initialValue * the initial value of the underlying {@link BehaviorSubject} * @return an Observable that emits {@code initialValue} followed by the results of invoking the selector - * on a {@ConnectableObservable} that shares a single subscription to the underlying Observable + * on a {@link ConnectableObservable} that shares a single subscription to the underlying Observable */ public final Observable publish(Func1, ? extends Observable> selector, final T initialValue) { return multicast(new Func0>() { @@ -6208,7 +6208,7 @@ public final Observable scan(R initialValue, Func2 accum * @throws IllegalArgumentException * if the source emits more than one item or no items * @see RxJava Wiki: single() - * @see MSDN: {@code Observable.singleAsync()} + * @see "MSDN: Observable.singleAsync()" */ public final Observable single() { return create(OperationSingle. single(this)); @@ -6229,7 +6229,7 @@ public final Observable single() { * if the source Observable emits either more than one item that matches the predicate or no * items that match the predicate * @see RxJava Wiki: single() - * @see MSDN: {@code Observable.singleAsync()} + * @see "MSDN: Observable.singleAsync()" */ public final Observable single(Func1 predicate) { return filter(predicate).single(); @@ -6249,7 +6249,7 @@ public final Observable single(Func1 predicate) { * @throws IllegalArgumentException * if the source Observable emits more than one item * @see RxJava Wiki: single() - * @see MSDN: {@code Observable.singleOrDefaultAsync()} + * @see "MSDN: Observable.singleOrDefaultAsync()" */ public final Observable singleOrDefault(T defaultValue) { return create(OperationSingle. singleOrDefault(this, defaultValue)); @@ -6272,7 +6272,7 @@ public final Observable singleOrDefault(T defaultValue) { * @throws IllegalArgumentException * if the source Observable emits more than one item that matches the predicate * @see RxJava Wiki: single() - * @see MSDN: {@code Observable.singleOrDefaultAsync()} + * @see "MSDN: Observable.singleOrDefaultAsync()" */ public final Observable singleOrDefault(T defaultValue, Func1 predicate) { return filter(predicate).singleOrDefault(defaultValue); @@ -7140,8 +7140,7 @@ public final Subscription subscribe(Subscriber observer, Scheduler sc } /** - * Asynchronously subscribes Observers to this Observable on the specified - * {@link Scheduler}. + * Asynchronously subscribes Observers to this Observable on the specified {@link Scheduler}. *

* * @@ -7150,7 +7149,6 @@ public final Subscription subscribe(Subscriber observer, Scheduler sc * @return the source Observable modified so that its subscriptions happen on the * specified {@link Scheduler} * @see RxJava Wiki: subscribeOn() - * @see #subscribeOn(rx.Scheduler, int) */ public final Observable subscribeOn(Scheduler scheduler) { return nest().lift(new OperatorSubscribeOn(scheduler)); @@ -7354,7 +7352,7 @@ public final Observable take(long time, TimeUnit unit, Scheduler scheduler) { * @return an Observable that emits only the very first item emitted by the source Observable, or an empty * Observable if the source Observable completes without emitting a single item * @see RxJava Wiki: first() - * @see MSDN: {@code Observable.firstAsync()} + * @see "MSDN: Observable.firstAsync()" * @deprecated use {@code take(1)} directly */ @Deprecated @@ -7374,7 +7372,7 @@ public final Observable takeFirst() { * the given condition, or that completes without emitting anything if the source Observable * completes without emitting a single condition-satisfying item * @see RxJava Wiki: first() - * @see MSDN: {@code Observable.firstAsync()} + * @see "MSDN: Observable.firstAsync()" */ public final Observable takeFirst(Func1 predicate) { return filter(predicate).take(1); @@ -7638,7 +7636,7 @@ public final Observable takeWhileWithIndex(final Func2 - * After an Observer calls an {@link Observable}'s Observable.subscribe method, the {@link Observable} calls the - * Observer's onNext method to provide notifications. A well-behaved {@link Observable} will call an Observer's - * onCompleted closure exactly once or the Observer's onError closure exactly once. + * After an Observer calls an {@link Observable}'s Observable.subscribe method, the + * {@link Observable} calls the Observer's onNext method to provide notifications. A well-behaved + * {@link Observable} will call an Observer's onCompleted closure exactly once or the Observer's + * onError closure exactly once. *

* For more information see the RxJava Wiki * @@ -38,7 +39,8 @@ public interface Observer { /** * Notifies the Observer that the {@link Observable} has experienced an error condition. *

- * If the {@link Observable} calls this closure, it will not thereafter call onNext or onCompleted. + * If the {@link Observable} calls this closure, it will not thereafter call onNext or + * onCompleted. * * @param e */ @@ -47,11 +49,13 @@ public interface Observer { /** * Provides the Observer with new data. *

- * The {@link Observable} calls this closure 1 or more times, unless it calls onError in which case this closure may never be called. + * The {@link Observable} calls this closure 1 or more times, unless it calls onError in which + * case this closure may never be called. *

- * The {@link Observable} will not call this closure again after it calls either onCompleted or onError. + * The {@link Observable} will not call this closure again after it calls either onCompleted or + * onError. * - * @param args + * @param t */ public abstract void onNext(T t); diff --git a/rxjava-core/src/main/java/rx/Scheduler.java b/rxjava-core/src/main/java/rx/Scheduler.java index 21d4791f44..8203ec567b 100644 --- a/rxjava-core/src/main/java/rx/Scheduler.java +++ b/rxjava-core/src/main/java/rx/Scheduler.java @@ -31,11 +31,15 @@ * Why is this an abstract class instead of an interface? *

*

    - *
  1. Java doesn't support extension methods and there are many overload methods needing default implementations.
  2. - *
  3. Virtual extension methods aren't available until Java8 which RxJava will not set as a minimum target for a long time.
  4. - *
  5. If only an interface were used Scheduler implementations would then need to extend from an AbstractScheduler pair that gives all of the functionality unless they intend on copy/pasting the - * functionality.
  6. - *
  7. Without virtual extension methods even additive changes are breaking and thus severely impede library maintenance.
  8. + *
  9. Java doesn't support extension methods and there are many overload methods needing default + * implementations.
  10. + *
  11. Virtual extension methods aren't available until Java8 which RxJava will not set as a minimum target for + * a long time.
  12. + *
  13. If only an interface were used Scheduler implementations would then need to extend from an + * AbstractScheduler pair that gives all of the functionality unless they intend on copy/pasting the + * functionality.
  14. + *
  15. Without virtual extension methods even additive changes are breaking and thus severely impede library + * maintenance.
  16. *
*/ public abstract class Scheduler { @@ -44,38 +48,40 @@ public abstract class Scheduler { * Schedules an Action on a new Scheduler instance (typically another thread) for execution. * * @param action - * Action to schedule. - * @return a subscription to be able to unsubscribe from action. + * Action to schedule + * @return a subscription to be able to unsubscribe from action */ public abstract Subscription schedule(Action1 action); /** - * Schedules an Action on a new Scheduler instance (typically another thread) for execution at some point in the future. + * Schedules an Action on a new Scheduler instance (typically another thread) for execution at some point + * in the future. * * @param action + * the Action to schedule * @param delayTime + * time to wait before executing the action * @param unit - * @return + * the time unit the delay time is given in + * @return a subscription to be able to unsubscribe from action */ public abstract Subscription schedule(final Action1 action, final long delayTime, final TimeUnit unit); /** - * Schedules a cancelable action to be executed periodically. - * This default implementation schedules recursively and waits for actions to complete (instead of potentially executing - * long-running actions concurrently). Each scheduler that can do periodic scheduling in a better way should override this. + * Schedules a cancelable action to be executed periodically. This default implementation schedules + * recursively and waits for actions to complete (instead of potentially executing long-running actions + * concurrently). Each scheduler that can do periodic scheduling in a better way should override this. * - * @param state - * State to pass into the action. * @param action - * The action to execute periodically. + * the Action to execute periodically * @param initialDelay - * Time to wait before executing the action for the first time. + * time to wait before executing the action for the first time * @param period - * The time interval to wait each time in between executing the action. + * the time interval to wait each time in between executing the action * @param unit - * The time unit the interval above is given in. - * @return A subscription to be able to unsubscribe from action. + * the time unit the interval above is given in + * @return a subscription to be able to unsubscribe from action */ public Subscription schedulePeriodically(final Action1 action, long initialDelay, long period, TimeUnit unit) { final long periodInNanos = unit.toNanos(period); @@ -151,9 +157,9 @@ public abstract static class Inner implements Subscription { * Schedules an action to be executed in delayTime. * * @param delayTime - * Time the action is to be delayed before executing. + * time the action is to be delayed before executing * @param unit - * Time unit of the delay time. + * time unit of the delay time */ public abstract void schedule(Action1 action, long delayTime, TimeUnit unit); @@ -174,9 +180,10 @@ public long now() { /** * Parallelism available to a Scheduler. *

- * This defaults to {@code Runtime.getRuntime().availableProcessors()} but can be overridden for use cases such as scheduling work on a computer cluster. + * This defaults to {@code Runtime.getRuntime().availableProcessors()} but can be overridden for use cases + * such as scheduling work on a computer cluster. * - * @return the scheduler's available degree of parallelism. + * @return the scheduler's available degree of parallelism */ public int degreeOfParallelism() { return Runtime.getRuntime().availableProcessors(); diff --git a/rxjava-core/src/main/java/rx/observers/SynchronizedSubscriber.java b/rxjava-core/src/main/java/rx/observers/SynchronizedSubscriber.java index 8c01f506e1..8dbf1b20f5 100644 --- a/rxjava-core/src/main/java/rx/observers/SynchronizedSubscriber.java +++ b/rxjava-core/src/main/java/rx/observers/SynchronizedSubscriber.java @@ -42,7 +42,7 @@ public SynchronizedSubscriber(Subscriber subscriber, Object lock) { /** * Used when synchronizing an Subscriber without access to the subscription. * - * @param Observer + * @param subscriber */ public SynchronizedSubscriber(Subscriber subscriber) { this(subscriber, new Object()); diff --git a/rxjava-core/src/main/java/rx/operators/OperationBuffer.java b/rxjava-core/src/main/java/rx/operators/OperationBuffer.java index b3dd33400b..d4181504ba 100644 --- a/rxjava-core/src/main/java/rx/operators/OperationBuffer.java +++ b/rxjava-core/src/main/java/rx/operators/OperationBuffer.java @@ -42,24 +42,25 @@ public Buffer call() { } /** - *

This method creates a {@link Func1} object which represents the buffer operation. This operation takes - * values from the specified {@link Observable} source and stores them in a buffer until the {@link Observable} constructed using the {@link Func0} argument, produces a - * value. The buffer is then - * emitted, and a new buffer is created to replace it. A new {@link Observable} will be constructed using the - * provided {@link Func0} object, which will determine when this new buffer is emitted. When the source {@link Observable} completes or produces an error, the current buffer is emitted, and the - * event is propagated - * to all subscribed {@link Observer}s.

- * - *

Note that this operation only produces non-overlapping chunks. At all times there is - * exactly one buffer actively storing values.

+ * This method creates a {@link Func1} object which represents the buffer operation. This operation takes + * values from the specified {@link Observable} source and stores them in a buffer until the + * {@link Observable} constructed using the {@link Func0} argument, produces a value. The buffer is then + * emitted, and a new buffer is created to replace it. A new {@link Observable} will be constructed using + * the provided {@link Func0} object, which will determine when this new buffer is emitted. When the source + * {@link Observable} completes or produces an error, the current buffer is emitted, and the event is + * propagated to all subscribed {@link Observer}s. + *

+ * Note that this operation only produces non-overlapping chunks. At all times there is + * exactly one buffer actively storing values. + *

* * @param source - * The {@link Observable} which produces values. + * the {@link Observable} which produces values * @param bufferClosingSelector - * A {@link Func0} object which produces {@link Observable}s. These {@link Observable}s determine when a buffer is emitted and replaced by simply - * producing an object. + * a {@link Func0} object which produces {@link Observable}s. These {@link Observable}s determine + * when a buffer is emitted and replaced by simply producing an object. * @return - * the {@link Func1} object representing the specified buffer operation. + * the {@link Func1} object representing the specified buffer operation */ public static OnSubscribeFunc> buffer(final Observable source, final Func0> bufferClosingSelector) { return new OnSubscribeFunc>() { @@ -76,29 +77,31 @@ public Subscription onSubscribe(Observer> observer) { } /** - *

This method creates a {@link Func1} object which represents the buffer operation. This operation takes - * values from the specified {@link Observable} source and stores them in the currently active chunks. Initially - * there are no chunks active.

- * - *

Chunks can be created by pushing a {@link rx.util.Opening} value to the "bufferOpenings" {@link Observable}. - * This creates a new buffer which will then start recording values which are produced by the "source" {@link Observable}. Additionally the "bufferClosingSelector" will be used to construct an - * {@link Observable} which can produce values. When it does so it will close this (and only this) newly created - * buffer. When the source {@link Observable} completes or produces an error, all chunks are emitted, and the - * event is propagated to all subscribed {@link Observer}s.

- * - *

Note that when using this operation multiple overlapping chunks - * could be active at any one point.

+ * This method creates a {@link Func1} object which represents the buffer operation. This operation takes + * values from the specified {@link Observable} source and stores them in the currently active chunks. + * Initially there are no chunks active. + *

+ * Chunks can be created by pushing a {@link rx.util.TOpening} value to the "bufferOpenings" + * {@link Observable}. This creates a new buffer which will then start recording values which are produced + * by the "source" {@link Observable}. Additionally the "bufferClosingSelector" will be used to construct an + * {@link Observable} which can produce values. When it does so it will close this (and only this) newly + * created buffer. When the source {@link Observable} completes or produces an error, all chunks are + * emitted, and the event is propagated to all subscribed {@link Observer}s. + *

+ * Note that when using this operation multiple overlapping chunks could be active at any + * one point. + *

* * @param source - * The {@link Observable} which produces values. + * the {@link Observable} which produces values * @param bufferOpenings - * An {@link Observable} which when it produces a {@link rx.util.Opening} value will - * create a new buffer which instantly starts recording the "source" {@link Observable}. + * an {@link Observable} which when it produces a {@link rx.util.TOpening} value will create a + * new buffer which instantly starts recording the "source" {@link Observable} * @param bufferClosingSelector - * A {@link Func0} object which produces {@link Observable}s. These {@link Observable}s determine when a buffer is emitted and replaced by simply - * producing an object. + * a {@link Func0} object which produces {@link Observable}s. These {@link Observable}s determine + * when a buffer is emitted and replaced by simply producing an object. * @return - * the {@link Func1} object representing the specified buffer operation. + * the {@link Func1} object representing the specified buffer operation */ public static OnSubscribeFunc> buffer(final Observable source, final Observable bufferOpenings, final Func1> bufferClosingSelector) { return new OnSubscribeFunc>() { @@ -114,48 +117,50 @@ public Subscription onSubscribe(final Observer> observer) { } /** - *

This method creates a {@link Func1} object which represents the buffer operation. This operation takes + * This method creates a {@link Func1} object which represents the buffer operation. This operation takes * values from the specified {@link Observable} source and stores them in a buffer until the buffer contains * a specified number of elements. The buffer is then emitted, and a new buffer is created to replace it. * When the source {@link Observable} completes or produces an error, the current buffer is emitted, and - * the event is propagated to all subscribed {@link Observer}s.

- * - *

Note that this operation only produces non-overlapping chunks. At all times there is - * exactly one buffer actively storing values.

+ * the event is propagated to all subscribed {@link Observer}s. + *

+ * Note that this operation only produces non-overlapping chunks. At all times there is + * exactly one buffer actively storing values. + *

* * @param source - * The {@link Observable} which produces values. + * the {@link Observable} which produces values * @param count - * The number of elements a buffer should have before being emitted and replaced. + * the number of elements a buffer should have before being emitted and replaced * @return - * the {@link Func1} object representing the specified buffer operation. + * the {@link Func1} object representing the specified buffer operation */ public static OnSubscribeFunc> buffer(Observable source, int count) { return buffer(source, count, count); } /** - *

This method creates a {@link Func1} object which represents the buffer operation. This operation takes + * This method creates a {@link Func1} object which represents the buffer operation. This operation takes * values from the specified {@link Observable} source and stores them in all active chunks until the buffer * contains a specified number of elements. The buffer is then emitted. Chunks are created after a certain - * amount of values have been received. When the source {@link Observable} completes or produces an error, the - * currently active chunks are emitted, and the event is propagated to all subscribed {@link Observer}s.

- * - *

Note that this operation can produce non-connected, connected non-overlapping, or overlapping - * chunks depending on the input parameters.

+ * amount of values have been received. When the source {@link Observable} completes or produces an error, + * the currently active chunks are emitted, and the event is propagated to all subscribed {@link Observer}s. + *

+ * Note that this operation can produce non-connected, connected non-overlapping, or overlapping + * chunks depending on the input parameters. + *

* * @param source - * The {@link Observable} which produces values. + * the {@link Observable} which produces values * @param count - * The number of elements a buffer should have before being emitted. + * the number of elements a buffer should have before being emitted * @param skip - * The interval with which chunks have to be created. Note that when "skip" == "count" - * that this is the same as calling {@link OperationBuffer#buffer(Observable, int)}. - * If "skip" < "count", this buffer operation will produce overlapping chunks and if "skip" - * > "count" non-overlapping chunks will be created and some values will not be pushed + * the interval with which chunks have to be created. Note that when {@code skip == count} that + * this is the same as calling {@link OperationBuffer#buffer(Observable, int)}. If + * {@code skip < count}, this buffer operation will produce overlapping chunks and if + * {@code skip > count} non-overlapping chunks will be created and some values will not be pushed * into a buffer at all! * @return - * the {@link Func1} object representing the specified buffer operation. + * the {@link Func1} object representing the specified buffer operation */ public static OnSubscribeFunc> buffer(final Observable source, final int count, final int skip) { return new OnSubscribeFunc>() { @@ -171,48 +176,50 @@ public Subscription onSubscribe(final Observer> observer) { } /** - *

This method creates a {@link Func1} object which represents the buffer operation. This operation takes + * This method creates a {@link Func1} object which represents the buffer operation. This operation takes * values from the specified {@link Observable} source and stores them in a buffer. Periodically the buffer * is emitted and replaced with a new buffer. How often this is done depends on the specified timespan. * When the source {@link Observable} completes or produces an error, the current buffer is emitted, and - * the event is propagated to all subscribed {@link Observer}s.

- * - *

Note that this operation only produces non-overlapping chunks. At all times there is - * exactly one buffer actively storing values.

+ * the event is propagated to all subscribed {@link Observer}s. + *

+ * Note that this operation only produces non-overlapping chunks. At all times there is + * exactly one buffer actively storing values. + *

* * @param source - * The {@link Observable} which produces values. + * the {@link Observable} which produces values * @param timespan - * The amount of time all chunks must be actively collect values before being emitted. + * the amount of time all chunks must be actively collect values before being emitted * @param unit - * The {@link TimeUnit} defining the unit of time for the timespan. + * the {@link TimeUnit} defining the unit of time for the timespan * @return - * the {@link Func1} object representing the specified buffer operation. + * the {@link Func1} object representing the specified buffer operation */ public static OnSubscribeFunc> buffer(Observable source, long timespan, TimeUnit unit) { return buffer(source, timespan, unit, Schedulers.threadPoolForComputation()); } /** - *

This method creates a {@link Func1} object which represents the buffer operation. This operation takes + * This method creates a {@link Func1} object which represents the buffer operation. This operation takes * values from the specified {@link Observable} source and stores them in a buffer. Periodically the buffer * is emitted and replaced with a new buffer. How often this is done depends on the specified timespan. * When the source {@link Observable} completes or produces an error, the current buffer is emitted, and - * the event is propagated to all subscribed {@link Observer}s.

- * - *

Note that this operation only produces non-overlapping chunks. At all times there is - * exactly one buffer actively storing values.

+ * the event is propagated to all subscribed {@link Observer}s. + *

+ * Note that this operation only produces non-overlapping chunks. At all times there is + * exactly one buffer actively storing values. + *

* * @param source - * The {@link Observable} which produces values. + * the {@link Observable} which produces values * @param timespan - * The amount of time all chunks must be actively collect values before being emitted. + * the amount of time all chunks must be actively collect values before being emitted * @param unit - * The {@link TimeUnit} defining the unit of time for the timespan. + * the {@link TimeUnit} defining the unit of time for the timespan * @param scheduler - * The {@link Scheduler} to use for timing chunks. + * the {@link Scheduler} to use for timing chunks * @return - * the {@link Func1} object representing the specified buffer operation. + * the {@link Func1} object representing the specified buffer operation */ public static OnSubscribeFunc> buffer(final Observable source, final long timespan, final TimeUnit unit, final Scheduler scheduler) { return new OnSubscribeFunc>() { @@ -228,54 +235,56 @@ public Subscription onSubscribe(final Observer> observer) { } /** - *

This method creates a {@link Func1} object which represents the buffer operation. This operation takes + * This method creates a {@link Func1} object which represents the buffer operation. This operation takes * values from the specified {@link Observable} source and stores them in a buffer. Periodically the buffer * is emitted and replaced with a new buffer. How often this is done depends on the specified timespan. * Additionally the buffer is automatically emitted once it reaches a specified number of elements. * When the source {@link Observable} completes or produces an error, the current buffer is emitted, and - * the event is propagated to all subscribed {@link Observer}s.

- * - *

Note that this operation only produces non-overlapping chunks. At all times there is - * exactly one buffer actively storing values.

+ * the event is propagated to all subscribed {@link Observer}s. + *

+ * Note that this operation only produces non-overlapping chunks. At all times there is + * exactly one buffer actively storing values. + *

* * @param source - * The {@link Observable} which produces values. + * the {@link Observable} which produces values * @param timespan - * The amount of time all chunks must be actively collect values before being emitted. + * the amount of time all chunks must be actively collect values before being emitted * @param unit - * The {@link TimeUnit} defining the unit of time for the timespan. + * the {@link TimeUnit} defining the unit of time for the timespan * @param count - * The maximum size of the buffer. Once a buffer reaches this size, it is emitted. + * the maximum size of the buffer. Once a buffer reaches this size, it is emitted * @return - * the {@link Func1} object representing the specified buffer operation. + * the {@link Func1} object representing the specified buffer operation */ public static OnSubscribeFunc> buffer(Observable source, long timespan, TimeUnit unit, int count) { return buffer(source, timespan, unit, count, Schedulers.threadPoolForComputation()); } /** - *

This method creates a {@link Func1} object which represents the buffer operation. This operation takes + * This method creates a {@link Func1} object which represents the buffer operation. This operation takes * values from the specified {@link Observable} source and stores them in a buffer. Periodically the buffer * is emitted and replaced with a new buffer. How often this is done depends on the specified timespan. * Additionally the buffer is automatically emitted once it reaches a specified number of elements. * When the source {@link Observable} completes or produces an error, the current buffer is emitted, and - * the event is propagated to all subscribed {@link Observer}s.

- * - *

Note that this operation only produces non-overlapping chunks. At all times there is - * exactly one buffer actively storing values.

+ * the event is propagated to all subscribed {@link Observer}s. + *

+ * Note that this operation only produces non-overlapping chunks. At all times there is + * exactly one buffer actively storing values. + *

* * @param source - * The {@link Observable} which produces values. + * the {@link Observable} which produces values * @param timespan - * The amount of time all chunks must be actively collect values before being emitted. + * the amount of time all chunks must be actively collect values before being emitted * @param unit - * The {@link TimeUnit} defining the unit of time for the timespan. + * the {@link TimeUnit} defining the unit of time for the timespan * @param count - * The maximum size of the buffer. Once a buffer reaches this size, it is emitted. + * the maximum size of the buffer. Once a buffer reaches this size, it is emitted * @param scheduler - * The {@link Scheduler} to use for timing chunks. + * the {@link Scheduler} to use for timing chunks * @return - * the {@link Func1} object representing the specified buffer operation. + * the {@link Func1} object representing the specified buffer operation */ public static OnSubscribeFunc> buffer(final Observable source, final long timespan, final TimeUnit unit, final int count, final Scheduler scheduler) { return new OnSubscribeFunc>() { @@ -292,54 +301,56 @@ public Subscription onSubscribe(final Observer> observer) { } /** - *

This method creates a {@link Func1} object which represents the buffer operation. This operation takes + * This method creates a {@link Func1} object which represents the buffer operation. This operation takes * values from the specified {@link Observable} source and stores them in a buffer. Periodically the buffer * is emitted and replaced with a new buffer. How often this is done depends on the specified timespan. * The creation of chunks is also periodical. How often this is done depends on the specified timeshift. * When the source {@link Observable} completes or produces an error, the current buffer is emitted, and - * the event is propagated to all subscribed {@link Observer}s.

- * - *

Note that this operation can produce non-connected, or overlapping chunks depending - * on the input parameters.

+ * the event is propagated to all subscribed {@link Observer}s. + *

+ * Note that this operation can produce non-connected, or overlapping chunks depending + * on the input parameters. + *

* * @param source - * The {@link Observable} which produces values. + * the {@link Observable} which produces values * @param timespan - * The amount of time all chunks must be actively collect values before being emitted. + * the amount of time all chunks must be actively collect values before being emitted * @param timeshift - * The amount of time between creating chunks. + * the amount of time between creating chunks * @param unit - * The {@link TimeUnit} defining the unit of time for the timespan. + * the {@link TimeUnit} defining the unit of time for the timespan * @return - * the {@link Func1} object representing the specified buffer operation. + * the {@link Func1} object representing the specified buffer operation */ public static OnSubscribeFunc> buffer(Observable source, long timespan, long timeshift, TimeUnit unit) { return buffer(source, timespan, timeshift, unit, Schedulers.threadPoolForComputation()); } /** - *

This method creates a {@link Func1} object which represents the buffer operation. This operation takes + * This method creates a {@link Func1} object which represents the buffer operation. This operation takes * values from the specified {@link Observable} source and stores them in a buffer. Periodically the buffer * is emitted and replaced with a new buffer. How often this is done depends on the specified timespan. * The creation of chunks is also periodical. How often this is done depends on the specified timeshift. * When the source {@link Observable} completes or produces an error, the current buffer is emitted, and - * the event is propagated to all subscribed {@link Observer}s.

- * - *

Note that this operation can produce non-connected, or overlapping chunks depending - * on the input parameters.

+ * the event is propagated to all subscribed {@link Observer}s. + *

+ * Note that this operation can produce non-connected, or overlapping chunks depending + * on the input parameters. + *

* * @param source - * The {@link Observable} which produces values. + * the {@link Observable} which produces values * @param timespan - * The amount of time all chunks must be actively collect values before being emitted. + * the amount of time all chunks must be actively collect values before being emitted * @param timeshift - * The amount of time between creating chunks. + * the amount of time between creating chunks * @param unit - * The {@link TimeUnit} defining the unit of time for the timespan. + * the {@link TimeUnit} defining the unit of time for the timespan * @param scheduler - * The {@link Scheduler} to use for timing chunks. + * the {@link Scheduler} to use for timing chunks * @return - * the {@link Func1} object representing the specified buffer operation. + * the {@link Func1} object representing the specified buffer operation */ public static OnSubscribeFunc> buffer(final Observable source, final long timespan, final long timeshift, final TimeUnit unit, final Scheduler scheduler) { return new OnSubscribeFunc>() { @@ -359,7 +370,7 @@ public Subscription onSubscribe(final Observer> observer) { * This class represents a single buffer: A sequence of recorded values. * * @param - * The type of objects which this {@link Buffer} can hold. + * the type of objects which this {@link Buffer} can hold */ protected static class Buffer extends Chunk> { /** @@ -402,6 +413,10 @@ public boolean isUnsubscribed() { /** * Create a buffer operator with the given observable sequence as the buffer boundary. + * + * @param source + * @param boundary + * @return */ public static OnSubscribeFunc> bufferWithBoundaryObservable(Observable source, Observable boundary) { return new BufferWithObservableBoundary(source, boundary, 16); @@ -410,6 +425,11 @@ public static OnSubscribeFunc> bufferWithBoundaryObservable(Obser /** * Create a buffer operator with the given observable sequence as the buffer boundary and * with the given initial capacity for buffers. + * + * @param source + * @param boundary + * @param initialCapacity + * @return */ public static OnSubscribeFunc> bufferWithBoundaryObservable(Observable source, Observable boundary, int initialCapacity) { if (initialCapacity <= 0) { diff --git a/rxjava-core/src/main/java/rx/operators/OperationWindow.java b/rxjava-core/src/main/java/rx/operators/OperationWindow.java index c0d96f3a79..5b170c56a0 100644 --- a/rxjava-core/src/main/java/rx/operators/OperationWindow.java +++ b/rxjava-core/src/main/java/rx/operators/OperationWindow.java @@ -42,24 +42,26 @@ public Window call() { } /** - *

This method creates a {@link rx.functions.Func1} object which represents the window operation. This operation takes - * values from the specified {@link rx.Observable} source and stores them in a window until the {@link rx.Observable} constructed using the {@link rx.functions.Func0} argument, produces a - * value. The window is then - * emitted, and a new window is created to replace it. A new {@link rx.Observable} will be constructed using the - * provided {@link rx.functions.Func0} object, which will determine when this new window is emitted. When the source {@link rx.Observable} completes or produces an error, the current window - * is emitted, and the event is propagated - * to all subscribed {@link rx.Observer}s.

- * - *

Note that this operation only produces non-overlapping windows. At all times there is - * exactly one window actively storing values.

+ * This method creates a {@link rx.functions.Func1} object which represents the window operation. This + * operation takes values from the specified {@link rx.Observable} source and stores them in a window until + * the {@link rx.Observable} constructed using the {@link rx.functions.Func0} argument, produces a value. + * The window is then emitted, and a new window is created to replace it. A new {@link rx.Observable} will + * be constructed using the provided {@link rx.functions.Func0} object, which will determine when this new + * window is emitted. When the source {@link rx.Observable} completes or produces an error, the current + * window is emitted, and the event is propagated to all subscribed {@link rx.Observer}s. + *

+ * Note that this operation only produces non-overlapping windows. At all times there is + * exactly one window actively storing values. + *

* * @param source - * The {@link rx.Observable} which produces values. + * the {@link rx.Observable} which produces values * @param windowClosingSelector - * A {@link rx.functions.Func0} object which produces {@link rx.Observable}s. These {@link rx.Observable}s determine when a window is emitted and replaced by simply + * a {@link rx.functions.Func0} object that produces {@link rx.Observable}s. These + * {@link rx.Observable}s determine when a window is emitted and replaced by simply * producing an object. * @return - * the {@link rx.functions.Func1} object representing the specified window operation. + * the {@link rx.functions.Func1} object representing the specified window operation */ public static OnSubscribeFunc> window(final Observable source, final Func0> windowClosingSelector) { return new OnSubscribeFunc>() { @@ -74,29 +76,33 @@ public Subscription onSubscribe(final Observer> observer) } /** - *

This method creates a {@link rx.functions.Func1} object which represents the window operation. This operation takes - * values from the specified {@link rx.Observable} source and stores them in the currently active window. Initially - * there are no windows active.

- * - *

Windows can be created by pushing a {@link rx.util.Opening} value to the "windowOpenings" {@link rx.Observable}. - * This creates a new window which will then start recording values which are produced by the "source" {@link rx.Observable}. Additionally the "windowClosingSelector" will be used to construct an - * {@link rx.Observable} which can produce values. When it does so it will close this (and only this) newly created - * window. When the source {@link rx.Observable} completes or produces an error, all windows are emitted, and the - * event is propagated to all subscribed {@link rx.Observer}s.

- * - *

Note that when using this operation multiple overlapping windows - * could be active at any one point.

+ * This method creates a {@link rx.functions.Func1} object which represents the window operation. This + * operation takes values from the specified {@link rx.Observable} source and stores them in the currently + * active window. Initially there are no windows active. + *

+ * Windows can be created by pushing a {@link rx.util.TOpening} value to the {@code windowOpenings} + * {@link rx.Observable}. This creates a new window which will then start recording values which are + * produced by the {@code source} {@link rx.Observable}. Additionally the {@code windowClosingSelector} + * will be used to construct an {@link rx.Observable} which can produce values. When it does so it will + * close this (and only this) newly created window. When the source {@link rx.Observable} completes or + * produces an error, all windows are emitted, and the event is propagated to all subscribed + * {@link rx.Observer}s. + *

+ * Note that when using this operation multiple overlapping windows could be active at any + * one point. + *

* * @param source - * The {@link rx.Observable} which produces values. + * the {@link rx.Observable} which produces values * @param windowOpenings - * An {@link rx.Observable} which when it produces a {@link rx.util.Opening} value will - * create a new window which instantly starts recording the "source" {@link rx.Observable}. + * an {@link rx.Observable} which when it produces a {@link rx.util.TOpening} value will create a + * new window which instantly starts recording the {@code source} {@link rx.Observable} * @param windowClosingSelector - * A {@link rx.functions.Func0} object which produces {@link rx.Observable}s. These {@link rx.Observable}s determine when a window is emitted and replaced by simply - * producing an object. + * a {@link rx.functions.Func0} object that produces {@link rx.Observable}s. These + * {@link rx.Observable}s determine when a window is emitted and replaced by simply producing an + * object. * @return - * the {@link rx.functions.Func1} object representing the specified window operation. + * the {@link rx.functions.Func1} object representing the specified window operation */ public static OnSubscribeFunc> window(final Observable source, final Observable windowOpenings, final Func1> windowClosingSelector) { return new OnSubscribeFunc>() { @@ -110,48 +116,51 @@ public Subscription onSubscribe(final Observer> observer) } /** - *

This method creates a {@link rx.functions.Func1} object which represents the window operation. This operation takes - * values from the specified {@link rx.Observable} source and stores them in a window until the window contains - * a specified number of elements. The window is then emitted, and a new window is created to replace it. - * When the source {@link rx.Observable} completes or produces an error, the current window is emitted, and - * the event is propagated to all subscribed {@link rx.Observer}s.

- * - *

Note that this operation only produces non-overlapping windows. At all times there is - * exactly one window actively storing values.

+ * This method creates a {@link rx.functions.Func1} object which represents the window operation. This + * operation takes values from the specified {@link rx.Observable} source and stores them in a window until + * the window contains a specified number of elements. The window is then emitted, and a new window is + * created to replace it. When the source {@link rx.Observable} completes or produces an error, the current + * window is emitted, and the event is propagated to all subscribed {@link rx.Observer}s. + *

+ * Note that this operation only produces non-overlapping windows. At all times there is + * exactly one window actively storing values. + *

* * @param source - * The {@link rx.Observable} which produces values. + * the {@link rx.Observable} which produces values * @param count - * The number of elements a window should have before being emitted and replaced. + * the number of elements a window should have before being emitted and replaced * @return - * the {@link rx.functions.Func1} object representing the specified window operation. + * the {@link rx.functions.Func1} object representing the specified window operation */ public static OnSubscribeFunc> window(Observable source, int count) { return window(source, count, count); } /** - *

This method creates a {@link rx.functions.Func1} object which represents the window operation. This operation takes - * values from the specified {@link rx.Observable} source and stores them in all active windows until the window - * contains a specified number of elements. The window is then emitted. windows are created after a certain - * amount of values have been received. When the source {@link rx.Observable} completes or produces an error, the - * currently active windows are emitted, and the event is propagated to all subscribed {@link rx.Observer}s.

- * - *

Note that this operation can produce non-connected, connected non-overlapping, or overlapping - * windows depending on the input parameters.

+ * This method creates a {@link rx.functions.Func1} object which represents the window operation. This + * operation takes values from the specified {@link rx.Observable} source and stores them in all active + * windows until the window contains a specified number of elements. The window is then emitted. Windows are + * created after a certain amount of values have been received. When the source {@link rx.Observable} + * completes or produces an error, the currently active windows are emitted, and the event is propagated to + * all subscribed {@link rx.Observer}s. + *

+ * Note that this operation can produce non-connected, connected non-overlapping, or overlapping + * windows depending on the input parameters. + *

* * @param source - * The {@link rx.Observable} which produces values. + * the {@link rx.Observable} which produces values * @param count - * The number of elements a window should have before being emitted. + * the number of elements a window should have before being emitted * @param skip - * The interval with which windows have to be created. Note that when "skip" == "count" - * that this is the same as calling {@link rx.operators.OperationWindow#window(rx.Observable, int)}. - * If "skip" < "count", this window operation will produce overlapping windows and if "skip" - * > "count" non-overlapping windows will be created and some values will not be pushed - * into a window at all! + * the interval with which windows have to be created. Note that when {@code skip == count} that + * this is the same as calling {@link rx.operators.OperationWindow#window(rx.Observable, int)}. + * If {@code skip < count}, this window operation will produce overlapping windows and if + * {@code skip > count} non-overlapping windows will be created and some values will not be + * pushed into a window at all! * @return - * the {@link rx.functions.Func1} object representing the specified window operation. + * the {@link rx.functions.Func1} object representing the specified window operation */ public static OnSubscribeFunc> window(final Observable source, final int count, final int skip) { return new OnSubscribeFunc>() { @@ -165,48 +174,50 @@ public Subscription onSubscribe(final Observer> observer) } /** - *

This method creates a {@link rx.functions.Func1} object which represents the window operation. This operation takes - * values from the specified {@link rx.Observable} source and stores them in a window. Periodically the window - * is emitted and replaced with a new window. How often this is done depends on the specified timespan. - * When the source {@link rx.Observable} completes or produces an error, the current window is emitted, and - * the event is propagated to all subscribed {@link rx.Observer}s.

- * - *

Note that this operation only produces non-overlapping windows. At all times there is - * exactly one window actively storing values.

+ * This method creates a {@link rx.functions.Func1} object which represents the window operation. This + * operation takes values from the specified {@link rx.Observable} source and stores them in a window. + * Periodically the window is emitted and replaced with a new window. How often this is done depends on the + * specified timespan. When the source {@link rx.Observable} completes or produces an error, the current + * window is emitted, and the event is propagated to all subscribed {@link rx.Observer}s. + *

+ * Note that this operation only produces non-overlapping windows. At all times there is + * exactly one window actively storing values. + *

* * @param source - * The {@link rx.Observable} which produces values. + * the {@link rx.Observable} which produces values * @param timespan - * The amount of time all windows must be actively collect values before being emitted. + * the amount of time all windows must be actively collect values before being emitted * @param unit - * The {@link java.util.concurrent.TimeUnit} defining the unit of time for the timespan. + * the {@link java.util.concurrent.TimeUnit} defining the unit of time for the timespan * @return - * the {@link rx.functions.Func1} object representing the specified window operation. + * the {@link rx.functions.Func1} object representing the specified window operation */ public static OnSubscribeFunc> window(Observable source, long timespan, TimeUnit unit) { return window(source, timespan, unit, Schedulers.threadPoolForComputation()); } /** - *

This method creates a {@link rx.functions.Func1} object which represents the window operation. This operation takes - * values from the specified {@link rx.Observable} source and stores them in a window. Periodically the window - * is emitted and replaced with a new window. How often this is done depends on the specified timespan. - * When the source {@link rx.Observable} completes or produces an error, the current window is emitted, and - * the event is propagated to all subscribed {@link rx.Observer}s.

- * - *

Note that this operation only produces non-overlapping windows. At all times there is - * exactly one window actively storing values.

+ * This method creates a {@link rx.functions.Func1} object which represents the window operation. This + * operation takes values from the specified {@link rx.Observable} source and stores them in a window. + * Periodically the window is emitted and replaced with a new window. How often this is done depends on the + * specified timespan. When the source {@link rx.Observable} completes or produces an error, the current + * window is emitted, and the event is propagated to all subscribed {@link rx.Observer}s. + *

+ * Note that this operation only produces non-overlapping windows. At all times there is + * exactly one window actively storing values. + *

* * @param source - * The {@link rx.Observable} which produces values. + * the {@link rx.Observable} which produces values * @param timespan - * The amount of time all windows must be actively collect values before being emitted. + * the amount of time all windows must be actively collect values before being emitted * @param unit - * The {@link java.util.concurrent.TimeUnit} defining the unit of time for the timespan. + * the {@link java.util.concurrent.TimeUnit} defining the unit of time for the timespan * @param scheduler - * The {@link rx.Scheduler} to use for timing windows. + * the {@link rx.Scheduler} to use for timing windows * @return - * the {@link rx.functions.Func1} object representing the specified window operation. + * the {@link rx.functions.Func1} object representing the specified window operation */ public static OnSubscribeFunc> window(final Observable source, final long timespan, final TimeUnit unit, final Scheduler scheduler) { return new OnSubscribeFunc>() { @@ -220,54 +231,56 @@ public Subscription onSubscribe(final Observer> observer) } /** - *

This method creates a {@link rx.functions.Func1} object which represents the window operation. This operation takes - * values from the specified {@link rx.Observable} source and stores them in a window. Periodically the window - * is emitted and replaced with a new window. How often this is done depends on the specified timespan. - * Additionally the window is automatically emitted once it reaches a specified number of elements. - * When the source {@link rx.Observable} completes or produces an error, the current window is emitted, and - * the event is propagated to all subscribed {@link rx.Observer}s.

- * - *

Note that this operation only produces non-overlapping windows. At all times there is - * exactly one window actively storing values.

+ * This method creates a {@link rx.functions.Func1} object which represents the window operation. This + * operation takes values from the specified {@link rx.Observable} source and stores them in a window. + * Periodically the window is emitted and replaced with a new window. How often this is done depends on the + * specified timespan. Additionally the window is automatically emitted once it reaches a specified number + * of elements. When the source {@link rx.Observable} completes or produces an error, the current window is + * emitted, and the event is propagated to all subscribed {@link rx.Observer}s. + *

+ * Note that this operation only produces non-overlapping windows. At all times there is + * exactly one window actively storing values. + *

* * @param source - * The {@link rx.Observable} which produces values. + * the {@link rx.Observable} which produces values * @param timespan - * The amount of time all windows must be actively collect values before being emitted. + * the amount of time all windows must be actively collect values before being emitted * @param unit - * The {@link java.util.concurrent.TimeUnit} defining the unit of time for the timespan. + * the {@link java.util.concurrent.TimeUnit} defining the unit of time for the timespan * @param count - * The maximum size of the window. Once a window reaches this size, it is emitted. + * the maximum size of the window. Once a window reaches this size, it is emitted * @return - * the {@link rx.functions.Func1} object representing the specified window operation. + * the {@link rx.functions.Func1} object representing the specified window operation */ public static OnSubscribeFunc> window(Observable source, long timespan, TimeUnit unit, int count) { return window(source, timespan, unit, count, Schedulers.threadPoolForComputation()); } /** - *

This method creates a {@link rx.functions.Func1} object which represents the window operation. This operation takes - * values from the specified {@link rx.Observable} source and stores them in a window. Periodically the window - * is emitted and replaced with a new window. How often this is done depends on the specified timespan. - * Additionally the window is automatically emitted once it reaches a specified number of elements. - * When the source {@link rx.Observable} completes or produces an error, the current window is emitted, and - * the event is propagated to all subscribed {@link rx.Observer}s.

- * - *

Note that this operation only produces non-overlapping windows. At all times there is - * exactly one window actively storing values.

+ * This method creates a {@link rx.functions.Func1} object which represents the window operation. This + * operation takes values from the specified {@link rx.Observable} source and stores them in a window. + * Periodically the window is emitted and replaced with a new window. How often this is done depends on the + * specified timespan. Additionally the window is automatically emitted once it reaches a specified number + * of elements. When the source {@link rx.Observable} completes or produces an error, the current window is + * emitted, and the event is propagated to all subscribed {@link rx.Observer}s. + *

+ * Note that this operation only produces non-overlapping windows. At all times there is + * exactly one window actively storing values. + *

* * @param source - * The {@link rx.Observable} which produces values. + * the {@link rx.Observable} which produces values * @param timespan - * The amount of time all windows must be actively collect values before being emitted. + * the amount of time all windows must be actively collect values before being emitted * @param unit - * The {@link java.util.concurrent.TimeUnit} defining the unit of time for the timespan. + * the {@link java.util.concurrent.TimeUnit} defining the unit of time for the timespan * @param count - * The maximum size of the window. Once a window reaches this size, it is emitted. + * the maximum size of the window. Once a window reaches this size, it is emitted * @param scheduler - * The {@link rx.Scheduler} to use for timing windows. + * the {@link rx.Scheduler} to use for timing windows * @return - * the {@link rx.functions.Func1} object representing the specified window operation. + * the {@link rx.functions.Func1} object representing the specified window operation */ public static OnSubscribeFunc> window(final Observable source, final long timespan, final TimeUnit unit, final int count, final Scheduler scheduler) { return new OnSubscribeFunc>() { @@ -281,54 +294,56 @@ public Subscription onSubscribe(final Observer> observer) } /** - *

This method creates a {@link rx.functions.Func1} object which represents the window operation. This operation takes - * values from the specified {@link rx.Observable} source and stores them in a window. Periodically the window - * is emitted and replaced with a new window. How often this is done depends on the specified timespan. - * The creation of windows is also periodical. How often this is done depends on the specified timeshift. - * When the source {@link rx.Observable} completes or produces an error, the current window is emitted, and - * the event is propagated to all subscribed {@link rx.Observer}s.

- * - *

Note that this operation can produce non-connected, or overlapping windows depending - * on the input parameters.

+ * This method creates a {@link rx.functions.Func1} object which represents the window operation. This + * operation takes values from the specified {@link rx.Observable} source and stores them in a window. + * Periodically the window is emitted and replaced with a new window. How often this is done depends on the + * specified timespan. The creation of windows is also periodical. How often this is done depends on the + * specified timeshift. When the source {@link rx.Observable} completes or produces an error, the current + * window is emitted, and the event is propagated to all subscribed {@link rx.Observer}s. + *

+ * Note that this operation can produce non-connected, or overlapping windows depending on + * the input parameters. + *

* * @param source - * The {@link rx.Observable} which produces values. + * the {@link rx.Observable} which produces values * @param timespan - * The amount of time all windows must be actively collect values before being emitted. + * the amount of time all windows must be actively collect values before being emitted * @param timeshift - * The amount of time between creating windows. + * the amount of time between creating windows * @param unit - * The {@link java.util.concurrent.TimeUnit} defining the unit of time for the timespan. + * the {@link java.util.concurrent.TimeUnit} defining the unit of time for the timespan * @return - * the {@link rx.functions.Func1} object representing the specified window operation. + * the {@link rx.functions.Func1} object representing the specified window operation */ public static OnSubscribeFunc> window(Observable source, long timespan, long timeshift, TimeUnit unit) { return window(source, timespan, timeshift, unit, Schedulers.threadPoolForComputation()); } /** - *

This method creates a {@link rx.functions.Func1} object which represents the window operation. This operation takes - * values from the specified {@link rx.Observable} source and stores them in a window. Periodically the window - * is emitted and replaced with a new window. How often this is done depends on the specified timespan. - * The creation of windows is also periodical. How often this is done depends on the specified timeshift. - * When the source {@link rx.Observable} completes or produces an error, the current window is emitted, and - * the event is propagated to all subscribed {@link rx.Observer}s.

- * - *

Note that this operation can produce non-connected, or overlapping windows depending - * on the input parameters.

+ * This method creates a {@link rx.functions.Func1} object which represents the window operation. This + * operation takes values from the specified {@link rx.Observable} source and stores them in a window. + * Periodically the window is emitted and replaced with a new window. How often this is done depends on the + * specified timespan. The creation of windows is also periodical. How often this is done depends on the + * specified timeshift. When the source {@link rx.Observable} completes or produces an error, the current + * window is emitted, and the event is propagated to all subscribed {@link rx.Observer}s. + *

+ * Note that this operation can produce non-connected, or overlapping windows depending on + * the input parameters. + *

* * @param source - * The {@link rx.Observable} which produces values. + * the {@link rx.Observable} which produces values * @param timespan - * The amount of time all windows must be actively collect values before being emitted. + * the amount of time all windows must be actively collect values before being emitted * @param timeshift - * The amount of time between creating windows. + * the amount of time between creating windows * @param unit - * The {@link java.util.concurrent.TimeUnit} defining the unit of time for the timespan. + * the {@link java.util.concurrent.TimeUnit} defining the unit of time for the timespan * @param scheduler - * The {@link rx.Scheduler} to use for timing windows. + * the {@link rx.Scheduler} to use for timing windows * @return - * the {@link rx.functions.Func1} object representing the specified window operation. + * the {@link rx.functions.Func1} object representing the specified window operation */ public static OnSubscribeFunc> window(final Observable source, final long timespan, final long timeshift, final TimeUnit unit, final Scheduler scheduler) { return new OnSubscribeFunc>() { @@ -345,13 +360,13 @@ public Subscription onSubscribe(final Observer> observer) * This class represents a single window: A sequence of recorded values. * * @param - * The type of objects which this {@link Window} can hold. + * the type of objects which this {@link Window} can hold */ protected static class Window extends Chunk> { /** * @return - * The mutable underlying {@link Observable} which contains all the - * recorded values in this {@link Window} object. + * the mutable underlying {@link Observable} which contains all the recorded values in this + * {@link Window} object */ @Override public Observable getContents() { @@ -360,16 +375,20 @@ public Observable getContents() { } /** - * Emits windows of values of the source Observable where the window boundary is - * determined by the items of the boundary Observable. + * Emits windows of values of the source Observable where the window boundary is determined by the items of + * the boundary Observable. + * + * @param source + * @param boundary + * @return */ public static OnSubscribeFunc> window(Observable source, Observable boundary) { return new WindowViaObservable(source, boundary); } /** - * Create non-overlapping windows from the source values by using another observable's - * values as to when to replace a window. + * Create non-overlapping windows from the source values by using another observable's values as to when to + * replace a window. */ private static final class WindowViaObservable implements OnSubscribeFunc> { final Observable source; diff --git a/rxjava-core/src/main/java/rx/operators/OperatorScan.java b/rxjava-core/src/main/java/rx/operators/OperatorScan.java index 2cd8de2626..d1e9b769a9 100644 --- a/rxjava-core/src/main/java/rx/operators/OperatorScan.java +++ b/rxjava-core/src/main/java/rx/operators/OperatorScan.java @@ -21,17 +21,17 @@ import rx.functions.Func2; /** - * Returns an Observable that applies a function to the first item emitted by a source Observable, - * then feeds the result of that function along with the second item emitted by an Observable into - * the same function, and so on until all items have been emitted by the source Observable, - * emitting the result of each of these iterations. + * Returns an Observable that applies a function to the first item emitted by a source Observable, then feeds + * the result of that function along with the second item emitted by an Observable into the same function, and + * so on until all items have been emitted by the source Observable, emitting the result of each of these + * iterations. *

* *

* This sort of function is sometimes called an accumulator. *

- * Note that when you pass a seed to scan() the resulting Observable will emit that - * seed as its first emitted item. + * Note that when you pass a seed to scan() the resulting Observable will emit that seed as its + * first emitted item. */ public final class OperatorScan implements Operator { @@ -41,19 +41,14 @@ public final class OperatorScan implements Operator { private static final Object NO_INITIAL_VALUE = new Object(); /** - * Applies an accumulator function over an observable sequence and returns each intermediate - * result with the specified source and accumulator. + * Applies an accumulator function over an observable sequence and returns each intermediate result with the + * specified source and accumulator. * - * @param sequence - * An observable sequence of elements to project. * @param initialValue - * The initial (seed) accumulator value. + * the initial (seed) accumulator value * @param accumulator - * An accumulator function to be invoked on each element from the sequence. - * - * @return An observable sequence whose elements are the result of accumulating the output from - * the list of Observables. - * @see Observable.Scan(TSource, TAccumulate) Method (IObservable(TSource), TAccumulate, Func(TAccumulate, TSource, + * an accumulator function to be invoked on each element from the sequence + * @see Observable.Scan(TSource, TAccumulate) Method (IObservable(TSource), TAccumulate, Func(TAccumulate, TSource, * TAccumulate)) */ public OperatorScan(R initialValue, Func2 accumulator) { @@ -62,17 +57,12 @@ public OperatorScan(R initialValue, Func2 accumulator) { } /** - * Applies an accumulator function over an observable sequence and returns each intermediate - * result with the specified source and accumulator. + * Applies an accumulator function over an observable sequence and returns each intermediate result with the + * specified source and accumulator. * - * @param sequence - * An observable sequence of elements to project. * @param accumulator - * An accumulator function to be invoked on each element from the sequence. - * - * @return An observable sequence whose elements are the result of accumulating the output from - * the list of Observables. - * @see Observable.Scan(TSource) Method (IObservable(TSource), Func(TSource, TSource, TSource)) + * an accumulator function to be invoked on each element from the sequence + * @see Observable.Scan(TSource) Method (IObservable(TSource), Func(TSource, TSource, TSource)) */ @SuppressWarnings("unchecked") public OperatorScan(final Func2 accumulator) { diff --git a/rxjava-core/src/main/java/rx/operators/SafeObserver.java b/rxjava-core/src/main/java/rx/operators/SafeObserver.java index f776619905..cf632b4dc5 100644 --- a/rxjava-core/src/main/java/rx/operators/SafeObserver.java +++ b/rxjava-core/src/main/java/rx/operators/SafeObserver.java @@ -29,7 +29,8 @@ /** * Wrapper around Observer to ensure compliance with Rx contract. *

- * The following is taken from the Rx Design Guidelines document: http://go.microsoft.com/fwlink/?LinkID=205219 + * The following is taken from the Rx Design Guidelines + * document: *

  * Messages sent to instances of the IObserver interface follow the following grammar:
  * 
@@ -38,8 +39,8 @@
  * This grammar allows observable sequences to send any amount (0 or more) of OnNext messages to the subscribed
  * observer instance, optionally followed by a single success (OnCompleted) or failure (OnError) message.
  * 
- * The single message indicating that an observable sequence has finished ensures that consumers of the observable
- * sequence can deterministically establish that it is safe to perform cleanup operations.
+ * The single message indicating that an observable sequence has finished ensures that consumers of the
+ * observable sequence can deterministically establish that it is safe to perform cleanup operations.
  * 
  * A single failure further ensures that abort semantics can be maintained for operators that work on
  * multiple observable sequences (see paragraph 6.6).
@@ -57,7 +58,7 @@
  * It will not synchronize onNext execution. Use the {@link SynchronizedObserver} to do that.
  * 
  * @param 
- * @Deprecated Replaced by SafeSubscriber
+ * @deprecated replaced by SafeSubscriber
  */
 @Deprecated
 public class SafeObserver implements Observer {
diff --git a/rxjava-core/src/main/java/rx/plugins/RxJavaDefaultSchedulers.java b/rxjava-core/src/main/java/rx/plugins/RxJavaDefaultSchedulers.java
index 5adc50e0d1..f3e7708669 100644
--- a/rxjava-core/src/main/java/rx/plugins/RxJavaDefaultSchedulers.java
+++ b/rxjava-core/src/main/java/rx/plugins/RxJavaDefaultSchedulers.java
@@ -18,29 +18,30 @@
 import rx.Scheduler;
 
 /**
- * Define alternate Scheduler implementations to be returned by the `Schedulers` factory methods.
+ * Define alternate Scheduler implementations to be returned by the {@code Schedulers} factory methods.
  * 

- * See {@link RxJavaPlugins} or the RxJava GitHub Wiki for information on configuring plugins: https://github.com/Netflix/RxJava/wiki/Plugins. + * See {@link RxJavaPlugins} or the RxJava GitHub Wiki for information on configuring plugins: + * https://github.com/Netflix/RxJava/wiki/Plugins. */ public abstract class RxJavaDefaultSchedulers { /** - * Scheduler to return from {@link Schedulers.computation()} or null if default should be used. + * Scheduler to return from {@link rx.schedulers.Schedulers#computation()} or null if default should be + * used. * * This instance should be or behave like a stateless singleton; */ public abstract Scheduler getComputationScheduler(); /** - * Scheduler to return from {@link Schedulers.io()} or null if default should be used. + * Scheduler to return from {@link rx.schedulers.Schedulers#io()} or null if default should be used. * * This instance should be or behave like a stateless singleton; */ public abstract Scheduler getIOScheduler(); /** - * Scheduler to return from {@link Schedulers.newThread()} or null if default should be used. + * Scheduler to return from {@link rx.schedulers.Schedulers#newThread()} or null if default should be used. * * This instance should be or behave like a stateless singleton; */ diff --git a/rxjava-core/src/main/java/rx/plugins/RxJavaObservableExecutionHook.java b/rxjava-core/src/main/java/rx/plugins/RxJavaObservableExecutionHook.java index 64402fe0de..0b21d20ffe 100644 --- a/rxjava-core/src/main/java/rx/plugins/RxJavaObservableExecutionHook.java +++ b/rxjava-core/src/main/java/rx/plugins/RxJavaObservableExecutionHook.java @@ -23,34 +23,33 @@ import rx.functions.Func1; /** - * Abstract ExecutionHook with invocations at different lifecycle points of {@link Observable} - * execution with a default no-op implementation. + * Abstract ExecutionHook with invocations at different lifecycle points of {@link Observable} execution with a + * default no-op implementation. *

- * See {@link RxJavaPlugins} or the RxJava GitHub Wiki for information on configuring plugins: https://github.com/Netflix/RxJava/wiki/ - * Plugins . + * See {@link RxJavaPlugins} or the RxJava GitHub Wiki for information on configuring plugins: + * https://github.com/Netflix/RxJava/wiki/Plugins. *

- * Note on thread-safety and performance + * Note on thread-safety and performance: *

- * A single implementation of this class will be used globally so methods on this class will be - * invoked concurrently from multiple threads so all functionality must be thread-safe. + * A single implementation of this class will be used globally so methods on this class will be invoked + * concurrently from multiple threads so all functionality must be thread-safe. *

- * Methods are also invoked synchronously and will add to execution time of the observable so all - * behavior should be fast. If anything time-consuming is to be done it should be spawned - * asynchronously onto separate worker threads. + * Methods are also invoked synchronously and will add to execution time of the observable so all behavior + * should be fast. If anything time-consuming is to be done it should be spawned asynchronously onto separate + * worker threads. * */ public abstract class RxJavaObservableExecutionHook { /** * Invoked during the construction by {@link Observable#create(OnSubscribe)} *

- * This can be used to decorate or replace the onSubscribe function or just perform - * extra logging, metrics and other such things and pass-thru the function. + * This can be used to decorate or replace the onSubscribe function or just perform extra + * logging, metrics and other such things and pass-thru the function. * - * @param onSubscribe + * @param f * original {@link OnSubscribe}<{@code T}> to be executed - * @return {@link OnSubscribe}<{@code T}> function that can be modified, decorated, replaced or - * just returned as a pass-thru. + * @return {@link OnSubscribe}<{@code T}> function that can be modified, decorated, replaced or just + * returned as a pass-thru */ public OnSubscribe onCreate(OnSubscribe f) { return f; @@ -59,13 +58,13 @@ public OnSubscribe onCreate(OnSubscribe f) { /** * Invoked before {@link Observable#subscribe(rx.Subscriber)} is about to be executed. *

- * This can be used to decorate or replace the onSubscribe function or just perform - * extra logging, metrics and other such things and pass-thru the function. + * This can be used to decorate or replace the onSubscribe function or just perform extra + * logging, metrics and other such things and pass-thru the function. * * @param onSubscribe * original {@link OnSubscribe}<{@code T}> to be executed - * @return {@link OnSubscribe}<{@code T}> function that can be modified, decorated, replaced or - * just returned as a pass-thru. + * @return {@link OnSubscribe}<{@code T}> function that can be modified, decorated, replaced or just + * returned as a pass-thru */ public OnSubscribe onSubscribeStart(Observable observableInsance, final OnSubscribe onSubscribe) { // pass-thru by default @@ -73,16 +72,16 @@ public OnSubscribe onSubscribeStart(Observable observableIns } /** - * Invoked after successful execution of {@link Observable#subscribe(rx.Subscriber)} with - * returned {@link Subscription}. + * Invoked after successful execution of {@link Observable#subscribe(rx.Subscriber)} with returned + * {@link Subscription}. *

- * This can be used to decorate or replace the {@link Subscription} instance or just perform - * extra logging, metrics and other such things and pass-thru the subscription. + * This can be used to decorate or replace the {@link Subscription} instance or just perform extra logging, + * metrics and other such things and pass-thru the subscription. * * @param subscription * original {@link Subscription} - * @return {@link Subscription} subscription that can be modified, decorated, replaced or just - * returned as a pass-thru. + * @return {@link Subscription} subscription that can be modified, decorated, replaced or just returned as a + * pass-thru */ public Subscription onSubscribeReturn(Subscription subscription) { // pass-thru by default @@ -90,16 +89,14 @@ public Subscription onSubscribeReturn(Subscription subscription) { } /** - * Invoked after failed execution of {@link Observable#subscribe(Subscriber)} with thrown - * Throwable. + * Invoked after failed execution of {@link Observable#subscribe(Subscriber)} with thrown Throwable. *

- * This is NOT errors emitted via {@link Subscriber#onError(Throwable)} but exceptions thrown - * when attempting to subscribe to a {@link Func1}<{@link Subscriber}{@code }, - * {@link Subscription}>. + * This is not errors emitted via {@link Subscriber#onError(Throwable)} but exceptions thrown when + * attempting to subscribe to a {@link Func1}<{@link Subscriber}{@code }, {@link Subscription}>. * * @param e * Throwable thrown by {@link Observable#subscribe(Subscriber)} - * @return Throwable that can be decorated, replaced or just returned as a pass-thru. + * @return Throwable that can be decorated, replaced or just returned as a pass-thru */ public Throwable onSubscribeError(Throwable e) { // pass-thru by default @@ -114,9 +111,9 @@ public Throwable onSubscribeError(Throwable e) { * logging, metrics and other such things and pass-thru the onSubscribe. * * @param lift - * original {@link Operator}{@code} ' * @return {@link Operator}{@code } - * function that can be modified, decorated, replaced or - * just returned as a pass-thru. + * original {@link Operator}{@code } + * @return {@link Operator}{@code } function that can be modified, decorated, replaced or just + * returned as a pass-thru */ public Operator onLift(final Operator lift) { return lift; diff --git a/rxjava-core/src/main/java/rx/schedulers/ExecutorScheduler.java b/rxjava-core/src/main/java/rx/schedulers/ExecutorScheduler.java index c6f1344320..ff994f0759 100644 --- a/rxjava-core/src/main/java/rx/schedulers/ExecutorScheduler.java +++ b/rxjava-core/src/main/java/rx/schedulers/ExecutorScheduler.java @@ -29,16 +29,17 @@ import rx.subscriptions.Subscriptions; /** - * A {@link Scheduler} implementation that uses an {@link Executor} or {@link ScheduledExecutorService} implementation. + * A {@link Scheduler} implementation that uses an {@link Executor} or {@link ScheduledExecutorService} + * implementation. *

- * Note that if an {@link Executor} implementation is used instead of {@link ScheduledExecutorService} then a system-wide Timer will be used to handle delayed events. + * Note that if an {@link Executor} implementation is used instead of {@link ScheduledExecutorService} then a + * system-wide Timer will be used to handle delayed events. */ public class ExecutorScheduler extends Scheduler { private final Executor executor; /** * @deprecated Use Schedulers.executor(); - * @return */ @Deprecated public ExecutorScheduler(Executor executor) { @@ -47,7 +48,6 @@ public ExecutorScheduler(Executor executor) { /** * @deprecated Use Schedulers.executor(); - * @return */ @Deprecated public ExecutorScheduler(ScheduledExecutorService executor) { diff --git a/rxjava-core/src/main/java/rx/schedulers/Schedulers.java b/rxjava-core/src/main/java/rx/schedulers/Schedulers.java index 7bbfe61eaa..7aa8cc0e55 100644 --- a/rxjava-core/src/main/java/rx/schedulers/Schedulers.java +++ b/rxjava-core/src/main/java/rx/schedulers/Schedulers.java @@ -73,7 +73,7 @@ public static Scheduler immediate() { * {@link Scheduler} that queues work on the current thread to be executed after the current work completes. * * @return {@link TrampolineScheduler} instance - * @deprecated Use trampoline() instead + * @deprecated use {@link #trampoline()} instead */ @Deprecated public static Scheduler currentThread() { @@ -121,14 +121,15 @@ public static Scheduler executor(ScheduledExecutorService executor) { /** * {@link Scheduler} intended for computational work. *

- * The implementation is backed by a {@link ScheduledExecutorService} thread-pool sized to the number of CPU cores. + * The implementation is backed by a {@link ScheduledExecutorService} thread-pool sized to the number of CPU + * cores. *

* This can be used for event-loops, processing callbacks and other computational work. *

* Do not perform IO-bound work on this scheduler. Use {@link #io()} instead. * - * @return {@link ExecutorScheduler} for computation-bound work. - * @Deprecated Use {@link #computation()} + * @return {@link ExecutorScheduler} for computation-bound work + * @deprecated use {@link #computation()} */ @Deprecated public static Scheduler threadPoolForComputation() { @@ -142,7 +143,7 @@ public static Scheduler threadPoolForComputation() { *

* Do not perform IO-bound work on this scheduler. Use {@link #io()} instead. * - * @return {@link Scheduler} for computation-bound work. + * @return {@link Scheduler} for computation-bound work */ public static Scheduler computation() { return INSTANCE.computationScheduler; @@ -157,8 +158,8 @@ public static Scheduler computation() { *

* Do not perform computational work on this scheduler. Use {@link #computation()} instead. * - * @return {@link ExecutorScheduler} for IO-bound work. - * @deprecated Use {@link #io()} instead. + * @return {@link ExecutorScheduler} for IO-bound work + * @deprecated use {@link #io()} instead */ @Deprecated public static Scheduler threadPoolForIO() { @@ -174,7 +175,7 @@ public static Scheduler threadPoolForIO() { *

* Do not perform computational work on this scheduler. Use {@link #computation()} instead. * - * @return {@link ExecutorScheduler} for IO-bound work. + * @return {@link ExecutorScheduler} for IO-bound work */ public static Scheduler io() { return INSTANCE.ioScheduler; diff --git a/rxjava-core/src/main/java/rx/subjects/BehaviorSubject.java b/rxjava-core/src/main/java/rx/subjects/BehaviorSubject.java index 5e33a85637..036da20a48 100644 --- a/rxjava-core/src/main/java/rx/subjects/BehaviorSubject.java +++ b/rxjava-core/src/main/java/rx/subjects/BehaviorSubject.java @@ -67,23 +67,27 @@ public final class BehaviorSubject extends Subject { /** - * Creates a {@link BehaviorSubject} which publishes the last and all subsequent events to each {@link Observer} that subscribes to it. + * Creates a {@link BehaviorSubject} which publishes the last and all subsequent events to each + * {@link Observer} that subscribes to it. * * @param defaultValue - * The value which will be published to any {@link Observer} as long as the {@link BehaviorSubject} has not yet received any events. - * @return the constructed {@link BehaviorSubject}. - * @deprecated Use {@link create()} instead. + * the value which will be published to any {@link Observer} as long as the + * {@link BehaviorSubject} has not yet received any events + * @return the constructed {@link BehaviorSubject} + * @deprecated use {@link #create(T)} instead */ public static BehaviorSubject createWithDefaultValue(T defaultValue) { return create(defaultValue); } /** - * Creates a {@link BehaviorSubject} which publishes the last and all subsequent events to each {@link Observer} that subscribes to it. + * Creates a {@link BehaviorSubject} which publishes the last and all subsequent events to each + * {@link Observer} that subscribes to it. * * @param defaultValue - * The value which will be published to any {@link Observer} as long as the {@link BehaviorSubject} has not yet received any events. - * @return the constructed {@link BehaviorSubject}. + * the value which will be published to any {@link Observer} as long as the + * {@link BehaviorSubject} has not yet received any events + * @return the constructed {@link BehaviorSubject} */ public static BehaviorSubject create(T defaultValue) { final SubjectSubscriptionManager subscriptionManager = new SubjectSubscriptionManager(); @@ -179,4 +183,4 @@ public void onNext(T v) { } } -} \ No newline at end of file +} From d46af6391fe41c05250c1f8b956c940c310db135 Mon Sep 17 00:00:00 2001 From: zsxwing Date: Mon, 10 Mar 2014 14:41:59 +0800 Subject: [PATCH 083/422] Add amb, delay and delaySubscription in rxjava-scala --- .../rx/lang/scala/examples/RxScalaDemo.scala | 31 ++++++++ .../main/scala/rx/lang/scala/Observable.scala | 72 +++++++++++++++++++ 2 files changed, 103 insertions(+) diff --git a/language-adaptors/rxjava-scala/src/examples/scala/rx/lang/scala/examples/RxScalaDemo.scala b/language-adaptors/rxjava-scala/src/examples/scala/rx/lang/scala/examples/RxScalaDemo.scala index e7c09e769c..e09e6a7ec9 100644 --- a/language-adaptors/rxjava-scala/src/examples/scala/rx/lang/scala/examples/RxScalaDemo.scala +++ b/language-adaptors/rxjava-scala/src/examples/scala/rx/lang/scala/examples/RxScalaDemo.scala @@ -533,4 +533,35 @@ class RxScalaDemo extends JUnitSuite { println(result) } + @Test def ambExample(): Unit = { + val o1 = List(100L, 200L, 300L).toObservable.delay(4 seconds) + val o2 = List(1000L, 2000L, 3000L).toObservable.delay(2 seconds) + val result = o1.amb(o2).toBlockingObservable.toList + println(result) + } + + @Test def delayExample(): Unit = { + val o = List(100L, 200L, 300L).toObservable.delay(2 seconds) + val result = o.toBlockingObservable.toList + println(result) + } + + @Test def delayExample2(): Unit = { + val o = List(100L, 200L, 300L).toObservable.delay(2 seconds, IOScheduler()) + val result = o.toBlockingObservable.toList + println(result) + } + + @Test def delaySubscriptionExample(): Unit = { + val o = List(100L, 200L, 300L).toObservable.delaySubscription(2 seconds) + val result = o.toBlockingObservable.toList + println(result) + } + + @Test def delaySubscriptionExample2(): Unit = { + val o = List(100L, 200L, 300L).toObservable.delaySubscription(2 seconds, IOScheduler()) + val result = o.toBlockingObservable.toList + println(result) + } + } diff --git a/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/Observable.scala b/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/Observable.scala index 6794770232..75abe048a7 100644 --- a/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/Observable.scala +++ b/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/Observable.scala @@ -2236,6 +2236,78 @@ trait Observable[+T] def doOnEach(onNext: T => Unit, onError: Throwable => Unit, onCompleted: () => Unit): Observable[T] = { toScalaObservable[T](asJavaObservable.doOnEach(Observer(onNext, onError,onCompleted))) } + + /** + * Given two Observables, mirror the one that first emits an item. + * + * + * + * You can combine items emitted by two Observables so that they act like a single + * Observable by using the `merge` method. + * + * @param that + * an Observable competing to react first + * @return an Observable that emits the same sequence of items as whichever of `this` or `that` first emitted an item. + */ + def amb[U >: T](that: Observable[U]): Observable[U] = { + val thisJava: rx.Observable[_ <: U] = this.asJavaObservable + val thatJava: rx.Observable[_ <: U] = that.asJavaObservable + toScalaObservable[U](rx.Observable.amb(thisJava, thatJava)) + } + + /** + * Returns an Observable that emits the items emitted by the source Observable shifted forward in time by a + * specified delay. Error notifications from the source Observable are not delayed. + * + * + * + * @param delay the delay to shift the source by + * @return the source Observable shifted in time by the specified delay + */ + def delay(delay: Duration): Observable[T] = { + toScalaObservable[T](asJavaObservable.delay(delay.length, delay.unit)) + } + + /** + * Returns an Observable that emits the items emitted by the source Observable shifted forward in time by a + * specified delay. Error notifications from the source Observable are not delayed. + * + * + * + * @param delay the delay to shift the source by + * @param scheduler the Scheduler to use for delaying + * @return the source Observable shifted in time by the specified delay + */ + def delay(delay: Duration, scheduler: Scheduler): Observable[T] = { + toScalaObservable[T](asJavaObservable.delay(delay.length, delay.unit, scheduler)) + } + + /** + * Return an Observable that delays the subscription to the source Observable by a given amount of time. + * + * + * + * @param delay the time to delay the subscription + * @return an Observable that delays the subscription to the source Observable by the given amount + */ + def delaySubscription(delay: Duration): Observable[T] = { + toScalaObservable[T](asJavaObservable.delaySubscription(delay.length, delay.unit)) + } + + /** + * Return an Observable that delays the subscription to the source Observable by a given amount of time, + * both waiting and subscribing on a given Scheduler. + * + * + * + * @param delay the time to delay the subscription + * @param scheduler the Scheduler on which the waiting and subscription will happen + * @return an Observable that delays the subscription to the source Observable by a given + * amount, waiting and subscribing on the given Scheduler + */ + def delaySubscription(delay: Duration, scheduler: Scheduler): Observable[T] = { + toScalaObservable[T](asJavaObservable.delaySubscription(delay.length, delay.unit, scheduler)) + } } /** From b8a3cad8b684177be06e65a0bad7850d8e7e9382 Mon Sep 17 00:00:00 2001 From: zsxwing Date: Mon, 10 Mar 2014 14:50:44 +0800 Subject: [PATCH 084/422] Reimplement the amb operator --- .../main/java/rx/operators/OperationAmb.java | 79 +++++++++---------- .../java/rx/operators/OperationAmbTest.java | 28 ++++--- 2 files changed, 54 insertions(+), 53 deletions(-) diff --git a/rxjava-core/src/main/java/rx/operators/OperationAmb.java b/rxjava-core/src/main/java/rx/operators/OperationAmb.java index 16a6c307dd..7f723825d9 100644 --- a/rxjava-core/src/main/java/rx/operators/OperationAmb.java +++ b/rxjava-core/src/main/java/rx/operators/OperationAmb.java @@ -20,24 +20,23 @@ import java.util.concurrent.atomic.AtomicInteger; import rx.Observable; -import rx.Observable.OnSubscribeFunc; +import rx.Observable.OnSubscribe; import rx.Observer; -import rx.Subscription; -import rx.subscriptions.CompositeSubscription; +import rx.Subscriber; /** * Propagates the observable sequence that reacts first. */ -public class OperationAmb { +public final class OperationAmb implements OnSubscribe{ - public static OnSubscribeFunc amb(Observable o1, Observable o2) { + public static OnSubscribe amb(Observable o1, Observable o2) { List> sources = new ArrayList>(); sources.add(o1); sources.add(o2); return amb(sources); } - public static OnSubscribeFunc amb(Observable o1, Observable o2, Observable o3) { + public static OnSubscribe amb(Observable o1, Observable o2, Observable o3) { List> sources = new ArrayList>(); sources.add(o1); sources.add(o2); @@ -45,7 +44,7 @@ public static OnSubscribeFunc amb(Observable o1, Observable< return amb(sources); } - public static OnSubscribeFunc amb(Observable o1, Observable o2, Observable o3, Observable o4) { + public static OnSubscribe amb(Observable o1, Observable o2, Observable o3, Observable o4) { List> sources = new ArrayList>(); sources.add(o1); sources.add(o2); @@ -54,7 +53,7 @@ public static OnSubscribeFunc amb(Observable o1, Observable< return amb(sources); } - public static OnSubscribeFunc amb(Observable o1, Observable o2, Observable o3, Observable o4, Observable o5) { + public static OnSubscribe amb(Observable o1, Observable o2, Observable o3, Observable o4, Observable o5) { List> sources = new ArrayList>(); sources.add(o1); sources.add(o2); @@ -64,7 +63,7 @@ public static OnSubscribeFunc amb(Observable o1, Observable< return amb(sources); } - public static OnSubscribeFunc amb(Observable o1, Observable o2, Observable o3, Observable o4, Observable o5, Observable o6) { + public static OnSubscribe amb(Observable o1, Observable o2, Observable o3, Observable o4, Observable o5, Observable o6) { List> sources = new ArrayList>(); sources.add(o1); sources.add(o2); @@ -75,7 +74,7 @@ public static OnSubscribeFunc amb(Observable o1, Observable< return amb(sources); } - public static OnSubscribeFunc amb(Observable o1, Observable o2, Observable o3, Observable o4, Observable o5, Observable o6, Observable o7) { + public static OnSubscribe amb(Observable o1, Observable o2, Observable o3, Observable o4, Observable o5, Observable o6, Observable o7) { List> sources = new ArrayList>(); sources.add(o1); sources.add(o2); @@ -87,7 +86,7 @@ public static OnSubscribeFunc amb(Observable o1, Observable< return amb(sources); } - public static OnSubscribeFunc amb(Observable o1, Observable o2, Observable o3, Observable o4, Observable o5, Observable o6, Observable o7, Observable o8) { + public static OnSubscribe amb(Observable o1, Observable o2, Observable o3, Observable o4, Observable o5, Observable o6, Observable o7, Observable o8) { List> sources = new ArrayList>(); sources.add(o1); sources.add(o2); @@ -100,7 +99,7 @@ public static OnSubscribeFunc amb(Observable o1, Observable< return amb(sources); } - public static OnSubscribeFunc amb(Observable o1, Observable o2, Observable o3, Observable o4, Observable o5, Observable o6, Observable o7, Observable o8, Observable o9) { + public static OnSubscribe amb(Observable o1, Observable o2, Observable o3, Observable o4, Observable o5, Observable o6, Observable o7, Observable o8, Observable o9) { List> sources = new ArrayList>(); sources.add(o1); sources.add(o2); @@ -114,40 +113,19 @@ public static OnSubscribeFunc amb(Observable o1, Observable< return amb(sources); } - public static OnSubscribeFunc amb( - final Iterable> sources) { - return new OnSubscribeFunc() { - - @Override - public Subscription onSubscribe(final Observer observer) { - AtomicInteger choice = new AtomicInteger(AmbObserver.NONE); - int index = 0; - CompositeSubscription parentSubscription = new CompositeSubscription(); - for (Observable source : sources) { - SafeObservableSubscription subscription = new SafeObservableSubscription(); - AmbObserver ambObserver = new AmbObserver( - subscription, observer, index, choice); - parentSubscription.add(subscription.wrap(source - .subscribe(ambObserver))); - index++; - } - return parentSubscription; - } - }; + public static OnSubscribe amb(final Iterable> sources) { + return new OperationAmb(sources); } - private static class AmbObserver implements Observer { + private static final class AmbSubscriber extends Subscriber { private static final int NONE = -1; - private Subscription subscription; private Observer observer; private int index; private AtomicInteger choice; - private AmbObserver(Subscription subscription, - Observer observer, int index, AtomicInteger choice) { - this.subscription = subscription; + private AmbSubscriber(Subscriber observer, int index, AtomicInteger choice) { this.observer = observer; this.choice = choice; this.index = index; @@ -156,7 +134,7 @@ private AmbObserver(Subscription subscription, @Override public void onNext(T args) { if (!isSelected()) { - subscription.unsubscribe(); + unsubscribe(); return; } observer.onNext(args); @@ -165,7 +143,7 @@ public void onNext(T args) { @Override public void onCompleted() { if (!isSelected()) { - subscription.unsubscribe(); + unsubscribe(); return; } observer.onCompleted(); @@ -174,7 +152,7 @@ public void onCompleted() { @Override public void onError(Throwable e) { if (!isSelected()) { - subscription.unsubscribe(); + unsubscribe(); return; } observer.onError(e); @@ -188,4 +166,25 @@ private boolean isSelected() { } } + private final Iterable> sources; + + private OperationAmb(Iterable> sources) { + this.sources = sources; + } + + @Override + public void call(Subscriber subscriber) { + AtomicInteger choice = new AtomicInteger(AmbSubscriber.NONE); + int index = 0; + for (Observable source : sources) { + if (subscriber.isUnsubscribed()) { + break; + } + AmbSubscriber ambSubscriber = new AmbSubscriber(subscriber, index, choice); + subscriber.add(ambSubscriber); + source.subscribe(ambSubscriber); + index++; + } + } + } diff --git a/rxjava-core/src/test/java/rx/operators/OperationAmbTest.java b/rxjava-core/src/test/java/rx/operators/OperationAmbTest.java index 42d48f30df..07010830ec 100644 --- a/rxjava-core/src/test/java/rx/operators/OperationAmbTest.java +++ b/rxjava-core/src/test/java/rx/operators/OperationAmbTest.java @@ -15,8 +15,10 @@ */ package rx.operators; -import static org.mockito.Mockito.*; -import static rx.operators.OperationAmb.*; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static rx.operators.OperationAmb.amb; import java.io.IOException; import java.util.concurrent.TimeUnit; @@ -26,10 +28,10 @@ import org.mockito.InOrder; import rx.Observable; -import rx.Observable.OnSubscribeFunc; +import rx.Observable.OnSubscribe; import rx.Observer; import rx.Scheduler.Inner; -import rx.Subscription; +import rx.Subscriber; import rx.functions.Action1; import rx.schedulers.TestScheduler; import rx.subscriptions.CompositeSubscription; @@ -45,17 +47,18 @@ public void setUp() { private Observable createObservable(final String[] values, final long interval, final Throwable e) { - return Observable.create(new OnSubscribeFunc() { + return Observable.create(new OnSubscribe() { @Override - public Subscription onSubscribe(final Observer observer) { + public void call(final Subscriber subscriber) { CompositeSubscription parentSubscription = new CompositeSubscription(); + subscriber.add(parentSubscription); long delay = interval; for (final String value : values) { parentSubscription.add(scheduler.schedule(new Action1() { @Override public void call(Inner inner) { - observer.onNext(value); + subscriber.onNext(value); } }, delay, TimeUnit.MILLISECONDS)); delay += interval; @@ -64,13 +67,12 @@ public void call(Inner inner) { @Override public void call(Inner inner) { if (e == null) { - observer.onCompleted(); + subscriber.onCompleted(); } else { - observer.onError(e); + subscriber.onError(e); } } }, delay, TimeUnit.MILLISECONDS)); - return parentSubscription; } }); } @@ -104,12 +106,12 @@ public void testAmb() { @Test public void testAmb2() { - IOException needHappenedException = new IOException( + IOException expectedException = new IOException( "fake exception"); Observable observable1 = createObservable(new String[] {}, 2000, new IOException("fake exception")); Observable observable2 = createObservable(new String[] { - "2", "22", "222", "2222" }, 1000, needHappenedException); + "2", "22", "222", "2222" }, 1000, expectedException); Observable observable3 = createObservable(new String[] {}, 3000, new IOException("fake exception")); @@ -127,7 +129,7 @@ public void testAmb2() { inOrder.verify(observer, times(1)).onNext("22"); inOrder.verify(observer, times(1)).onNext("222"); inOrder.verify(observer, times(1)).onNext("2222"); - inOrder.verify(observer, times(1)).onError(needHappenedException); + inOrder.verify(observer, times(1)).onError(expectedException); inOrder.verifyNoMoreInteractions(); } From cd84189b1c762f5a2861c1653808624e06f3a3da Mon Sep 17 00:00:00 2001 From: zsxwing Date: Mon, 10 Mar 2014 15:13:25 +0800 Subject: [PATCH 085/422] Rename OperationAmb to OperatorAmb --- rxjava-core/src/main/java/rx/Observable.java | 20 +++++++++---------- .../{OperationAmb.java => OperatorAmb.java} | 6 +++--- ...ationAmbTest.java => OperatorAmbTest.java} | 4 ++-- 3 files changed, 15 insertions(+), 15 deletions(-) rename rxjava-core/src/main/java/rx/operators/{OperationAmb.java => OperatorAmb.java} (97%) rename rxjava-core/src/test/java/rx/operators/{OperationAmbTest.java => OperatorAmbTest.java} (98%) diff --git a/rxjava-core/src/main/java/rx/Observable.java b/rxjava-core/src/main/java/rx/Observable.java index 5bfb5ad278..ab5405f916 100644 --- a/rxjava-core/src/main/java/rx/Observable.java +++ b/rxjava-core/src/main/java/rx/Observable.java @@ -52,7 +52,7 @@ import rx.operators.OnSubscribeFromIterable; import rx.operators.OnSubscribeRange; import rx.operators.OperationAll; -import rx.operators.OperationAmb; +import rx.operators.OperatorAmb; import rx.operators.OperationAny; import rx.operators.OperationAsObservable; import rx.operators.OperationAverage; @@ -300,7 +300,7 @@ public void call(Subscriber o) { * @see MSDN: Observable.Amb */ public final static Observable amb(Iterable> sources) { - return create(OperationAmb.amb(sources)); + return create(OperatorAmb.amb(sources)); } /** @@ -318,7 +318,7 @@ public final static Observable amb(IterableMSDN: Observable.Amb */ public final static Observable amb(Observable o1, Observable o2) { - return create(OperationAmb.amb(o1, o2)); + return create(OperatorAmb.amb(o1, o2)); } /** @@ -338,7 +338,7 @@ public final static Observable amb(Observable o1, Observable * @see MSDN: Observable.Amb */ public final static Observable amb(Observable o1, Observable o2, Observable o3) { - return create(OperationAmb.amb(o1, o2, o3)); + return create(OperatorAmb.amb(o1, o2, o3)); } /** @@ -360,7 +360,7 @@ public final static Observable amb(Observable o1, Observable * @see MSDN: Observable.Amb */ public final static Observable amb(Observable o1, Observable o2, Observable o3, Observable o4) { - return create(OperationAmb.amb(o1, o2, o3, o4)); + return create(OperatorAmb.amb(o1, o2, o3, o4)); } /** @@ -384,7 +384,7 @@ public final static Observable amb(Observable o1, Observable * @see MSDN: Observable.Amb */ public final static Observable amb(Observable o1, Observable o2, Observable o3, Observable o4, Observable o5) { - return create(OperationAmb.amb(o1, o2, o3, o4, o5)); + return create(OperatorAmb.amb(o1, o2, o3, o4, o5)); } /** @@ -410,7 +410,7 @@ public final static Observable amb(Observable o1, Observable * @see MSDN: Observable.Amb */ public final static Observable amb(Observable o1, Observable o2, Observable o3, Observable o4, Observable o5, Observable o6) { - return create(OperationAmb.amb(o1, o2, o3, o4, o5, o6)); + return create(OperatorAmb.amb(o1, o2, o3, o4, o5, o6)); } /** @@ -438,7 +438,7 @@ public final static Observable amb(Observable o1, Observable * @see MSDN: Observable.Amb */ public final static Observable amb(Observable o1, Observable o2, Observable o3, Observable o4, Observable o5, Observable o6, Observable o7) { - return create(OperationAmb.amb(o1, o2, o3, o4, o5, o6, o7)); + return create(OperatorAmb.amb(o1, o2, o3, o4, o5, o6, o7)); } /** @@ -468,7 +468,7 @@ public final static Observable amb(Observable o1, Observable * @see MSDN: Observable.Amb */ public final static Observable amb(Observable o1, Observable o2, Observable o3, Observable o4, Observable o5, Observable o6, Observable o7, Observable o8) { - return create(OperationAmb.amb(o1, o2, o3, o4, o5, o6, o7, o8)); + return create(OperatorAmb.amb(o1, o2, o3, o4, o5, o6, o7, o8)); } /** @@ -500,7 +500,7 @@ public final static Observable amb(Observable o1, Observable * @see MSDN: Observable.Amb */ public final static Observable amb(Observable o1, Observable o2, Observable o3, Observable o4, Observable o5, Observable o6, Observable o7, Observable o8, Observable o9) { - return create(OperationAmb.amb(o1, o2, o3, o4, o5, o6, o7, o8, o9)); + return create(OperatorAmb.amb(o1, o2, o3, o4, o5, o6, o7, o8, o9)); } /** diff --git a/rxjava-core/src/main/java/rx/operators/OperationAmb.java b/rxjava-core/src/main/java/rx/operators/OperatorAmb.java similarity index 97% rename from rxjava-core/src/main/java/rx/operators/OperationAmb.java rename to rxjava-core/src/main/java/rx/operators/OperatorAmb.java index 7f723825d9..9ca467fea9 100644 --- a/rxjava-core/src/main/java/rx/operators/OperationAmb.java +++ b/rxjava-core/src/main/java/rx/operators/OperatorAmb.java @@ -27,7 +27,7 @@ /** * Propagates the observable sequence that reacts first. */ -public final class OperationAmb implements OnSubscribe{ +public final class OperatorAmb implements OnSubscribe{ public static OnSubscribe amb(Observable o1, Observable o2) { List> sources = new ArrayList>(); @@ -114,7 +114,7 @@ public static OnSubscribe amb(Observable o1, Observable OnSubscribe amb(final Iterable> sources) { - return new OperationAmb(sources); + return new OperatorAmb(sources); } private static final class AmbSubscriber extends Subscriber { @@ -168,7 +168,7 @@ private boolean isSelected() { private final Iterable> sources; - private OperationAmb(Iterable> sources) { + private OperatorAmb(Iterable> sources) { this.sources = sources; } diff --git a/rxjava-core/src/test/java/rx/operators/OperationAmbTest.java b/rxjava-core/src/test/java/rx/operators/OperatorAmbTest.java similarity index 98% rename from rxjava-core/src/test/java/rx/operators/OperationAmbTest.java rename to rxjava-core/src/test/java/rx/operators/OperatorAmbTest.java index 07010830ec..323eda3ca5 100644 --- a/rxjava-core/src/test/java/rx/operators/OperationAmbTest.java +++ b/rxjava-core/src/test/java/rx/operators/OperatorAmbTest.java @@ -18,7 +18,7 @@ import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; -import static rx.operators.OperationAmb.amb; +import static rx.operators.OperatorAmb.amb; import java.io.IOException; import java.util.concurrent.TimeUnit; @@ -36,7 +36,7 @@ import rx.schedulers.TestScheduler; import rx.subscriptions.CompositeSubscription; -public class OperationAmbTest { +public class OperatorAmbTest { private TestScheduler scheduler; From a3f7a8359ff3df56047357cb1058bfa22f5c5aa7 Mon Sep 17 00:00:00 2001 From: zsxwing Date: Mon, 10 Mar 2014 22:27:39 +0800 Subject: [PATCH 086/422] Rename observer to subscriber and add a special logic to exit the loop --- .../main/java/rx/operators/OperatorAmb.java | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/rxjava-core/src/main/java/rx/operators/OperatorAmb.java b/rxjava-core/src/main/java/rx/operators/OperatorAmb.java index 9ca467fea9..c9b408546b 100644 --- a/rxjava-core/src/main/java/rx/operators/OperatorAmb.java +++ b/rxjava-core/src/main/java/rx/operators/OperatorAmb.java @@ -21,7 +21,6 @@ import rx.Observable; import rx.Observable.OnSubscribe; -import rx.Observer; import rx.Subscriber; /** @@ -121,12 +120,12 @@ private static final class AmbSubscriber extends Subscriber { private static final int NONE = -1; - private Observer observer; - private int index; - private AtomicInteger choice; + private final Subscriber subscriber; + private final int index; + private final AtomicInteger choice; - private AmbSubscriber(Subscriber observer, int index, AtomicInteger choice) { - this.observer = observer; + private AmbSubscriber(Subscriber subscriber, int index, AtomicInteger choice) { + this.subscriber = subscriber; this.choice = choice; this.index = index; } @@ -137,7 +136,7 @@ public void onNext(T args) { unsubscribe(); return; } - observer.onNext(args); + subscriber.onNext(args); } @Override @@ -146,7 +145,7 @@ public void onCompleted() { unsubscribe(); return; } - observer.onCompleted(); + subscriber.onCompleted(); } @Override @@ -155,7 +154,7 @@ public void onError(Throwable e) { unsubscribe(); return; } - observer.onError(e); + subscriber.onError(e); } private boolean isSelected() { @@ -180,6 +179,10 @@ public void call(Subscriber subscriber) { if (subscriber.isUnsubscribed()) { break; } + if (choice.get() != AmbSubscriber.NONE) { + // Already choose someone, the rest Observables can be skipped. + break; + } AmbSubscriber ambSubscriber = new AmbSubscriber(subscriber, index, choice); subscriber.add(ambSubscriber); source.subscribe(ambSubscriber); From 0979f84a1663f5a7b9ead5629022bb5d78defcd1 Mon Sep 17 00:00:00 2001 From: zsxwing Date: Mon, 10 Mar 2014 22:38:29 +0800 Subject: [PATCH 087/422] Fix docs --- .../rxjava-scala/src/main/scala/rx/lang/scala/Observable.scala | 3 --- 1 file changed, 3 deletions(-) diff --git a/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/Observable.scala b/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/Observable.scala index 75abe048a7..8dafa30e2b 100644 --- a/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/Observable.scala +++ b/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/Observable.scala @@ -2242,9 +2242,6 @@ trait Observable[+T] * * * - * You can combine items emitted by two Observables so that they act like a single - * Observable by using the `merge` method. - * * @param that * an Observable competing to react first * @return an Observable that emits the same sequence of items as whichever of `this` or `that` first emitted an item. From 29b5150d06593f266fb18e1aa5aea0bfd1c390f4 Mon Sep 17 00:00:00 2001 From: zsxwing Date: Mon, 10 Mar 2014 23:15:54 +0800 Subject: [PATCH 088/422] Change to a single 'get' call --- rxjava-core/src/main/java/rx/operators/OperatorAmb.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/rxjava-core/src/main/java/rx/operators/OperatorAmb.java b/rxjava-core/src/main/java/rx/operators/OperatorAmb.java index c9b408546b..a2bb92ebaf 100644 --- a/rxjava-core/src/main/java/rx/operators/OperatorAmb.java +++ b/rxjava-core/src/main/java/rx/operators/OperatorAmb.java @@ -158,10 +158,11 @@ public void onError(Throwable e) { } private boolean isSelected() { - if (choice.get() == NONE) { + int ch = choice.get(); + if (ch == NONE) { return choice.compareAndSet(NONE, index); } - return choice.get() == index; + return ch == index; } } From 29fde71d5a5bb95d6c589253db9a45d2ee80e7ce Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Mon, 10 Mar 2014 10:28:16 -0700 Subject: [PATCH 089/422] Version 0.17.0 --- CHANGES.md | 95 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 527d52ba3c..d89b86558f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,100 @@ # RxJava Releases # +### Version 0.17.0 ([Maven Central](http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22com.netflix.rxjava%22%20AND%20v%3A%220.17.0%22)) ### + +* [Pull 767](https://github.com/Netflix/RxJava/pull/767) Zip fix for multiple onCompleted and moved unsubscribe outside the lock. +* [Pull 770](https://github.com/Netflix/RxJava/pull/770) Bind Operator +* [Pull 778](https://github.com/Netflix/RxJava/pull/778) Fix zip race condition +* [Pull 784](https://github.com/Netflix/RxJava/pull/784) Lift and Observer+Subscription +* [Pull 793](https://github.com/Netflix/RxJava/pull/793) Observer + Subscriber +* [Pull 796](https://github.com/Netflix/RxJava/pull/796) Add Subscription.isUnsubscribed() +* [Pull 797](https://github.com/Netflix/RxJava/pull/797) Scheduler Outer/Inner [Preview] +* [Pull 805](https://github.com/Netflix/RxJava/pull/805) Fix CompositeException +* [Pull 785](https://github.com/Netflix/RxJava/pull/785) Reimplement Zip Operator Using Lift [Preview] +* [Pull 814](https://github.com/Netflix/RxJava/pull/814) RunAsync method for outputting multiple values +* [Pull 812](https://github.com/Netflix/RxJava/pull/812) Fixed OperationSubscribeOn so OperationConditionalsTest works again. +* [Pull 816](https://github.com/Netflix/RxJava/pull/816) One global onCompleted object +* [Pull 818](https://github.com/Netflix/RxJava/pull/818) CompositeSubscription memory reduction +* [Pull 817](https://github.com/Netflix/RxJava/pull/817) Scala Scheduler Bindings Fix +* [Pull 819](https://github.com/Netflix/RxJava/pull/819) CompositeSubscription performance increase +* [Pull 781](https://github.com/Netflix/RxJava/pull/781) Fixed buglet in join binding, simplified types +* [Pull 783](https://github.com/Netflix/RxJava/pull/783) Implement some Android UI related operators +* [Pull 821](https://github.com/Netflix/RxJava/pull/821) Update to use Subscriber/Subscriptions.create +* [Pull 826](https://github.com/Netflix/RxJava/pull/826) Return wrapped Subscription +* [Pull 824](https://github.com/Netflix/RxJava/pull/824) Set setDaemon on NewThreadScheduler +* [Pull 828](https://github.com/Netflix/RxJava/pull/828) Repeat Operator +* [Pull 827](https://github.com/Netflix/RxJava/pull/827) Fixed cut & paster error in io scheduler +* [Pull 833](https://github.com/Netflix/RxJava/pull/833) Take operator was breaking the unsubscribe chain +* [Pull 822](https://github.com/Netflix/RxJava/pull/822) Reimplement 'subscribeOn' using 'lift' +* [Pull 832](https://github.com/Netflix/RxJava/pull/832) Issue #831 Fix for OperationJoin race condition +* [Pull 834](https://github.com/Netflix/RxJava/pull/834) Update clojure for 0.17 +* [Pull 839](https://github.com/Netflix/RxJava/pull/839) Error Handling: OnErrorNotImplemented and java.lang.Error +* [Pull 838](https://github.com/Netflix/RxJava/pull/838) Make Scala OnCompleted Notification an object +* [Pull 837](https://github.com/Netflix/RxJava/pull/837) Perf with JMH +* [Pull 841](https://github.com/Netflix/RxJava/pull/841) Range OnSubscribe +* [Pull 842](https://github.com/Netflix/RxJava/pull/842) Test Unsubscribe +* [Pull 845](https://github.com/Netflix/RxJava/pull/845) Fix problem with Subscription +* [Pull 847](https://github.com/Netflix/RxJava/pull/847) Various Changes While Fixing GroupBy +* [Pull 849](https://github.com/Netflix/RxJava/pull/849) Add 'Fragment-Host' to rxjava-contrib modules for OSGi +* [Pull 851](https://github.com/Netflix/RxJava/pull/851) Reimplement the timeout operator and fix timeout bugs +* [Pull 846](https://github.com/Netflix/RxJava/pull/846) Added overloaded createRequest method that takes an HttpContext instance +* [Pull 777](https://github.com/Netflix/RxJava/pull/777) Fixed testSingleSourceManyIterators +* [Pull 852](https://github.com/Netflix/RxJava/pull/852) rxjava-debug +* [Pull 853](https://github.com/Netflix/RxJava/pull/853) StringObservable Update +* [Pull 763](https://github.com/Netflix/RxJava/pull/763) Added support for custom functions in combineLatest. +* [Pull 854](https://github.com/Netflix/RxJava/pull/854) The onCreate hook disappeared +* [Pull 857](https://github.com/Netflix/RxJava/pull/857) Change Lift to use rx.Observable.Operator +* [Pull 859](https://github.com/Netflix/RxJava/pull/859) Add 'Fragment-Host' to rxjava-contrib/debug module for OSGi +* [Pull 860](https://github.com/Netflix/RxJava/pull/860) Fixing the generics for merge and lift +* [Pull 863](https://github.com/Netflix/RxJava/pull/863) Optimize SwingMouseEventSource.fromRelativeMouseMotion +* [Pull 862](https://github.com/Netflix/RxJava/pull/862) Update the timeout docs +* [Pull 790](https://github.com/Netflix/RxJava/pull/790) Convert to scan to use lift +* [Pull 866](https://github.com/Netflix/RxJava/pull/866) Update OperationScan to OperatorScan +* [Pull 870](https://github.com/Netflix/RxJava/pull/870) Add the selector variants of timeout in RxScala +* [Pull 874](https://github.com/Netflix/RxJava/pull/874) Update CompositeSubscriptionTest.java +* [Pull 869](https://github.com/Netflix/RxJava/pull/869) subscribeOn + groupBy +* [Pull 751](https://github.com/Netflix/RxJava/pull/751) Provide Observable.timestamp(Scheduler) to be used in the tests. +* [Pull 878](https://github.com/Netflix/RxJava/pull/878) Scheduler.scheduleRecursive +* [Pull 877](https://github.com/Netflix/RxJava/pull/877) Correct synchronization guard in groupByUntil +* [Pull 880](https://github.com/Netflix/RxJava/pull/880) Force ViewObservable be subscribed and unsubscribed in the UI thread +* [Pull 887](https://github.com/Netflix/RxJava/pull/887) Remove Bad Filter Logic +* [Pull 890](https://github.com/Netflix/RxJava/pull/890) Split SubscribeOn into SubscribeOn/UnsubscribeOn +* [Pull 891](https://github.com/Netflix/RxJava/pull/891) Eliminate rx.util.* dumping grounds +* [Pull 881](https://github.com/Netflix/RxJava/pull/881) Lift Performance +* [Pull 893](https://github.com/Netflix/RxJava/pull/893) Change Parallel to use Long instead of Int +* [Pull 894](https://github.com/Netflix/RxJava/pull/894) Synchronized Operator Check for isTerminated +* [Pull 885](https://github.com/Netflix/RxJava/pull/885) Fixed an issue with the from(Reader) added a bunch of unit tests. +* [Pull 896](https://github.com/Netflix/RxJava/pull/896) removing java 7 dep +* [Pull 883](https://github.com/Netflix/RxJava/pull/883) Make Subscriptions of SwingObservable thread-safe +* [Pull 895](https://github.com/Netflix/RxJava/pull/895) Rewrite OperationObserveFromAndroidComponent to OperatorObserveFromAndroid +* [Pull 892](https://github.com/Netflix/RxJava/pull/892) onErrorFlatMap + OnErrorThrowable +* [Pull 898](https://github.com/Netflix/RxJava/pull/898) Handle illegal errors thrown from plugin +* [Pull 901](https://github.com/Netflix/RxJava/pull/901) GroupBy Unit Test from #900 +* [Pull 902](https://github.com/Netflix/RxJava/pull/902) Fixed NullPointerException that may happen on timeout +* [Pull 903](https://github.com/Netflix/RxJava/pull/903) Scheduler.Recurse fields should be private +* [Pull 904](https://github.com/Netflix/RxJava/pull/904) Merge: Unsubscribe Completed Inner Observables +* [Pull 905](https://github.com/Netflix/RxJava/pull/905) RxJavaSchedulers Plugin +* [Pull 909](https://github.com/Netflix/RxJava/pull/909) Scheduler Plugin Refactor +* [Pull 910](https://github.com/Netflix/RxJava/pull/910) Remove groupBy with selector +* [Pull 918](https://github.com/Netflix/RxJava/pull/918) Operator: doOnTerminate +* [Pull 919](https://github.com/Netflix/RxJava/pull/919) BugFix: Zip Never Completes When Zero Observables +* [Pull 920](https://github.com/Netflix/RxJava/pull/920) Delete Deprecated onSubscribeStart That Doesn't Work +* [Pull 922](https://github.com/Netflix/RxJava/pull/922) Changes made while integrating it with our internal system +* [Pull 924](https://github.com/Netflix/RxJava/pull/924) Localized Operator Error Handling +* [Pull 925](https://github.com/Netflix/RxJava/pull/925) Rxjava clojure bindings final +* [Pull 926](https://github.com/Netflix/RxJava/pull/926) TestSubscriber: Default onError and Terminal Latch Behavior +* [Pull 927](https://github.com/Netflix/RxJava/pull/927) TestSubscriber lastSeenThread +* [Pull 936](https://github.com/Netflix/RxJava/pull/936) Skip fixed +* [Pull 942](https://github.com/Netflix/RxJava/pull/942) MathObservable +* [Pull 944](https://github.com/Netflix/RxJava/pull/944) OperationRetry -> OperatorRetry +* [Pull 945](https://github.com/Netflix/RxJava/pull/945) refactor the debug hooks before they become a breaking change. +* [Pull 934](https://github.com/Netflix/RxJava/pull/934) add Observable.startWith(Observable) method and unit test +* [Pull 929](https://github.com/Netflix/RxJava/pull/929) correct link to maven search +* [Pull 923](https://github.com/Netflix/RxJava/pull/923) Observable creation from Subscriber[T]=>Unit for Scala +* [Pull 931](https://github.com/Netflix/RxJava/pull/931) A number of improvements to OperatorObserveFromAndroidComponent +* [Pull 950](https://github.com/Netflix/RxJava/pull/950) Add support for Eclipse PDE + + ### Version 0.16.1 ([Maven Central](http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22com.netflix.rxjava%22%20AND%20v%3A%220.16.1%22)) ### * [Pull 730](https://github.com/Netflix/RxJava/pull/730) Improve Error Handling and Stacktraces When Unsubscribe Fails From 7983e575d72ca854ae392152139afff13f265e36 Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Mon, 10 Mar 2014 10:44:23 -0700 Subject: [PATCH 090/422] 0.17.0 Release Notes --- CHANGES.md | 641 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 641 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index d89b86558f..9fd9365484 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,647 @@ ### Version 0.17.0 ([Maven Central](http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22com.netflix.rxjava%22%20AND%20v%3A%220.17.0%22)) ### + +# 0.17.0 Release Notes + + +Version 0.17.0 contains some significant signature changes that allow us to significantly improve handling of synchronous Observables and simplify Schedulers. Many of the changes have backwards compatible deprecated methods to ease the migration while some are breaking. + +The new signatures related to `Observable` in this release are: + +```java +// A new create method takes `OnSubscribe` instead of `OnSubscribeFunc` +public final static Observable create(OnSubscribe f) + +// The new OnSubscribe type accepts a Subscriber instead of Observer and does not return a Subscription +public static interface OnSubscribe extends Action1> + +// Subscriber is an Observer + Subscription +public abstract class Subscriber implements Observer, Subscription + +// The main `subscribe` behavior receives a Subscriber instead of Observer +public final Subscription subscribe(Subscriber subscriber) + +// Subscribing with an Observer however is still appropriate +// and the Observer is automatically converted into a Subscriber +public final Subscription subscribe(Observer observer) + +// A new 'lift' function allows composing Operator implementations together +public Observable lift(final Operator lift) + +// The `Operator` used with `lift` +public interface Operator extends Func1, Subscriber> + +``` + +Also changed is the `Scheduler` interface which is much simpler: + +```java +public abstract class Scheduler { + public Subscription schedule(Action1 action); + public Subscription schedule(Action1 action, long delayTime, TimeUnit unit); + public Subscription schedulePeriodically(Action1 action, long initialDelay, long period, TimeUnit unit); + public final Subscription scheduleRecursive(final Action1 action) + public long now(); + public int degreeOfParallelism(); + + public static class Inner implements Subscription { + public abstract void schedule(Action1 action, long delayTime, TimeUnit unit); + public abstract void schedule(Action1 action); + public long now(); + } + + public static final class Recurse { + public final void schedule(); + public final void schedule(long delay, TimeUnit unit); + } +} +``` + + +This release applies many lessons learned over the past year and seeks to streamline the API before we hit 1.0. + +As shown in the code above the changes fall into 2 major sections: + +#### 1) Lift/Operator/OnSubscribe/Subscriber + +Changes that allow unsubscribing from synchronous Observables without needing to add concurrency. + +#### 2) Schedulers + +Simplification of the `Scheduler` interface and make clearer the concept of "outer" and "inner" Schedulers for recursion. + + +## Lift/Operator/OnSubscribe/Subscriber + +New types `Subscriber` and `OnSubscribe` along with the new `lift` function have been added. The reasons and benefits are as follows: + +### 1) Synchronous Unsubscribe + +RxJava versions up until 0.16.x are unable to unsubscribe from a synchronous Observable such as this: + +```java +Observable oi = Observable.create(new OnSubscribe() { + + @Override + public void call(Observer Observer) { + for (int i = 1; i < 1000000; i++) { + subscriber.onNext(i); + } + subscriber.onCompleted(); + } +}); +``` + +Subscribing to this `Observable` will always emit all 1,000,000 values even if unsubscribed such as via `oi.take(10)`. + +Version 0.17.0 fixes this issue by injecting the `Subscription` into the `OnSubscribe` function to allow code like this: + +```java +Observable oi = Observable.create(new OnSubscribe() { + + @Override + public void call(Subscriber subscriber) { + // we now receive a Subscriber instead of Observer + for (int i = 1; i < 1000000; i++) { + // the OnSubscribe can now check for isUnsubscribed + if (subscriber.isUnsubscribed()) { + return; + } + subscriber.onNext(i); + } + subscriber.onCompleted(); + } + +}); +``` + +Subscribing to this will now correctly only emit 10 `onNext` and unsubscribe: + +```java +// subscribe with an Observer +oi.take(10).subscribe(new Observer() { + + @Override + public void onCompleted() { + + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onNext(Integer t) { + println("Received: " + t); + } + +}) +``` + +Or the new `Subscriber` type can be used and the `Subscriber` itself can `unsubscribe`: + +```java +// or subscribe with a Subscriber which supports unsubscribe +oi.subscribe(new Subscriber() { + + @Override + public void onCompleted() { + + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onNext(Integer t) { + println("Received: " + t); + if(t >= 10) { + // a Subscriber can unsubscribe + this.unsubscribe(); + } + } + +}) +``` + + +### 2) Custom Operator Chaining + +Because Java doesn't support extension methods, the only approach to applying custom operators without getting them added to `rx.Observable` is using static methods. This has meant code like this: + +```java +MyCustomerOperators.operate(observable.map(...).filter(...).take(5)).map(...).subscribe() +``` + +In reality we want: + +```java +observable.map(...).filter(...).take(5).myCustomOperator().map(...).subscribe() +``` + +Using the newly added `lift` we can get quite close to this: + + +```java +observable.map(...).filter(...).take(5).lift(MyCustomOperator.operate()).map(...).subscribe() +``` + +Here is how the proposed `lift` method looks if all operators were applied with it: + +```java +Observable os = OBSERVABLE_OF_INTEGERS.lift(TAKE_5).lift(MAP_INTEGER_TO_STRING); +``` + +Along with the `lift` function comes a new `Operator` signature: + +```java +public interface Operator extends Func1, Subscriber> +``` + +All operator implementations in the `rx.operators` package will over time be migrated to this new signature. + +NOTE: Operators that have not yet been migrated do not work with synchronous unsubscribe. + + +### 3) Simpler Operator Implementations + +The `lift` operator injects the necessary `Observer` and `Subscription` instances (via the new `Subscriber` type) and eliminates (for most use cases) the need for manual subscription management. Because the `Subscription` is available in-scope there are no awkward coding patterns needed for creating a `Subscription`, closing over it and returning and taking into account synchronous vs asynchronous. + +For example, the body of `fromIterable` is simply: + +```java +public void call(Subscriber o) { + for (T i : is) { + if (o.isUnsubscribed()) { + return; + } + o.onNext(i); + } + o.onCompleted(); +} +``` + +The `take` operator is: + +```java +public Subscriber call(final Subscriber child) { + final CompositeSubscription parent = new CompositeSubscription(); + if (limit == 0) { + child.onCompleted(); + parent.unsubscribe(); + } + + child.add(parent); + return new Subscriber(parent) { + + int count = 0; + boolean completed = false; + + @Override + public void onCompleted() { + if (!completed) { + child.onCompleted(); + } + } + + @Override + public void onError(Throwable e) { + if (!completed) { + child.onError(e); + } + } + + @Override + public void onNext(T i) { + if (!isUnsubscribed()) { + child.onNext(i); + if (++count >= limit) { + completed = true; + child.onCompleted(); + unsubscribe(); + } + } + } + + }; + } +``` + + +### 4) Recursion/Loop Performance with Unsubscribe + +The `fromIterable` use case is 20x faster when implemented as a loop instead of recursive scheduler (see https://github.com/Netflix/RxJava/commit/a18b8c1a572b7b9509b7a7fe1a5075ce93657771). + +Several places we can remove recursive scheduling used originally for unsubscribe support and use a loop instead. + + + + + +## Schedulers + + +Schedulers were greatly simplified to a design based around `Action1`. + +```java +public abstract class Scheduler { + public Subscription schedule(Action1 action); + public Subscription schedule(Action1 action, long delayTime, TimeUnit unit); + public Subscription schedulePeriodically(Action1 action, long initialDelay, long period, TimeUnit unit); + public final Subscription scheduleRecursive(final Action1 action) + public long now(); + public int degreeOfParallelism(); + + public static class Inner implements Subscription { + public abstract void schedule(Action1 action, long delayTime, TimeUnit unit); + public abstract void schedule(Action1 action); + public long now(); + } + + public static final class Recurse { + public final void schedule(); + public final void schedule(long delay, TimeUnit unit); + } +} +``` + +This design change originated from three findings: + +1) It was very easy to cause memory leaks or inadvertent parallel execution since the distinction between outer and inner scheduling was not obvious. + +To solve this the new design explicitly has the outer `Scheduler` and then `Scheduler.Inner` for recursion. + +2) The passing of state is not useful since scheduling over network boundaries with this model does not work. + +In this new design all state passing signatures have been removed. This was determined while implementing a `RemoteScheduler` that attempted to use `observeOn` to transition execution from one machine to another. This does not work because of the requirement for serializing/deserializing the state of the entire execution stack. Migration of work over the network has been bound to be better suited to explicit boundaries established by Subjects. Thus, the complications within the Schedulers are unnecessary. + +3) The number of overloads with different ways of doing the same things were confusing. + +This new design removes all but the essential and simplest methods. + +4) A scheduled task could not do work in a loop and easily be unsubscribed which generally meant less efficient recursive scheduling. + +This new design applies similar principles as done with `lift`/`create`/`OnSubscribe`/`Subscriber` and injects the `Subscription` via the `Inner` interface so a running task can check `isUnsubscribed()`. + + + +WIth this new design, the simplest execution of a single task is: + +```java +Schedulers.newThread().schedule(new Action1() { + + @Override + public void call(Inner inner) { + doWork(); + } + +}); +``` + +Recursion is easily invoked like this: + +```java +Schedulers.newThread().scheduleRecursive(new Action1() { + + @Override + public void call(Recurse recurse) { + doWork(); + // recurse until unsubscribed (the schedule will do nothing if unsubscribed) + recurse.schedule(); + } + + +}); +``` + +or like this if the outer and inner actions need different behavior: + +```java +Schedulers.newThread().schedule(new Action1() { + + @Override + public void call(Inner inner) { + doWork(); + // recurse until unsubscribed (the schedule will do nothing if unsubscribed) + inner.schedule(this); + } + +}); + + +The use of `Action1` on both the outer and inner levels makes it so recursion that refer to `this` and it works easily. + + +Similar to the new `lift`/`create` pattern with `Subscriber` the `Inner` is also a `Subscription` so it allows efficient loops with `unsubscribe` support: + +```java +Schedulers.newThread().schedule(new Action1() { + + @Override + public void call(Inner inner) { + while(!inner.isUnsubscribed()) { + doWork(); + } + } + +}); +``` + +An action can now `unsubscribe` the `Scheduler.Inner`: + +```java +Schedulers.newThread().schedule(new Action1() { + + @Override + public void call(Inner inner) { + while(!inner.isUnsubscribed()) { + int i = doOtherWork(); + if(i > 100) { + // an Action can cause the Scheduler to unsubscribe and stop + inner.unsubscribe(); + } + } + } + +}); +``` + +Typically just stopping is sufficient: + +```java +Schedulers.newThread().schedule(new Action1() { + + @Override + public void call(Inner inner) { + int i = doOtherWork(); + if (i < 10) { + // recurse until done 10 + inner.schedule(this); + } + } + +}); +``` + +but if other work in other tasks is being done and you want to unsubscribe conditionally you could: + +```java +Schedulers.newThread().schedule(new Action1() { + + @Override + public void call(Inner inner) { + int i = doOtherWork(); + if (i < 10) { + // recurse until done 10 + inner.schedule(this); + } else { + inner.unsubscribe(); + } + } + +}); +``` + +and the recursion can be delayed: + +```java +Schedulers.newThread().schedule(new Action1() { + + @Override + public void call(Inner inner) { + doWork(); + // recurse until unsubscribed ... but delay the recursion + inner.schedule(this, 500, TimeUnit.MILLISECONDS); + } + +}); +``` + +The same pattern works with the `Recurse` signature: + +```java +Schedulers.newThread().scheduleRecursive(new Action1() { + + @Override + public void call(Recurse recurse) { + doWork(); + // recurse until unsubscribed (the schedule will do nothing if unsubscribed) + recurse.schedule(500, TimeUnit.MILLISECONDS); + } + + +}); +`` + +The methods on the `Inner` never return a `Subscription` because they are always a single thread/event-loop/actor/etc and controlled by the `Subscription` returned by the initial `Scheduler.schedule` method. This is part of clarifying the contract. + +Thus an `unsubscribe` controlled from the outside would be done like this: + +```java +Subscription s = Schedulers.newThread().schedule(new Action1() { + + @Override + public void call(Inner inner) { + while(!inner.isUnsubscribed()) { + doWork(); + } + } + +}); + +// unsubscribe from outside +s.unsubscribe(); +``` + + + +## Migration Path + +#### 1) Lift/OnSubscribe/Subscriber + +The `lift` function will not be used by most and is additive so will not affect backwards compatibility. The `Subscriber` type is also additive and for most use cases does not need to be used directly, the `Observer` interface can continue being used. + +The previous `create(OnSubscribeFunc f)` signature has been deprecated so code will work but now have warnings. Please begin migrating code as this will be deleted prior to the 1.0 release. + +Code such as this: + +```java +Observable.create(new OnSubscribeFunc() { + + @Override + public Subscription onSubscribe(Observer o) { + o.onNext(1); + o.onCompleted(); + return Subscriptions.empty(); + } +}); +``` + +should change to this: + +```java +Observable.create(new OnSubscribe() { + + @Override + public void call(Subscriber subscriber) { + subscriber.onNext(1); + subscriber.onCompleted(); + } +}); +``` + +If concurrency was being injected to allow unsubscribe support: + +```java +Observable.create(new OnSubscribeFunc() { + + @Override + public Subscription onSubscribe(final Observer o) { + final BooleanSubscription s = new BooleanSubscription(); + Thread t = new Thread(new Runnable() { + + @Override + public void run() { + int i = 0; + while (s.isUnsubscribed()) { + o.onNext(i++); + } + } + + }); + t.start(); + return s; + } +}); +``` + +you may no longer need it and can implement like this instead: + +```java +Observable.create(new OnSubscribe() { + + @Override + public void call(Subscriber subscriber) { + int i = 0; + while (subscriber.isUnsubscribed()) { + subscriber.onNext(i++); + } + } +}); +``` + +or if the concurreny is still desired you can simplify the `Subscription` management: + +```java +Observable.create(new OnSubscribe() { + + @Override + public void call(final Subscriber subscriber) { + Thread t = new Thread(new Runnable() { + + @Override + public void run() { + int i = 0; + while (subscriber.isUnsubscribed()) { + subscriber.onNext(i++); + } + } + + }); + t.start(); + } +}); +``` + +or use `subscribeOn` which now works to make synchronous `Observables` async while supporting `unsubscribe` (this didn't work before): + +```java +Observable.create(new OnSubscribe() { + + @Override + public void call(Subscriber subscriber) { + int i = 0; + while (subscriber.isUnsubscribed()) { + subscriber.onNext(i++); + } + } +}).subscribeOn(Schedulers.newThread()); +``` + + + +#### 2) Schedulers + +Custom `Scheduler` implementations will need to be re-implemented and any direct use of the `Scheduler` interface will also need to be updated. + + +##### 3) Subscription + +If you have custom `Subscription` implementations you will see they now need an `isUnsubscribed()` method. + +You can either add this method, or wrap your function using `Subscriptions.create` and it will handle the `isUnsubscribed` behavior and execute your function when `unsubscribe()` is called. + +It is recommended to use `Subscriptions.create` for most `Subscription` usage. + + + +# The Future... + +We have most if not all operators from Rx.Net that we want or intend to port. We think we have got the `create`/`subscribe` signatures as we want and the `Subscription` and `Scheduler` interfaces are now clean. There is at least one more major topic related to back pressure that may result in signature change in a future release. Beyond that no further major signature changing work is expected prior to 1.0. + +We still need to improve on some of the `Subject` implementations still, particularly `ReplaySubject`. We are beginning to focus after this release on cleaning up all of the operator implementations, stabilizing, fixing bugs and performance tuning. + +As we get closer to 1.0 there will be a release that focused on deleting all deprecated methods so it is suggested to start migrating off of them. + +We appreciate your usage, feedback and contributions and hope the library is creating value for you! + + + + * [Pull 767](https://github.com/Netflix/RxJava/pull/767) Zip fix for multiple onCompleted and moved unsubscribe outside the lock. * [Pull 770](https://github.com/Netflix/RxJava/pull/770) Bind Operator * [Pull 778](https://github.com/Netflix/RxJava/pull/778) Fix zip race condition From 8166d5da219bdd7529ad749e266dfab1c0c8b227 Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Mon, 10 Mar 2014 10:48:12 -0700 Subject: [PATCH 091/422] Version 0.17.0 Release Notes --- CHANGES.md | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 9fd9365484..9451d876a4 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,9 +3,6 @@ ### Version 0.17.0 ([Maven Central](http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22com.netflix.rxjava%22%20AND%20v%3A%220.17.0%22)) ### -# 0.17.0 Release Notes - - Version 0.17.0 contains some significant signature changes that allow us to significantly improve handling of synchronous Observables and simplify Schedulers. Many of the changes have backwards compatible deprecated methods to ease the migration while some are breaking. The new signatures related to `Observable` in this release are: @@ -64,20 +61,20 @@ This release applies many lessons learned over the past year and seeks to stream As shown in the code above the changes fall into 2 major sections: -#### 1) Lift/Operator/OnSubscribe/Subscriber +##### 1) Lift/Operator/OnSubscribe/Subscriber Changes that allow unsubscribing from synchronous Observables without needing to add concurrency. -#### 2) Schedulers +##### 2) Schedulers Simplification of the `Scheduler` interface and make clearer the concept of "outer" and "inner" Schedulers for recursion. -## Lift/Operator/OnSubscribe/Subscriber +#### Lift/Operator/OnSubscribe/Subscriber New types `Subscriber` and `OnSubscribe` along with the new `lift` function have been added. The reasons and benefits are as follows: -### 1) Synchronous Unsubscribe +##### 1) Synchronous Unsubscribe RxJava versions up until 0.16.x are unable to unsubscribe from a synchronous Observable such as this: @@ -170,7 +167,7 @@ oi.subscribe(new Subscriber() { ``` -### 2) Custom Operator Chaining +##### 2) Custom Operator Chaining Because Java doesn't support extension methods, the only approach to applying custom operators without getting them added to `rx.Observable` is using static methods. This has meant code like this: @@ -208,7 +205,7 @@ All operator implementations in the `rx.operators` package will over time be mig NOTE: Operators that have not yet been migrated do not work with synchronous unsubscribe. -### 3) Simpler Operator Implementations +##### 3) Simpler Operator Implementations The `lift` operator injects the necessary `Observer` and `Subscription` instances (via the new `Subscriber` type) and eliminates (for most use cases) the need for manual subscription management. Because the `Subscription` is available in-scope there are no awkward coding patterns needed for creating a `Subscription`, closing over it and returning and taking into account synchronous vs asynchronous. @@ -273,7 +270,7 @@ public Subscriber call(final Subscriber child) { ``` -### 4) Recursion/Loop Performance with Unsubscribe +##### 4) Recursion/Loop Performance with Unsubscribe The `fromIterable` use case is 20x faster when implemented as a loop instead of recursive scheduler (see https://github.com/Netflix/RxJava/commit/a18b8c1a572b7b9509b7a7fe1a5075ce93657771). @@ -283,7 +280,7 @@ Several places we can remove recursive scheduling used originally for unsubscrib -## Schedulers +#### Schedulers Schedulers were greatly simplified to a design based around `Action1`. @@ -500,9 +497,9 @@ s.unsubscribe(); -## Migration Path +#### Migration Path -#### 1) Lift/OnSubscribe/Subscriber +##### 1) Lift/OnSubscribe/Subscriber The `lift` function will not be used by most and is additive so will not affect backwards compatibility. The `Subscriber` type is also additive and for most use cases does not need to be used directly, the `Observer` interface can continue being used. @@ -615,7 +612,7 @@ Observable.create(new OnSubscribe() { -#### 2) Schedulers +##### 2) Schedulers Custom `Scheduler` implementations will need to be re-implemented and any direct use of the `Scheduler` interface will also need to be updated. @@ -630,7 +627,7 @@ It is recommended to use `Subscriptions.create` for most `Subscription` usage. -# The Future... +#### The Future... We have most if not all operators from Rx.Net that we want or intend to port. We think we have got the `create`/`subscribe` signatures as we want and the `Subscription` and `Scheduler` interfaces are now clean. There is at least one more major topic related to back pressure that may result in signature change in a future release. Beyond that no further major signature changing work is expected prior to 1.0. @@ -641,6 +638,7 @@ As we get closer to 1.0 there will be a release that focused on deleting all dep We appreciate your usage, feedback and contributions and hope the library is creating value for you! +#### Pull Requests * [Pull 767](https://github.com/Netflix/RxJava/pull/767) Zip fix for multiple onCompleted and moved unsubscribe outside the lock. From f5eaa1c3e00a5e695b45be31611867dfbf0c0f75 Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Mon, 10 Mar 2014 10:49:10 -0700 Subject: [PATCH 092/422] Update CHANGES.md --- CHANGES.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 9451d876a4..8a9c54ef7e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -369,7 +369,7 @@ Schedulers.newThread().schedule(new Action1() { } }); - +``` The use of `Action1` on both the outer and inner levels makes it so recursion that refer to `this` and it works easily. @@ -473,7 +473,7 @@ Schedulers.newThread().scheduleRecursive(new Action1() { }); -`` +``` The methods on the `Inner` never return a `Subscription` because they are always a single thread/event-loop/actor/etc and controlled by the `Subscription` returned by the initial `Scheduler.schedule` method. This is part of clarifying the contract. From dd2bd02365c00ffc056dfd7978ba31d87328a1b8 Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Mon, 10 Mar 2014 10:51:04 -0700 Subject: [PATCH 093/422] 0.17.0-SNAPSHOT --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 2e8a8bdd53..40da016b78 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -version=0.17.0-RC8-SNAPSHOT +version=0.17.0-SNAPSHOT From 04a6703e9eaa22e7784dcfe8562b09b22c89c6d1 Mon Sep 17 00:00:00 2001 From: Bob T Builder Date: Mon, 10 Mar 2014 18:01:12 +0000 Subject: [PATCH 094/422] [Gradle Release Plugin] - pre tag commit: '0.17.0'. --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 40da016b78..9f28a3ffc2 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -version=0.17.0-SNAPSHOT +version=0.17.0 From 5425b32aa8519cae5c0adda22cf630296b2f13ad Mon Sep 17 00:00:00 2001 From: Bob T Builder Date: Mon, 10 Mar 2014 18:01:16 +0000 Subject: [PATCH 095/422] [Gradle Release Plugin] - new version commit: '0.17.1-SNAPSHOT'. --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 9f28a3ffc2..641a9d1d9b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -version=0.17.0 +version=0.17.1-SNAPSHOT From a870624a9fc0c8ec0216acd41818ba570149c087 Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Mon, 10 Mar 2014 12:42:51 -0700 Subject: [PATCH 096/422] Update CHANGES.md --- CHANGES.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 8a9c54ef7e..1c7481a9bb 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -545,7 +545,7 @@ Observable.create(new OnSubscribeFunc() { @Override public void run() { int i = 0; - while (s.isUnsubscribed()) { + while (!s.isUnsubscribed()) { o.onNext(i++); } } @@ -565,7 +565,7 @@ Observable.create(new OnSubscribe() { @Override public void call(Subscriber subscriber) { int i = 0; - while (subscriber.isUnsubscribed()) { + while (!subscriber.isUnsubscribed()) { subscriber.onNext(i++); } } @@ -584,7 +584,7 @@ Observable.create(new OnSubscribe() { @Override public void run() { int i = 0; - while (subscriber.isUnsubscribed()) { + while (!subscriber.isUnsubscribed()) { subscriber.onNext(i++); } } @@ -603,7 +603,7 @@ Observable.create(new OnSubscribe() { @Override public void call(Subscriber subscriber) { int i = 0; - while (subscriber.isUnsubscribed()) { + while (!subscriber.isUnsubscribed()) { subscriber.onNext(i++); } } From c0c2e8b77e4b01954fcfd712fdefd90e25b387e5 Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Mon, 10 Mar 2014 16:42:23 -0700 Subject: [PATCH 097/422] Make OperatorObserveOnTest.testNonBlockingOuterWhileBlockingOnNext deterministic reported at https://twitter.com/jaceklaskowski/status/443153927069249536 --- .../rx/operators/OperatorObserveOnTest.java | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/rxjava-core/src/test/java/rx/operators/OperatorObserveOnTest.java b/rxjava-core/src/test/java/rx/operators/OperatorObserveOnTest.java index 569aee115a..037060298b 100644 --- a/rxjava-core/src/test/java/rx/operators/OperatorObserveOnTest.java +++ b/rxjava-core/src/test/java/rx/operators/OperatorObserveOnTest.java @@ -309,16 +309,17 @@ public void call(Integer t1) { @Test public void testNonBlockingOuterWhileBlockingOnNext() throws InterruptedException { - final CountDownLatch latch = new CountDownLatch(1); + final CountDownLatch completedLatch = new CountDownLatch(1); + final CountDownLatch nextLatch = new CountDownLatch(1); final AtomicLong completeTime = new AtomicLong(); // use subscribeOn to make async, observeOn to move - Observable.range(1, 1000).subscribeOn(Schedulers.newThread()).observeOn(Schedulers.newThread()).subscribe(new Observer() { + Observable.range(1, 2).subscribeOn(Schedulers.newThread()).observeOn(Schedulers.newThread()).subscribe(new Observer() { @Override public void onCompleted() { System.out.println("onCompleted"); completeTime.set(System.nanoTime()); - latch.countDown(); + completedLatch.countDown(); } @Override @@ -328,20 +329,27 @@ public void onError(Throwable e) { @Override public void onNext(Integer t) { - + // don't let this thing finish yet + try { + if (!nextLatch.await(1000, TimeUnit.MILLISECONDS)) { + throw new RuntimeException("it shouldn't have timed out"); + } + } catch (InterruptedException e) { + throw new RuntimeException("it shouldn't have failed"); + } } }); long afterSubscribeTime = System.nanoTime(); - System.out.println("After subscribe: " + latch.getCount()); - assertEquals(1, latch.getCount()); - latch.await(); + System.out.println("After subscribe: " + completedLatch.getCount()); + assertEquals(1, completedLatch.getCount()); + nextLatch.countDown(); + completedLatch.await(1000, TimeUnit.MILLISECONDS); assertTrue(completeTime.get() > afterSubscribeTime); System.out.println("onComplete nanos after subscribe: " + (completeTime.get() - afterSubscribeTime)); } - private static int randomIntFrom0to100() { // XORShift instead of Math.random http://javamex.com/tutorials/random_numbers/xorshift.shtml long x = System.nanoTime(); From 731e0a024ec4747aea706f03b61bcd4bc71afe16 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Tue, 11 Mar 2014 14:36:51 +0100 Subject: [PATCH 098/422] Fixed ReplaySubject leak --- .../src/main/java/rx/subjects/AsyncSubject.java | 2 +- .../main/java/rx/subjects/BehaviorSubject.java | 2 +- .../main/java/rx/subjects/PublishSubject.java | 2 +- .../src/main/java/rx/subjects/ReplaySubject.java | 16 ++++++++++++++-- .../rx/subjects/SubjectSubscriptionManager.java | 10 ++++++++-- .../test/java/rx/subjects/ReplaySubjectTest.java | 11 +++++++++++ 6 files changed, 36 insertions(+), 7 deletions(-) diff --git a/rxjava-core/src/main/java/rx/subjects/AsyncSubject.java b/rxjava-core/src/main/java/rx/subjects/AsyncSubject.java index 268628b013..7b50cd9468 100644 --- a/rxjava-core/src/main/java/rx/subjects/AsyncSubject.java +++ b/rxjava-core/src/main/java/rx/subjects/AsyncSubject.java @@ -82,7 +82,7 @@ public void call(SubjectObserver o) { // to send onCompleted if the last value is an onNext emitValueToObserver(lastNotification.get(), o); } - }); + }, null); return new AsyncSubject(onSubscribe, subscriptionManager, lastNotification); } diff --git a/rxjava-core/src/main/java/rx/subjects/BehaviorSubject.java b/rxjava-core/src/main/java/rx/subjects/BehaviorSubject.java index 036da20a48..f59dc03133 100644 --- a/rxjava-core/src/main/java/rx/subjects/BehaviorSubject.java +++ b/rxjava-core/src/main/java/rx/subjects/BehaviorSubject.java @@ -128,7 +128,7 @@ public void call(SubjectObserver o) { */ lastNotification.get().accept(o); } - }); + }, null); return new BehaviorSubject(onSubscribe, subscriptionManager, lastNotification); } diff --git a/rxjava-core/src/main/java/rx/subjects/PublishSubject.java b/rxjava-core/src/main/java/rx/subjects/PublishSubject.java index 8cc484a36e..4d94a087fd 100644 --- a/rxjava-core/src/main/java/rx/subjects/PublishSubject.java +++ b/rxjava-core/src/main/java/rx/subjects/PublishSubject.java @@ -79,7 +79,7 @@ public void call(SubjectObserver o) { */ lastNotification.get().accept(o); } - }); + }, null); return new PublishSubject(onSubscribe, subscriptionManager, lastNotification); } diff --git a/rxjava-core/src/main/java/rx/subjects/ReplaySubject.java b/rxjava-core/src/main/java/rx/subjects/ReplaySubject.java index 264747d7f4..7d659822c2 100644 --- a/rxjava-core/src/main/java/rx/subjects/ReplaySubject.java +++ b/rxjava-core/src/main/java/rx/subjects/ReplaySubject.java @@ -84,8 +84,15 @@ public void call(SubjectObserver o) { @Override public void call(SubjectObserver o) { + Integer idx = state.replayState.remove(o); // we will finish replaying if there is anything left - replayObserverFromIndex(state.history, state.replayState.get(o), o); + replayObserverFromIndex(state.history, idx, o); + } + }, + new Action1>() { + @Override + public void call(SubjectObserver o) { + state.replayState.remove(o); } }); @@ -229,5 +236,10 @@ public void complete(Notification n) { terminalValue.set(n); } } - + /** + * @return Returns the number of subscribers. + */ + /* Support test.*/ int subscriberCount() { + return state.replayState.size(); + } } \ No newline at end of file diff --git a/rxjava-core/src/main/java/rx/subjects/SubjectSubscriptionManager.java b/rxjava-core/src/main/java/rx/subjects/SubjectSubscriptionManager.java index 0524100659..e6626d434d 100644 --- a/rxjava-core/src/main/java/rx/subjects/SubjectSubscriptionManager.java +++ b/rxjava-core/src/main/java/rx/subjects/SubjectSubscriptionManager.java @@ -39,13 +39,16 @@ * Always runs at the beginning of 'subscribe' regardless of terminal state. * @param onTerminated * Only runs if Subject is in terminal state and the Observer ends up not being registered. + * @param onUnsubscribe called after the child subscription is removed from the state * @return */ - public OnSubscribe getOnSubscribeFunc(final Action1> onSubscribe, final Action1> onTerminated) { + public OnSubscribe getOnSubscribeFunc(final Action1> onSubscribe, + final Action1> onTerminated, + final Action1> onUnsubscribe) { return new OnSubscribe() { @Override public void call(Subscriber actualOperator) { - SubjectObserver observer = new SubjectObserver(actualOperator); + final SubjectObserver observer = new SubjectObserver(actualOperator); // invoke onSubscribe logic if (onSubscribe != null) { onSubscribe.call(observer); @@ -84,6 +87,9 @@ public void call() { // on unsubscribe remove it from the map of outbound observers to notify newState = current.removeObserver(subscription); } while (!state.compareAndSet(current, newState)); + if (onUnsubscribe != null) { + onUnsubscribe.call(observer); + } } })); diff --git a/rxjava-core/src/test/java/rx/subjects/ReplaySubjectTest.java b/rxjava-core/src/test/java/rx/subjects/ReplaySubjectTest.java index 3a360300f5..5a963eba02 100644 --- a/rxjava-core/src/test/java/rx/subjects/ReplaySubjectTest.java +++ b/rxjava-core/src/test/java/rx/subjects/ReplaySubjectTest.java @@ -344,5 +344,16 @@ public void onNext(String v) { assertEquals("three", lastValueForObserver2.get()); } + @Test + public void testSubscriptionLeak() { + ReplaySubject replaySubject = ReplaySubject.create(); + + Subscription s = replaySubject.subscribe(); + + assertEquals(1, replaySubject.subscriberCount()); + s.unsubscribe(); + + assertEquals(0, replaySubject.subscriberCount()); + } } From 271eda44a52f5aa8dc1c9dac3679ec3fd0596eab Mon Sep 17 00:00:00 2001 From: akarnokd Date: Tue, 11 Mar 2014 14:41:43 +0100 Subject: [PATCH 099/422] Fixed byLine test to use line.separator system property instead of \n. --- .../src/test/java/rx/observables/StringObservableTest.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/rxjava-contrib/rxjava-string/src/test/java/rx/observables/StringObservableTest.java b/rxjava-contrib/rxjava-string/src/test/java/rx/observables/StringObservableTest.java index 01dcc8f435..566ee92666 100644 --- a/rxjava-contrib/rxjava-string/src/test/java/rx/observables/StringObservableTest.java +++ b/rxjava-contrib/rxjava-string/src/test/java/rx/observables/StringObservableTest.java @@ -247,7 +247,9 @@ public void testFromReader() { @Test public void testByLine() { - List lines = StringObservable.byLine(Observable.from(Arrays.asList("qwer", "\nasdf\n", "zx", "cv"))).toList().toBlockingObservable().single(); + String newLine = System.getProperty("line.separator"); + + List lines = StringObservable.byLine(Observable.from(Arrays.asList("qwer", newLine + "asdf" + newLine, "zx", "cv"))).toList().toBlockingObservable().single(); assertEquals(Arrays.asList(new Line(0, "qwer"), new Line(1, "asdf"), new Line(2, "zxcv")), lines); } From 9f990cacf3db5fec605adaaa5e14c7d602786efd Mon Sep 17 00:00:00 2001 From: Rick Warren Date: Tue, 11 Mar 2014 23:42:20 -0700 Subject: [PATCH 100/422] OperationToFuture must throw CancellationException on get() if cancelled. The documentation for Future.get() requires the method to throw CancellationException if the Future was cancelled before the task completed. The Futures returned by OperationToFuture.toFuture() did not respect this contract. Now they do. --- .../java/rx/operators/OperationToFuture.java | 4 ++ .../rx/operators/OperationToFutureTest.java | 37 ++++++++++++++++++- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/rxjava-core/src/main/java/rx/operators/OperationToFuture.java b/rxjava-core/src/main/java/rx/operators/OperationToFuture.java index 005cd3a1e4..712930c4c5 100644 --- a/rxjava-core/src/main/java/rx/operators/OperationToFuture.java +++ b/rxjava-core/src/main/java/rx/operators/OperationToFuture.java @@ -15,6 +15,7 @@ */ package rx.operators; +import java.util.concurrent.CancellationException; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; @@ -120,6 +121,9 @@ public T get(long timeout, TimeUnit unit) throws InterruptedException, Execution private T getValue() throws ExecutionException { if (error.get() != null) { throw new ExecutionException("Observable onError", error.get()); + } else if (cancelled) { + // Contract of Future.get() requires us to throw this: + throw new CancellationException("Subscription unsubscribed"); } else { return value.get(); } diff --git a/rxjava-core/src/test/java/rx/operators/OperationToFutureTest.java b/rxjava-core/src/test/java/rx/operators/OperationToFutureTest.java index 8c2791c615..efe840dacc 100644 --- a/rxjava-core/src/test/java/rx/operators/OperationToFutureTest.java +++ b/rxjava-core/src/test/java/rx/operators/OperationToFutureTest.java @@ -15,18 +15,23 @@ */ package rx.operators; -import static org.junit.Assert.*; -import static rx.operators.OperationToFuture.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static rx.operators.OperationToFuture.toFuture; import java.util.List; +import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; import org.junit.Test; import rx.Observable; import rx.Observable.OnSubscribeFunc; import rx.Observer; +import rx.Subscriber; import rx.Subscription; import rx.subscriptions.Subscriptions; @@ -77,6 +82,34 @@ public Subscription onSubscribe(Observer observer) { } } + @Test(expected=CancellationException.class) + public void testGetAfterCancel() throws Exception { + Observable obs = Observable.create(new OperationNeverComplete()); + Future f = toFuture(obs); + boolean cancelled = f.cancel(true); + assertTrue(cancelled); // because OperationNeverComplete never does + f.get(); // Future.get() docs require this to throw + } + + @Test(expected=CancellationException.class) + public void testGetWithTimeoutAfterCancel() throws Exception { + Observable obs = Observable.create(new OperationNeverComplete()); + Future f = toFuture(obs); + boolean cancelled = f.cancel(true); + assertTrue(cancelled); // because OperationNeverComplete never does + f.get(Long.MAX_VALUE, TimeUnit.NANOSECONDS); // Future.get() docs require this to throw + } + + /** + * Emits no observations. Used to simulate a long-running asynchronous operation. + */ + private static class OperationNeverComplete implements Observable.OnSubscribe { + @Override + public void call(Subscriber unused) { + // do nothing + } + } + private static class TestException extends RuntimeException { private static final long serialVersionUID = 1L; } From 16e17c428124c0e2ae675a0c76a1f205e07e061c Mon Sep 17 00:00:00 2001 From: Matthias Kaeppler Date: Wed, 12 Mar 2014 10:27:57 +0100 Subject: [PATCH 101/422] Bump Android plugin version to 0.9 --- rxjava-contrib/rxjava-android-samples/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rxjava-contrib/rxjava-android-samples/build.gradle b/rxjava-contrib/rxjava-android-samples/build.gradle index db342a5e2a..0ba4fcdb79 100644 --- a/rxjava-contrib/rxjava-android-samples/build.gradle +++ b/rxjava-contrib/rxjava-android-samples/build.gradle @@ -5,7 +5,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:0.8.+' + classpath 'com.android.tools.build:gradle:0.9.+' } } From 1275d0858aacb6ea810f30cff7ed45bc99c8e4a7 Mon Sep 17 00:00:00 2001 From: Matthias Kaeppler Date: Wed, 12 Mar 2014 11:10:09 +0100 Subject: [PATCH 102/422] Switch to customizable Gradle wrapper --- .../gradle/wrapper/gradle-wrapper.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rxjava-contrib/rxjava-android-samples/gradle/wrapper/gradle-wrapper.properties b/rxjava-contrib/rxjava-android-samples/gradle/wrapper/gradle-wrapper.properties index 5de946b072..169f28a055 100644 --- a/rxjava-contrib/rxjava-android-samples/gradle/wrapper/gradle-wrapper.properties +++ b/rxjava-contrib/rxjava-android-samples/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Wed Apr 10 15:27:10 PDT 2013 +#Wed Mar 12 11:02:06 CET 2014 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=http\://services.gradle.org/distributions/gradle-1.10-all.zip +distributionUrl=http\://services.gradle.org/distributions/gradle-1.10-bin.zip From a40f8c51cf287770230bcbdb930ca0a6a80f9ef4 Mon Sep 17 00:00:00 2001 From: Matthias Kaeppler Date: Wed, 12 Mar 2014 11:11:31 +0100 Subject: [PATCH 103/422] Trigger samples build manually via Gradle build property --- .../rxjava-android-samples-build-wrapper/build.gradle | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/rxjava-contrib/rxjava-android-samples-build-wrapper/build.gradle b/rxjava-contrib/rxjava-android-samples-build-wrapper/build.gradle index de7ef0162c..03513b751f 100644 --- a/rxjava-contrib/rxjava-android-samples-build-wrapper/build.gradle +++ b/rxjava-contrib/rxjava-android-samples-build-wrapper/build.gradle @@ -1,8 +1,6 @@ -def androidHome = System.getenv("ANDROID_HOME") tasks.build.doLast { - if (androidHome.isEmpty()) { - println("No Android SDK detected; skipping Android samples build") - } else { + def androidHome = System.getenv("ANDROID_HOME") + if (project.hasProperty('buildAndroidSamples') && !androidHome.isEmpty()) { println("Android SDK detected at $androidHome, running samples build") project.exec { workingDir '../rxjava-android-samples' @@ -10,4 +8,4 @@ tasks.build.doLast { commandLine "./gradlew", "clean", "packageDebug" } } -} +} \ No newline at end of file From ffffa57f988f447745fb26992a5f7e6c62075be6 Mon Sep 17 00:00:00 2001 From: Matthias Kaeppler Date: Wed, 12 Mar 2014 12:12:27 +0100 Subject: [PATCH 104/422] Revert to using the all Gradle wrapper dist type --- .../gradle/wrapper/gradle-wrapper.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rxjava-contrib/rxjava-android-samples/gradle/wrapper/gradle-wrapper.properties b/rxjava-contrib/rxjava-android-samples/gradle/wrapper/gradle-wrapper.properties index 169f28a055..a4b6756f97 100644 --- a/rxjava-contrib/rxjava-android-samples/gradle/wrapper/gradle-wrapper.properties +++ b/rxjava-contrib/rxjava-android-samples/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Wed Mar 12 11:02:06 CET 2014 +#Wed Mar 12 12:09:46 CET 2014 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=http\://services.gradle.org/distributions/gradle-1.10-bin.zip +distributionUrl=http\://services.gradle.org/distributions/gradle-1.10-all.zip From ea73bd1f51aaff22215b839441aafbe3757a76d8 Mon Sep 17 00:00:00 2001 From: akarnokd Date: Wed, 12 Mar 2014 15:01:35 +0100 Subject: [PATCH 105/422] OperatorSkipWhile --- rxjava-core/src/main/java/rx/Observable.java | 6 +- .../java/rx/operators/OperationSkipWhile.java | 98 ------------------- .../java/rx/operators/OperatorSkipWhile.java | 78 +++++++++++++++ .../rx/operators/OperationSkipWhileTest.java | 33 +++++-- 4 files changed, 106 insertions(+), 109 deletions(-) delete mode 100644 rxjava-core/src/main/java/rx/operators/OperationSkipWhile.java create mode 100644 rxjava-core/src/main/java/rx/operators/OperatorSkipWhile.java diff --git a/rxjava-core/src/main/java/rx/Observable.java b/rxjava-core/src/main/java/rx/Observable.java index 5bfb5ad278..0f8fa4e5c8 100644 --- a/rxjava-core/src/main/java/rx/Observable.java +++ b/rxjava-core/src/main/java/rx/Observable.java @@ -91,7 +91,7 @@ import rx.operators.OperationSkip; import rx.operators.OperationSkipLast; import rx.operators.OperationSkipUntil; -import rx.operators.OperationSkipWhile; +import rx.operators.OperatorSkipWhile; import rx.operators.OperationSum; import rx.operators.OperationSwitch; import rx.operators.OperationSynchronize; @@ -6427,7 +6427,7 @@ public final Observable skipUntil(Observable other) { * @see MSDN: Observable.SkipWhile */ public final Observable skipWhile(Func1 predicate) { - return create(OperationSkipWhile.skipWhile(this, predicate)); + return lift(new OperatorSkipWhile(OperatorSkipWhile.toPredicate2(predicate))); } /** @@ -6445,7 +6445,7 @@ public final Observable skipWhile(Func1 predicate) { * @see MSDN: Observable.SkipWhile */ public final Observable skipWhileWithIndex(Func2 predicate) { - return create(OperationSkipWhile.skipWhileWithIndex(this, predicate)); + return lift(new OperatorSkipWhile(predicate)); } /** diff --git a/rxjava-core/src/main/java/rx/operators/OperationSkipWhile.java b/rxjava-core/src/main/java/rx/operators/OperationSkipWhile.java deleted file mode 100644 index aaa2914986..0000000000 --- a/rxjava-core/src/main/java/rx/operators/OperationSkipWhile.java +++ /dev/null @@ -1,98 +0,0 @@ -/** - * Copyright 2014 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package rx.operators; - -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; - -import rx.Observable; -import rx.Observable.OnSubscribeFunc; -import rx.Observer; -import rx.Subscription; -import rx.functions.Func1; -import rx.functions.Func2; - -/** - * Skips any emitted source items as long as the specified condition holds true. Emits all further source items - * as soon as the condition becomes false. - */ -public final class OperationSkipWhile { - public static OnSubscribeFunc skipWhileWithIndex(Observable source, Func2 predicate) { - return new SkipWhile(source, predicate); - } - - public static OnSubscribeFunc skipWhile(Observable source, final Func1 predicate) { - return new SkipWhile(source, new Func2() { - @Override - public Boolean call(T value, Integer index) { - return predicate.call(value); - } - }); - } - - private static class SkipWhile implements OnSubscribeFunc { - private final Observable source; - private final Func2 predicate; - private final AtomicBoolean skipping = new AtomicBoolean(true); - private final AtomicInteger index = new AtomicInteger(0); - - SkipWhile(Observable source, Func2 pred) { - this.source = source; - this.predicate = pred; - } - - public Subscription onSubscribe(Observer observer) { - return source.subscribe(new SkipWhileObserver(observer)); - } - - private class SkipWhileObserver implements Observer { - private final Observer observer; - - public SkipWhileObserver(Observer observer) { - this.observer = observer; - } - - @Override - public void onCompleted() { - observer.onCompleted(); - } - - @Override - public void onError(Throwable e) { - observer.onError(e); - } - - @Override - public void onNext(T next) { - if (!skipping.get()) { - observer.onNext(next); - } else { - try { - if (!predicate.call(next, index.getAndIncrement())) { - skipping.set(false); - observer.onNext(next); - } else { - } - } catch (Throwable t) { - observer.onError(t); - } - } - } - - } - - } -} diff --git a/rxjava-core/src/main/java/rx/operators/OperatorSkipWhile.java b/rxjava-core/src/main/java/rx/operators/OperatorSkipWhile.java new file mode 100644 index 0000000000..70e5fc220a --- /dev/null +++ b/rxjava-core/src/main/java/rx/operators/OperatorSkipWhile.java @@ -0,0 +1,78 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package rx.operators; + +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import rx.Observable; +import rx.Observable.OnSubscribeFunc; +import rx.Observable.Operator; +import rx.Observer; +import rx.Subscriber; +import rx.Subscription; +import rx.functions.Func1; +import rx.functions.Func2; + +/** + * Skips any emitted source items as long as the specified condition holds true. Emits all further source items + * as soon as the condition becomes false. + */ +public final class OperatorSkipWhile implements Operator { + private final Func2 predicate; + + public OperatorSkipWhile(Func2 predicate) { + this.predicate = predicate; + } + @Override + public Subscriber call(final Subscriber child) { + return new Subscriber(child) { + boolean skipping = true; + int index; + @Override + public void onNext(T t) { + if (!skipping) { + child.onNext(t); + } else { + if (!predicate.call(t, index++)) { + skipping = false; + child.onNext(t); + } + } + } + + @Override + public void onError(Throwable e) { + child.onError(e); + } + + @Override + public void onCompleted() { + child.onCompleted(); + } + }; + } + /** Convert to Func2 type predicate. */ + public static Func2 toPredicate2(final Func1 predicate) { + return new Func2() { + + @Override + public Boolean call(T t1, Integer t2) { + return predicate.call(t1); + } + }; + } +} diff --git a/rxjava-core/src/test/java/rx/operators/OperationSkipWhileTest.java b/rxjava-core/src/test/java/rx/operators/OperationSkipWhileTest.java index 708ac42d50..b769f4f453 100644 --- a/rxjava-core/src/test/java/rx/operators/OperationSkipWhileTest.java +++ b/rxjava-core/src/test/java/rx/operators/OperationSkipWhileTest.java @@ -15,9 +15,7 @@ */ package rx.operators; -import static org.mockito.Matchers.*; import static org.mockito.Mockito.*; -import static rx.operators.OperationSkipWhile.*; import org.junit.Test; import org.mockito.InOrder; @@ -51,7 +49,7 @@ public Boolean call(Integer value, Integer index) { @Test public void testSkipWithIndex() { Observable src = Observable.from(1, 2, 3, 4, 5); - Observable.create(skipWhileWithIndex(src, INDEX_LESS_THAN_THREE)).subscribe(w); + src.skipWhileWithIndex(INDEX_LESS_THAN_THREE).subscribe(w); InOrder inOrder = inOrder(w); inOrder.verify(w, times(1)).onNext(4); @@ -63,7 +61,7 @@ public void testSkipWithIndex() { @Test public void testSkipEmpty() { Observable src = Observable.empty(); - Observable.create(skipWhile(src, LESS_THAN_FIVE)).subscribe(w); + src.skipWhile(LESS_THAN_FIVE).subscribe(w); verify(w, never()).onNext(anyInt()); verify(w, never()).onError(any(Throwable.class)); verify(w, times(1)).onCompleted(); @@ -72,7 +70,7 @@ public void testSkipEmpty() { @Test public void testSkipEverything() { Observable src = Observable.from(1, 2, 3, 4, 3, 2, 1); - Observable.create(skipWhile(src, LESS_THAN_FIVE)).subscribe(w); + src.skipWhile(LESS_THAN_FIVE).subscribe(w); verify(w, never()).onNext(anyInt()); verify(w, never()).onError(any(Throwable.class)); verify(w, times(1)).onCompleted(); @@ -81,7 +79,7 @@ public void testSkipEverything() { @Test public void testSkipNothing() { Observable src = Observable.from(5, 3, 1); - Observable.create(skipWhile(src, LESS_THAN_FIVE)).subscribe(w); + src.skipWhile(LESS_THAN_FIVE).subscribe(w); InOrder inOrder = inOrder(w); inOrder.verify(w, times(1)).onNext(5); @@ -94,7 +92,7 @@ public void testSkipNothing() { @Test public void testSkipSome() { Observable src = Observable.from(1, 2, 3, 4, 5, 3, 1, 5); - Observable.create(skipWhile(src, LESS_THAN_FIVE)).subscribe(w); + src.skipWhile(LESS_THAN_FIVE).subscribe(w); InOrder inOrder = inOrder(w); inOrder.verify(w, times(1)).onNext(5); @@ -108,11 +106,30 @@ public void testSkipSome() { @Test public void testSkipError() { Observable src = Observable.from(1, 2, 42, 5, 3, 1); - Observable.create(skipWhile(src, LESS_THAN_FIVE)).subscribe(w); + src.skipWhile(LESS_THAN_FIVE).subscribe(w); InOrder inOrder = inOrder(w); inOrder.verify(w, never()).onNext(anyInt()); inOrder.verify(w, never()).onCompleted(); inOrder.verify(w, times(1)).onError(any(RuntimeException.class)); } + + @Test + public void testSkipManySubscribers() { + Observable src = Observable.range(1, 10).skipWhile(LESS_THAN_FIVE); + int n = 5; + for (int i = 0; i < n; i++) { + @SuppressWarnings("unchecked") + Observer o = mock(Observer.class); + InOrder inOrder = inOrder(o); + + src.subscribe(o); + + for (int j = 5; j < 10; j++) { + inOrder.verify(o).onNext(j); + } + inOrder.verify(o).onCompleted(); + verify(o, never()).onError(any(Throwable.class)); + } + } } From 10fa25558ec092fee9e824849697abddd3f1c51c Mon Sep 17 00:00:00 2001 From: Matthias Kaeppler Date: Wed, 12 Mar 2014 15:54:30 +0100 Subject: [PATCH 106/422] Add full example for Activity + retained fragment + cache --- .../samples/src/main/AndroidManifest.xml | 3 +- .../android/samples/RetainedFragment.java | 65 ----------- .../samples/RetainedFragmentActivity.java | 105 ++++++++++++++++++ .../samples/SampleFragmentActivity.java | 17 --- .../android/samples/SampleObservables.java | 19 +++- .../android/samples/SamplesApplication.java | 13 +++ ...ity.xml => retained_fragment_activity.xml} | 4 +- .../samples/src/main/res/menu/menu.xml | 7 ++ 8 files changed, 145 insertions(+), 88 deletions(-) delete mode 100644 rxjava-contrib/rxjava-android-samples/samples/src/main/java/com/netflix/rxjava/android/samples/RetainedFragment.java create mode 100644 rxjava-contrib/rxjava-android-samples/samples/src/main/java/com/netflix/rxjava/android/samples/RetainedFragmentActivity.java delete mode 100644 rxjava-contrib/rxjava-android-samples/samples/src/main/java/com/netflix/rxjava/android/samples/SampleFragmentActivity.java create mode 100644 rxjava-contrib/rxjava-android-samples/samples/src/main/java/com/netflix/rxjava/android/samples/SamplesApplication.java rename rxjava-contrib/rxjava-android-samples/samples/src/main/res/layout/{sample_activity.xml => retained_fragment_activity.xml} (80%) create mode 100644 rxjava-contrib/rxjava-android-samples/samples/src/main/res/menu/menu.xml diff --git a/rxjava-contrib/rxjava-android-samples/samples/src/main/AndroidManifest.xml b/rxjava-contrib/rxjava-android-samples/samples/src/main/AndroidManifest.xml index 6296812264..adcaf359f7 100644 --- a/rxjava-contrib/rxjava-android-samples/samples/src/main/AndroidManifest.xml +++ b/rxjava-contrib/rxjava-android-samples/samples/src/main/AndroidManifest.xml @@ -3,12 +3,13 @@ package="com.netflix.rxjava.android.samples" > + android:name=".RetainedFragmentActivity"> diff --git a/rxjava-contrib/rxjava-android-samples/samples/src/main/java/com/netflix/rxjava/android/samples/RetainedFragment.java b/rxjava-contrib/rxjava-android-samples/samples/src/main/java/com/netflix/rxjava/android/samples/RetainedFragment.java deleted file mode 100644 index 0db00e3ac8..0000000000 --- a/rxjava-contrib/rxjava-android-samples/samples/src/main/java/com/netflix/rxjava/android/samples/RetainedFragment.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.netflix.rxjava.android.samples; - -import android.app.Fragment; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.TextView; - -import rx.Observable; -import rx.Subscription; -import rx.android.observables.AndroidObservable; -import rx.functions.Action1; -import rx.subscriptions.Subscriptions; - -/** - * This fragment shows one of the most common use cases: listening for data emitted from a background - * task on the UI. - *

- * We achieve this by retaining the fragment, creating the observable sequence in onCreate (thus - * also retaining the sequence), and connecting to it in onViewCreated. Note how we use the cache - * operator to replay items already emitted. This ensure that if we should go through a configuration - * change, and the activity gets destroyed, we do not lose any data, but simply cache and re-emit - * it when the views get recreated. - */ -public class RetainedFragment extends Fragment { - - private Observable strings; - private Subscription subscription = Subscriptions.empty(); - - public RetainedFragment() { - setRetainInstance(true); - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - strings = SampleObservables.numberStrings().cache(); - } - - @Override - public void onDestroyView() { - subscription.unsubscribe(); - super.onDestroyView(); - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - return inflater.inflate(R.layout.retained_fragment, container, false); - } - - @Override - public void onViewCreated(final View view, Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - - subscription = AndroidObservable.fromFragment(this, strings).subscribe(new Action1() { - @Override - public void call(String s) { - final TextView textView = (TextView) view.findViewById(android.R.id.text1); - textView.setText(s); - } - }); - } -} diff --git a/rxjava-contrib/rxjava-android-samples/samples/src/main/java/com/netflix/rxjava/android/samples/RetainedFragmentActivity.java b/rxjava-contrib/rxjava-android-samples/samples/src/main/java/com/netflix/rxjava/android/samples/RetainedFragmentActivity.java new file mode 100644 index 0000000000..82c5225101 --- /dev/null +++ b/rxjava-contrib/rxjava-android-samples/samples/src/main/java/com/netflix/rxjava/android/samples/RetainedFragmentActivity.java @@ -0,0 +1,105 @@ +package com.netflix.rxjava.android.samples; + +import android.app.Activity; +import android.app.Fragment; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.widget.TextView; + +import org.json.JSONException; +import org.json.JSONObject; + +import rx.Observable; +import rx.Subscription; +import rx.android.schedulers.AndroidSchedulers; +import rx.functions.Action1; +import rx.functions.Func1; +import rx.subscriptions.Subscriptions; + +/** + * Problem: + * You have a data source (where that data is potentially expensive to obtain), and you want to + * emit this data into a fragment. However, you want to gracefully deal with rotation changes and + * not lose any data already emitted. + *

+ * Solution: + * Combine {@link android.app.Fragment#setRetainInstance(boolean)} with + * {@link rx.android.schedulers.AndroidSchedulers#mainThread()} and {@link rx.Observable#cache()} + */ +public class RetainedFragmentActivity extends Activity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); + setTitle("Fake API call"); + setContentView(R.layout.retained_fragment_activity); + } + + @SuppressWarnings("ConstantConditions") + public static class RetainedFragment extends Fragment { + + // in a production app, you don't want to have JSON parser code in your fragment, + // but we'll simplify a little here + private static final Func1 PARSE_JSON = new Func1() { + @Override + public String call(String json) { + try { + JSONObject jsonObject = new JSONObject(json); + return String.valueOf(jsonObject.getInt("result")); + } catch (JSONException e) { + throw new RuntimeException(e); + } + } + }; + + private Observable strings; + private Subscription subscription = Subscriptions.empty(); + + public RetainedFragment() { + setRetainInstance(true); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // simulate fetching a JSON document with a latency of 2 seconds + strings = SampleObservables.fakeApiCall(2000).map(PARSE_JSON) + .observeOn(AndroidSchedulers.mainThread()) + .cache(); + } + + @Override + public void onDestroyView() { + subscription.unsubscribe(); + super.onDestroyView(); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + getActivity().setProgressBarIndeterminateVisibility(true); + return inflater.inflate(R.layout.retained_fragment, container, false); + } + + @Override + public void onViewCreated(final View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + final TextView textView = (TextView) view.findViewById(android.R.id.text1); + + // (re-)subscribe to the sequence, which either emits the cached result or simply re- + // attaches the subscriber to wait for it to arrive + subscription = strings.subscribe(new Action1() { + @Override + public void call(String result) { + textView.setText(result); + getActivity().setProgressBarIndeterminateVisibility(false); + } + }); + } + } +} diff --git a/rxjava-contrib/rxjava-android-samples/samples/src/main/java/com/netflix/rxjava/android/samples/SampleFragmentActivity.java b/rxjava-contrib/rxjava-android-samples/samples/src/main/java/com/netflix/rxjava/android/samples/SampleFragmentActivity.java deleted file mode 100644 index 5da3b3b7e6..0000000000 --- a/rxjava-contrib/rxjava-android-samples/samples/src/main/java/com/netflix/rxjava/android/samples/SampleFragmentActivity.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.netflix.rxjava.android.samples; - -import android.app.Activity; -import android.os.Bundle; -import android.os.StrictMode; - -public class SampleFragmentActivity extends Activity { - - @Override - protected void onCreate(Bundle savedInstanceState) { - StrictMode.enableDefaults(); - - super.onCreate(savedInstanceState); - setContentView(R.layout.sample_activity); - } - -} diff --git a/rxjava-contrib/rxjava-android-samples/samples/src/main/java/com/netflix/rxjava/android/samples/SampleObservables.java b/rxjava-contrib/rxjava-android-samples/samples/src/main/java/com/netflix/rxjava/android/samples/SampleObservables.java index e2b67b104a..7a17bedad5 100644 --- a/rxjava-contrib/rxjava-android-samples/samples/src/main/java/com/netflix/rxjava/android/samples/SampleObservables.java +++ b/rxjava-contrib/rxjava-android-samples/samples/src/main/java/com/netflix/rxjava/android/samples/SampleObservables.java @@ -3,6 +3,7 @@ import android.os.SystemClock; import rx.Observable; +import rx.Subscriber; import rx.functions.Action1; import rx.functions.Func1; import rx.schedulers.Schedulers; @@ -12,8 +13,8 @@ public class SampleObservables { /** * Emits numbers as strings, where these numbers a generated on a background thread. */ - public static Observable numberStrings() { - return Observable.range(1, 10).map(new Func1() { + public static Observable numberStrings(int from, int to, final long delay) { + return Observable.range(from, to).map(new Func1() { @Override public String call(Integer integer) { return integer.toString(); @@ -21,9 +22,21 @@ public String call(Integer integer) { }).doOnNext(new Action1() { @Override public void call(String s) { - SystemClock.sleep(1000); + SystemClock.sleep(delay); } }).subscribeOn(Schedulers.newThread()); } + public static Observable fakeApiCall(final long delay) { + return Observable.create(new Observable.OnSubscribe() { + @Override + public void call(Subscriber subscriber) { + // simulate I/O latency + SystemClock.sleep(delay); + final String fakeJson = "{\"result\": 42}"; + subscriber.onNext(fakeJson); + subscriber.onCompleted(); + } + }).subscribeOn(Schedulers.io()); + } } diff --git a/rxjava-contrib/rxjava-android-samples/samples/src/main/java/com/netflix/rxjava/android/samples/SamplesApplication.java b/rxjava-contrib/rxjava-android-samples/samples/src/main/java/com/netflix/rxjava/android/samples/SamplesApplication.java new file mode 100644 index 0000000000..e91e7d543e --- /dev/null +++ b/rxjava-contrib/rxjava-android-samples/samples/src/main/java/com/netflix/rxjava/android/samples/SamplesApplication.java @@ -0,0 +1,13 @@ +package com.netflix.rxjava.android.samples; + +import android.app.Application; +import android.os.StrictMode; + +public class SamplesApplication extends Application { + + @Override + public void onCreate() { + super.onCreate(); + StrictMode.enableDefaults(); + } +} diff --git a/rxjava-contrib/rxjava-android-samples/samples/src/main/res/layout/sample_activity.xml b/rxjava-contrib/rxjava-android-samples/samples/src/main/res/layout/retained_fragment_activity.xml similarity index 80% rename from rxjava-contrib/rxjava-android-samples/samples/src/main/res/layout/sample_activity.xml rename to rxjava-contrib/rxjava-android-samples/samples/src/main/res/layout/retained_fragment_activity.xml index 5900d1d74e..e5eb51fb86 100644 --- a/rxjava-contrib/rxjava-android-samples/samples/src/main/res/layout/sample_activity.xml +++ b/rxjava-contrib/rxjava-android-samples/samples/src/main/res/layout/retained_fragment_activity.xml @@ -2,13 +2,13 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - tools:context="com.netflix.rxjava.android.samples.SampleFragmentActivity"> + tools:context="com.netflix.rxjava.android.samples.RetainedFragmentActivity"> diff --git a/rxjava-contrib/rxjava-android-samples/samples/src/main/res/menu/menu.xml b/rxjava-contrib/rxjava-android-samples/samples/src/main/res/menu/menu.xml new file mode 100644 index 0000000000..3c1d36c3d1 --- /dev/null +++ b/rxjava-contrib/rxjava-android-samples/samples/src/main/res/menu/menu.xml @@ -0,0 +1,7 @@ +

+ + + From adb54b46922e901f052950caa28813092e8be687 Mon Sep 17 00:00:00 2001 From: Matthias Kaeppler Date: Wed, 12 Mar 2014 16:05:31 +0100 Subject: [PATCH 107/422] Add sample for fragments and connectable observables --- .../samples/src/main/AndroidManifest.xml | 9 ++ .../samples/ListeningFragmentActivity.java | 96 +++++++++++++++++++ .../layout/listening_fragment_activity.xml | 14 +++ 3 files changed, 119 insertions(+) create mode 100644 rxjava-contrib/rxjava-android-samples/samples/src/main/java/com/netflix/rxjava/android/samples/ListeningFragmentActivity.java create mode 100644 rxjava-contrib/rxjava-android-samples/samples/src/main/res/layout/listening_fragment_activity.xml diff --git a/rxjava-contrib/rxjava-android-samples/samples/src/main/AndroidManifest.xml b/rxjava-contrib/rxjava-android-samples/samples/src/main/AndroidManifest.xml index adcaf359f7..18bbab228b 100644 --- a/rxjava-contrib/rxjava-android-samples/samples/src/main/AndroidManifest.xml +++ b/rxjava-contrib/rxjava-android-samples/samples/src/main/AndroidManifest.xml @@ -11,6 +11,15 @@ + + + + + + + + diff --git a/rxjava-contrib/rxjava-android-samples/samples/src/main/java/com/netflix/rxjava/android/samples/ListeningFragmentActivity.java b/rxjava-contrib/rxjava-android-samples/samples/src/main/java/com/netflix/rxjava/android/samples/ListeningFragmentActivity.java new file mode 100644 index 0000000000..b402135bc1 --- /dev/null +++ b/rxjava-contrib/rxjava-android-samples/samples/src/main/java/com/netflix/rxjava/android/samples/ListeningFragmentActivity.java @@ -0,0 +1,96 @@ +package com.netflix.rxjava.android.samples; + +import android.app.Activity; +import android.app.Fragment; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; +import android.widget.Toast; + +import rx.Subscriber; +import rx.Subscription; +import rx.observables.ConnectableObservable; +import rx.subscriptions.Subscriptions; + +import static rx.android.schedulers.AndroidSchedulers.mainThread; + +/** + * Problem: + * You have a background sequence which keeps emitting items (either a limited or unlimited number) + * and your UI component should be able to "listen in" to the sequence, i.e. it's okay to miss + * in-flight items when going e.g. through a screen rotation or being otherwise detached from the + * screen for a limited period of time. (Another example is a "page out" in a fragment ViewPager.) + *

+ * This is useful if you need behavior that mimics event buses. Think of a publishing + * Observable as a channel or queue on an event bus. + *

+ * Solution: + * Combine {@link android.app.Fragment#setRetainInstance(boolean)} with + * {@link rx.android.schedulers.AndroidSchedulers#mainThread()} and {@link rx.Observable#publish()} + */ +public class ListeningFragmentActivity extends Activity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.listening_fragment_activity); + } + + @SuppressWarnings("ConstantConditions") + public static class ListeningFragment extends Fragment { + + private ConnectableObservable strings; + private Subscription subscription = Subscriptions.empty(); + + public ListeningFragment() { + setRetainInstance(true); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + strings = SampleObservables.numberStrings(1, 50, 250).observeOn(mainThread()).publish(); + strings.connect(); // trigger the sequence + } + + @Override + public void onDestroyView() { + subscription.unsubscribe(); // stop listening + super.onDestroyView(); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + return inflater.inflate(R.layout.retained_fragment, container, false); + } + + @Override + public void onViewCreated(final View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + final TextView textView = (TextView) view.findViewById(android.R.id.text1); + + // re-connect to sequence + subscription = strings.subscribe(new Subscriber() { + + @Override + public void onCompleted() { + Toast.makeText(getActivity(), "Done!", Toast.LENGTH_SHORT).show(); + } + + @Override + public void onError(Throwable throwable) { + + } + + @Override + public void onNext(String s) { + textView.setText(s); + } + }); + } + } +} diff --git a/rxjava-contrib/rxjava-android-samples/samples/src/main/res/layout/listening_fragment_activity.xml b/rxjava-contrib/rxjava-android-samples/samples/src/main/res/layout/listening_fragment_activity.xml new file mode 100644 index 0000000000..ecfc325d20 --- /dev/null +++ b/rxjava-contrib/rxjava-android-samples/samples/src/main/res/layout/listening_fragment_activity.xml @@ -0,0 +1,14 @@ + + + + + From a91c1d46f9fd7aa21a9544057fd79072f54d6210 Mon Sep 17 00:00:00 2001 From: Matthias Kaeppler Date: Wed, 12 Mar 2014 16:13:03 +0100 Subject: [PATCH 108/422] Add sample for binding to list adapters in ListFragment --- .../samples/src/main/AndroidManifest.xml | 9 +++ .../android/samples/ListFragmentActivity.java | 75 +++++++++++++++++++ .../res/layout/list_fragment_activity.xml | 13 ++++ 3 files changed, 97 insertions(+) create mode 100644 rxjava-contrib/rxjava-android-samples/samples/src/main/java/com/netflix/rxjava/android/samples/ListFragmentActivity.java create mode 100644 rxjava-contrib/rxjava-android-samples/samples/src/main/res/layout/list_fragment_activity.xml diff --git a/rxjava-contrib/rxjava-android-samples/samples/src/main/AndroidManifest.xml b/rxjava-contrib/rxjava-android-samples/samples/src/main/AndroidManifest.xml index 18bbab228b..f307d55bb0 100644 --- a/rxjava-contrib/rxjava-android-samples/samples/src/main/AndroidManifest.xml +++ b/rxjava-contrib/rxjava-android-samples/samples/src/main/AndroidManifest.xml @@ -20,6 +20,15 @@ + + + + + + + + diff --git a/rxjava-contrib/rxjava-android-samples/samples/src/main/java/com/netflix/rxjava/android/samples/ListFragmentActivity.java b/rxjava-contrib/rxjava-android-samples/samples/src/main/java/com/netflix/rxjava/android/samples/ListFragmentActivity.java new file mode 100644 index 0000000000..446bffa55d --- /dev/null +++ b/rxjava-contrib/rxjava-android-samples/samples/src/main/java/com/netflix/rxjava/android/samples/ListFragmentActivity.java @@ -0,0 +1,75 @@ +package com.netflix.rxjava.android.samples; + +import android.app.Activity; +import android.app.ListFragment; +import android.os.Bundle; +import android.widget.ArrayAdapter; + +import rx.Observable; +import rx.Subscriber; + +import static rx.android.schedulers.AndroidSchedulers.mainThread; + +/** + * Problem: + * You have an asynchronous sequence that emits items to be displayed in a list. You want the data + * to survive rotation changes. + *

+ * Solution: + * Combine {@link android.app.Fragment#setRetainInstance(boolean)} in a ListFragment with + * {@link rx.android.schedulers.AndroidSchedulers#mainThread()} and an {@link rx.Observable.Operator} + * that binds to the list adapter. + */ +public class ListFragmentActivity extends Activity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setTitle("Lists"); + setContentView(R.layout.list_fragment_activity); + } + + @SuppressWarnings("ConstantConditions") + public static class RetainedListFragment extends ListFragment { + + private ArrayAdapter adapter; + + public RetainedListFragment() { + setRetainInstance(true); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + adapter = new ArrayAdapter(getActivity(), android.R.layout.simple_list_item_1); + setListAdapter(adapter); + SampleObservables.numberStrings(1, 20, 250) + .observeOn(mainThread()) + .lift(new BindAdapter()) + .subscribe(); + } + + private final class BindAdapter implements Observable.Operator { + @Override + public Subscriber call(Subscriber subscriber) { + return new Subscriber() { + @Override + public void onCompleted() { + adapter.notifyDataSetChanged(); + } + + @Override + public void onError(Throwable throwable) { + + } + + @Override + public void onNext(String strings) { + adapter.add(strings); + } + }; + } + } + } +} diff --git a/rxjava-contrib/rxjava-android-samples/samples/src/main/res/layout/list_fragment_activity.xml b/rxjava-contrib/rxjava-android-samples/samples/src/main/res/layout/list_fragment_activity.xml new file mode 100644 index 0000000000..f741ee62dc --- /dev/null +++ b/rxjava-contrib/rxjava-android-samples/samples/src/main/res/layout/list_fragment_activity.xml @@ -0,0 +1,13 @@ + + + + + From d7037b5c5270c0f6af0381af03d60fc4f916141d Mon Sep 17 00:00:00 2001 From: Matthias Kaeppler Date: Wed, 12 Mar 2014 18:20:46 +0100 Subject: [PATCH 109/422] OperatorWeakBinding supports predicates now --- .../observables/AndroidObservable.java | 56 ++++++++++-- .../rx/operators/OperatorWeakBinding.java | 54 +++++++++--- .../rx/operators/OperatorWeakBindingTest.java | 86 +++++++++++++++++++ 3 files changed, 178 insertions(+), 18 deletions(-) create mode 100644 rxjava-contrib/rxjava-android/src/test/java/rx/operators/OperatorWeakBindingTest.java diff --git a/rxjava-contrib/rxjava-android/src/main/java/rx/android/observables/AndroidObservable.java b/rxjava-contrib/rxjava-android/src/main/java/rx/android/observables/AndroidObservable.java index 8da366265d..28d8d704aa 100644 --- a/rxjava-contrib/rxjava-android/src/main/java/rx/android/observables/AndroidObservable.java +++ b/rxjava-contrib/rxjava-android/src/main/java/rx/android/observables/AndroidObservable.java @@ -18,6 +18,7 @@ import static rx.android.schedulers.AndroidSchedulers.mainThread; import rx.Observable; +import rx.functions.Func1; import rx.operators.OperatorObserveFromAndroidComponent; import rx.operators.OperatorWeakBinding; @@ -40,7 +41,30 @@ public final class AndroidObservable { USES_SUPPORT_FRAGMENTS = supportFragmentsAvailable; } - private AndroidObservable() {} + private static final Func1 ACTIVITY_VALIDATOR = new Func1() { + @Override + public Boolean call(Activity activity) { + return !activity.isFinishing(); + } + }; + + private static final Func1 FRAGMENT_VALIDATOR = new Func1() { + @Override + public Boolean call(Fragment fragment) { + return fragment.isAdded(); + } + }; + + private static final Func1 FRAGMENTV4_VALIDATOR = + new Func1() { + @Override + public Boolean call(android.support.v4.app.Fragment fragment) { + return fragment.isAdded(); + } + }; + + private AndroidObservable() { + } /** * Transforms a source observable to be attached to the given Activity, in such a way that notifications will always @@ -61,6 +85,7 @@ private AndroidObservable() {} * @param sourceObservable the observable sequence to observe from the given Activity * @param * @return a new observable sequence that will emit notifications on the main UI thread + * @deprecated Use {@link #bindActivity(android.app.Activity, rx.Observable)} instead */ @Deprecated public static Observable fromActivity(Activity activity, Observable sourceObservable) { @@ -91,6 +116,7 @@ public static Observable fromActivity(Activity activity, Observable so * @param sourceObservable the observable sequence to observe from the given fragment * @param * @return a new observable sequence that will emit notifications on the main UI thread + * @deprecated Use {@link #bindFragment(Object, rx.Observable)} instead */ @Deprecated public static Observable fromFragment(Object fragment, Observable sourceObservable) { @@ -104,18 +130,38 @@ public static Observable fromFragment(Object fragment, Observable sour } } - public static Observable bindActivity(Activity activity, Observable cachedSequence) { - return cachedSequence.observeOn(mainThread()).lift(new OperatorWeakBinding(activity)); + /** + * Binds the given source sequence to the life-cycle of an activity. + *

+ * This helper will schedule the given sequence to be observed on the main UI thread and ensure + * that no notifications will be forwarded to the activity in case it gets destroyed by the Android runtime + * or garbage collected by the VM. + * + * @param activity the activity to bind the source sequence to + * @param source the source sequence + */ + public static Observable bindActivity(Activity activity, Observable source) { + return source.observeOn(mainThread()).lift(new OperatorWeakBinding(activity, ACTIVITY_VALIDATOR)); } + /** + * Binds the given source sequence to the life-cycle of a fragment (native or support-v4). + *

+ * This helper will schedule the given sequence to be observed on the main UI thread and ensure + * that no notifications will be forwarded to the fragment in case it gets detached from its + * activity or garbage collected by the VM. + * + * @param fragment the fragment to bind the source sequence to + * @param source the source sequence + */ public static Observable bindFragment(Object fragment, Observable cachedSequence) { Observable source = cachedSequence.observeOn(mainThread()); if (USES_SUPPORT_FRAGMENTS && fragment instanceof android.support.v4.app.Fragment) { android.support.v4.app.Fragment f = (android.support.v4.app.Fragment) fragment; - return source.lift(new OperatorWeakBinding(f)); + return source.lift(new OperatorWeakBinding(f, FRAGMENTV4_VALIDATOR)); } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB && fragment instanceof Fragment) { Fragment f = (Fragment) fragment; - return source.lift(new OperatorWeakBinding(f)); + return source.lift(new OperatorWeakBinding(f, FRAGMENT_VALIDATOR)); } else { throw new IllegalArgumentException("Target fragment is neither a native nor support library Fragment"); } diff --git a/rxjava-contrib/rxjava-android/src/main/java/rx/operators/OperatorWeakBinding.java b/rxjava-contrib/rxjava-android/src/main/java/rx/operators/OperatorWeakBinding.java index 3392cc887a..43a501b489 100644 --- a/rxjava-contrib/rxjava-android/src/main/java/rx/operators/OperatorWeakBinding.java +++ b/rxjava-contrib/rxjava-android/src/main/java/rx/operators/OperatorWeakBinding.java @@ -2,40 +2,58 @@ import rx.Observable; import rx.Subscriber; +import rx.functions.Func1; +import rx.functions.Functions; import android.util.Log; import java.lang.ref.WeakReference; +/** + * Ties a source sequence to the life-cycle of the given target object, and/or the subscriber + * using weak references. When either object is gone, this operator automatically unsubscribes + * from the source sequence. + *

+ * You can also pass in an optional predicate function, which whenever it evaluates to false + * on the target object, will also result in the operator unsubscribing from the sequence. + * + * @param the type of the objects emitted to a subscriber + * @param the type of the target object to bind to + */ public final class OperatorWeakBinding implements Observable.Operator { private static final String LOG_TAG = "WeakBinding"; - private final WeakReference boundRef; + final WeakReference boundRef; + private final Func1 predicate; + + public OperatorWeakBinding(R bound, Func1 predicate) { + boundRef = new WeakReference(bound); + this.predicate = predicate; + } public OperatorWeakBinding(R bound) { boundRef = new WeakReference(bound); + this.predicate = Functions.alwaysTrue(); } @Override public Subscriber call(final Subscriber child) { - return new WeakSubscriber(child, boundRef); + return new WeakSubscriber(child); } - private static final class WeakSubscriber extends Subscriber { + final class WeakSubscriber extends Subscriber { - private final WeakReference> subscriberRef; - private final WeakReference boundRef; + final WeakReference> subscriberRef; - private WeakSubscriber(Subscriber op, WeakReference boundRef) { - subscriberRef = new WeakReference>(op); - this.boundRef = boundRef; + private WeakSubscriber(Subscriber source) { + subscriberRef = new WeakReference>(source); } @Override public void onCompleted() { Subscriber sub = subscriberRef.get(); - if (sub != null && boundRef.get() != null) { + if (shouldForwardNotification(sub)) { sub.onCompleted(); } else { handleLostBinding(sub, "onCompleted"); @@ -45,7 +63,7 @@ public void onCompleted() { @Override public void onError(Throwable e) { Subscriber sub = subscriberRef.get(); - if (sub != null && boundRef.get() != null) { + if (shouldForwardNotification(sub)) { sub.onError(e); } else { handleLostBinding(sub, "onError"); @@ -55,21 +73,31 @@ public void onError(Throwable e) { @Override public void onNext(T t) { Subscriber sub = subscriberRef.get(); - if (sub != null && boundRef.get() != null) { + if (shouldForwardNotification(sub)) { sub.onNext(t); } else { handleLostBinding(sub, "onNext"); } } + private boolean shouldForwardNotification(Subscriber sub) { + final R target = boundRef.get(); + return sub != null && target != null && predicate.call(target); + } + private void handleLostBinding(Subscriber sub, String context) { if (sub == null) { Log.d(LOG_TAG, "subscriber gone; skipping " + context); } else { - Log.d(LOG_TAG, "bound component gone; skipping " + context); + final R r = boundRef.get(); + if (r != null) { // the predicate failed to validate + Log.d(LOG_TAG, "bound component has become invalid; skipping " + context); + } else { + Log.d(LOG_TAG, "bound component gone; skipping " + context); + } } + Log.d(LOG_TAG, "unsubscribing..."); unsubscribe(); } - } } diff --git a/rxjava-contrib/rxjava-android/src/test/java/rx/operators/OperatorWeakBindingTest.java b/rxjava-contrib/rxjava-android/src/test/java/rx/operators/OperatorWeakBindingTest.java new file mode 100644 index 0000000000..e5445ce228 --- /dev/null +++ b/rxjava-contrib/rxjava-android/src/test/java/rx/operators/OperatorWeakBindingTest.java @@ -0,0 +1,86 @@ +package rx.operators; + +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.verifyZeroInteractions; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import rx.Subscriber; +import rx.functions.Functions; + +@RunWith(RobolectricTestRunner.class) +public class OperatorWeakBindingTest { + + @Mock + private Subscriber subscriber; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + } + + @Test + public void shouldForwardAllNotificationsWhenSubscriberAndTargetAlive() { + OperatorWeakBinding op = new OperatorWeakBinding(new Object()); + OperatorWeakBinding.WeakSubscriber weakSub = (OperatorWeakBinding.WeakSubscriber) op.call(subscriber); + weakSub.onNext("one"); + weakSub.onNext("two"); + weakSub.onCompleted(); + weakSub.onError(new Exception()); + + verify(subscriber).onNext("one"); + verify(subscriber).onNext("two"); + verify(subscriber).onCompleted(); + verify(subscriber).onError(any(Exception.class)); + } + + @Test + public void shouldUnsubscribeFromSourceSequenceWhenSubscriberReleased() { + OperatorWeakBinding op = new OperatorWeakBinding(new Object()); + + OperatorWeakBinding.WeakSubscriber weakSub = (OperatorWeakBinding.WeakSubscriber) op.call(subscriber); + weakSub.onNext("one"); + weakSub.subscriberRef.clear(); + weakSub.onNext("two"); + weakSub.onCompleted(); + weakSub.onError(new Exception()); + + verify(subscriber).onNext("one"); + verifyNoMoreInteractions(subscriber); + } + + @Test + public void shouldUnsubscribeFromSourceSequenceWhenTargetObjectReleased() { + OperatorWeakBinding op = new OperatorWeakBinding(new Object()); + + OperatorWeakBinding.WeakSubscriber weakSub = (OperatorWeakBinding.WeakSubscriber) op.call(subscriber); + weakSub.onNext("one"); + op.boundRef.clear(); + weakSub.onNext("two"); + weakSub.onCompleted(); + weakSub.onError(new Exception()); + + verify(subscriber).onNext("one"); + verifyNoMoreInteractions(subscriber); + } + + @Test + public void shouldUnsubscribeFromSourceSequenceWhenPredicateFailsToPass() { + OperatorWeakBinding op = new OperatorWeakBinding( + new Object(), Functions.alwaysFalse()); + + OperatorWeakBinding.WeakSubscriber weakSub = (OperatorWeakBinding.WeakSubscriber) op.call(subscriber); + weakSub.onNext("one"); + weakSub.onNext("two"); + weakSub.onCompleted(); + weakSub.onError(new Exception()); + + verifyZeroInteractions(subscriber); + } +} From 135b6c13e03dbb519f8163f5ee5b2c8d7c87684c Mon Sep 17 00:00:00 2001 From: Matthias Kaeppler Date: Wed, 12 Mar 2014 18:29:41 +0100 Subject: [PATCH 110/422] Some cleaning up --- .../src/main/java/rx/operators/OperatorWeakBinding.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/rxjava-contrib/rxjava-android/src/main/java/rx/operators/OperatorWeakBinding.java b/rxjava-contrib/rxjava-android/src/main/java/rx/operators/OperatorWeakBinding.java index 43a501b489..b1adb3d34d 100644 --- a/rxjava-contrib/rxjava-android/src/main/java/rx/operators/OperatorWeakBinding.java +++ b/rxjava-contrib/rxjava-android/src/main/java/rx/operators/OperatorWeakBinding.java @@ -52,7 +52,7 @@ private WeakSubscriber(Subscriber source) { @Override public void onCompleted() { - Subscriber sub = subscriberRef.get(); + final Subscriber sub = subscriberRef.get(); if (shouldForwardNotification(sub)) { sub.onCompleted(); } else { @@ -62,7 +62,7 @@ public void onCompleted() { @Override public void onError(Throwable e) { - Subscriber sub = subscriberRef.get(); + final Subscriber sub = subscriberRef.get(); if (shouldForwardNotification(sub)) { sub.onError(e); } else { @@ -72,7 +72,7 @@ public void onError(Throwable e) { @Override public void onNext(T t) { - Subscriber sub = subscriberRef.get(); + final Subscriber sub = subscriberRef.get(); if (shouldForwardNotification(sub)) { sub.onNext(t); } else { @@ -90,7 +90,8 @@ private void handleLostBinding(Subscriber sub, String context) { Log.d(LOG_TAG, "subscriber gone; skipping " + context); } else { final R r = boundRef.get(); - if (r != null) { // the predicate failed to validate + if (r != null) { + // the predicate failed to validate Log.d(LOG_TAG, "bound component has become invalid; skipping " + context); } else { Log.d(LOG_TAG, "bound component gone; skipping " + context); From 2f0670ecc6fdae7ce8cf30848e79a773f520934e Mon Sep 17 00:00:00 2001 From: Matthias Kaeppler Date: Wed, 12 Mar 2014 18:31:12 +0100 Subject: [PATCH 111/422] Guard the log calls --- .../java/rx/operators/OperatorWeakBinding.java | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/rxjava-contrib/rxjava-android/src/main/java/rx/operators/OperatorWeakBinding.java b/rxjava-contrib/rxjava-android/src/main/java/rx/operators/OperatorWeakBinding.java index b1adb3d34d..986341396f 100644 --- a/rxjava-contrib/rxjava-android/src/main/java/rx/operators/OperatorWeakBinding.java +++ b/rxjava-contrib/rxjava-android/src/main/java/rx/operators/OperatorWeakBinding.java @@ -87,18 +87,24 @@ private boolean shouldForwardNotification(Subscriber sub) { private void handleLostBinding(Subscriber sub, String context) { if (sub == null) { - Log.d(LOG_TAG, "subscriber gone; skipping " + context); + log("subscriber gone; skipping " + context); } else { final R r = boundRef.get(); if (r != null) { // the predicate failed to validate - Log.d(LOG_TAG, "bound component has become invalid; skipping " + context); + log("bound component has become invalid; skipping " + context); } else { - Log.d(LOG_TAG, "bound component gone; skipping " + context); + log("bound component gone; skipping " + context); } } - Log.d(LOG_TAG, "unsubscribing..."); + log("unsubscribing..."); unsubscribe(); } + + private void log(String message) { + if (Log.isLoggable(LOG_TAG, Log.DEBUG)) { + Log.d(LOG_TAG, message); + } + } } } From 45afeb9fb5ce49a9f0e4d931057ebaf6990111ab Mon Sep 17 00:00:00 2001 From: Matthias Kaeppler Date: Thu, 13 Mar 2014 11:14:25 +0100 Subject: [PATCH 112/422] Forward subscription of wrapped subscriber --- .../rx/operators/OperatorWeakBinding.java | 1 + .../rx/operators/OperatorWeakBindingTest.java | 42 +++++++++++-------- 2 files changed, 26 insertions(+), 17 deletions(-) diff --git a/rxjava-contrib/rxjava-android/src/main/java/rx/operators/OperatorWeakBinding.java b/rxjava-contrib/rxjava-android/src/main/java/rx/operators/OperatorWeakBinding.java index 986341396f..3cc8258302 100644 --- a/rxjava-contrib/rxjava-android/src/main/java/rx/operators/OperatorWeakBinding.java +++ b/rxjava-contrib/rxjava-android/src/main/java/rx/operators/OperatorWeakBinding.java @@ -47,6 +47,7 @@ final class WeakSubscriber extends Subscriber { final WeakReference> subscriberRef; private WeakSubscriber(Subscriber source) { + super(source); subscriberRef = new WeakReference>(source); } diff --git a/rxjava-contrib/rxjava-android/src/test/java/rx/operators/OperatorWeakBindingTest.java b/rxjava-contrib/rxjava-android/src/test/java/rx/operators/OperatorWeakBindingTest.java index e5445ce228..6dd72c606d 100644 --- a/rxjava-contrib/rxjava-android/src/test/java/rx/operators/OperatorWeakBindingTest.java +++ b/rxjava-contrib/rxjava-android/src/test/java/rx/operators/OperatorWeakBindingTest.java @@ -1,24 +1,21 @@ package rx.operators; -import static org.mockito.Matchers.any; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; -import static org.mockito.Mockito.verifyZeroInteractions; +import static org.junit.Assert.assertEquals; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; -import rx.Subscriber; import rx.functions.Functions; +import rx.observers.TestSubscriber; + +import java.util.Arrays; @RunWith(RobolectricTestRunner.class) public class OperatorWeakBindingTest { - @Mock - private Subscriber subscriber; + private TestSubscriber subscriber = new TestSubscriber(); @Before public void setUp() throws Exception { @@ -34,10 +31,9 @@ public void shouldForwardAllNotificationsWhenSubscriberAndTargetAlive() { weakSub.onCompleted(); weakSub.onError(new Exception()); - verify(subscriber).onNext("one"); - verify(subscriber).onNext("two"); - verify(subscriber).onCompleted(); - verify(subscriber).onError(any(Exception.class)); + subscriber.assertReceivedOnNext(Arrays.asList("one", "two")); + assertEquals(1, subscriber.getOnCompletedEvents().size()); + assertEquals(1, subscriber.getOnErrorEvents().size()); } @Test @@ -51,8 +47,9 @@ public void shouldUnsubscribeFromSourceSequenceWhenSubscriberReleased() { weakSub.onCompleted(); weakSub.onError(new Exception()); - verify(subscriber).onNext("one"); - verifyNoMoreInteractions(subscriber); + subscriber.assertReceivedOnNext(Arrays.asList("one")); + assertEquals(0, subscriber.getOnCompletedEvents().size()); + assertEquals(0, subscriber.getOnErrorEvents().size()); } @Test @@ -66,8 +63,9 @@ public void shouldUnsubscribeFromSourceSequenceWhenTargetObjectReleased() { weakSub.onCompleted(); weakSub.onError(new Exception()); - verify(subscriber).onNext("one"); - verifyNoMoreInteractions(subscriber); + subscriber.assertReceivedOnNext(Arrays.asList("one")); + assertEquals(0, subscriber.getOnCompletedEvents().size()); + assertEquals(0, subscriber.getOnErrorEvents().size()); } @Test @@ -81,6 +79,16 @@ public void shouldUnsubscribeFromSourceSequenceWhenPredicateFailsToPass() { weakSub.onCompleted(); weakSub.onError(new Exception()); - verifyZeroInteractions(subscriber); + assertEquals(0, subscriber.getOnNextEvents().size()); + assertEquals(0, subscriber.getOnCompletedEvents().size()); + assertEquals(0, subscriber.getOnErrorEvents().size()); + } + + @Test + public void unsubscribeWillUnsubscribeFromWrappedSubscriber() { + OperatorWeakBinding op = new OperatorWeakBinding(new Object()); + + op.call(subscriber).unsubscribe(); + subscriber.assertUnsubscribed(); } } From 7ba5be9797ad17bdcbc34307049fdfc1f33ac8f3 Mon Sep 17 00:00:00 2001 From: Matthias Kaeppler Date: Thu, 13 Mar 2014 11:18:01 +0100 Subject: [PATCH 113/422] Rewrite AndroidObservableTest to test the new operator --- .../java/rx/android/observables/AndroidObservable.java | 4 +++- .../rx/android/observables/AndroidObservableTest.java | 10 +++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/rxjava-contrib/rxjava-android/src/main/java/rx/android/observables/AndroidObservable.java b/rxjava-contrib/rxjava-android/src/main/java/rx/android/observables/AndroidObservable.java index 28d8d704aa..46d0e48285 100644 --- a/rxjava-contrib/rxjava-android/src/main/java/rx/android/observables/AndroidObservable.java +++ b/rxjava-contrib/rxjava-android/src/main/java/rx/android/observables/AndroidObservable.java @@ -141,6 +141,7 @@ public static Observable fromFragment(Object fragment, Observable sour * @param source the source sequence */ public static Observable bindActivity(Activity activity, Observable source) { + Assertions.assertUiThread(); return source.observeOn(mainThread()).lift(new OperatorWeakBinding(activity, ACTIVITY_VALIDATOR)); } @@ -155,7 +156,8 @@ public static Observable bindActivity(Activity activity, Observable so * @param source the source sequence */ public static Observable bindFragment(Object fragment, Observable cachedSequence) { - Observable source = cachedSequence.observeOn(mainThread()); + Assertions.assertUiThread(); + final Observable source = cachedSequence.observeOn(mainThread()); if (USES_SUPPORT_FRAGMENTS && fragment instanceof android.support.v4.app.Fragment) { android.support.v4.app.Fragment f = (android.support.v4.app.Fragment) fragment; return source.lift(new OperatorWeakBinding(f, FRAGMENTV4_VALIDATOR)); diff --git a/rxjava-contrib/rxjava-android/src/test/java/rx/android/observables/AndroidObservableTest.java b/rxjava-contrib/rxjava-android/src/test/java/rx/android/observables/AndroidObservableTest.java index 0591c6454e..fe7263da0f 100644 --- a/rxjava-contrib/rxjava-android/src/test/java/rx/android/observables/AndroidObservableTest.java +++ b/rxjava-contrib/rxjava-android/src/test/java/rx/android/observables/AndroidObservableTest.java @@ -69,21 +69,21 @@ public void setup() { @Test public void itSupportsFragmentsFromTheSupportV4Library() { - AndroidObservable.fromFragment(supportFragment, Observable.just("success")).subscribe(new TestObserver(observer)); + AndroidObservable.bindFragment(supportFragment, Observable.just("success")).subscribe(new TestObserver(observer)); verify(observer).onNext("success"); verify(observer).onCompleted(); } @Test public void itSupportsNativeFragments() { - AndroidObservable.fromFragment(fragment, Observable.just("success")).subscribe(new TestObserver(observer)); + AndroidObservable.bindFragment(fragment, Observable.just("success")).subscribe(new TestObserver(observer)); verify(observer).onNext("success"); verify(observer).onCompleted(); } @Test(expected = IllegalArgumentException.class) public void itThrowsIfObjectPassedIsNotAFragment() { - AndroidObservable.fromFragment("not a fragment", Observable.never()); + AndroidObservable.bindFragment("not a fragment", Observable.never()); } @Test(expected = IllegalStateException.class) @@ -91,7 +91,7 @@ public void itThrowsIfObserverCallsFromFragmentFromBackgroundThread() throws Thr final Future future = Executors.newSingleThreadExecutor().submit(new Callable() { @Override public Object call() throws Exception { - AndroidObservable.fromFragment(fragment, Observable.empty()); + AndroidObservable.bindFragment(fragment, Observable.empty()); return null; } }); @@ -107,7 +107,7 @@ public void itThrowsIfObserverCallsFromActivityFromBackgroundThread() throws Thr final Future future = Executors.newSingleThreadExecutor().submit(new Callable() { @Override public Object call() throws Exception { - AndroidObservable.fromActivity(activity, Observable.empty()); + AndroidObservable.bindActivity(activity, Observable.empty()); return null; } }); From 4feddf8ae49db32d7441f8d274377f951b5ffb8b Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Thu, 13 Mar 2014 09:37:11 -0700 Subject: [PATCH 114/422] Retry Unit Test from #879 From https://github.com/Netflix/RxJava/issues/879 --- .../java/rx/operators/OperatorRetryTest.java | 42 +++++++++++++++++-- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/rxjava-core/src/test/java/rx/operators/OperatorRetryTest.java b/rxjava-core/src/test/java/rx/operators/OperatorRetryTest.java index 6fbcf19678..1092156fd8 100644 --- a/rxjava-core/src/test/java/rx/operators/OperatorRetryTest.java +++ b/rxjava-core/src/test/java/rx/operators/OperatorRetryTest.java @@ -18,7 +18,6 @@ import static org.junit.Assert.*; import static org.mockito.Matchers.*; import static org.mockito.Mockito.*; -import static rx.operators.OperatorRetry.*; import java.util.concurrent.atomic.AtomicInteger; @@ -26,6 +25,7 @@ import org.mockito.InOrder; import rx.Observable; +import rx.Observable.OnSubscribeFunc; import rx.Observer; import rx.Subscription; import rx.functions.Action1; @@ -130,7 +130,7 @@ public Subscription onSubscribe(Observer o) { return Subscriptions.empty(); } } - + @Test public void testUnsubscribeFromRetry() { PublishSubject subject = PublishSubject.create(); @@ -139,10 +139,44 @@ public void testUnsubscribeFromRetry() { @Override public void call(Integer n) { count.incrementAndGet(); - }}); + } + }); subject.onNext(1); sub.unsubscribe(); subject.onNext(2); - assertEquals(1,count.get()); + assertEquals(1, count.get()); + } + + @Test + public void testRetryAllowsSubscriptionAfterAllSubscriptionsUnsubsribed() throws InterruptedException { + final AtomicInteger subsCount = new AtomicInteger(0); + OnSubscribeFunc onSubscribe = new OnSubscribeFunc() { + @Override + public Subscription onSubscribe(Observer observer) { + subsCount.incrementAndGet(); + return new Subscription() { + boolean unsubscribed = false; + + @Override + public void unsubscribe() { + subsCount.decrementAndGet(); + unsubscribed = true; + } + + @Override + public boolean isUnsubscribed() { + return unsubscribed; + } + }; + } + }; + Observable stream = Observable.create(onSubscribe); + Observable streamWithRetry = stream.retry(); + Subscription sub = streamWithRetry.subscribe(); + assertEquals(1, subsCount.get()); + sub.unsubscribe(); + assertEquals(0, subsCount.get()); + streamWithRetry.subscribe(); + assertEquals(1, subsCount.get()); } } From 9a0f54f2c0d498c43ab4df621233262b969cccb2 Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Fri, 28 Feb 2014 13:21:14 -0800 Subject: [PATCH 115/422] OperationSynchronize -> OperatorSynchronize --- rxjava-core/src/main/java/rx/Observable.java | 8 +- .../rx/operators/OperationSynchronize.java | 99 --- .../rx/operators/OperatorSynchronize.java | 75 ++ .../rx/operators/OperatorSynchronizeTest.java | 688 ++++++++++++++++++ 4 files changed, 767 insertions(+), 103 deletions(-) delete mode 100644 rxjava-core/src/main/java/rx/operators/OperationSynchronize.java create mode 100644 rxjava-core/src/main/java/rx/operators/OperatorSynchronize.java create mode 100644 rxjava-core/src/test/java/rx/operators/OperatorSynchronizeTest.java diff --git a/rxjava-core/src/main/java/rx/Observable.java b/rxjava-core/src/main/java/rx/Observable.java index 0d9f74d60e..772ca0ed72 100644 --- a/rxjava-core/src/main/java/rx/Observable.java +++ b/rxjava-core/src/main/java/rx/Observable.java @@ -94,7 +94,6 @@ import rx.operators.OperatorSkipWhile; import rx.operators.OperationSum; import rx.operators.OperationSwitch; -import rx.operators.OperationSynchronize; import rx.operators.OperationTakeLast; import rx.operators.OperationTakeTimed; import rx.operators.OperationTakeUntil; @@ -122,6 +121,7 @@ import rx.operators.OperatorScan; import rx.operators.OperatorSkip; import rx.operators.OperatorSubscribeOn; +import rx.operators.OperatorSynchronize; import rx.operators.OperatorTake; import rx.operators.OperatorTimeout; import rx.operators.OperatorTimeoutWithSelector; @@ -2712,7 +2712,7 @@ public final static Observable switchOnNext(Observable Observable synchronize(Observable source) { - return create(OperationSynchronize.synchronize(source)); + return source.synchronize(); } /** @@ -7261,7 +7261,7 @@ public final Observable switchMap(Func1RxJava Wiki: synchronize() */ public final Observable synchronize() { - return create(OperationSynchronize.synchronize(this)); + return lift(new OperatorSynchronize()); } /** @@ -7285,7 +7285,7 @@ public final Observable synchronize() { * @see RxJava Wiki: synchronize() */ public final Observable synchronize(Object lock) { - return create(OperationSynchronize.synchronize(this, lock)); + return lift(new OperatorSynchronize(lock)); } /** diff --git a/rxjava-core/src/main/java/rx/operators/OperationSynchronize.java b/rxjava-core/src/main/java/rx/operators/OperationSynchronize.java deleted file mode 100644 index e94411b313..0000000000 --- a/rxjava-core/src/main/java/rx/operators/OperationSynchronize.java +++ /dev/null @@ -1,99 +0,0 @@ -/** - * Copyright 2014 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package rx.operators; - -import rx.Observable; -import rx.Observable.OnSubscribeFunc; -import rx.Observer; -import rx.Subscription; -import rx.observers.SynchronizedObserver; - -/** - * Wraps an Observable in another Observable that ensures that the resulting Observable is - * chronologically well-behaved. - *

- * - *

- * A well-behaved Observable does not interleave its invocations of the onNext, - * onCompleted, and onError methods of its Observers; it invokes - * onCompleted or onError only once; and it never invokes - * onNext after invoking either onCompleted or onError. The - * synchronize operation enforces this, and the Observable it returns invokes onNext - * and onCompleted or onError synchronously. - *

- * NOTE: {@link Observable#create} already wraps Observables so this is generally redundant. - * - * @param - * The type of the observable sequence. - */ -public final class OperationSynchronize { - - /** - * Accepts an observable and wraps it in another observable which ensures that the resulting observable is well-behaved. - * - * A well-behaved observable ensures onNext, onCompleted, or onError calls to its subscribers are - * not interleaved, onCompleted and onError are only called once respectively, and no - * onNext calls follow onCompleted and onError calls. - * - * @param observable - * @param - * @return the wrapped synchronized observable sequence - */ - public static OnSubscribeFunc synchronize(Observable observable) { - return new Synchronize(observable, null); - } - - /** - * Accepts an observable and wraps it in another observable which ensures that the resulting observable is well-behaved. - * This is accomplished by acquiring a mutual-exclusion lock for the object provided as the lock parameter. - * - * A well-behaved observable ensures onNext, onCompleted, or onError calls to its subscribers are - * not interleaved, onCompleted and onError are only called once respectively, and no - * onNext calls follow onCompleted and onError calls. - * - * @param observable - * @param lock - * The lock object to synchronize each observer call on - * @param - * @return the wrapped synchronized observable sequence - */ - public static OnSubscribeFunc synchronize(Observable observable, Object lock) { - return new Synchronize(observable, lock); - } - - private static class Synchronize implements OnSubscribeFunc { - - public Synchronize(Observable innerObservable, Object lock) { - this.innerObservable = innerObservable; - this.lock = lock; - } - - private Observable innerObservable; - private SynchronizedObserver atomicObserver; - private Object lock; - - public Subscription onSubscribe(Observer observer) { - if (lock == null) { - atomicObserver = new SynchronizedObserver(observer); - } - else { - atomicObserver = new SynchronizedObserver(observer, lock); - } - return innerObservable.subscribe(atomicObserver); - } - - } -} diff --git a/rxjava-core/src/main/java/rx/operators/OperatorSynchronize.java b/rxjava-core/src/main/java/rx/operators/OperatorSynchronize.java new file mode 100644 index 0000000000..0cdc9eded8 --- /dev/null +++ b/rxjava-core/src/main/java/rx/operators/OperatorSynchronize.java @@ -0,0 +1,75 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package rx.operators; + +import rx.Observable; +import rx.Observable.Operator; +import rx.Subscriber; +import rx.observers.SynchronizedSubscriber; + +/** + * Wraps an Observable in another Observable that ensures that the resulting Observable is + * chronologically well-behaved. + *

+ * + *

+ * A well-behaved Observable does not interleave its invocations of the onNext, + * onCompleted, and onError methods of its Observers; it invokes + * onCompleted or onError only once; and it never invokes + * onNext after invoking either onCompleted or onError. The + * synchronize operation enforces this, and the Observable it returns invokes onNext + * and onCompleted or onError synchronously. + *

+ * NOTE: {@link Observable#create} already wraps Observables so this is generally redundant. + * + * @param + * The type of the observable sequence. + */ +public final class OperatorSynchronize implements Operator { + + final Object lock; + + public OperatorSynchronize(Object lock) { + this.lock = lock; + } + + public OperatorSynchronize() { + this.lock = new Object(); + } + + @Override + public Subscriber call(final Subscriber s) { + return new SynchronizedSubscriber(new Subscriber(s) { + + @Override + public void onCompleted() { + s.onCompleted(); + } + + @Override + public void onError(Throwable e) { + s.onError(e); + } + + @Override + public void onNext(T t) { + s.onNext(t); + } + + }, lock); + } + +} diff --git a/rxjava-core/src/test/java/rx/operators/OperatorSynchronizeTest.java b/rxjava-core/src/test/java/rx/operators/OperatorSynchronizeTest.java new file mode 100644 index 0000000000..9ec9396f5b --- /dev/null +++ b/rxjava-core/src/test/java/rx/operators/OperatorSynchronizeTest.java @@ -0,0 +1,688 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package rx.operators; + +import static org.junit.Assert.*; +import static org.mockito.Matchers.*; +import static org.mockito.Mockito.*; + +import java.util.Random; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import rx.Observable; +import rx.Observer; +import rx.Subscriber; +import rx.Subscription; +import rx.observers.TestSubscriber; + +public class OperatorSynchronizeTest { + + @Mock + Observer observer; + + @Before + public void before() { + MockitoAnnotations.initMocks(this); + } + + @Test + public void testSingleThreadedBasic() { + Subscription s = mock(Subscription.class); + TestSingleThreadedObservable onSubscribe = new TestSingleThreadedObservable(s, "one", "two", "three"); + Observable w = Observable.create(onSubscribe); + + w.synchronize().subscribe(observer); + onSubscribe.waitToFinish(); + + verify(observer, times(1)).onNext("one"); + verify(observer, times(1)).onNext("two"); + verify(observer, times(1)).onNext("three"); + verify(observer, never()).onError(any(Throwable.class)); + verify(observer, times(1)).onCompleted(); + // non-deterministic because unsubscribe happens after 'waitToFinish' releases + // so commenting out for now as this is not a critical thing to test here + // verify(s, times(1)).unsubscribe(); + } + + @Test + public void testMultiThreadedBasic() { + Subscription s = mock(Subscription.class); + TestMultiThreadedObservable onSubscribe = new TestMultiThreadedObservable(s, "one", "two", "three"); + Observable w = Observable.create(onSubscribe); + + BusyObserver busyobserver = new BusyObserver(); + + w.synchronize().subscribe(busyobserver); + onSubscribe.waitToFinish(); + + assertEquals(3, busyobserver.onNextCount.get()); + assertFalse(busyobserver.onError); + assertTrue(busyobserver.onCompleted); + // non-deterministic because unsubscribe happens after 'waitToFinish' releases + // so commenting out for now as this is not a critical thing to test here + // verify(s, times(1)).unsubscribe(); + + // we can have concurrency ... + assertTrue(onSubscribe.maxConcurrentThreads.get() > 1); + // ... but the onNext execution should be single threaded + assertEquals(1, busyobserver.maxConcurrentThreads.get()); + } + + @Test + public void testMultiThreadedBasicWithLock() { + Subscription s = mock(Subscription.class); + TestMultiThreadedObservable onSubscribe = new TestMultiThreadedObservable(s, "one", "two", "three"); + Observable w = Observable.create(onSubscribe); + + BusyObserver busyobserver = new BusyObserver(); + + Object lock = new Object(); + ExternalBusyThread externalBusyThread = new ExternalBusyThread(busyobserver, lock, 10, 100); + + externalBusyThread.start(); + + w.synchronize(lock).subscribe(busyobserver); + onSubscribe.waitToFinish(); + + try { + externalBusyThread.join(10000); + assertFalse(externalBusyThread.isAlive()); + assertFalse(externalBusyThread.fail); + } catch (InterruptedException e) { + // ignore + } + + assertEquals(3, busyobserver.onNextCount.get()); + assertFalse(busyobserver.onError); + assertTrue(busyobserver.onCompleted); + // non-deterministic because unsubscribe happens after 'waitToFinish' releases + // so commenting out for now as this is not a critical thing to test here + // verify(s, times(1)).unsubscribe(); + + // we can have concurrency ... + assertTrue(onSubscribe.maxConcurrentThreads.get() > 1); + // ... but the onNext execution should be single threaded + assertEquals(1, busyobserver.maxConcurrentThreads.get()); + } + + @Test + public void testMultiThreadedWithNPE() { + Subscription s = mock(Subscription.class); + TestMultiThreadedObservable onSubscribe = new TestMultiThreadedObservable(s, "one", "two", "three", null); + Observable w = Observable.create(onSubscribe); + + BusyObserver busyobserver = new BusyObserver(); + + w.synchronize().subscribe(busyobserver); + onSubscribe.waitToFinish(); + + System.out.println("maxConcurrentThreads: " + onSubscribe.maxConcurrentThreads.get()); + + // we can't know how many onNext calls will occur since they each run on a separate thread + // that depends on thread scheduling so 0, 1, 2 and 3 are all valid options + // assertEquals(3, busyobserver.onNextCount.get()); + assertTrue(busyobserver.onNextCount.get() < 4); + assertTrue(busyobserver.onError); + // no onCompleted because onError was invoked + assertFalse(busyobserver.onCompleted); + // non-deterministic because unsubscribe happens after 'waitToFinish' releases + // so commenting out for now as this is not a critical thing to test here + //verify(s, times(1)).unsubscribe(); + + // we can have concurrency ... + assertTrue(onSubscribe.maxConcurrentThreads.get() > 1); + // ... but the onNext execution should be single threaded + assertEquals(1, busyobserver.maxConcurrentThreads.get()); + } + + @Test + public void testMultiThreadedWithNPEAndLock() { + Subscription s = mock(Subscription.class); + TestMultiThreadedObservable onSubscribe = new TestMultiThreadedObservable(s, "one", "two", "three", null); + Observable w = Observable.create(onSubscribe); + + BusyObserver busyobserver = new BusyObserver(); + + Object lock = new Object(); + ExternalBusyThread externalBusyThread = new ExternalBusyThread(busyobserver, lock, 10, 100); + + externalBusyThread.start(); + + w.synchronize(lock).subscribe(busyobserver); + onSubscribe.waitToFinish(); + + try { + externalBusyThread.join(10000); + assertFalse(externalBusyThread.isAlive()); + assertFalse(externalBusyThread.fail); + } catch (InterruptedException e) { + // ignore + } + + System.out.println("maxConcurrentThreads: " + onSubscribe.maxConcurrentThreads.get()); + + // we can't know how many onNext calls will occur since they each run on a separate thread + // that depends on thread scheduling so 0, 1, 2 and 3 are all valid options + // assertEquals(3, busyobserver.onNextCount.get()); + assertTrue(busyobserver.onNextCount.get() < 4); + assertTrue(busyobserver.onError); + // no onCompleted because onError was invoked + assertFalse(busyobserver.onCompleted); + // non-deterministic because unsubscribe happens after 'waitToFinish' releases + // so commenting out for now as this is not a critical thing to test here + //verify(s, times(1)).unsubscribe(); + + // we can have concurrency ... + assertTrue(onSubscribe.maxConcurrentThreads.get() > 1); + // ... but the onNext execution should be single threaded + assertEquals(1, busyobserver.maxConcurrentThreads.get()); + } + + @Test + public void testMultiThreadedWithNPEinMiddle() { + Subscription s = mock(Subscription.class); + TestMultiThreadedObservable onSubscribe = new TestMultiThreadedObservable(s, "one", "two", "three", null, "four", "five", "six", "seven", "eight", "nine"); + Observable w = Observable.create(onSubscribe); + + BusyObserver busyobserver = new BusyObserver(); + + w.synchronize().subscribe(busyobserver); + onSubscribe.waitToFinish(); + + System.out.println("maxConcurrentThreads: " + onSubscribe.maxConcurrentThreads.get()); + // this should not be the full number of items since the error should stop it before it completes all 9 + System.out.println("onNext count: " + busyobserver.onNextCount.get()); + assertTrue(busyobserver.onNextCount.get() < 9); + assertTrue(busyobserver.onError); + // no onCompleted because onError was invoked + assertFalse(busyobserver.onCompleted); + // non-deterministic because unsubscribe happens after 'waitToFinish' releases + // so commenting out for now as this is not a critical thing to test here + // verify(s, times(1)).unsubscribe(); + + // we can have concurrency ... + assertTrue(onSubscribe.maxConcurrentThreads.get() > 1); + // ... but the onNext execution should be single threaded + assertEquals(1, busyobserver.maxConcurrentThreads.get()); + } + + @Test + public void testMultiThreadedWithNPEinMiddleAndLock() { + Subscription s = mock(Subscription.class); + TestMultiThreadedObservable onSubscribe = new TestMultiThreadedObservable(s, "one", "two", "three", null, "four", "five", "six", "seven", "eight", "nine"); + Observable w = Observable.create(onSubscribe); + + BusyObserver busyobserver = new BusyObserver(); + + Object lock = new Object(); + ExternalBusyThread externalBusyThread = new ExternalBusyThread(busyobserver, lock, 10, 100); + + externalBusyThread.start(); + + w.synchronize(lock).subscribe(busyobserver); + onSubscribe.waitToFinish(); + + try { + externalBusyThread.join(10000); + assertFalse(externalBusyThread.isAlive()); + assertFalse(externalBusyThread.fail); + } catch (InterruptedException e) { + // ignore + } + + System.out.println("maxConcurrentThreads: " + onSubscribe.maxConcurrentThreads.get()); + // this should not be the full number of items since the error should stop it before it completes all 9 + System.out.println("onNext count: " + busyobserver.onNextCount.get()); + assertTrue(busyobserver.onNextCount.get() < 9); + assertTrue(busyobserver.onError); + // no onCompleted because onError was invoked + assertFalse(busyobserver.onCompleted); + // non-deterministic because unsubscribe happens after 'waitToFinish' releases + // so commenting out for now as this is not a critical thing to test here + // verify(s, times(1)).unsubscribe(); + + // we can have concurrency ... + assertTrue(onSubscribe.maxConcurrentThreads.get() > 1); + // ... but the onNext execution should be single threaded + assertEquals(1, busyobserver.maxConcurrentThreads.get()); + } + + /** + * A thread that will pass data to onNext + */ + public static class OnNextThread implements Runnable { + + private final Observer observer; + private final int numStringsToSend; + + OnNextThread(Observer observer, int numStringsToSend) { + this.observer = observer; + this.numStringsToSend = numStringsToSend; + } + + @Override + public void run() { + for (int i = 0; i < numStringsToSend; i++) { + observer.onNext("aString"); + } + } + } + + /** + * A thread that will call onError or onNext + */ + public static class CompletionThread implements Runnable { + + private final Observer observer; + private final TestConcurrencyobserverEvent event; + private final Future[] waitOnThese; + + CompletionThread(Observer observer, TestConcurrencyobserverEvent event, Future... waitOnThese) { + this.observer = observer; + this.event = event; + this.waitOnThese = waitOnThese; + } + + @Override + public void run() { + /* if we have 'waitOnThese' futures, we'll wait on them before proceeding */ + if (waitOnThese != null) { + for (Future f : waitOnThese) { + try { + f.get(); + } catch (Throwable e) { + System.err.println("Error while waiting on future in CompletionThread"); + } + } + } + + /* send the event */ + if (event == TestConcurrencyobserverEvent.onError) { + observer.onError(new RuntimeException("mocked exception")); + } else if (event == TestConcurrencyobserverEvent.onCompleted) { + observer.onCompleted(); + + } else { + throw new IllegalArgumentException("Expecting either onError or onCompleted"); + } + } + } + + private static enum TestConcurrencyobserverEvent { + onCompleted, onError, onNext + } + + private static class TestConcurrencyobserver extends Subscriber { + + /** + * used to store the order and number of events received + */ + private final LinkedBlockingQueue events = new LinkedBlockingQueue(); + private final int waitTime; + + @SuppressWarnings("unused") + public TestConcurrencyobserver(int waitTimeInNext) { + this.waitTime = waitTimeInNext; + } + + public TestConcurrencyobserver() { + this.waitTime = 0; + } + + @Override + public void onCompleted() { + events.add(TestConcurrencyobserverEvent.onCompleted); + } + + @Override + public void onError(Throwable e) { + events.add(TestConcurrencyobserverEvent.onError); + } + + @Override + public void onNext(String args) { + events.add(TestConcurrencyobserverEvent.onNext); + // do some artificial work to make the thread scheduling/timing vary + int s = 0; + for (int i = 0; i < 20; i++) { + s += s * i; + } + + if (waitTime > 0) { + try { + Thread.sleep(waitTime); + } catch (InterruptedException e) { + // ignore + } + } + } + + /** + * Assert the order of events is correct and return the number of onNext executions. + * + * @param expectedEndingEvent + * @return int count of onNext calls + * @throws IllegalStateException + * If order of events was invalid. + */ + public int assertEvents(TestConcurrencyobserverEvent expectedEndingEvent) throws IllegalStateException { + int nextCount = 0; + boolean finished = false; + for (TestConcurrencyobserverEvent e : events) { + if (e == TestConcurrencyobserverEvent.onNext) { + if (finished) { + // already finished, we shouldn't get this again + throw new IllegalStateException("Received onNext but we're already finished."); + } + nextCount++; + } else if (e == TestConcurrencyobserverEvent.onError) { + if (finished) { + // already finished, we shouldn't get this again + throw new IllegalStateException("Received onError but we're already finished."); + } + if (expectedEndingEvent != null && TestConcurrencyobserverEvent.onError != expectedEndingEvent) { + throw new IllegalStateException("Received onError ending event but expected " + expectedEndingEvent); + } + finished = true; + } else if (e == TestConcurrencyobserverEvent.onCompleted) { + if (finished) { + // already finished, we shouldn't get this again + throw new IllegalStateException("Received onCompleted but we're already finished."); + } + if (expectedEndingEvent != null && TestConcurrencyobserverEvent.onCompleted != expectedEndingEvent) { + throw new IllegalStateException("Received onCompleted ending event but expected " + expectedEndingEvent); + } + finished = true; + } + } + + return nextCount; + } + + } + + /** + * This spawns a single thread for the subscribe execution + */ + private static class TestSingleThreadedObservable implements Observable.OnSubscribeFunc { + + final Subscription s; + final String[] values; + private Thread t = null; + + public TestSingleThreadedObservable(final Subscription s, final String... values) { + this.s = s; + this.values = values; + + } + + public Subscription onSubscribe(final Observer observer) { + System.out.println("TestSingleThreadedObservable subscribed to ..."); + t = new Thread(new Runnable() { + + @Override + public void run() { + try { + System.out.println("running TestSingleThreadedObservable thread"); + for (String s : values) { + System.out.println("TestSingleThreadedObservable onNext: " + s); + observer.onNext(s); + } + observer.onCompleted(); + } catch (Throwable e) { + throw new RuntimeException(e); + } + } + + }); + System.out.println("starting TestSingleThreadedObservable thread"); + t.start(); + System.out.println("done starting TestSingleThreadedObservable thread"); + return s; + } + + public void waitToFinish() { + try { + t.join(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + } + + /** + * This spawns a thread for the subscription, then a separate thread for each onNext call. + */ + private static class TestMultiThreadedObservable implements Observable.OnSubscribeFunc { + + final Subscription s; + final String[] values; + Thread t = null; + AtomicInteger threadsRunning = new AtomicInteger(); + AtomicInteger maxConcurrentThreads = new AtomicInteger(); + ExecutorService threadPool; + + public TestMultiThreadedObservable(Subscription s, String... values) { + this.s = s; + this.values = values; + this.threadPool = Executors.newCachedThreadPool(); + } + + @Override + public Subscription onSubscribe(final Observer observer) { + System.out.println("TestMultiThreadedObservable subscribed to ..."); + t = new Thread(new Runnable() { + + @Override + public void run() { + try { + System.out.println("running TestMultiThreadedObservable thread"); + for (final String s : values) { + threadPool.execute(new Runnable() { + + @Override + public void run() { + threadsRunning.incrementAndGet(); + try { + // perform onNext call + System.out.println("TestMultiThreadedObservable onNext: " + s); + if (s == null) { + // force an error + throw new NullPointerException(); + } + observer.onNext(s); + // capture 'maxThreads' + int concurrentThreads = threadsRunning.get(); + int maxThreads = maxConcurrentThreads.get(); + if (concurrentThreads > maxThreads) { + maxConcurrentThreads.compareAndSet(maxThreads, concurrentThreads); + } + } catch (Throwable e) { + observer.onError(e); + } finally { + threadsRunning.decrementAndGet(); + } + } + }); + } + // we are done spawning threads + threadPool.shutdown(); + } catch (Throwable e) { + throw new RuntimeException(e); + } + + // wait until all threads are done, then mark it as COMPLETED + try { + // wait for all the threads to finish + threadPool.awaitTermination(2, TimeUnit.SECONDS); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + observer.onCompleted(); + } + }); + System.out.println("starting TestMultiThreadedObservable thread"); + t.start(); + System.out.println("done starting TestMultiThreadedObservable thread"); + return s; + } + + public void waitToFinish() { + try { + t.join(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + } + + private static class BusyObserver extends Subscriber { + volatile boolean onCompleted = false; + volatile boolean onError = false; + AtomicInteger onNextCount = new AtomicInteger(); + AtomicInteger threadsRunning = new AtomicInteger(); + AtomicInteger maxConcurrentThreads = new AtomicInteger(); + + @Override + public void onCompleted() { + threadsRunning.incrementAndGet(); + + System.out.println(">>> Busyobserver received onCompleted"); + onCompleted = true; + + int concurrentThreads = threadsRunning.get(); + int maxThreads = maxConcurrentThreads.get(); + if (concurrentThreads > maxThreads) { + maxConcurrentThreads.compareAndSet(maxThreads, concurrentThreads); + } + threadsRunning.decrementAndGet(); + } + + @Override + public void onError(Throwable e) { + threadsRunning.incrementAndGet(); + + System.out.println(">>> Busyobserver received onError: " + e.getMessage()); + onError = true; + + int concurrentThreads = threadsRunning.get(); + int maxThreads = maxConcurrentThreads.get(); + if (concurrentThreads > maxThreads) { + maxConcurrentThreads.compareAndSet(maxThreads, concurrentThreads); + } + threadsRunning.decrementAndGet(); + } + + @Override + public void onNext(String args) { + threadsRunning.incrementAndGet(); + try { + onNextCount.incrementAndGet(); + System.out.println(">>> Busyobserver received onNext: " + args); + try { + // simulate doing something computational + Thread.sleep(200); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } finally { + // capture 'maxThreads' + int concurrentThreads = threadsRunning.get(); + int maxThreads = maxConcurrentThreads.get(); + if (concurrentThreads > maxThreads) { + maxConcurrentThreads.compareAndSet(maxThreads, concurrentThreads); + } + threadsRunning.decrementAndGet(); + } + } + + } + + private static class ExternalBusyThread extends Thread { + + private BusyObserver observer; + private Object lock; + private int lockTimes; + private int waitTime; + public volatile boolean fail; + + public ExternalBusyThread(BusyObserver observer, Object lock, int lockTimes, int waitTime) { + this.observer = observer; + this.lock = lock; + this.lockTimes = lockTimes; + this.waitTime = waitTime; + this.fail = false; + } + + @Override + public void run() { + Random r = new Random(); + for (int i = 0; i < lockTimes; i++) { + synchronized (lock) { + int oldOnNextCount = observer.onNextCount.get(); + boolean oldOnCompleted = observer.onCompleted; + boolean oldOnError = observer.onError; + try { + Thread.sleep(r.nextInt(waitTime)); + } catch (InterruptedException e) { + // ignore + } + // Since we own the lock, onNextCount, onCompleted and + // onError must not be changed. + int newOnNextCount = observer.onNextCount.get(); + boolean newOnCompleted = observer.onCompleted; + boolean newOnError = observer.onError; + if (oldOnNextCount != newOnNextCount) { + System.out.println(">>> ExternalBusyThread received different onNextCount: " + + oldOnNextCount + + " -> " + + newOnNextCount); + fail = true; + break; + } + if (oldOnCompleted != newOnCompleted) { + System.out.println(">>> ExternalBusyThread received different onCompleted: " + + oldOnCompleted + + " -> " + + newOnCompleted); + fail = true; + break; + } + if (oldOnError != newOnError) { + System.out.println(">>> ExternalBusyThread received different onError: " + + oldOnError + + " -> " + + newOnError); + fail = true; + break; + } + } + } + } + + } +} From 4427d03db0d8ba67137654447c6c7a57615c0b00 Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Fri, 28 Feb 2014 21:11:32 -0800 Subject: [PATCH 116/422] OperatorSerialize --- rxjava-core/src/main/java/rx/Observable.java | 9 +- .../java/rx/observers/SerializedObserver.java | 183 ++++++ .../rx/observers/SerializedSubscriber.java | 28 + .../java/rx/operators/OperatorSerialize.java | 46 ++ .../OperatorSerializePerformance.java | 230 +++++++ .../OperatorSynchronizePerformance.java | 236 +++++++ .../rx/observers/SerializedObserverTest.java | 575 ++++++++++++++++++ .../rx/operators/OperatorSerializeTest.java | 566 +++++++++++++++++ 8 files changed, 1871 insertions(+), 2 deletions(-) create mode 100644 rxjava-core/src/main/java/rx/observers/SerializedObserver.java create mode 100644 rxjava-core/src/main/java/rx/observers/SerializedSubscriber.java create mode 100644 rxjava-core/src/main/java/rx/operators/OperatorSerialize.java create mode 100644 rxjava-core/src/perf/java/rx/operators/OperatorSerializePerformance.java create mode 100644 rxjava-core/src/perf/java/rx/operators/OperatorSynchronizePerformance.java create mode 100644 rxjava-core/src/test/java/rx/observers/SerializedObserverTest.java create mode 100644 rxjava-core/src/test/java/rx/operators/OperatorSerializeTest.java diff --git a/rxjava-core/src/main/java/rx/Observable.java b/rxjava-core/src/main/java/rx/Observable.java index 772ca0ed72..7874ff8f3f 100644 --- a/rxjava-core/src/main/java/rx/Observable.java +++ b/rxjava-core/src/main/java/rx/Observable.java @@ -52,7 +52,6 @@ import rx.operators.OnSubscribeFromIterable; import rx.operators.OnSubscribeRange; import rx.operators.OperationAll; -import rx.operators.OperatorAmb; import rx.operators.OperationAny; import rx.operators.OperationAsObservable; import rx.operators.OperationAverage; @@ -91,7 +90,6 @@ import rx.operators.OperationSkip; import rx.operators.OperationSkipLast; import rx.operators.OperationSkipUntil; -import rx.operators.OperatorSkipWhile; import rx.operators.OperationSum; import rx.operators.OperationSwitch; import rx.operators.OperationTakeLast; @@ -106,6 +104,7 @@ import rx.operators.OperationToObservableFuture; import rx.operators.OperationUsing; import rx.operators.OperationWindow; +import rx.operators.OperatorAmb; import rx.operators.OperatorCast; import rx.operators.OperatorDoOnEach; import rx.operators.OperatorFilter; @@ -119,7 +118,9 @@ import rx.operators.OperatorRepeat; import rx.operators.OperatorRetry; import rx.operators.OperatorScan; +import rx.operators.OperatorSerialize; import rx.operators.OperatorSkip; +import rx.operators.OperatorSkipWhile; import rx.operators.OperatorSubscribeOn; import rx.operators.OperatorSynchronize; import rx.operators.OperatorTake; @@ -6197,6 +6198,10 @@ public final Observable scan(R initialValue, Func2 accum return lift(new OperatorScan(initialValue, accumulator)); } + public final Observable serialize() { + return lift(new OperatorSerialize()); + } + /** * If the source Observable completes after emitting a single item, return an Observable that emits that * item. If the source Observable emits more than one item or no items, throw an diff --git a/rxjava-core/src/main/java/rx/observers/SerializedObserver.java b/rxjava-core/src/main/java/rx/observers/SerializedObserver.java new file mode 100644 index 0000000000..fd4730290f --- /dev/null +++ b/rxjava-core/src/main/java/rx/observers/SerializedObserver.java @@ -0,0 +1,183 @@ +package rx.observers; + +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +import rx.Observer; + +public class SerializedObserver implements Observer { + + private final AtomicReference state = new AtomicReference(State.createNew()); + private final Observer s; + + public SerializedObserver(Observer s) { + this.s = s; + } + + @Override + public void onCompleted() { + State current = null; + State newState = null; + do { + current = state.get(); + if (current.isTerminated()) { + // already received terminal state + return; + } + newState = current.complete(); + } while (!state.compareAndSet(current, newState)); + if (newState.count == 0) { + s.onCompleted(); + } + } + + @Override + public void onError(Throwable e) { + State current = null; + State newState = null; + do { + current = state.get(); + if (current.isTerminated()) { + // already received terminal state + return; + } + newState = current.error(e); + } while (!state.compareAndSet(current, newState)); + if (newState.count == 0) { + s.onError(e); + } + } + + @SuppressWarnings("unchecked") + @Override + public void onNext(T t) { + State current = null; + State newState = null; + do { + current = state.get(); + if (current.isTerminated()) { + // already received terminal state + return; + } + newState = current.increment(t); + } while (!state.compareAndSet(current, newState)); + + if (newState.count == 1) { + // this thread wins and will emit then drain queue if it concurrently gets added to + try { + s.onNext(t); + } finally { + // decrement after finishing + do { + current = state.get(); + newState = current.decrement(); + } while (!state.compareAndSet(current, newState)); + } + + // drain queue if exists + // we do "if" instead of "while" so we don't starve one thread + if (newState.queue.length > 0) { + Object[] items = newState.queue; + for (int i = 0; i < items.length; i++) { + s.onNext((T) items[i]); + } + // clear state of queue + do { + current = state.get(); + newState = current.drain(items.length); + } while (!state.compareAndSet(current, newState)); + terminateIfNecessary(newState); + } else { + terminateIfNecessary(newState); + } + + } + } + + private void terminateIfNecessary(State state) { + if (state.isTerminated()) { + if (state.onComplete) { + s.onCompleted(); + } else { + s.onError(state.onError); + } + } + } + + public static class State { + final int count; + final Object[] queue; + final boolean onComplete; + final Throwable onError; + + private final static Object[] EMPTY = new Object[0]; + + private final static State NON_TERMINATED_EMPTY = new State(0, false, null, EMPTY); + private final static State NON_TERMINATED_SINGLE = new State(1, false, null, EMPTY); + + public State(int count, boolean onComplete, Throwable onError, Object[] queue) { + this.count = count; + this.queue = queue; + this.onComplete = onComplete; + this.onError = onError; + } + + public static State createNew() { + return new State(0, false, null, EMPTY); + } + + public boolean isTerminated() { + return onComplete || onError != null; + } + + public State complete() { + return new State(count, true, onError, queue); + } + + public State error(Throwable e) { + return new State(count, onComplete, e, queue); + } + + AtomicInteger max = new AtomicInteger(); + + public State increment(Object item) { + if (count == 0) { + // no concurrent requests so don't queue, we'll process immediately + if (isTerminated()) { + // return count of 0 meaning don't emit as we are terminated + return new State(0, onComplete, onError, EMPTY); + } else { + return NON_TERMINATED_SINGLE; + } + } else { + // concurrent requests so need to queue + int idx = queue.length; + Object[] newQueue = new Object[idx + 1]; + System.arraycopy(queue, 0, newQueue, 0, idx); + newQueue[idx] = item; + + if (max.get() < newQueue.length) { + max.set(newQueue.length); + System.out.println("max queue: " + newQueue.length); + } + + return new State(count + 1, onComplete, onError, newQueue); + } + } + + public State decrement() { + if (count > 1 || isTerminated()) { + return new State(count - 1, onComplete, onError, queue); + } else { + return NON_TERMINATED_EMPTY; + } + } + + public State drain(int c) { + Object[] newQueue = new Object[queue.length - c]; + System.arraycopy(queue, c, newQueue, 0, newQueue.length); + return new State(count - c, onComplete, onError, newQueue); + } + + } +} diff --git a/rxjava-core/src/main/java/rx/observers/SerializedSubscriber.java b/rxjava-core/src/main/java/rx/observers/SerializedSubscriber.java new file mode 100644 index 0000000000..377cef9d74 --- /dev/null +++ b/rxjava-core/src/main/java/rx/observers/SerializedSubscriber.java @@ -0,0 +1,28 @@ +package rx.observers; + +import rx.Observer; +import rx.Subscriber; + +public class SerializedSubscriber extends Subscriber { + + private final Observer s; + + public SerializedSubscriber(Subscriber s) { + this.s = new SerializedObserver(s); + } + + @Override + public void onCompleted() { + s.onCompleted(); + } + + @Override + public void onError(Throwable e) { + s.onError(e); + } + + @Override + public void onNext(T t) { + s.onNext(t); + } +} diff --git a/rxjava-core/src/main/java/rx/operators/OperatorSerialize.java b/rxjava-core/src/main/java/rx/operators/OperatorSerialize.java new file mode 100644 index 0000000000..f41620bab2 --- /dev/null +++ b/rxjava-core/src/main/java/rx/operators/OperatorSerialize.java @@ -0,0 +1,46 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package rx.operators; + +import rx.Observable.Operator; +import rx.Subscriber; +import rx.observers.SerializedSubscriber; + +public final class OperatorSerialize implements Operator { + + @Override + public Subscriber call(final Subscriber s) { + return new SerializedSubscriber(new Subscriber(s) { + + @Override + public void onCompleted() { + s.onCompleted(); + } + + @Override + public void onError(Throwable e) { + s.onError(e); + } + + @Override + public void onNext(T t) { + s.onNext(t); + } + + }); + } + +} diff --git a/rxjava-core/src/perf/java/rx/operators/OperatorSerializePerformance.java b/rxjava-core/src/perf/java/rx/operators/OperatorSerializePerformance.java new file mode 100644 index 0000000000..79596d5733 --- /dev/null +++ b/rxjava-core/src/perf/java/rx/operators/OperatorSerializePerformance.java @@ -0,0 +1,230 @@ +package rx.operators; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import rx.Observable; +import rx.Observable.OnSubscribe; +import rx.Subscriber; +import rx.functions.Action0; +import rx.functions.Action1; +import rx.functions.Func1; +import rx.perf.AbstractPerformanceTester; +import rx.perf.IntegerSumObserver; +import rx.schedulers.Schedulers; + +public class OperatorSerializePerformance extends AbstractPerformanceTester { + // static int reps = Integer.MAX_VALUE / 16384; // timeTwoStreams + + // static int reps = Integer.MAX_VALUE / 1024; // timeSingleStream + + static int reps = 1000; // interval streams + + OperatorSerializePerformance() { + super(reps); + } + + public static void main(String args[]) { + + final OperatorSerializePerformance spt = new OperatorSerializePerformance(); + try { + spt.runTest(new Action0() { + + @Override + public void call() { + spt.timeTwoStreams(); + } + }); + } catch (Exception e) { + e.printStackTrace(); + } + + } + + /** + * Run: 10 - 36,891,795 ops/sec + * Run: 11 - 29,854,808 ops/sec + * Run: 12 - 36,162,140 ops/sec + * Run: 13 - 35,727,201 ops/sec + * Run: 14 - 34,897,262 ops/sec + */ + public long timeSingleStream() { + + final Observable s1 = Observable.range(0, reps).subscribeOn(Schedulers.newThread()); + + Observable s = Observable.create(new OnSubscribe() { + + @Override + public void call(final Subscriber s) { + final CountDownLatch latch = new CountDownLatch(1); + // first + s1.doOnTerminate(new Action0() { + + @Override + public void call() { + latch.countDown(); + } + + }).subscribe(new Action1() { + + @Override + public void call(Integer t1) { + s.onNext(t1); + } + + }); + + try { + latch.await(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + s.onCompleted(); + } + + }).serialize(); + + IntegerSumObserver o = new IntegerSumObserver(); + s.subscribe(o); + // System.out.println("sum : " + o.sum); + + return o.sum; + } + + public long timeTwoStreams() { + + final Observable s1 = Observable.range(0, reps).subscribeOn(Schedulers.newThread()); + final Observable s2 = Observable.range(0, reps).subscribeOn(Schedulers.newThread()); + + Observable s = Observable.create(new OnSubscribe() { + + @Override + public void call(final Subscriber s) { + final CountDownLatch latch = new CountDownLatch(2); + // first + s1.doOnTerminate(new Action0() { + + @Override + public void call() { + latch.countDown(); + } + + }).subscribe(new Action1() { + + @Override + public void call(Integer t1) { + s.onNext(t1); + } + + }); + + // second + s2.doOnTerminate(new Action0() { + + @Override + public void call() { + latch.countDown(); + } + + }).subscribe(new Action1() { + + @Override + public void call(Integer t1) { + s.onNext(t1); + } + + }); + + try { + latch.await(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + s.onCompleted(); + } + + }).serialize(); + + IntegerSumObserver o = new IntegerSumObserver(); + s.subscribe(o); + System.out.println("sum : " + o.sum); + + return o.sum; + } + + /** + * Run: 10 - 1,996 ops/sec + * Run: 11 - 1,996 ops/sec + * Run: 12 - 1,996 ops/sec + * Run: 13 - 1,996 ops/sec + * Run: 14 - 1,996 ops/sec + */ + public long timeTwoStreamsIntervals() { + + final Observable s1 = Observable.interval(1, TimeUnit.MILLISECONDS).take(reps / 2).flatMap(new Func1>() { + + @Override + public Observable call(Long l) { + return Observable.range(l.intValue(), 100); + } + + }).subscribeOn(Schedulers.newThread()); + final Observable s2 = Observable.range(1, reps / 2).subscribeOn(Schedulers.newThread()); + + Observable s = Observable.create(new OnSubscribe() { + + @Override + public void call(final Subscriber s) { + final CountDownLatch latch = new CountDownLatch(2); + // first + s1.doOnTerminate(new Action0() { + + @Override + public void call() { + latch.countDown(); + } + + }).subscribe(new Action1() { + + @Override + public void call(Integer t1) { + s.onNext(t1); + } + + }); + + // second + s2.doOnTerminate(new Action0() { + + @Override + public void call() { + latch.countDown(); + } + + }).subscribe(new Action1() { + + @Override + public void call(Integer t1) { + s.onNext(t1); + } + + }); + + try { + latch.await(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + s.onCompleted(); + } + + }).serialize(); + + IntegerSumObserver o = new IntegerSumObserver(); + s.subscribe(o); + // System.out.println("sum : " + o.sum); + + return o.sum; + } + +} diff --git a/rxjava-core/src/perf/java/rx/operators/OperatorSynchronizePerformance.java b/rxjava-core/src/perf/java/rx/operators/OperatorSynchronizePerformance.java new file mode 100644 index 0000000000..fdd3ced3d3 --- /dev/null +++ b/rxjava-core/src/perf/java/rx/operators/OperatorSynchronizePerformance.java @@ -0,0 +1,236 @@ +package rx.operators; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import rx.Observable; +import rx.Observable.OnSubscribe; +import rx.Subscriber; +import rx.functions.Action0; +import rx.functions.Action1; +import rx.functions.Func1; +import rx.perf.AbstractPerformanceTester; +import rx.perf.IntegerSumObserver; +import rx.schedulers.Schedulers; + +public class OperatorSynchronizePerformance extends AbstractPerformanceTester { + // static int reps = Integer.MAX_VALUE / 1024; + + static int reps = 1000; // timeTwoStreamsIntervals + + OperatorSynchronizePerformance() { + super(reps); + } + + public static void main(String args[]) { + + final OperatorSynchronizePerformance spt = new OperatorSynchronizePerformance(); + try { + spt.runTest(new Action0() { + + @Override + public void call() { + spt.timeTwoStreamsIntervals(); + } + }); + } catch (Exception e) { + e.printStackTrace(); + } + + } + + /** + * Run: 10 - 59,593,390 ops/sec + * Run: 11 - 55,784,194 ops/sec + * Run: 12 - 58,778,300 ops/sec + * Run: 13 - 60,679,696 ops/sec + * Run: 14 - 59,370,693 ops/sec + */ + public long timeSingleStream() { + + final Observable s1 = Observable.range(0, reps).subscribeOn(Schedulers.newThread()); + + Observable s = Observable.create(new OnSubscribe() { + + @Override + public void call(final Subscriber s) { + final CountDownLatch latch = new CountDownLatch(1); + // first + s1.doOnTerminate(new Action0() { + + @Override + public void call() { + latch.countDown(); + } + + }).subscribe(new Action1() { + + @Override + public void call(Integer t1) { + s.onNext(t1); + } + + }); + + try { + latch.await(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + s.onCompleted(); + } + + }).synchronize(); + + IntegerSumObserver o = new IntegerSumObserver(); + s.subscribe(o); + // System.out.println("sum : " + o.sum); + + return o.sum; + } + + /** + * Run: 10 - 9,139,226 ops/sec + * Run: 11 - 8,456,526 ops/sec + * Run: 12 - 8,072,174 ops/sec + * Run: 13 - 8,667,381 ops/sec + * Run: 14 - 8,853,370 ops/sec + */ + public long timeTwoStreams() { + + final Observable s1 = Observable.range(0, reps).subscribeOn(Schedulers.newThread()); + final Observable s2 = Observable.range(0, reps).subscribeOn(Schedulers.newThread()); + + Observable s = Observable.create(new OnSubscribe() { + + @Override + public void call(final Subscriber s) { + final CountDownLatch latch = new CountDownLatch(2); + // first + s1.doOnTerminate(new Action0() { + + @Override + public void call() { + latch.countDown(); + } + + }).subscribe(new Action1() { + + @Override + public void call(Integer t1) { + s.onNext(t1); + } + + }); + + // second + s2.doOnTerminate(new Action0() { + + @Override + public void call() { + latch.countDown(); + } + + }).subscribe(new Action1() { + + @Override + public void call(Integer t1) { + s.onNext(t1); + } + + }); + + try { + latch.await(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + s.onCompleted(); + } + + }).synchronize(); + + IntegerSumObserver o = new IntegerSumObserver(); + s.subscribe(o); + // System.out.println("sum : " + o.sum); + + return o.sum; + } + + /** + * Run: 10 - 1,996 ops/sec + * Run: 11 - 1,997 ops/sec + * Run: 12 - 1,996 ops/sec + * Run: 13 - 1,996 ops/sec + * Run: 14 - 1,995 ops/sec + * + * @return + */ + public long timeTwoStreamsIntervals() { + + final Observable s1 = Observable.interval(1, TimeUnit.MILLISECONDS).take(reps / 2).flatMap(new Func1>() { + + @Override + public Observable call(Long l) { + return Observable.range(l.intValue(), 100); + } + + }).subscribeOn(Schedulers.newThread()); + final Observable s2 = Observable.range(1, reps / 2).subscribeOn(Schedulers.newThread()); + + Observable s = Observable.create(new OnSubscribe() { + + @Override + public void call(final Subscriber s) { + final CountDownLatch latch = new CountDownLatch(2); + // first + s1.doOnTerminate(new Action0() { + + @Override + public void call() { + latch.countDown(); + } + + }).subscribe(new Action1() { + + @Override + public void call(Integer t1) { + s.onNext(t1); + } + + }); + + // second + s2.doOnTerminate(new Action0() { + + @Override + public void call() { + latch.countDown(); + } + + }).subscribe(new Action1() { + + @Override + public void call(Integer t1) { + s.onNext(t1); + } + + }); + + try { + latch.await(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + s.onCompleted(); + } + + }).synchronize(); + + IntegerSumObserver o = new IntegerSumObserver(); + s.subscribe(o); + // System.out.println("sum : " + o.sum); + + return o.sum; + } +} diff --git a/rxjava-core/src/test/java/rx/observers/SerializedObserverTest.java b/rxjava-core/src/test/java/rx/observers/SerializedObserverTest.java new file mode 100644 index 0000000000..bbdc17c65f --- /dev/null +++ b/rxjava-core/src/test/java/rx/observers/SerializedObserverTest.java @@ -0,0 +1,575 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package rx.observers; + +import static org.junit.Assert.*; +import static org.mockito.Matchers.*; +import static org.mockito.Mockito.*; + +import java.util.Random; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import rx.Observable; +import rx.Observer; +import rx.Subscriber; +import rx.Subscription; + +public class SerializedObserverTest { + + @Mock + Subscriber observer; + + @Before + public void before() { + MockitoAnnotations.initMocks(this); + } + + @Test + public void testSingleThreadedBasic() { + Subscription s = mock(Subscription.class); + TestSingleThreadedObservable onSubscribe = new TestSingleThreadedObservable(s, "one", "two", "three"); + Observable w = Observable.create(onSubscribe); + + SerializedObserver aw = new SerializedObserver(observer); + + w.subscribe(aw); + onSubscribe.waitToFinish(); + + verify(observer, times(1)).onNext("one"); + verify(observer, times(1)).onNext("two"); + verify(observer, times(1)).onNext("three"); + verify(observer, never()).onError(any(Throwable.class)); + verify(observer, times(1)).onCompleted(); + // non-deterministic because unsubscribe happens after 'waitToFinish' releases + // so commenting out for now as this is not a critical thing to test here + // verify(s, times(1)).unsubscribe(); + } + + @Test + public void testMultiThreadedBasic() { + Subscription s = mock(Subscription.class); + TestMultiThreadedObservable onSubscribe = new TestMultiThreadedObservable(s, "one", "two", "three"); + Observable w = Observable.create(onSubscribe); + + BusyObserver busyObserver = new BusyObserver(); + SerializedObserver aw = new SerializedObserver(busyObserver); + + w.subscribe(aw); + onSubscribe.waitToFinish(); + + assertEquals(3, busyObserver.onNextCount.get()); + assertFalse(busyObserver.onError); + assertTrue(busyObserver.onCompleted); + // non-deterministic because unsubscribe happens after 'waitToFinish' releases + // so commenting out for now as this is not a critical thing to test here + // verify(s, times(1)).unsubscribe(); + + // we can have concurrency ... + assertTrue(onSubscribe.maxConcurrentThreads.get() > 1); + // ... but the onNext execution should be single threaded + assertEquals(1, busyObserver.maxConcurrentThreads.get()); + } + + @Test + public void testMultiThreadedWithNPE() { + Subscription s = mock(Subscription.class); + TestMultiThreadedObservable onSubscribe = new TestMultiThreadedObservable(s, "one", "two", "three", null); + Observable w = Observable.create(onSubscribe); + + BusyObserver busyObserver = new BusyObserver(); + SerializedObserver aw = new SerializedObserver(busyObserver); + + w.subscribe(aw); + onSubscribe.waitToFinish(); + + System.out.println("maxConcurrentThreads: " + onSubscribe.maxConcurrentThreads.get()); + + // we can't know how many onNext calls will occur since they each run on a separate thread + // that depends on thread scheduling so 0, 1, 2 and 3 are all valid options + // assertEquals(3, busyObserver.onNextCount.get()); + assertTrue(busyObserver.onNextCount.get() < 4); + assertTrue(busyObserver.onError); + // no onCompleted because onError was invoked + assertFalse(busyObserver.onCompleted); + // non-deterministic because unsubscribe happens after 'waitToFinish' releases + // so commenting out for now as this is not a critical thing to test here + //verify(s, times(1)).unsubscribe(); + + // we can have concurrency ... + assertTrue(onSubscribe.maxConcurrentThreads.get() > 1); + // ... but the onNext execution should be single threaded + assertEquals(1, busyObserver.maxConcurrentThreads.get()); + } + + @Test + public void testMultiThreadedWithNPEinMiddle() { + Subscription s = mock(Subscription.class); + TestMultiThreadedObservable onSubscribe = new TestMultiThreadedObservable(s, "one", "two", "three", null, "four", "five", "six", "seven", "eight", "nine"); + Observable w = Observable.create(onSubscribe); + + BusyObserver busyObserver = new BusyObserver(); + SerializedObserver aw = new SerializedObserver(busyObserver); + + w.subscribe(aw); + onSubscribe.waitToFinish(); + + System.out.println("maxConcurrentThreads: " + onSubscribe.maxConcurrentThreads.get()); + // this should not be the full number of items since the error should stop it before it completes all 9 + System.out.println("onNext count: " + busyObserver.onNextCount.get()); + assertTrue(busyObserver.onNextCount.get() < 9); + assertTrue(busyObserver.onError); + // no onCompleted because onError was invoked + assertFalse(busyObserver.onCompleted); + // non-deterministic because unsubscribe happens after 'waitToFinish' releases + // so commenting out for now as this is not a critical thing to test here + // verify(s, times(1)).unsubscribe(); + + // we can have concurrency ... + assertTrue(onSubscribe.maxConcurrentThreads.get() > 1); + // ... but the onNext execution should be single threaded + assertEquals(1, busyObserver.maxConcurrentThreads.get()); + } + + /** + * A non-realistic use case that tries to expose thread-safety issues by throwing lots of out-of-order + * events on many threads. + * + * @param w + * @param tw + */ + @Test + public void runConcurrencyTest() { + ExecutorService tp = Executors.newFixedThreadPool(20); + try { + TestConcurrencyObserver tw = new TestConcurrencyObserver(); + // we need Synchronized + SafeSubscriber to handle synchronization plus life-cycle + SerializedObserver w = new SerializedObserver(new SafeSubscriber(tw)); + + Future f1 = tp.submit(new OnNextThread(w, 12000)); + Future f2 = tp.submit(new OnNextThread(w, 5000)); + Future f3 = tp.submit(new OnNextThread(w, 75000)); + Future f4 = tp.submit(new OnNextThread(w, 13500)); + Future f5 = tp.submit(new OnNextThread(w, 22000)); + Future f6 = tp.submit(new OnNextThread(w, 15000)); + Future f7 = tp.submit(new OnNextThread(w, 7500)); + Future f8 = tp.submit(new OnNextThread(w, 23500)); + + Future f10 = tp.submit(new CompletionThread(w, TestConcurrencyObserverEvent.onCompleted, f1, f2, f3, f4)); + try { + Thread.sleep(1); + } catch (InterruptedException e) { + // ignore + } + Future f11 = tp.submit(new CompletionThread(w, TestConcurrencyObserverEvent.onCompleted, f4, f6, f7)); + Future f12 = tp.submit(new CompletionThread(w, TestConcurrencyObserverEvent.onCompleted, f4, f6, f7)); + Future f13 = tp.submit(new CompletionThread(w, TestConcurrencyObserverEvent.onCompleted, f4, f6, f7)); + Future f14 = tp.submit(new CompletionThread(w, TestConcurrencyObserverEvent.onCompleted, f4, f6, f7)); + // // the next 4 onError events should wait on same as f10 + Future f15 = tp.submit(new CompletionThread(w, TestConcurrencyObserverEvent.onError, f1, f2, f3, f4)); + Future f16 = tp.submit(new CompletionThread(w, TestConcurrencyObserverEvent.onError, f1, f2, f3, f4)); + Future f17 = tp.submit(new CompletionThread(w, TestConcurrencyObserverEvent.onError, f1, f2, f3, f4)); + Future f18 = tp.submit(new CompletionThread(w, TestConcurrencyObserverEvent.onError, f1, f2, f3, f4)); + + waitOnThreads(f1, f2, f3, f4, f5, f6, f7, f8, f10, f11, f12, f13, f14, f15, f16, f17, f18); + @SuppressWarnings("unused") + int numNextEvents = tw.assertEvents(null); // no check of type since we don't want to test barging results here, just interleaving behavior + // System.out.println("Number of events executed: " + numNextEvents); + } catch (Throwable e) { + fail("Concurrency test failed: " + e.getMessage()); + e.printStackTrace(); + } finally { + tp.shutdown(); + try { + tp.awaitTermination(5000, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + + private static void waitOnThreads(Future... futures) { + for (Future f : futures) { + try { + f.get(10, TimeUnit.SECONDS); + } catch (Throwable e) { + System.err.println("Failed while waiting on future."); + e.printStackTrace(); + } + } + } + + /** + * A thread that will pass data to onNext + */ + public static class OnNextThread implements Runnable { + + private final Observer Observer; + private final int numStringsToSend; + + OnNextThread(Observer Observer, int numStringsToSend) { + this.Observer = Observer; + this.numStringsToSend = numStringsToSend; + } + + @Override + public void run() { + for (int i = 0; i < numStringsToSend; i++) { + Observer.onNext("aString"); + } + } + } + + /** + * A thread that will call onError or onNext + */ + public static class CompletionThread implements Runnable { + + private final Observer Observer; + private final TestConcurrencyObserverEvent event; + private final Future[] waitOnThese; + + CompletionThread(Observer Observer, TestConcurrencyObserverEvent event, Future... waitOnThese) { + this.Observer = Observer; + this.event = event; + this.waitOnThese = waitOnThese; + } + + @Override + public void run() { + /* if we have 'waitOnThese' futures, we'll wait on them before proceeding */ + if (waitOnThese != null) { + for (Future f : waitOnThese) { + try { + f.get(); + } catch (Throwable e) { + System.err.println("Error while waiting on future in CompletionThread"); + } + } + } + + /* send the event */ + if (event == TestConcurrencyObserverEvent.onError) { + Observer.onError(new RuntimeException("mocked exception")); + } else if (event == TestConcurrencyObserverEvent.onCompleted) { + Observer.onCompleted(); + + } else { + throw new IllegalArgumentException("Expecting either onError or onCompleted"); + } + } + } + + private static enum TestConcurrencyObserverEvent { + onCompleted, onError, onNext + } + + private static class TestConcurrencyObserver extends Subscriber { + + /** + * used to store the order and number of events received + */ + private final LinkedBlockingQueue events = new LinkedBlockingQueue(); + private final int waitTime; + + @SuppressWarnings("unused") + public TestConcurrencyObserver(int waitTimeInNext) { + this.waitTime = waitTimeInNext; + } + + public TestConcurrencyObserver() { + this.waitTime = 0; + } + + @Override + public void onCompleted() { + events.add(TestConcurrencyObserverEvent.onCompleted); + } + + @Override + public void onError(Throwable e) { + events.add(TestConcurrencyObserverEvent.onError); + } + + @Override + public void onNext(String args) { + events.add(TestConcurrencyObserverEvent.onNext); + // do some artificial work to make the thread scheduling/timing vary + int s = 0; + for (int i = 0; i < 20; i++) { + s += s * i; + } + + if (waitTime > 0) { + try { + Thread.sleep(waitTime); + } catch (InterruptedException e) { + // ignore + } + } + } + + /** + * Assert the order of events is correct and return the number of onNext executions. + * + * @param expectedEndingEvent + * @return int count of onNext calls + * @throws IllegalStateException + * If order of events was invalid. + */ + public int assertEvents(TestConcurrencyObserverEvent expectedEndingEvent) throws IllegalStateException { + int nextCount = 0; + boolean finished = false; + for (TestConcurrencyObserverEvent e : events) { + if (e == TestConcurrencyObserverEvent.onNext) { + if (finished) { + // already finished, we shouldn't get this again + throw new IllegalStateException("Received onNext but we're already finished."); + } + nextCount++; + } else if (e == TestConcurrencyObserverEvent.onError) { + if (finished) { + // already finished, we shouldn't get this again + throw new IllegalStateException("Received onError but we're already finished."); + } + if (expectedEndingEvent != null && TestConcurrencyObserverEvent.onError != expectedEndingEvent) { + throw new IllegalStateException("Received onError ending event but expected " + expectedEndingEvent); + } + finished = true; + } else if (e == TestConcurrencyObserverEvent.onCompleted) { + if (finished) { + // already finished, we shouldn't get this again + throw new IllegalStateException("Received onCompleted but we're already finished."); + } + if (expectedEndingEvent != null && TestConcurrencyObserverEvent.onCompleted != expectedEndingEvent) { + throw new IllegalStateException("Received onCompleted ending event but expected " + expectedEndingEvent); + } + finished = true; + } + } + + return nextCount; + } + + } + + /** + * This spawns a single thread for the subscribe execution + */ + private static class TestSingleThreadedObservable implements Observable.OnSubscribeFunc { + + final Subscription s; + final String[] values; + private Thread t = null; + + public TestSingleThreadedObservable(final Subscription s, final String... values) { + this.s = s; + this.values = values; + + } + + public Subscription onSubscribe(final Observer observer) { + System.out.println("TestSingleThreadedObservable subscribed to ..."); + t = new Thread(new Runnable() { + + @Override + public void run() { + try { + System.out.println("running TestSingleThreadedObservable thread"); + for (String s : values) { + System.out.println("TestSingleThreadedObservable onNext: " + s); + observer.onNext(s); + } + observer.onCompleted(); + } catch (Throwable e) { + throw new RuntimeException(e); + } + } + + }); + System.out.println("starting TestSingleThreadedObservable thread"); + t.start(); + System.out.println("done starting TestSingleThreadedObservable thread"); + return s; + } + + public void waitToFinish() { + try { + t.join(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + } + + /** + * This spawns a thread for the subscription, then a separate thread for each onNext call. + */ + private static class TestMultiThreadedObservable implements Observable.OnSubscribeFunc { + + final Subscription s; + final String[] values; + Thread t = null; + AtomicInteger threadsRunning = new AtomicInteger(); + AtomicInteger maxConcurrentThreads = new AtomicInteger(); + ExecutorService threadPool; + + public TestMultiThreadedObservable(Subscription s, String... values) { + this.s = s; + this.values = values; + this.threadPool = Executors.newCachedThreadPool(); + } + + @Override + public Subscription onSubscribe(final Observer observer) { + System.out.println("TestMultiThreadedObservable subscribed to ..."); + t = new Thread(new Runnable() { + + @Override + public void run() { + try { + System.out.println("running TestMultiThreadedObservable thread"); + for (final String s : values) { + threadPool.execute(new Runnable() { + + @Override + public void run() { + threadsRunning.incrementAndGet(); + try { + // perform onNext call + System.out.println("TestMultiThreadedObservable onNext: " + s); + if (s == null) { + // force an error + throw new NullPointerException(); + } + observer.onNext(s); + // capture 'maxThreads' + int concurrentThreads = threadsRunning.get(); + int maxThreads = maxConcurrentThreads.get(); + if (concurrentThreads > maxThreads) { + maxConcurrentThreads.compareAndSet(maxThreads, concurrentThreads); + } + } catch (Throwable e) { + observer.onError(e); + } finally { + threadsRunning.decrementAndGet(); + } + } + }); + } + // we are done spawning threads + threadPool.shutdown(); + } catch (Throwable e) { + throw new RuntimeException(e); + } + + // wait until all threads are done, then mark it as COMPLETED + try { + // wait for all the threads to finish + threadPool.awaitTermination(2, TimeUnit.SECONDS); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + observer.onCompleted(); + } + }); + System.out.println("starting TestMultiThreadedObservable thread"); + t.start(); + System.out.println("done starting TestMultiThreadedObservable thread"); + return s; + } + + public void waitToFinish() { + try { + t.join(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + } + + private static class BusyObserver extends Subscriber { + volatile boolean onCompleted = false; + volatile boolean onError = false; + AtomicInteger onNextCount = new AtomicInteger(); + AtomicInteger threadsRunning = new AtomicInteger(); + AtomicInteger maxConcurrentThreads = new AtomicInteger(); + + @Override + public void onCompleted() { + threadsRunning.incrementAndGet(); + + System.out.println(">>> BusyObserver received onCompleted"); + onCompleted = true; + + int concurrentThreads = threadsRunning.get(); + int maxThreads = maxConcurrentThreads.get(); + if (concurrentThreads > maxThreads) { + maxConcurrentThreads.compareAndSet(maxThreads, concurrentThreads); + } + threadsRunning.decrementAndGet(); + } + + @Override + public void onError(Throwable e) { + threadsRunning.incrementAndGet(); + + System.out.println(">>> BusyObserver received onError: " + e.getMessage()); + onError = true; + + int concurrentThreads = threadsRunning.get(); + int maxThreads = maxConcurrentThreads.get(); + if (concurrentThreads > maxThreads) { + maxConcurrentThreads.compareAndSet(maxThreads, concurrentThreads); + } + threadsRunning.decrementAndGet(); + } + + @Override + public void onNext(String args) { + threadsRunning.incrementAndGet(); + try { + onNextCount.incrementAndGet(); + System.out.println(">>> BusyObserver received onNext: " + args); + try { + // simulate doing something computational + Thread.sleep(200); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } finally { + // capture 'maxThreads' + int concurrentThreads = threadsRunning.get(); + int maxThreads = maxConcurrentThreads.get(); + if (concurrentThreads > maxThreads) { + maxConcurrentThreads.compareAndSet(maxThreads, concurrentThreads); + } + threadsRunning.decrementAndGet(); + } + } + + } +} diff --git a/rxjava-core/src/test/java/rx/operators/OperatorSerializeTest.java b/rxjava-core/src/test/java/rx/operators/OperatorSerializeTest.java new file mode 100644 index 0000000000..6e396b2139 --- /dev/null +++ b/rxjava-core/src/test/java/rx/operators/OperatorSerializeTest.java @@ -0,0 +1,566 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package rx.operators; + +import static org.junit.Assert.*; +import static org.mockito.Matchers.*; +import static org.mockito.Mockito.*; + +import java.util.Random; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import rx.Observable; +import rx.Observer; +import rx.Subscriber; +import rx.Subscription; + +public class OperatorSerializeTest { + + @Mock + Observer observer; + + @Before + public void before() { + MockitoAnnotations.initMocks(this); + } + + @Test + public void testSingleThreadedBasic() { + Subscription s = mock(Subscription.class); + TestSingleThreadedObservable onSubscribe = new TestSingleThreadedObservable(s, "one", "two", "three"); + Observable w = Observable.create(onSubscribe); + + w.serialize().subscribe(observer); + onSubscribe.waitToFinish(); + + verify(observer, times(1)).onNext("one"); + verify(observer, times(1)).onNext("two"); + verify(observer, times(1)).onNext("three"); + verify(observer, never()).onError(any(Throwable.class)); + verify(observer, times(1)).onCompleted(); + // non-deterministic because unsubscribe happens after 'waitToFinish' releases + // so commenting out for now as this is not a critical thing to test here + // verify(s, times(1)).unsubscribe(); + } + + @Test + public void testMultiThreadedBasic() { + Subscription s = mock(Subscription.class); + TestMultiThreadedObservable onSubscribe = new TestMultiThreadedObservable(s, "one", "two", "three"); + Observable w = Observable.create(onSubscribe); + + BusyObserver busyobserver = new BusyObserver(); + + w.serialize().subscribe(busyobserver); + onSubscribe.waitToFinish(); + + assertEquals(3, busyobserver.onNextCount.get()); + assertFalse(busyobserver.onError); + assertTrue(busyobserver.onCompleted); + // non-deterministic because unsubscribe happens after 'waitToFinish' releases + // so commenting out for now as this is not a critical thing to test here + // verify(s, times(1)).unsubscribe(); + + // we can have concurrency ... + assertTrue(onSubscribe.maxConcurrentThreads.get() > 1); + // ... but the onNext execution should be single threaded + assertEquals(1, busyobserver.maxConcurrentThreads.get()); + } + + @Test + public void testMultiThreadedWithNPE() { + Subscription s = mock(Subscription.class); + TestMultiThreadedObservable onSubscribe = new TestMultiThreadedObservable(s, "one", "two", "three", null); + Observable w = Observable.create(onSubscribe); + + BusyObserver busyobserver = new BusyObserver(); + + w.serialize().subscribe(busyobserver); + onSubscribe.waitToFinish(); + + System.out.println("maxConcurrentThreads: " + onSubscribe.maxConcurrentThreads.get()); + + // we can't know how many onNext calls will occur since they each run on a separate thread + // that depends on thread scheduling so 0, 1, 2 and 3 are all valid options + // assertEquals(3, busyobserver.onNextCount.get()); + assertTrue(busyobserver.onNextCount.get() < 4); + assertTrue(busyobserver.onError); + // no onCompleted because onError was invoked + assertFalse(busyobserver.onCompleted); + // non-deterministic because unsubscribe happens after 'waitToFinish' releases + // so commenting out for now as this is not a critical thing to test here + //verify(s, times(1)).unsubscribe(); + + // we can have concurrency ... + assertTrue(onSubscribe.maxConcurrentThreads.get() > 1); + // ... but the onNext execution should be single threaded + assertEquals(1, busyobserver.maxConcurrentThreads.get()); + } + + @Test + public void testMultiThreadedWithNPEinMiddle() { + Subscription s = mock(Subscription.class); + TestMultiThreadedObservable onSubscribe = new TestMultiThreadedObservable(s, "one", "two", "three", null, "four", "five", "six", "seven", "eight", "nine"); + Observable w = Observable.create(onSubscribe); + + BusyObserver busyobserver = new BusyObserver(); + + w.serialize().subscribe(busyobserver); + onSubscribe.waitToFinish(); + + System.out.println("maxConcurrentThreads: " + onSubscribe.maxConcurrentThreads.get()); + // this should not be the full number of items since the error should stop it before it completes all 9 + System.out.println("onNext count: " + busyobserver.onNextCount.get()); + assertTrue(busyobserver.onNextCount.get() < 9); + assertTrue(busyobserver.onError); + // no onCompleted because onError was invoked + assertFalse(busyobserver.onCompleted); + // non-deterministic because unsubscribe happens after 'waitToFinish' releases + // so commenting out for now as this is not a critical thing to test here + // verify(s, times(1)).unsubscribe(); + + // we can have concurrency ... + assertTrue(onSubscribe.maxConcurrentThreads.get() > 1); + // ... but the onNext execution should be single threaded + assertEquals(1, busyobserver.maxConcurrentThreads.get()); + } + + /** + * A thread that will pass data to onNext + */ + public static class OnNextThread implements Runnable { + + private final Observer observer; + private final int numStringsToSend; + + OnNextThread(Observer observer, int numStringsToSend) { + this.observer = observer; + this.numStringsToSend = numStringsToSend; + } + + @Override + public void run() { + for (int i = 0; i < numStringsToSend; i++) { + observer.onNext("aString"); + } + } + } + + /** + * A thread that will call onError or onNext + */ + public static class CompletionThread implements Runnable { + + private final Observer observer; + private final TestConcurrencyobserverEvent event; + private final Future[] waitOnThese; + + CompletionThread(Observer observer, TestConcurrencyobserverEvent event, Future... waitOnThese) { + this.observer = observer; + this.event = event; + this.waitOnThese = waitOnThese; + } + + @Override + public void run() { + /* if we have 'waitOnThese' futures, we'll wait on them before proceeding */ + if (waitOnThese != null) { + for (Future f : waitOnThese) { + try { + f.get(); + } catch (Throwable e) { + System.err.println("Error while waiting on future in CompletionThread"); + } + } + } + + /* send the event */ + if (event == TestConcurrencyobserverEvent.onError) { + observer.onError(new RuntimeException("mocked exception")); + } else if (event == TestConcurrencyobserverEvent.onCompleted) { + observer.onCompleted(); + + } else { + throw new IllegalArgumentException("Expecting either onError or onCompleted"); + } + } + } + + private static enum TestConcurrencyobserverEvent { + onCompleted, onError, onNext + } + + private static class TestConcurrencyobserver extends Subscriber { + + /** + * used to store the order and number of events received + */ + private final LinkedBlockingQueue events = new LinkedBlockingQueue(); + private final int waitTime; + + @SuppressWarnings("unused") + public TestConcurrencyobserver(int waitTimeInNext) { + this.waitTime = waitTimeInNext; + } + + public TestConcurrencyobserver() { + this.waitTime = 0; + } + + @Override + public void onCompleted() { + events.add(TestConcurrencyobserverEvent.onCompleted); + } + + @Override + public void onError(Throwable e) { + events.add(TestConcurrencyobserverEvent.onError); + } + + @Override + public void onNext(String args) { + events.add(TestConcurrencyobserverEvent.onNext); + // do some artificial work to make the thread scheduling/timing vary + int s = 0; + for (int i = 0; i < 20; i++) { + s += s * i; + } + + if (waitTime > 0) { + try { + Thread.sleep(waitTime); + } catch (InterruptedException e) { + // ignore + } + } + } + + /** + * Assert the order of events is correct and return the number of onNext executions. + * + * @param expectedEndingEvent + * @return int count of onNext calls + * @throws IllegalStateException + * If order of events was invalid. + */ + public int assertEvents(TestConcurrencyobserverEvent expectedEndingEvent) throws IllegalStateException { + int nextCount = 0; + boolean finished = false; + for (TestConcurrencyobserverEvent e : events) { + if (e == TestConcurrencyobserverEvent.onNext) { + if (finished) { + // already finished, we shouldn't get this again + throw new IllegalStateException("Received onNext but we're already finished."); + } + nextCount++; + } else if (e == TestConcurrencyobserverEvent.onError) { + if (finished) { + // already finished, we shouldn't get this again + throw new IllegalStateException("Received onError but we're already finished."); + } + if (expectedEndingEvent != null && TestConcurrencyobserverEvent.onError != expectedEndingEvent) { + throw new IllegalStateException("Received onError ending event but expected " + expectedEndingEvent); + } + finished = true; + } else if (e == TestConcurrencyobserverEvent.onCompleted) { + if (finished) { + // already finished, we shouldn't get this again + throw new IllegalStateException("Received onCompleted but we're already finished."); + } + if (expectedEndingEvent != null && TestConcurrencyobserverEvent.onCompleted != expectedEndingEvent) { + throw new IllegalStateException("Received onCompleted ending event but expected " + expectedEndingEvent); + } + finished = true; + } + } + + return nextCount; + } + + } + + /** + * This spawns a single thread for the subscribe execution + */ + private static class TestSingleThreadedObservable implements Observable.OnSubscribeFunc { + + final Subscription s; + final String[] values; + private Thread t = null; + + public TestSingleThreadedObservable(final Subscription s, final String... values) { + this.s = s; + this.values = values; + + } + + public Subscription onSubscribe(final Observer observer) { + System.out.println("TestSingleThreadedObservable subscribed to ..."); + t = new Thread(new Runnable() { + + @Override + public void run() { + try { + System.out.println("running TestSingleThreadedObservable thread"); + for (String s : values) { + System.out.println("TestSingleThreadedObservable onNext: " + s); + observer.onNext(s); + } + observer.onCompleted(); + } catch (Throwable e) { + throw new RuntimeException(e); + } + } + + }); + System.out.println("starting TestSingleThreadedObservable thread"); + t.start(); + System.out.println("done starting TestSingleThreadedObservable thread"); + return s; + } + + public void waitToFinish() { + try { + t.join(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + } + + /** + * This spawns a thread for the subscription, then a separate thread for each onNext call. + */ + private static class TestMultiThreadedObservable implements Observable.OnSubscribeFunc { + + final Subscription s; + final String[] values; + Thread t = null; + AtomicInteger threadsRunning = new AtomicInteger(); + AtomicInteger maxConcurrentThreads = new AtomicInteger(); + ExecutorService threadPool; + + public TestMultiThreadedObservable(Subscription s, String... values) { + this.s = s; + this.values = values; + this.threadPool = Executors.newCachedThreadPool(); + } + + @Override + public Subscription onSubscribe(final Observer observer) { + System.out.println("TestMultiThreadedObservable subscribed to ..."); + t = new Thread(new Runnable() { + + @Override + public void run() { + try { + System.out.println("running TestMultiThreadedObservable thread"); + for (final String s : values) { + threadPool.execute(new Runnable() { + + @Override + public void run() { + threadsRunning.incrementAndGet(); + try { + // perform onNext call + System.out.println("TestMultiThreadedObservable onNext: " + s); + if (s == null) { + // force an error + throw new NullPointerException(); + } + observer.onNext(s); + // capture 'maxThreads' + int concurrentThreads = threadsRunning.get(); + int maxThreads = maxConcurrentThreads.get(); + if (concurrentThreads > maxThreads) { + maxConcurrentThreads.compareAndSet(maxThreads, concurrentThreads); + } + } catch (Throwable e) { + observer.onError(e); + } finally { + threadsRunning.decrementAndGet(); + } + } + }); + } + // we are done spawning threads + threadPool.shutdown(); + } catch (Throwable e) { + throw new RuntimeException(e); + } + + // wait until all threads are done, then mark it as COMPLETED + try { + // wait for all the threads to finish + threadPool.awaitTermination(2, TimeUnit.SECONDS); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + observer.onCompleted(); + } + }); + System.out.println("starting TestMultiThreadedObservable thread"); + t.start(); + System.out.println("done starting TestMultiThreadedObservable thread"); + return s; + } + + public void waitToFinish() { + try { + t.join(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + } + + private static class BusyObserver extends Subscriber { + volatile boolean onCompleted = false; + volatile boolean onError = false; + AtomicInteger onNextCount = new AtomicInteger(); + AtomicInteger threadsRunning = new AtomicInteger(); + AtomicInteger maxConcurrentThreads = new AtomicInteger(); + + @Override + public void onCompleted() { + threadsRunning.incrementAndGet(); + + System.out.println(">>> Busyobserver received onCompleted"); + onCompleted = true; + + int concurrentThreads = threadsRunning.get(); + int maxThreads = maxConcurrentThreads.get(); + if (concurrentThreads > maxThreads) { + maxConcurrentThreads.compareAndSet(maxThreads, concurrentThreads); + } + threadsRunning.decrementAndGet(); + } + + @Override + public void onError(Throwable e) { + threadsRunning.incrementAndGet(); + + System.out.println(">>> Busyobserver received onError: " + e.getMessage()); + onError = true; + + int concurrentThreads = threadsRunning.get(); + int maxThreads = maxConcurrentThreads.get(); + if (concurrentThreads > maxThreads) { + maxConcurrentThreads.compareAndSet(maxThreads, concurrentThreads); + } + threadsRunning.decrementAndGet(); + } + + @Override + public void onNext(String args) { + threadsRunning.incrementAndGet(); + try { + onNextCount.incrementAndGet(); + System.out.println(">>> Busyobserver received onNext: " + args); + try { + // simulate doing something computational + Thread.sleep(200); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } finally { + // capture 'maxThreads' + int concurrentThreads = threadsRunning.get(); + int maxThreads = maxConcurrentThreads.get(); + if (concurrentThreads > maxThreads) { + maxConcurrentThreads.compareAndSet(maxThreads, concurrentThreads); + } + threadsRunning.decrementAndGet(); + } + } + + } + + private static class ExternalBusyThread extends Thread { + + private BusyObserver observer; + private Object lock; + private int lockTimes; + private int waitTime; + public volatile boolean fail; + + public ExternalBusyThread(BusyObserver observer, Object lock, int lockTimes, int waitTime) { + this.observer = observer; + this.lock = lock; + this.lockTimes = lockTimes; + this.waitTime = waitTime; + this.fail = false; + } + + @Override + public void run() { + Random r = new Random(); + for (int i = 0; i < lockTimes; i++) { + synchronized (lock) { + int oldOnNextCount = observer.onNextCount.get(); + boolean oldOnCompleted = observer.onCompleted; + boolean oldOnError = observer.onError; + try { + Thread.sleep(r.nextInt(waitTime)); + } catch (InterruptedException e) { + // ignore + } + // Since we own the lock, onNextCount, onCompleted and + // onError must not be changed. + int newOnNextCount = observer.onNextCount.get(); + boolean newOnCompleted = observer.onCompleted; + boolean newOnError = observer.onError; + if (oldOnNextCount != newOnNextCount) { + System.out.println(">>> ExternalBusyThread received different onNextCount: " + + oldOnNextCount + + " -> " + + newOnNextCount); + fail = true; + break; + } + if (oldOnCompleted != newOnCompleted) { + System.out.println(">>> ExternalBusyThread received different onCompleted: " + + oldOnCompleted + + " -> " + + newOnCompleted); + fail = true; + break; + } + if (oldOnError != newOnError) { + System.out.println(">>> ExternalBusyThread received different onError: " + + oldOnError + + " -> " + + newOnError); + fail = true; + break; + } + } + } + } + + } +} From 8b3862e679c545bb2185cd0745c1e4cac6ef582a Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Sat, 1 Mar 2014 10:14:57 -0800 Subject: [PATCH 117/422] not quite functional ... and slow --- .../java/rx/observers/SerializedObserver.java | 146 ++++++++++-------- .../OperatorSerializePerformance.java | 4 +- .../perf/java/rx/perf/IntegerSumObserver.java | 1 + .../rx/observers/SerializedObserverTest.java | 115 ++++++++++---- .../observers/SynchronizedObserverTest.java | 53 ++++++- 5 files changed, 218 insertions(+), 101 deletions(-) diff --git a/rxjava-core/src/main/java/rx/observers/SerializedObserver.java b/rxjava-core/src/main/java/rx/observers/SerializedObserver.java index fd4730290f..1652f948ca 100644 --- a/rxjava-core/src/main/java/rx/observers/SerializedObserver.java +++ b/rxjava-core/src/main/java/rx/observers/SerializedObserver.java @@ -26,9 +26,8 @@ public void onCompleted() { } newState = current.complete(); } while (!state.compareAndSet(current, newState)); - if (newState.count == 0) { - s.onCompleted(); - } + System.out.println("********** onCompleted"); + terminateIfNecessary(newState); } @Override @@ -43,9 +42,8 @@ public void onError(Throwable e) { } newState = current.error(e); } while (!state.compareAndSet(current, newState)); - if (newState.count == 0) { - s.onError(e); - } + System.out.println("********** onError"); + terminateIfNecessary(newState); } @SuppressWarnings("unchecked") @@ -59,71 +57,82 @@ public void onNext(T t) { // already received terminal state return; } - newState = current.increment(t); + newState = current.offerItem(t); } while (!state.compareAndSet(current, newState)); - if (newState.count == 1) { + if (newState.shouldProcess()) { // this thread wins and will emit then drain queue if it concurrently gets added to - try { - s.onNext(t); - } finally { - // decrement after finishing - do { - current = state.get(); - newState = current.decrement(); - } while (!state.compareAndSet(current, newState)); - } + s.onNext(t); // drain queue if exists // we do "if" instead of "while" so we don't starve one thread - if (newState.queue.length > 0) { - Object[] items = newState.queue; - for (int i = 0; i < items.length; i++) { - s.onNext((T) items[i]); - } - // clear state of queue - do { - current = state.get(); - newState = current.drain(items.length); - } while (!state.compareAndSet(current, newState)); - terminateIfNecessary(newState); - } else { - terminateIfNecessary(newState); + Object[] items = newState.queue; + for (int i = 0; i < items.length; i++) { + s.onNext((T) items[i]); } + // finish processing to let this thread move on + do { + current = state.get(); + newState = current.finishProcessing(items.length + 1); // the + 1 is for the first onNext of itself + } while (!state.compareAndSet(current, newState)); + System.out.println("********** finishProcessing"); + terminateIfNecessary(newState); } } - private void terminateIfNecessary(State state) { - if (state.isTerminated()) { - if (state.onComplete) { - s.onCompleted(); - } else { - s.onError(state.onError); + @SuppressWarnings("unchecked") + private void terminateIfNecessary(State current) { + if (current.isTerminated()) { + State newState = null; + do { + current = state.get(); + newState = current.startTermination(); + } while (!state.compareAndSet(current, newState)); + if (newState.shouldProcess()) { + // drain any items left + for (int i = 0; i < newState.queue.length; i++) { + s.onNext((T) newState.queue[i]); + } + + // now terminate + if (newState.onComplete) { + s.onCompleted(); + } else { + s.onError(newState.onError); + } } } } public static class State { - final int count; + final boolean shouldProcess; + final boolean isSomeoneProcessing; + final int queueSize; final Object[] queue; final boolean onComplete; final Throwable onError; private final static Object[] EMPTY = new Object[0]; - private final static State NON_TERMINATED_EMPTY = new State(0, false, null, EMPTY); - private final static State NON_TERMINATED_SINGLE = new State(1, false, null, EMPTY); + private final static State NON_TERMINATED_EMPTY = new State(false, false, 0, false, null, EMPTY); + private final static State NON_TERMINATED_PROCESS_SELF = new State(true, true, 1, false, null, EMPTY); - public State(int count, boolean onComplete, Throwable onError, Object[] queue) { - this.count = count; + public State(boolean shouldProcess, boolean isSomeoneProcessing, int queueSize, boolean onComplete, Throwable onError, Object[] queue) { + this.shouldProcess = shouldProcess; + this.isSomeoneProcessing = isSomeoneProcessing; + this.queueSize = queueSize; this.queue = queue; this.onComplete = onComplete; this.onError = onError; } public static State createNew() { - return new State(0, false, null, EMPTY); + return new State(false, false, 0, false, null, EMPTY); + } + + public boolean shouldProcess() { + return shouldProcess; } public boolean isTerminated() { @@ -131,53 +140,64 @@ public boolean isTerminated() { } public State complete() { - return new State(count, true, onError, queue); + return new State(false, isSomeoneProcessing, queueSize, true, onError, queue); } public State error(Throwable e) { - return new State(count, onComplete, e, queue); + // immediately empty the queue and emit error as soon as possible + return new State(false, isSomeoneProcessing, queueSize, onComplete, e, EMPTY); + } + + public State startTermination() { + if (isSomeoneProcessing) { + System.out.println("start terminate and DO NOT process => queue size: " + (queueSize + 1)); + return new State(false, isSomeoneProcessing, queueSize, onComplete, onError, queue); + } else { + System.out.println("start terminate and process => queue size: " + (queueSize + 1)); + return new State(true, isSomeoneProcessing, queueSize, onComplete, onError, queue); + } } AtomicInteger max = new AtomicInteger(); - public State increment(Object item) { - if (count == 0) { + public State offerItem(Object item) { + if (queueSize == 0) { // no concurrent requests so don't queue, we'll process immediately if (isTerminated()) { // return count of 0 meaning don't emit as we are terminated - return new State(0, onComplete, onError, EMPTY); + return new State(false, false, 0, onComplete, onError, EMPTY); } else { - return NON_TERMINATED_SINGLE; + return NON_TERMINATED_PROCESS_SELF; } } else { - // concurrent requests so need to queue + // there are items queued so we need to queue int idx = queue.length; Object[] newQueue = new Object[idx + 1]; System.arraycopy(queue, 0, newQueue, 0, idx); newQueue[idx] = item; - if (max.get() < newQueue.length) { - max.set(newQueue.length); - System.out.println("max queue: " + newQueue.length); + if (isSomeoneProcessing) { + // we just add to queue + return new State(false, isSomeoneProcessing, queueSize + 1, onComplete, onError, newQueue); + } else { + // we add to queue and claim work + return new State(false, true, queueSize + 1, onComplete, onError, newQueue); } - - return new State(count + 1, onComplete, onError, newQueue); } } - public State decrement() { - if (count > 1 || isTerminated()) { - return new State(count - 1, onComplete, onError, queue); + public State finishProcessing(int numOnNextSent) { + int numOnNextFromQueue = numOnNextSent - 1; // we remove the "self" onNext as it doesn't affect the queue + int size = queueSize - numOnNextFromQueue; + System.out.println("finishProcessing => queue size: " + size + " after processing: " + numOnNextSent); + if (size > 1 || isTerminated()) { + Object[] newQueue = new Object[queue.length - numOnNextFromQueue]; + System.arraycopy(queue, numOnNextFromQueue, newQueue, 0, newQueue.length); + return new State(false, false, size, onComplete, onError, newQueue); } else { return NON_TERMINATED_EMPTY; } } - public State drain(int c) { - Object[] newQueue = new Object[queue.length - c]; - System.arraycopy(queue, c, newQueue, 0, newQueue.length); - return new State(count - c, onComplete, onError, newQueue); - } - } } diff --git a/rxjava-core/src/perf/java/rx/operators/OperatorSerializePerformance.java b/rxjava-core/src/perf/java/rx/operators/OperatorSerializePerformance.java index 79596d5733..d33c04c0b3 100644 --- a/rxjava-core/src/perf/java/rx/operators/OperatorSerializePerformance.java +++ b/rxjava-core/src/perf/java/rx/operators/OperatorSerializePerformance.java @@ -14,11 +14,11 @@ import rx.schedulers.Schedulers; public class OperatorSerializePerformance extends AbstractPerformanceTester { - // static int reps = Integer.MAX_VALUE / 16384; // timeTwoStreams + static int reps = Integer.MAX_VALUE / 16384; // timeTwoStreams // static int reps = Integer.MAX_VALUE / 1024; // timeSingleStream - static int reps = 1000; // interval streams +// static int reps = 1000; // interval streams OperatorSerializePerformance() { super(reps); diff --git a/rxjava-core/src/perf/java/rx/perf/IntegerSumObserver.java b/rxjava-core/src/perf/java/rx/perf/IntegerSumObserver.java index 31d10f0cab..28a522fbd8 100644 --- a/rxjava-core/src/perf/java/rx/perf/IntegerSumObserver.java +++ b/rxjava-core/src/perf/java/rx/perf/IntegerSumObserver.java @@ -13,6 +13,7 @@ public void onCompleted() { @Override public void onError(Throwable e) { + e.printStackTrace(); throw new RuntimeException(e); } diff --git a/rxjava-core/src/test/java/rx/observers/SerializedObserverTest.java b/rxjava-core/src/test/java/rx/observers/SerializedObserverTest.java index bbdc17c65f..4c0124bc88 100644 --- a/rxjava-core/src/test/java/rx/observers/SerializedObserverTest.java +++ b/rxjava-core/src/test/java/rx/observers/SerializedObserverTest.java @@ -19,7 +19,6 @@ import static org.mockito.Matchers.*; import static org.mockito.Mockito.*; -import java.util.Random; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; @@ -136,7 +135,13 @@ public void testMultiThreadedWithNPEinMiddle() { w.subscribe(aw); onSubscribe.waitToFinish(); - System.out.println("maxConcurrentThreads: " + onSubscribe.maxConcurrentThreads.get()); + System.out.println("OnSubscribe maxConcurrentThreads: " + onSubscribe.maxConcurrentThreads.get() + " Observer maxConcurrentThreads: " + busyObserver.maxConcurrentThreads.get()); + + // we can have concurrency ... + assertTrue(onSubscribe.maxConcurrentThreads.get() > 1); + // ... but the onNext execution should be single threaded + assertEquals(1, busyObserver.maxConcurrentThreads.get()); + // this should not be the full number of items since the error should stop it before it completes all 9 System.out.println("onNext count: " + busyObserver.onNextCount.get()); assertTrue(busyObserver.onNextCount.get() < 9); @@ -146,11 +151,6 @@ public void testMultiThreadedWithNPEinMiddle() { // non-deterministic because unsubscribe happens after 'waitToFinish' releases // so commenting out for now as this is not a critical thing to test here // verify(s, times(1)).unsubscribe(); - - // we can have concurrency ... - assertTrue(onSubscribe.maxConcurrentThreads.get() > 1); - // ... but the onNext execution should be single threaded - assertEquals(1, busyObserver.maxConcurrentThreads.get()); } /** @@ -161,7 +161,7 @@ public void testMultiThreadedWithNPEinMiddle() { * @param tw */ @Test - public void runConcurrencyTest() { + public void runOutOfOrderConcurrencyTest() { ExecutorService tp = Executors.newFixedThreadPool(20); try { TestConcurrencyObserver tw = new TestConcurrencyObserver(); @@ -196,7 +196,7 @@ public void runConcurrencyTest() { waitOnThreads(f1, f2, f3, f4, f5, f6, f7, f8, f10, f11, f12, f13, f14, f15, f16, f17, f18); @SuppressWarnings("unused") int numNextEvents = tw.assertEvents(null); // no check of type since we don't want to test barging results here, just interleaving behavior - // System.out.println("Number of events executed: " + numNextEvents); + // System.out.println("Number of events executed: " + numNextEvents); } catch (Throwable e) { fail("Concurrency test failed: " + e.getMessage()); e.printStackTrace(); @@ -210,10 +210,59 @@ public void runConcurrencyTest() { } } + /** + * + * @param w + * @param tw + */ + @Test + public void runConcurrencyTest() { + ExecutorService tp = Executors.newFixedThreadPool(20); + try { + TestConcurrencyObserver tw = new TestConcurrencyObserver(); + // we need Synchronized + SafeSubscriber to handle synchronization plus life-cycle + SerializedObserver w = new SerializedObserver(new SafeSubscriber(tw)); + + Future f1 = tp.submit(new OnNextThread(w, 12000)); + Future f2 = tp.submit(new OnNextThread(w, 5000)); + Future f3 = tp.submit(new OnNextThread(w, 75000)); + Future f4 = tp.submit(new OnNextThread(w, 13500)); + Future f5 = tp.submit(new OnNextThread(w, 22000)); + Future f6 = tp.submit(new OnNextThread(w, 15000)); + Future f7 = tp.submit(new OnNextThread(w, 7500)); + Future f8 = tp.submit(new OnNextThread(w, 23500)); + + // 12000 + 5000 + 75000 + 13500 + 22000 + 15000 + 7500 + 23500 = 173500 + + Future f10 = tp.submit(new CompletionThread(w, TestConcurrencyObserverEvent.onCompleted, f1, f2, f3, f4, f5, f6, f7, f8)); + try { + Thread.sleep(1); + } catch (InterruptedException e) { + // ignore + } + + waitOnThreads(f1, f2, f3, f4, f5, f6, f7, f8, f10); + @SuppressWarnings("unused") + int numNextEvents = tw.assertEvents(null); // no check of type since we don't want to test barging results here, just interleaving behavior + assertEquals(173500, numNextEvents); + // System.out.println("Number of events executed: " + numNextEvents); + } catch (Throwable e) { + fail("Concurrency test failed: " + e.getMessage()); + e.printStackTrace(); + } finally { + tp.shutdown(); + try { + tp.awaitTermination(25000, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + private static void waitOnThreads(Future... futures) { for (Future f : futures) { try { - f.get(10, TimeUnit.SECONDS); + f.get(20, TimeUnit.SECONDS); } catch (Throwable e) { System.err.println("Failed while waiting on future."); e.printStackTrace(); @@ -521,31 +570,23 @@ private static class BusyObserver extends Subscriber { @Override public void onCompleted() { threadsRunning.incrementAndGet(); - - System.out.println(">>> BusyObserver received onCompleted"); - onCompleted = true; - - int concurrentThreads = threadsRunning.get(); - int maxThreads = maxConcurrentThreads.get(); - if (concurrentThreads > maxThreads) { - maxConcurrentThreads.compareAndSet(maxThreads, concurrentThreads); + try { + onCompleted = true; + } finally { + captureMaxThreads(); + threadsRunning.decrementAndGet(); } - threadsRunning.decrementAndGet(); } @Override public void onError(Throwable e) { threadsRunning.incrementAndGet(); - - System.out.println(">>> BusyObserver received onError: " + e.getMessage()); - onError = true; - - int concurrentThreads = threadsRunning.get(); - int maxThreads = maxConcurrentThreads.get(); - if (concurrentThreads > maxThreads) { - maxConcurrentThreads.compareAndSet(maxThreads, concurrentThreads); + try { + onError = true; + } finally { + captureMaxThreads(); + threadsRunning.decrementAndGet(); } - threadsRunning.decrementAndGet(); } @Override @@ -553,7 +594,6 @@ public void onNext(String args) { threadsRunning.incrementAndGet(); try { onNextCount.incrementAndGet(); - System.out.println(">>> BusyObserver received onNext: " + args); try { // simulate doing something computational Thread.sleep(200); @@ -562,14 +602,21 @@ public void onNext(String args) { } } finally { // capture 'maxThreads' - int concurrentThreads = threadsRunning.get(); - int maxThreads = maxConcurrentThreads.get(); - if (concurrentThreads > maxThreads) { - maxConcurrentThreads.compareAndSet(maxThreads, concurrentThreads); - } + captureMaxThreads(); threadsRunning.decrementAndGet(); } } + protected void captureMaxThreads() { + int concurrentThreads = threadsRunning.get(); + int maxThreads = maxConcurrentThreads.get(); + if (concurrentThreads > maxThreads) { + maxConcurrentThreads.compareAndSet(maxThreads, concurrentThreads); + if (concurrentThreads > 1) { + new RuntimeException("should not be greater than 1").printStackTrace(); + } + } + } + } } diff --git a/rxjava-core/src/test/java/rx/observers/SynchronizedObserverTest.java b/rxjava-core/src/test/java/rx/observers/SynchronizedObserverTest.java index bdaad282c6..aad021d2e3 100644 --- a/rxjava-core/src/test/java/rx/observers/SynchronizedObserverTest.java +++ b/rxjava-core/src/test/java/rx/observers/SynchronizedObserverTest.java @@ -288,7 +288,7 @@ public void testMultiThreadedWithNPEinMiddleAndLock() { * @param tw */ @Test - public void runConcurrencyTest() { + public void runOutOfOrderConcurrencyTest() { ExecutorService tp = Executors.newFixedThreadPool(20); try { TestConcurrencyObserver tw = new TestConcurrencyObserver(); @@ -304,7 +304,7 @@ public void runConcurrencyTest() { Future f7 = tp.submit(new OnNextThread(w, 7500)); Future f8 = tp.submit(new OnNextThread(w, 23500)); - Future f10 = tp.submit(new CompletionThread(w, TestConcurrencyObserverEvent.onCompleted, f1, f2, f3, f4)); + Future f10 = tp.submit(new CompletionThread(w, TestConcurrencyObserverEvent.onCompleted, f1, f2, f3, f4, f5, f6, f7, f8)); try { Thread.sleep(1); } catch (InterruptedException e) { @@ -337,6 +337,55 @@ public void runConcurrencyTest() { } } + /** + * + * @param w + * @param tw + */ + @Test + public void runConcurrencyTest() { + ExecutorService tp = Executors.newFixedThreadPool(20); + try { + TestConcurrencyObserver tw = new TestConcurrencyObserver(); + // we need Synchronized + SafeSubscriber to handle synchronization plus life-cycle + SynchronizedObserver w = new SynchronizedObserver(new SafeSubscriber(tw)); + + Future f1 = tp.submit(new OnNextThread(w, 12000)); + Future f2 = tp.submit(new OnNextThread(w, 5000)); + Future f3 = tp.submit(new OnNextThread(w, 75000)); + Future f4 = tp.submit(new OnNextThread(w, 13500)); + Future f5 = tp.submit(new OnNextThread(w, 22000)); + Future f6 = tp.submit(new OnNextThread(w, 15000)); + Future f7 = tp.submit(new OnNextThread(w, 7500)); + Future f8 = tp.submit(new OnNextThread(w, 23500)); + + // 12000 + 5000 + 75000 + 13500 + 22000 + 15000 + 7500 + 23500 = 173500 + + Future f10 = tp.submit(new CompletionThread(w, TestConcurrencyObserverEvent.onCompleted, f1, f2, f3, f4)); + try { + Thread.sleep(1); + } catch (InterruptedException e) { + // ignore + } + + waitOnThreads(f1, f2, f3, f4, f5, f6, f7, f8, f10); + @SuppressWarnings("unused") + int numNextEvents = tw.assertEvents(null); // no check of type since we don't want to test barging results here, just interleaving behavior + assertEquals(173500, numNextEvents); + // System.out.println("Number of events executed: " + numNextEvents); + } catch (Throwable e) { + fail("Concurrency test failed: " + e.getMessage()); + e.printStackTrace(); + } finally { + tp.shutdown(); + try { + tp.awaitTermination(5000, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + private static void waitOnThreads(Future... futures) { for (Future f : futures) { try { From cc3c6546583e9a0d6d8463f5054ade172e28ffd2 Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Tue, 11 Mar 2014 13:56:31 -0700 Subject: [PATCH 118/422] Functional now ... it seems. --- .../java/rx/observers/SerializedObserver.java | 97 +++++++++++-------- .../OperatorSerializePerformance.java | 2 +- .../OperatorSynchronizePerformance.java | 6 +- .../rx/observers/SerializedObserverTest.java | 22 +++-- 4 files changed, 76 insertions(+), 51 deletions(-) diff --git a/rxjava-core/src/main/java/rx/observers/SerializedObserver.java b/rxjava-core/src/main/java/rx/observers/SerializedObserver.java index 1652f948ca..462f38f1ff 100644 --- a/rxjava-core/src/main/java/rx/observers/SerializedObserver.java +++ b/rxjava-core/src/main/java/rx/observers/SerializedObserver.java @@ -1,5 +1,8 @@ package rx.observers; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; @@ -14,6 +17,11 @@ public SerializedObserver(Observer s) { this.s = s; } + final AtomicInteger received = new AtomicInteger(); + final AtomicInteger counter = new AtomicInteger(); + final AtomicInteger offered = new AtomicInteger(); + static AtomicInteger decremented = new AtomicInteger(); + @Override public void onCompleted() { State current = null; @@ -26,7 +34,6 @@ public void onCompleted() { } newState = current.complete(); } while (!state.compareAndSet(current, newState)); - System.out.println("********** onCompleted"); terminateIfNecessary(newState); } @@ -42,43 +49,54 @@ public void onError(Throwable e) { } newState = current.error(e); } while (!state.compareAndSet(current, newState)); - System.out.println("********** onError"); terminateIfNecessary(newState); } + AtomicInteger conc = new AtomicInteger(); + AtomicInteger lost = new AtomicInteger(); + Set items = Collections.synchronizedSet(new HashSet()); + @SuppressWarnings("unchecked") @Override public void onNext(T t) { State current = null; State newState = null; + + int contention = 0; + State orig = null; do { current = state.get(); + if (orig == null) { + orig = current; + } if (current.isTerminated()) { // already received terminal state return; } newState = current.offerItem(t); + contention++; } while (!state.compareAndSet(current, newState)); + do { + current = state.get(); + newState = current.startProcessing(); + } while (!state.compareAndSet(current, newState)); if (newState.shouldProcess()) { - // this thread wins and will emit then drain queue if it concurrently gets added to - s.onNext(t); - - // drain queue if exists - // we do "if" instead of "while" so we don't starve one thread + // drain queue Object[] items = newState.queue; for (int i = 0; i < items.length; i++) { s.onNext((T) items[i]); + counter.incrementAndGet(); } // finish processing to let this thread move on do { current = state.get(); - newState = current.finishProcessing(items.length + 1); // the + 1 is for the first onNext of itself + newState = current.finishProcessing(items.length); } while (!state.compareAndSet(current, newState)); - System.out.println("********** finishProcessing"); - terminateIfNecessary(newState); + } + terminateIfNecessary(newState); } @SuppressWarnings("unchecked") @@ -89,6 +107,7 @@ private void terminateIfNecessary(State current) { current = state.get(); newState = current.startTermination(); } while (!state.compareAndSet(current, newState)); + if (newState.shouldProcess()) { // drain any items left for (int i = 0; i < newState.queue.length; i++) { @@ -114,9 +133,9 @@ public static class State { final Throwable onError; private final static Object[] EMPTY = new Object[0]; + private final static Object[] PROCESS_SELF = new Object[1]; private final static State NON_TERMINATED_EMPTY = new State(false, false, 0, false, null, EMPTY); - private final static State NON_TERMINATED_PROCESS_SELF = new State(true, true, 1, false, null, EMPTY); public State(boolean shouldProcess, boolean isSomeoneProcessing, int queueSize, boolean onComplete, Throwable onError, Object[] queue) { this.shouldProcess = shouldProcess; @@ -145,59 +164,59 @@ public State complete() { public State error(Throwable e) { // immediately empty the queue and emit error as soon as possible - return new State(false, isSomeoneProcessing, queueSize, onComplete, e, EMPTY); + return new State(false, isSomeoneProcessing, 0, onComplete, e, EMPTY); } public State startTermination() { if (isSomeoneProcessing) { - System.out.println("start terminate and DO NOT process => queue size: " + (queueSize + 1)); return new State(false, isSomeoneProcessing, queueSize, onComplete, onError, queue); } else { - System.out.println("start terminate and process => queue size: " + (queueSize + 1)); - return new State(true, isSomeoneProcessing, queueSize, onComplete, onError, queue); + return new State(true, true, queueSize, onComplete, onError, queue); } } - AtomicInteger max = new AtomicInteger(); - public State offerItem(Object item) { - if (queueSize == 0) { - // no concurrent requests so don't queue, we'll process immediately - if (isTerminated()) { - // return count of 0 meaning don't emit as we are terminated - return new State(false, false, 0, onComplete, onError, EMPTY); - } else { - return NON_TERMINATED_PROCESS_SELF; - } + if (isTerminated()) { + // return count of 0 meaning don't emit as we are terminated + return new State(false, isSomeoneProcessing, 0, onComplete, onError, EMPTY); } else { - // there are items queued so we need to queue int idx = queue.length; Object[] newQueue = new Object[idx + 1]; System.arraycopy(queue, 0, newQueue, 0, idx); newQueue[idx] = item; - if (isSomeoneProcessing) { - // we just add to queue - return new State(false, isSomeoneProcessing, queueSize + 1, onComplete, onError, newQueue); - } else { - // we add to queue and claim work - return new State(false, true, queueSize + 1, onComplete, onError, newQueue); - } + // we just add to queue + return new State(false, isSomeoneProcessing, queueSize + 1, onComplete, onError, newQueue); + } + } + + public State startProcessing() { + if (isSomeoneProcessing) { + return new State(false, true, queueSize, onComplete, onError, queue); + } else { + return new State(true, true, queueSize, onComplete, onError, queue); } } public State finishProcessing(int numOnNextSent) { - int numOnNextFromQueue = numOnNextSent - 1; // we remove the "self" onNext as it doesn't affect the queue - int size = queueSize - numOnNextFromQueue; - System.out.println("finishProcessing => queue size: " + size + " after processing: " + numOnNextSent); - if (size > 1 || isTerminated()) { - Object[] newQueue = new Object[queue.length - numOnNextFromQueue]; - System.arraycopy(queue, numOnNextFromQueue, newQueue, 0, newQueue.length); + int size = queueSize - numOnNextSent; + if (size > 0 || isTerminated()) { + // if size == 0 but we are terminated then it's an empty queue + Object[] newQueue = EMPTY; + if (size > 0) { + newQueue = new Object[queue.length - numOnNextSent]; + System.arraycopy(queue, numOnNextSent, newQueue, 0, newQueue.length); + } return new State(false, false, size, onComplete, onError, newQueue); } else { return NON_TERMINATED_EMPTY; } } + @Override + public String toString() { + return "State => shouldProcess: " + shouldProcess + " processing: " + isSomeoneProcessing + " queueSize: " + queueSize + " queue: " + queue.length + " terminated: " + isTerminated(); + } + } } diff --git a/rxjava-core/src/perf/java/rx/operators/OperatorSerializePerformance.java b/rxjava-core/src/perf/java/rx/operators/OperatorSerializePerformance.java index d33c04c0b3..e46f983d32 100644 --- a/rxjava-core/src/perf/java/rx/operators/OperatorSerializePerformance.java +++ b/rxjava-core/src/perf/java/rx/operators/OperatorSerializePerformance.java @@ -147,7 +147,7 @@ public void call(Integer t1) { IntegerSumObserver o = new IntegerSumObserver(); s.subscribe(o); - System.out.println("sum : " + o.sum); +// System.out.println("sum : " + o.sum); return o.sum; } diff --git a/rxjava-core/src/perf/java/rx/operators/OperatorSynchronizePerformance.java b/rxjava-core/src/perf/java/rx/operators/OperatorSynchronizePerformance.java index fdd3ced3d3..a7396693fb 100644 --- a/rxjava-core/src/perf/java/rx/operators/OperatorSynchronizePerformance.java +++ b/rxjava-core/src/perf/java/rx/operators/OperatorSynchronizePerformance.java @@ -14,9 +14,9 @@ import rx.schedulers.Schedulers; public class OperatorSynchronizePerformance extends AbstractPerformanceTester { - // static int reps = Integer.MAX_VALUE / 1024; + static int reps = Integer.MAX_VALUE / 1024; - static int reps = 1000; // timeTwoStreamsIntervals + // static int reps = 1000; // timeTwoStreamsIntervals OperatorSynchronizePerformance() { super(reps); @@ -30,7 +30,7 @@ public static void main(String args[]) { @Override public void call() { - spt.timeTwoStreamsIntervals(); + spt.timeTwoStreams(); } }); } catch (Exception e) { diff --git a/rxjava-core/src/test/java/rx/observers/SerializedObserverTest.java b/rxjava-core/src/test/java/rx/observers/SerializedObserverTest.java index 4c0124bc88..07cd117c14 100644 --- a/rxjava-core/src/test/java/rx/observers/SerializedObserverTest.java +++ b/rxjava-core/src/test/java/rx/observers/SerializedObserverTest.java @@ -19,6 +19,7 @@ import static org.mockito.Matchers.*; import static org.mockito.Mockito.*; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; @@ -92,8 +93,8 @@ public void testMultiThreadedBasic() { assertEquals(1, busyObserver.maxConcurrentThreads.get()); } - @Test - public void testMultiThreadedWithNPE() { + @Test(timeout=1000) + public void testMultiThreadedWithNPE() throws InterruptedException { Subscription s = mock(Subscription.class); TestMultiThreadedObservable onSubscribe = new TestMultiThreadedObservable(s, "one", "two", "three", null); Observable w = Observable.create(onSubscribe); @@ -103,8 +104,9 @@ public void testMultiThreadedWithNPE() { w.subscribe(aw); onSubscribe.waitToFinish(); + busyObserver.terminalEvent.await(); - System.out.println("maxConcurrentThreads: " + onSubscribe.maxConcurrentThreads.get()); + System.out.println("OnSubscribe maxConcurrentThreads: " + onSubscribe.maxConcurrentThreads.get() + " Observer maxConcurrentThreads: " + busyObserver.maxConcurrentThreads.get()); // we can't know how many onNext calls will occur since they each run on a separate thread // that depends on thread scheduling so 0, 1, 2 and 3 are all valid options @@ -286,7 +288,7 @@ public static class OnNextThread implements Runnable { @Override public void run() { for (int i = 0; i < numStringsToSend; i++) { - Observer.onNext("aString"); + Observer.onNext(Thread.currentThread().getId() + "-" + i); } } } @@ -296,12 +298,12 @@ public void run() { */ public static class CompletionThread implements Runnable { - private final Observer Observer; + private final Observer observer; private final TestConcurrencyObserverEvent event; private final Future[] waitOnThese; CompletionThread(Observer Observer, TestConcurrencyObserverEvent event, Future... waitOnThese) { - this.Observer = Observer; + this.observer = Observer; this.event = event; this.waitOnThese = waitOnThese; } @@ -321,9 +323,9 @@ public void run() { /* send the event */ if (event == TestConcurrencyObserverEvent.onError) { - Observer.onError(new RuntimeException("mocked exception")); + observer.onError(new RuntimeException("mocked exception")); } else if (event == TestConcurrencyObserverEvent.onCompleted) { - Observer.onCompleted(); + observer.onCompleted(); } else { throw new IllegalArgumentException("Expecting either onError or onCompleted"); @@ -566,6 +568,7 @@ private static class BusyObserver extends Subscriber { AtomicInteger onNextCount = new AtomicInteger(); AtomicInteger threadsRunning = new AtomicInteger(); AtomicInteger maxConcurrentThreads = new AtomicInteger(); + final CountDownLatch terminalEvent = new CountDownLatch(1); @Override public void onCompleted() { @@ -575,17 +578,20 @@ public void onCompleted() { } finally { captureMaxThreads(); threadsRunning.decrementAndGet(); + terminalEvent.countDown(); } } @Override public void onError(Throwable e) { + System.out.println(">>>>>>>>>>>>>>>>>>>> onError received: " + e); threadsRunning.incrementAndGet(); try { onError = true; } finally { captureMaxThreads(); threadsRunning.decrementAndGet(); + terminalEvent.countDown(); } } From 423b470372103725405e03c2784b4812bac949c4 Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Tue, 11 Mar 2014 16:10:26 -0700 Subject: [PATCH 119/422] Migrate Merge from Synchronize to Serialize --- .../src/main/java/rx/observers/SerializedObserver.java | 4 ++-- .../src/main/java/rx/observers/SerializedSubscriber.java | 2 +- rxjava-core/src/main/java/rx/operators/OperatorMerge.java | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/rxjava-core/src/main/java/rx/observers/SerializedObserver.java b/rxjava-core/src/main/java/rx/observers/SerializedObserver.java index 462f38f1ff..72a0f9969e 100644 --- a/rxjava-core/src/main/java/rx/observers/SerializedObserver.java +++ b/rxjava-core/src/main/java/rx/observers/SerializedObserver.java @@ -11,9 +11,9 @@ public class SerializedObserver implements Observer { private final AtomicReference state = new AtomicReference(State.createNew()); - private final Observer s; + private final Observer s; - public SerializedObserver(Observer s) { + public SerializedObserver(Observer s) { this.s = s; } diff --git a/rxjava-core/src/main/java/rx/observers/SerializedSubscriber.java b/rxjava-core/src/main/java/rx/observers/SerializedSubscriber.java index 377cef9d74..2d674f4cd9 100644 --- a/rxjava-core/src/main/java/rx/observers/SerializedSubscriber.java +++ b/rxjava-core/src/main/java/rx/observers/SerializedSubscriber.java @@ -7,7 +7,7 @@ public class SerializedSubscriber extends Subscriber { private final Observer s; - public SerializedSubscriber(Subscriber s) { + public SerializedSubscriber(Subscriber s) { this.s = new SerializedObserver(s); } diff --git a/rxjava-core/src/main/java/rx/operators/OperatorMerge.java b/rxjava-core/src/main/java/rx/operators/OperatorMerge.java index 5b51485224..b3d822ca84 100644 --- a/rxjava-core/src/main/java/rx/operators/OperatorMerge.java +++ b/rxjava-core/src/main/java/rx/operators/OperatorMerge.java @@ -20,7 +20,7 @@ import rx.Observable; import rx.Observable.Operator; import rx.Subscriber; -import rx.observers.SynchronizedSubscriber; +import rx.observers.SerializedSubscriber; import rx.subscriptions.CompositeSubscription; /** @@ -36,7 +36,7 @@ public final class OperatorMerge implements Operator> call(final Subscriber outerOperation) { - final Subscriber o = new SynchronizedSubscriber(outerOperation); + final Subscriber o = new SerializedSubscriber(outerOperation); final CompositeSubscription childrenSubscriptions = new CompositeSubscription(); outerOperation.add(childrenSubscriptions); From 34a2561f9010bf81506da54ead6858e77ab5de40 Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Wed, 12 Mar 2014 09:41:21 -0700 Subject: [PATCH 120/422] Experimenting with different implementations and performance --- .../java/rx/observers/SerializedObserver.java | 236 ++++-------------- .../SerializedObserverViaStateMachine.java | 215 ++++++++++++++++ .../rx/observers/SerializedSubscriber.java | 2 +- .../OperatorSerializePerformance.java | 45 +++- ...SerializedObserverViaStateMachineTest.java | 5 + 5 files changed, 300 insertions(+), 203 deletions(-) create mode 100644 rxjava-core/src/main/java/rx/observers/SerializedObserverViaStateMachine.java create mode 100644 rxjava-core/src/test/java/rx/observers/SerializedObserverViaStateMachineTest.java diff --git a/rxjava-core/src/main/java/rx/observers/SerializedObserver.java b/rxjava-core/src/main/java/rx/observers/SerializedObserver.java index 72a0f9969e..ae5cdf1a0e 100644 --- a/rxjava-core/src/main/java/rx/observers/SerializedObserver.java +++ b/rxjava-core/src/main/java/rx/observers/SerializedObserver.java @@ -1,222 +1,70 @@ package rx.observers; -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; +import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; import rx.Observer; public class SerializedObserver implements Observer { + private final Observer actual; + private final AtomicInteger count = new AtomicInteger(); + private final ConcurrentLinkedQueue queue = new ConcurrentLinkedQueue(); - private final AtomicReference state = new AtomicReference(State.createNew()); - private final Observer s; + private static Sentinel NULL_SENTINEL = new Sentinel(); + private static Sentinel COMPLETE_SENTINEL = new Sentinel(); - public SerializedObserver(Observer s) { - this.s = s; + private static class Sentinel { + + } + + private static class ErrorSentinel extends Sentinel { + final Throwable e; + + ErrorSentinel(Throwable e) { + this.e = e; + } } - final AtomicInteger received = new AtomicInteger(); - final AtomicInteger counter = new AtomicInteger(); - final AtomicInteger offered = new AtomicInteger(); - static AtomicInteger decremented = new AtomicInteger(); + public SerializedObserver(Observer s) { + this.actual = s; + } @Override public void onCompleted() { - State current = null; - State newState = null; - do { - current = state.get(); - if (current.isTerminated()) { - // already received terminal state - return; - } - newState = current.complete(); - } while (!state.compareAndSet(current, newState)); - terminateIfNecessary(newState); + queue.add(COMPLETE_SENTINEL); + doIt(); } @Override - public void onError(Throwable e) { - State current = null; - State newState = null; - do { - current = state.get(); - if (current.isTerminated()) { - // already received terminal state - return; - } - newState = current.error(e); - } while (!state.compareAndSet(current, newState)); - terminateIfNecessary(newState); + public void onError(final Throwable e) { + queue.add(new ErrorSentinel(e)); + doIt(); } - AtomicInteger conc = new AtomicInteger(); - AtomicInteger lost = new AtomicInteger(); - Set items = Collections.synchronizedSet(new HashSet()); - - @SuppressWarnings("unchecked") @Override public void onNext(T t) { - State current = null; - State newState = null; - - int contention = 0; - State orig = null; - do { - current = state.get(); - if (orig == null) { - orig = current; - } - if (current.isTerminated()) { - // already received terminal state - return; - } - newState = current.offerItem(t); - contention++; - } while (!state.compareAndSet(current, newState)); - - do { - current = state.get(); - newState = current.startProcessing(); - } while (!state.compareAndSet(current, newState)); - if (newState.shouldProcess()) { - // drain queue - Object[] items = newState.queue; - for (int i = 0; i < items.length; i++) { - s.onNext((T) items[i]); - counter.incrementAndGet(); - } - - // finish processing to let this thread move on - do { - current = state.get(); - newState = current.finishProcessing(items.length); - } while (!state.compareAndSet(current, newState)); - - } - terminateIfNecessary(newState); + queue.add(t); + doIt(); } - @SuppressWarnings("unchecked") - private void terminateIfNecessary(State current) { - if (current.isTerminated()) { - State newState = null; + public void doIt() { + if (count.getAndIncrement() == 0) { do { - current = state.get(); - newState = current.startTermination(); - } while (!state.compareAndSet(current, newState)); - - if (newState.shouldProcess()) { - // drain any items left - for (int i = 0; i < newState.queue.length; i++) { - s.onNext((T) newState.queue[i]); - } - - // now terminate - if (newState.onComplete) { - s.onCompleted(); - } else { - s.onError(newState.onError); + Object v = queue.poll(); + if (v != null) { + if (v instanceof Sentinel) { + if (v == NULL_SENTINEL) { + actual.onNext(null); + } else if (v == COMPLETE_SENTINEL) { + actual.onCompleted(); + } else if (v instanceof ErrorSentinel) { + actual.onError(((ErrorSentinel) v).e); + } + } else { + actual.onNext((T) v); + } } - } - } - } - - public static class State { - final boolean shouldProcess; - final boolean isSomeoneProcessing; - final int queueSize; - final Object[] queue; - final boolean onComplete; - final Throwable onError; - - private final static Object[] EMPTY = new Object[0]; - private final static Object[] PROCESS_SELF = new Object[1]; - - private final static State NON_TERMINATED_EMPTY = new State(false, false, 0, false, null, EMPTY); - - public State(boolean shouldProcess, boolean isSomeoneProcessing, int queueSize, boolean onComplete, Throwable onError, Object[] queue) { - this.shouldProcess = shouldProcess; - this.isSomeoneProcessing = isSomeoneProcessing; - this.queueSize = queueSize; - this.queue = queue; - this.onComplete = onComplete; - this.onError = onError; + } while (count.decrementAndGet() > 0); } - - public static State createNew() { - return new State(false, false, 0, false, null, EMPTY); - } - - public boolean shouldProcess() { - return shouldProcess; - } - - public boolean isTerminated() { - return onComplete || onError != null; - } - - public State complete() { - return new State(false, isSomeoneProcessing, queueSize, true, onError, queue); - } - - public State error(Throwable e) { - // immediately empty the queue and emit error as soon as possible - return new State(false, isSomeoneProcessing, 0, onComplete, e, EMPTY); - } - - public State startTermination() { - if (isSomeoneProcessing) { - return new State(false, isSomeoneProcessing, queueSize, onComplete, onError, queue); - } else { - return new State(true, true, queueSize, onComplete, onError, queue); - } - } - - public State offerItem(Object item) { - if (isTerminated()) { - // return count of 0 meaning don't emit as we are terminated - return new State(false, isSomeoneProcessing, 0, onComplete, onError, EMPTY); - } else { - int idx = queue.length; - Object[] newQueue = new Object[idx + 1]; - System.arraycopy(queue, 0, newQueue, 0, idx); - newQueue[idx] = item; - - // we just add to queue - return new State(false, isSomeoneProcessing, queueSize + 1, onComplete, onError, newQueue); - } - } - - public State startProcessing() { - if (isSomeoneProcessing) { - return new State(false, true, queueSize, onComplete, onError, queue); - } else { - return new State(true, true, queueSize, onComplete, onError, queue); - } - } - - public State finishProcessing(int numOnNextSent) { - int size = queueSize - numOnNextSent; - if (size > 0 || isTerminated()) { - // if size == 0 but we are terminated then it's an empty queue - Object[] newQueue = EMPTY; - if (size > 0) { - newQueue = new Object[queue.length - numOnNextSent]; - System.arraycopy(queue, numOnNextSent, newQueue, 0, newQueue.length); - } - return new State(false, false, size, onComplete, onError, newQueue); - } else { - return NON_TERMINATED_EMPTY; - } - } - - @Override - public String toString() { - return "State => shouldProcess: " + shouldProcess + " processing: " + isSomeoneProcessing + " queueSize: " + queueSize + " queue: " + queue.length + " terminated: " + isTerminated(); - } - } } diff --git a/rxjava-core/src/main/java/rx/observers/SerializedObserverViaStateMachine.java b/rxjava-core/src/main/java/rx/observers/SerializedObserverViaStateMachine.java new file mode 100644 index 0000000000..fdd6e844ad --- /dev/null +++ b/rxjava-core/src/main/java/rx/observers/SerializedObserverViaStateMachine.java @@ -0,0 +1,215 @@ +package rx.observers; + +import java.util.concurrent.atomic.AtomicReference; + +import rx.Observer; + +public class SerializedObserverViaStateMachine implements Observer { + + private final AtomicReference state = new AtomicReference(State.createNew()); + private final Observer s; + + public SerializedObserverViaStateMachine(Observer s) { + this.s = s; + } + + @Override + public void onCompleted() { + State current = null; + State newState = null; + do { + current = state.get(); + if (current.isTerminated()) { + // already received terminal state + return; + } + newState = current.complete(); + } while (!state.compareAndSet(current, newState)); + terminateIfNecessary(newState); + } + + @Override + public void onError(Throwable e) { + State current = null; + State newState = null; + do { + current = state.get(); + if (current.isTerminated()) { + // already received terminal state + return; + } + newState = current.error(e); + } while (!state.compareAndSet(current, newState)); + terminateIfNecessary(newState); + } + + @SuppressWarnings("unchecked") + @Override + public void onNext(T t) { + State current = null; + State newState = null; + + State orig = null; + do { + current = state.get(); + if (orig == null) { + orig = current; + } + if (current.isTerminated()) { + // already received terminal state + return; + } + newState = current.offerItem(t); + } while (!state.compareAndSet(current, newState)); + + if (newState.shouldProcess()) { + if (newState == State.PROCESS_SELF) { + s.onNext(t); + + // finish processing to let this thread move on + do { + current = state.get(); + newState = current.finishProcessing(1); + } while (!state.compareAndSet(current, newState)); + } else { + // drain queue + Object[] items = newState.queue; + for (int i = 0; i < items.length; i++) { + s.onNext((T) items[i]); + } + + // finish processing to let this thread move on + do { + current = state.get(); + newState = current.finishProcessing(items.length); + } while (!state.compareAndSet(current, newState)); + } + } + terminateIfNecessary(newState); + } + + @SuppressWarnings("unchecked") + private void terminateIfNecessary(State current) { + if (current.isTerminated()) { + State newState = null; + do { + current = state.get(); + newState = current.startTermination(); + } while (!state.compareAndSet(current, newState)); + + if (newState.shouldProcess()) { + // drain any items left + for (int i = 0; i < newState.queue.length; i++) { + s.onNext((T) newState.queue[i]); + } + + // now terminate + if (newState.onComplete) { + s.onCompleted(); + } else { + s.onError(newState.onError); + } + } + } + } + + public static class State { + final boolean shouldProcess; + final boolean isSomeoneProcessing; + final int queueSize; + final Object[] queue; + final boolean onComplete; + final Throwable onError; + + private final static Object[] EMPTY = new Object[0]; + private final static Object[] PROCESS_SELF_QUEUE = new Object[1]; + + private final static State NON_TERMINATED_EMPTY = new State(false, false, 0, false, null, EMPTY); + private final static State PROCESS_SELF = new State(true, true, 1, false, null, PROCESS_SELF_QUEUE); + + public State(boolean shouldProcess, boolean isSomeoneProcessing, int queueSize, boolean onComplete, Throwable onError, Object[] queue) { + this.shouldProcess = shouldProcess; + this.isSomeoneProcessing = isSomeoneProcessing; + this.queueSize = queueSize; + this.queue = queue; + this.onComplete = onComplete; + this.onError = onError; + } + + public static State createNew() { + return new State(false, false, 0, false, null, EMPTY); + } + + public boolean shouldProcess() { + return shouldProcess; + } + + public boolean isTerminated() { + return onComplete || onError != null; + } + + public State complete() { + return new State(false, isSomeoneProcessing, queueSize, true, onError, queue); + } + + public State error(Throwable e) { + // immediately empty the queue and emit error as soon as possible + return new State(false, isSomeoneProcessing, 0, onComplete, e, EMPTY); + } + + public State startTermination() { + if (isSomeoneProcessing) { + return new State(false, isSomeoneProcessing, queueSize, onComplete, onError, queue); + } else { + return new State(true, true, queueSize, onComplete, onError, queue); + } + } + + public State offerItem(Object item) { + if (queueSize == 0) { + if (isTerminated()) { + // return count of 0 meaning don't emit as we are terminated + return new State(false, isSomeoneProcessing, 0, onComplete, onError, EMPTY); + } else { + // no concurrent requests so don't queue, we'll process immediately + return PROCESS_SELF; + } + } else { + // there are items queued so we need to queue + int idx = queue.length; + Object[] newQueue = new Object[idx + 1]; + System.arraycopy(queue, 0, newQueue, 0, idx); + newQueue[idx] = item; + + if (isSomeoneProcessing) { + // we just add to queue + return new State(false, true, queueSize + 1, onComplete, onError, newQueue); + } else { + // we add to queue and claim work + return new State(true, true, queueSize + 1, onComplete, onError, newQueue); + } + } + } + + public State finishProcessing(int numOnNextSent) { + int size = queueSize - numOnNextSent; + if (size > 0 || isTerminated()) { + // if size == 0 but we are terminated then it's an empty queue + Object[] newQueue = EMPTY; + if (size > 0) { + newQueue = new Object[queue.length - numOnNextSent]; + System.arraycopy(queue, numOnNextSent, newQueue, 0, newQueue.length); + } + return new State(false, false, size, onComplete, onError, newQueue); + } else { + return NON_TERMINATED_EMPTY; + } + } + + @Override + public String toString() { + return "State => shouldProcess: " + shouldProcess + " processing: " + isSomeoneProcessing + " queueSize: " + queueSize + " queue: " + queue.length + " terminated: " + isTerminated(); + } + + } +} diff --git a/rxjava-core/src/main/java/rx/observers/SerializedSubscriber.java b/rxjava-core/src/main/java/rx/observers/SerializedSubscriber.java index 2d674f4cd9..0eecc1e24e 100644 --- a/rxjava-core/src/main/java/rx/observers/SerializedSubscriber.java +++ b/rxjava-core/src/main/java/rx/observers/SerializedSubscriber.java @@ -8,7 +8,7 @@ public class SerializedSubscriber extends Subscriber { private final Observer s; public SerializedSubscriber(Subscriber s) { - this.s = new SerializedObserver(s); + this.s = new SerializedObserverViaStateMachine(s); } @Override diff --git a/rxjava-core/src/perf/java/rx/operators/OperatorSerializePerformance.java b/rxjava-core/src/perf/java/rx/operators/OperatorSerializePerformance.java index e46f983d32..3bebb580c0 100644 --- a/rxjava-core/src/perf/java/rx/operators/OperatorSerializePerformance.java +++ b/rxjava-core/src/perf/java/rx/operators/OperatorSerializePerformance.java @@ -14,11 +14,11 @@ import rx.schedulers.Schedulers; public class OperatorSerializePerformance extends AbstractPerformanceTester { - static int reps = Integer.MAX_VALUE / 16384; // timeTwoStreams + static int reps = Integer.MAX_VALUE / 16384; // timeTwoStreams // static int reps = Integer.MAX_VALUE / 1024; // timeSingleStream -// static int reps = 1000; // interval streams + // static int reps = 1000; // interval streams OperatorSerializePerformance() { super(reps); @@ -33,6 +33,7 @@ public static void main(String args[]) { @Override public void call() { spt.timeTwoStreams(); + // spt.timeSingleStream(); } }); } catch (Exception e) { @@ -42,11 +43,22 @@ public void call() { } /** - * Run: 10 - 36,891,795 ops/sec - * Run: 11 - 29,854,808 ops/sec - * Run: 12 - 36,162,140 ops/sec - * Run: 13 - 35,727,201 ops/sec - * Run: 14 - 34,897,262 ops/sec + * + * -> state machine technique + * + * Run: 10 - 34,668,810 ops/sec + * Run: 11 - 32,874,312 ops/sec + * Run: 12 - 33,389,339 ops/sec + * Run: 13 - 35,269,946 ops/sec + * Run: 14 - 34,165,013 ops/sec + * + * -> using "observeOn" technique + * + * Run: 10 - 19,548,387 ops/sec + * Run: 11 - 19,471,069 ops/sec + * Run: 12 - 19,480,112 ops/sec + * Run: 13 - 18,720,550 ops/sec + * Run: 14 - 19,070,383 ops/sec */ public long timeSingleStream() { @@ -91,6 +103,23 @@ public void call(Integer t1) { return o.sum; } + /** + * -> state machine technique + * + * Run: 10 - 3,432,256 ops/sec + * Run: 11 - 3,570,444 ops/sec + * Run: 12 - 3,791,137 ops/sec + * Run: 13 - 3,664,579 ops/sec + * Run: 14 - 5,211,156 ops/sec + * + * -> using "observeOn" technique + * + * Run: 10 - 3,995,336 ops/sec + * Run: 11 - 4,033,077 ops/sec + * Run: 12 - 4,510,978 ops/sec + * Run: 13 - 3,218,915 ops/sec + * Run: 14 - 3,938,549 ops/sec + */ public long timeTwoStreams() { final Observable s1 = Observable.range(0, reps).subscribeOn(Schedulers.newThread()); @@ -147,7 +176,7 @@ public void call(Integer t1) { IntegerSumObserver o = new IntegerSumObserver(); s.subscribe(o); -// System.out.println("sum : " + o.sum); + // System.out.println("sum : " + o.sum); return o.sum; } diff --git a/rxjava-core/src/test/java/rx/observers/SerializedObserverViaStateMachineTest.java b/rxjava-core/src/test/java/rx/observers/SerializedObserverViaStateMachineTest.java new file mode 100644 index 0000000000..491c3d8657 --- /dev/null +++ b/rxjava-core/src/test/java/rx/observers/SerializedObserverViaStateMachineTest.java @@ -0,0 +1,5 @@ +package rx.observers; + +public class SerializedObserverViaStateMachineTest extends SerializedObserverTest { + +} From 2136f8f73175f193fd5fdd124d140b544a0f3ba3 Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Wed, 12 Mar 2014 09:42:35 -0700 Subject: [PATCH 121/422] Clarify names during testing --- ...a => SerializedObserverViaQueueAndCounter.java} | 4 ++-- ... SerializedObserverViaQueueAndCounterTest.java} | 14 +++++++------- .../SerializedObserverViaStateMachineTest.java | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) rename rxjava-core/src/main/java/rx/observers/{SerializedObserver.java => SerializedObserverViaQueueAndCounter.java} (92%) rename rxjava-core/src/test/java/rx/observers/{SerializedObserverTest.java => SerializedObserverViaQueueAndCounterTest.java} (96%) diff --git a/rxjava-core/src/main/java/rx/observers/SerializedObserver.java b/rxjava-core/src/main/java/rx/observers/SerializedObserverViaQueueAndCounter.java similarity index 92% rename from rxjava-core/src/main/java/rx/observers/SerializedObserver.java rename to rxjava-core/src/main/java/rx/observers/SerializedObserverViaQueueAndCounter.java index ae5cdf1a0e..60e62d429d 100644 --- a/rxjava-core/src/main/java/rx/observers/SerializedObserver.java +++ b/rxjava-core/src/main/java/rx/observers/SerializedObserverViaQueueAndCounter.java @@ -5,7 +5,7 @@ import rx.Observer; -public class SerializedObserver implements Observer { +public class SerializedObserverViaQueueAndCounter implements Observer { private final Observer actual; private final AtomicInteger count = new AtomicInteger(); private final ConcurrentLinkedQueue queue = new ConcurrentLinkedQueue(); @@ -25,7 +25,7 @@ private static class ErrorSentinel extends Sentinel { } } - public SerializedObserver(Observer s) { + public SerializedObserverViaQueueAndCounter(Observer s) { this.actual = s; } diff --git a/rxjava-core/src/test/java/rx/observers/SerializedObserverTest.java b/rxjava-core/src/test/java/rx/observers/SerializedObserverViaQueueAndCounterTest.java similarity index 96% rename from rxjava-core/src/test/java/rx/observers/SerializedObserverTest.java rename to rxjava-core/src/test/java/rx/observers/SerializedObserverViaQueueAndCounterTest.java index 07cd117c14..7734245321 100644 --- a/rxjava-core/src/test/java/rx/observers/SerializedObserverTest.java +++ b/rxjava-core/src/test/java/rx/observers/SerializedObserverViaQueueAndCounterTest.java @@ -37,7 +37,7 @@ import rx.Subscriber; import rx.Subscription; -public class SerializedObserverTest { +public class SerializedObserverViaQueueAndCounterTest { @Mock Subscriber observer; @@ -53,7 +53,7 @@ public void testSingleThreadedBasic() { TestSingleThreadedObservable onSubscribe = new TestSingleThreadedObservable(s, "one", "two", "three"); Observable w = Observable.create(onSubscribe); - SerializedObserver aw = new SerializedObserver(observer); + SerializedObserverViaQueueAndCounter aw = new SerializedObserverViaQueueAndCounter(observer); w.subscribe(aw); onSubscribe.waitToFinish(); @@ -75,7 +75,7 @@ public void testMultiThreadedBasic() { Observable w = Observable.create(onSubscribe); BusyObserver busyObserver = new BusyObserver(); - SerializedObserver aw = new SerializedObserver(busyObserver); + SerializedObserverViaQueueAndCounter aw = new SerializedObserverViaQueueAndCounter(busyObserver); w.subscribe(aw); onSubscribe.waitToFinish(); @@ -100,7 +100,7 @@ public void testMultiThreadedWithNPE() throws InterruptedException { Observable w = Observable.create(onSubscribe); BusyObserver busyObserver = new BusyObserver(); - SerializedObserver aw = new SerializedObserver(busyObserver); + SerializedObserverViaQueueAndCounter aw = new SerializedObserverViaQueueAndCounter(busyObserver); w.subscribe(aw); onSubscribe.waitToFinish(); @@ -132,7 +132,7 @@ public void testMultiThreadedWithNPEinMiddle() { Observable w = Observable.create(onSubscribe); BusyObserver busyObserver = new BusyObserver(); - SerializedObserver aw = new SerializedObserver(busyObserver); + SerializedObserverViaQueueAndCounter aw = new SerializedObserverViaQueueAndCounter(busyObserver); w.subscribe(aw); onSubscribe.waitToFinish(); @@ -168,7 +168,7 @@ public void runOutOfOrderConcurrencyTest() { try { TestConcurrencyObserver tw = new TestConcurrencyObserver(); // we need Synchronized + SafeSubscriber to handle synchronization plus life-cycle - SerializedObserver w = new SerializedObserver(new SafeSubscriber(tw)); + SerializedObserverViaQueueAndCounter w = new SerializedObserverViaQueueAndCounter(new SafeSubscriber(tw)); Future f1 = tp.submit(new OnNextThread(w, 12000)); Future f2 = tp.submit(new OnNextThread(w, 5000)); @@ -223,7 +223,7 @@ public void runConcurrencyTest() { try { TestConcurrencyObserver tw = new TestConcurrencyObserver(); // we need Synchronized + SafeSubscriber to handle synchronization plus life-cycle - SerializedObserver w = new SerializedObserver(new SafeSubscriber(tw)); + SerializedObserverViaQueueAndCounter w = new SerializedObserverViaQueueAndCounter(new SafeSubscriber(tw)); Future f1 = tp.submit(new OnNextThread(w, 12000)); Future f2 = tp.submit(new OnNextThread(w, 5000)); diff --git a/rxjava-core/src/test/java/rx/observers/SerializedObserverViaStateMachineTest.java b/rxjava-core/src/test/java/rx/observers/SerializedObserverViaStateMachineTest.java index 491c3d8657..18fb4ce4aa 100644 --- a/rxjava-core/src/test/java/rx/observers/SerializedObserverViaStateMachineTest.java +++ b/rxjava-core/src/test/java/rx/observers/SerializedObserverViaStateMachineTest.java @@ -1,5 +1,5 @@ package rx.observers; -public class SerializedObserverViaStateMachineTest extends SerializedObserverTest { +public class SerializedObserverViaStateMachineTest extends SerializedObserverViaQueueAndCounterTest { } From 7c16c51c345dcf41f7f0c232c048e2b0d9c528a4 Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Wed, 12 Mar 2014 09:50:11 -0700 Subject: [PATCH 122/422] Unit tests for each --- .../rx/observers/SerializedObserverTest.java | 630 ++++++++++++++++++ ...ializedObserverViaQueueAndCounterTest.java | 611 +---------------- ...SerializedObserverViaStateMachineTest.java | 7 +- 3 files changed, 640 insertions(+), 608 deletions(-) create mode 100644 rxjava-core/src/test/java/rx/observers/SerializedObserverTest.java diff --git a/rxjava-core/src/test/java/rx/observers/SerializedObserverTest.java b/rxjava-core/src/test/java/rx/observers/SerializedObserverTest.java new file mode 100644 index 0000000000..9b7656be29 --- /dev/null +++ b/rxjava-core/src/test/java/rx/observers/SerializedObserverTest.java @@ -0,0 +1,630 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package rx.observers; + +import static org.junit.Assert.*; +import static org.mockito.Matchers.*; +import static org.mockito.Mockito.*; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import rx.Observable; +import rx.Observer; +import rx.Subscriber; +import rx.Subscription; + +public abstract class SerializedObserverTest { + + @Mock + Subscriber observer; + + @Before + public void before() { + MockitoAnnotations.initMocks(this); + } + + protected abstract Observer serializedObserver(Observer o); + + @Test + public void testSingleThreadedBasic() { + Subscription s = mock(Subscription.class); + TestSingleThreadedObservable onSubscribe = new TestSingleThreadedObservable(s, "one", "two", "three"); + Observable w = Observable.create(onSubscribe); + + Observer aw = serializedObserver(observer); + + w.subscribe(aw); + onSubscribe.waitToFinish(); + + verify(observer, times(1)).onNext("one"); + verify(observer, times(1)).onNext("two"); + verify(observer, times(1)).onNext("three"); + verify(observer, never()).onError(any(Throwable.class)); + verify(observer, times(1)).onCompleted(); + // non-deterministic because unsubscribe happens after 'waitToFinish' releases + // so commenting out for now as this is not a critical thing to test here + // verify(s, times(1)).unsubscribe(); + } + + @Test + public void testMultiThreadedBasic() { + Subscription s = mock(Subscription.class); + TestMultiThreadedObservable onSubscribe = new TestMultiThreadedObservable(s, "one", "two", "three"); + Observable w = Observable.create(onSubscribe); + + BusyObserver busyObserver = new BusyObserver(); + Observer aw = serializedObserver(busyObserver); + + w.subscribe(aw); + onSubscribe.waitToFinish(); + + assertEquals(3, busyObserver.onNextCount.get()); + assertFalse(busyObserver.onError); + assertTrue(busyObserver.onCompleted); + // non-deterministic because unsubscribe happens after 'waitToFinish' releases + // so commenting out for now as this is not a critical thing to test here + // verify(s, times(1)).unsubscribe(); + + // we can have concurrency ... + assertTrue(onSubscribe.maxConcurrentThreads.get() > 1); + // ... but the onNext execution should be single threaded + assertEquals(1, busyObserver.maxConcurrentThreads.get()); + } + + @Test(timeout = 1000) + public void testMultiThreadedWithNPE() throws InterruptedException { + Subscription s = mock(Subscription.class); + TestMultiThreadedObservable onSubscribe = new TestMultiThreadedObservable(s, "one", "two", "three", null); + Observable w = Observable.create(onSubscribe); + + BusyObserver busyObserver = new BusyObserver(); + Observer aw = serializedObserver(busyObserver); + + w.subscribe(aw); + onSubscribe.waitToFinish(); + busyObserver.terminalEvent.await(); + + System.out.println("OnSubscribe maxConcurrentThreads: " + onSubscribe.maxConcurrentThreads.get() + " Observer maxConcurrentThreads: " + busyObserver.maxConcurrentThreads.get()); + + // we can't know how many onNext calls will occur since they each run on a separate thread + // that depends on thread scheduling so 0, 1, 2 and 3 are all valid options + // assertEquals(3, busyObserver.onNextCount.get()); + assertTrue(busyObserver.onNextCount.get() < 4); + assertTrue(busyObserver.onError); + // no onCompleted because onError was invoked + assertFalse(busyObserver.onCompleted); + // non-deterministic because unsubscribe happens after 'waitToFinish' releases + // so commenting out for now as this is not a critical thing to test here + //verify(s, times(1)).unsubscribe(); + + // we can have concurrency ... + assertTrue(onSubscribe.maxConcurrentThreads.get() > 1); + // ... but the onNext execution should be single threaded + assertEquals(1, busyObserver.maxConcurrentThreads.get()); + } + + @Test + public void testMultiThreadedWithNPEinMiddle() { + Subscription s = mock(Subscription.class); + TestMultiThreadedObservable onSubscribe = new TestMultiThreadedObservable(s, "one", "two", "three", null, "four", "five", "six", "seven", "eight", "nine"); + Observable w = Observable.create(onSubscribe); + + BusyObserver busyObserver = new BusyObserver(); + Observer aw = serializedObserver(busyObserver); + + w.subscribe(aw); + onSubscribe.waitToFinish(); + + System.out.println("OnSubscribe maxConcurrentThreads: " + onSubscribe.maxConcurrentThreads.get() + " Observer maxConcurrentThreads: " + busyObserver.maxConcurrentThreads.get()); + + // we can have concurrency ... + assertTrue(onSubscribe.maxConcurrentThreads.get() > 1); + // ... but the onNext execution should be single threaded + assertEquals(1, busyObserver.maxConcurrentThreads.get()); + + // this should not be the full number of items since the error should stop it before it completes all 9 + System.out.println("onNext count: " + busyObserver.onNextCount.get()); + assertTrue(busyObserver.onNextCount.get() < 9); + assertTrue(busyObserver.onError); + // no onCompleted because onError was invoked + assertFalse(busyObserver.onCompleted); + // non-deterministic because unsubscribe happens after 'waitToFinish' releases + // so commenting out for now as this is not a critical thing to test here + // verify(s, times(1)).unsubscribe(); + } + + /** + * A non-realistic use case that tries to expose thread-safety issues by throwing lots of out-of-order + * events on many threads. + * + * @param w + * @param tw + */ + @Test + public void runOutOfOrderConcurrencyTest() { + ExecutorService tp = Executors.newFixedThreadPool(20); + try { + TestConcurrencyObserver tw = new TestConcurrencyObserver(); + // we need Synchronized + SafeSubscriber to handle synchronization plus life-cycle + Observer w = serializedObserver(new SafeSubscriber(tw)); + + Future f1 = tp.submit(new OnNextThread(w, 12000)); + Future f2 = tp.submit(new OnNextThread(w, 5000)); + Future f3 = tp.submit(new OnNextThread(w, 75000)); + Future f4 = tp.submit(new OnNextThread(w, 13500)); + Future f5 = tp.submit(new OnNextThread(w, 22000)); + Future f6 = tp.submit(new OnNextThread(w, 15000)); + Future f7 = tp.submit(new OnNextThread(w, 7500)); + Future f8 = tp.submit(new OnNextThread(w, 23500)); + + Future f10 = tp.submit(new CompletionThread(w, TestConcurrencyObserverEvent.onCompleted, f1, f2, f3, f4)); + try { + Thread.sleep(1); + } catch (InterruptedException e) { + // ignore + } + Future f11 = tp.submit(new CompletionThread(w, TestConcurrencyObserverEvent.onCompleted, f4, f6, f7)); + Future f12 = tp.submit(new CompletionThread(w, TestConcurrencyObserverEvent.onCompleted, f4, f6, f7)); + Future f13 = tp.submit(new CompletionThread(w, TestConcurrencyObserverEvent.onCompleted, f4, f6, f7)); + Future f14 = tp.submit(new CompletionThread(w, TestConcurrencyObserverEvent.onCompleted, f4, f6, f7)); + // // the next 4 onError events should wait on same as f10 + Future f15 = tp.submit(new CompletionThread(w, TestConcurrencyObserverEvent.onError, f1, f2, f3, f4)); + Future f16 = tp.submit(new CompletionThread(w, TestConcurrencyObserverEvent.onError, f1, f2, f3, f4)); + Future f17 = tp.submit(new CompletionThread(w, TestConcurrencyObserverEvent.onError, f1, f2, f3, f4)); + Future f18 = tp.submit(new CompletionThread(w, TestConcurrencyObserverEvent.onError, f1, f2, f3, f4)); + + waitOnThreads(f1, f2, f3, f4, f5, f6, f7, f8, f10, f11, f12, f13, f14, f15, f16, f17, f18); + @SuppressWarnings("unused") + int numNextEvents = tw.assertEvents(null); // no check of type since we don't want to test barging results here, just interleaving behavior + // System.out.println("Number of events executed: " + numNextEvents); + } catch (Throwable e) { + fail("Concurrency test failed: " + e.getMessage()); + e.printStackTrace(); + } finally { + tp.shutdown(); + try { + tp.awaitTermination(5000, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + + /** + * + * @param w + * @param tw + */ + @Test + public void runConcurrencyTest() { + ExecutorService tp = Executors.newFixedThreadPool(20); + try { + TestConcurrencyObserver tw = new TestConcurrencyObserver(); + // we need Synchronized + SafeSubscriber to handle synchronization plus life-cycle + Observer w = serializedObserver(new SafeSubscriber(tw)); + + Future f1 = tp.submit(new OnNextThread(w, 12000)); + Future f2 = tp.submit(new OnNextThread(w, 5000)); + Future f3 = tp.submit(new OnNextThread(w, 75000)); + Future f4 = tp.submit(new OnNextThread(w, 13500)); + Future f5 = tp.submit(new OnNextThread(w, 22000)); + Future f6 = tp.submit(new OnNextThread(w, 15000)); + Future f7 = tp.submit(new OnNextThread(w, 7500)); + Future f8 = tp.submit(new OnNextThread(w, 23500)); + + // 12000 + 5000 + 75000 + 13500 + 22000 + 15000 + 7500 + 23500 = 173500 + + Future f10 = tp.submit(new CompletionThread(w, TestConcurrencyObserverEvent.onCompleted, f1, f2, f3, f4, f5, f6, f7, f8)); + try { + Thread.sleep(1); + } catch (InterruptedException e) { + // ignore + } + + waitOnThreads(f1, f2, f3, f4, f5, f6, f7, f8, f10); + @SuppressWarnings("unused") + int numNextEvents = tw.assertEvents(null); // no check of type since we don't want to test barging results here, just interleaving behavior + assertEquals(173500, numNextEvents); + // System.out.println("Number of events executed: " + numNextEvents); + } catch (Throwable e) { + fail("Concurrency test failed: " + e.getMessage()); + e.printStackTrace(); + } finally { + tp.shutdown(); + try { + tp.awaitTermination(25000, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + + private static void waitOnThreads(Future... futures) { + for (Future f : futures) { + try { + f.get(20, TimeUnit.SECONDS); + } catch (Throwable e) { + System.err.println("Failed while waiting on future."); + e.printStackTrace(); + } + } + } + + /** + * A thread that will pass data to onNext + */ + public static class OnNextThread implements Runnable { + + private final Observer Observer; + private final int numStringsToSend; + + OnNextThread(Observer Observer, int numStringsToSend) { + this.Observer = Observer; + this.numStringsToSend = numStringsToSend; + } + + @Override + public void run() { + for (int i = 0; i < numStringsToSend; i++) { + Observer.onNext(Thread.currentThread().getId() + "-" + i); + } + } + } + + /** + * A thread that will call onError or onNext + */ + public static class CompletionThread implements Runnable { + + private final Observer observer; + private final TestConcurrencyObserverEvent event; + private final Future[] waitOnThese; + + CompletionThread(Observer Observer, TestConcurrencyObserverEvent event, Future... waitOnThese) { + this.observer = Observer; + this.event = event; + this.waitOnThese = waitOnThese; + } + + @Override + public void run() { + /* if we have 'waitOnThese' futures, we'll wait on them before proceeding */ + if (waitOnThese != null) { + for (Future f : waitOnThese) { + try { + f.get(); + } catch (Throwable e) { + System.err.println("Error while waiting on future in CompletionThread"); + } + } + } + + /* send the event */ + if (event == TestConcurrencyObserverEvent.onError) { + observer.onError(new RuntimeException("mocked exception")); + } else if (event == TestConcurrencyObserverEvent.onCompleted) { + observer.onCompleted(); + + } else { + throw new IllegalArgumentException("Expecting either onError or onCompleted"); + } + } + } + + private static enum TestConcurrencyObserverEvent { + onCompleted, onError, onNext + } + + private static class TestConcurrencyObserver extends Subscriber { + + /** + * used to store the order and number of events received + */ + private final LinkedBlockingQueue events = new LinkedBlockingQueue(); + private final int waitTime; + + @SuppressWarnings("unused") + public TestConcurrencyObserver(int waitTimeInNext) { + this.waitTime = waitTimeInNext; + } + + public TestConcurrencyObserver() { + this.waitTime = 0; + } + + @Override + public void onCompleted() { + events.add(TestConcurrencyObserverEvent.onCompleted); + } + + @Override + public void onError(Throwable e) { + events.add(TestConcurrencyObserverEvent.onError); + } + + @Override + public void onNext(String args) { + events.add(TestConcurrencyObserverEvent.onNext); + // do some artificial work to make the thread scheduling/timing vary + int s = 0; + for (int i = 0; i < 20; i++) { + s += s * i; + } + + if (waitTime > 0) { + try { + Thread.sleep(waitTime); + } catch (InterruptedException e) { + // ignore + } + } + } + + /** + * Assert the order of events is correct and return the number of onNext executions. + * + * @param expectedEndingEvent + * @return int count of onNext calls + * @throws IllegalStateException + * If order of events was invalid. + */ + public int assertEvents(TestConcurrencyObserverEvent expectedEndingEvent) throws IllegalStateException { + int nextCount = 0; + boolean finished = false; + for (TestConcurrencyObserverEvent e : events) { + if (e == TestConcurrencyObserverEvent.onNext) { + if (finished) { + // already finished, we shouldn't get this again + throw new IllegalStateException("Received onNext but we're already finished."); + } + nextCount++; + } else if (e == TestConcurrencyObserverEvent.onError) { + if (finished) { + // already finished, we shouldn't get this again + throw new IllegalStateException("Received onError but we're already finished."); + } + if (expectedEndingEvent != null && TestConcurrencyObserverEvent.onError != expectedEndingEvent) { + throw new IllegalStateException("Received onError ending event but expected " + expectedEndingEvent); + } + finished = true; + } else if (e == TestConcurrencyObserverEvent.onCompleted) { + if (finished) { + // already finished, we shouldn't get this again + throw new IllegalStateException("Received onCompleted but we're already finished."); + } + if (expectedEndingEvent != null && TestConcurrencyObserverEvent.onCompleted != expectedEndingEvent) { + throw new IllegalStateException("Received onCompleted ending event but expected " + expectedEndingEvent); + } + finished = true; + } + } + + return nextCount; + } + + } + + /** + * This spawns a single thread for the subscribe execution + */ + private static class TestSingleThreadedObservable implements Observable.OnSubscribeFunc { + + final Subscription s; + final String[] values; + private Thread t = null; + + public TestSingleThreadedObservable(final Subscription s, final String... values) { + this.s = s; + this.values = values; + + } + + public Subscription onSubscribe(final Observer observer) { + System.out.println("TestSingleThreadedObservable subscribed to ..."); + t = new Thread(new Runnable() { + + @Override + public void run() { + try { + System.out.println("running TestSingleThreadedObservable thread"); + for (String s : values) { + System.out.println("TestSingleThreadedObservable onNext: " + s); + observer.onNext(s); + } + observer.onCompleted(); + } catch (Throwable e) { + throw new RuntimeException(e); + } + } + + }); + System.out.println("starting TestSingleThreadedObservable thread"); + t.start(); + System.out.println("done starting TestSingleThreadedObservable thread"); + return s; + } + + public void waitToFinish() { + try { + t.join(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + } + + /** + * This spawns a thread for the subscription, then a separate thread for each onNext call. + */ + private static class TestMultiThreadedObservable implements Observable.OnSubscribeFunc { + + final Subscription s; + final String[] values; + Thread t = null; + AtomicInteger threadsRunning = new AtomicInteger(); + AtomicInteger maxConcurrentThreads = new AtomicInteger(); + ExecutorService threadPool; + + public TestMultiThreadedObservable(Subscription s, String... values) { + this.s = s; + this.values = values; + this.threadPool = Executors.newCachedThreadPool(); + } + + @Override + public Subscription onSubscribe(final Observer observer) { + System.out.println("TestMultiThreadedObservable subscribed to ..."); + t = new Thread(new Runnable() { + + @Override + public void run() { + try { + System.out.println("running TestMultiThreadedObservable thread"); + for (final String s : values) { + threadPool.execute(new Runnable() { + + @Override + public void run() { + threadsRunning.incrementAndGet(); + try { + // perform onNext call + System.out.println("TestMultiThreadedObservable onNext: " + s); + if (s == null) { + // force an error + throw new NullPointerException(); + } + observer.onNext(s); + // capture 'maxThreads' + int concurrentThreads = threadsRunning.get(); + int maxThreads = maxConcurrentThreads.get(); + if (concurrentThreads > maxThreads) { + maxConcurrentThreads.compareAndSet(maxThreads, concurrentThreads); + } + } catch (Throwable e) { + observer.onError(e); + } finally { + threadsRunning.decrementAndGet(); + } + } + }); + } + // we are done spawning threads + threadPool.shutdown(); + } catch (Throwable e) { + throw new RuntimeException(e); + } + + // wait until all threads are done, then mark it as COMPLETED + try { + // wait for all the threads to finish + threadPool.awaitTermination(2, TimeUnit.SECONDS); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + observer.onCompleted(); + } + }); + System.out.println("starting TestMultiThreadedObservable thread"); + t.start(); + System.out.println("done starting TestMultiThreadedObservable thread"); + return s; + } + + public void waitToFinish() { + try { + t.join(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + } + + private static class BusyObserver extends Subscriber { + volatile boolean onCompleted = false; + volatile boolean onError = false; + AtomicInteger onNextCount = new AtomicInteger(); + AtomicInteger threadsRunning = new AtomicInteger(); + AtomicInteger maxConcurrentThreads = new AtomicInteger(); + final CountDownLatch terminalEvent = new CountDownLatch(1); + + @Override + public void onCompleted() { + threadsRunning.incrementAndGet(); + try { + onCompleted = true; + } finally { + captureMaxThreads(); + threadsRunning.decrementAndGet(); + terminalEvent.countDown(); + } + } + + @Override + public void onError(Throwable e) { + System.out.println(">>>>>>>>>>>>>>>>>>>> onError received: " + e); + threadsRunning.incrementAndGet(); + try { + onError = true; + } finally { + captureMaxThreads(); + threadsRunning.decrementAndGet(); + terminalEvent.countDown(); + } + } + + @Override + public void onNext(String args) { + threadsRunning.incrementAndGet(); + try { + onNextCount.incrementAndGet(); + try { + // simulate doing something computational + Thread.sleep(200); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } finally { + // capture 'maxThreads' + captureMaxThreads(); + threadsRunning.decrementAndGet(); + } + } + + protected void captureMaxThreads() { + int concurrentThreads = threadsRunning.get(); + int maxThreads = maxConcurrentThreads.get(); + if (concurrentThreads > maxThreads) { + maxConcurrentThreads.compareAndSet(maxThreads, concurrentThreads); + if (concurrentThreads > 1) { + new RuntimeException("should not be greater than 1").printStackTrace(); + } + } + } + + } +} diff --git a/rxjava-core/src/test/java/rx/observers/SerializedObserverViaQueueAndCounterTest.java b/rxjava-core/src/test/java/rx/observers/SerializedObserverViaQueueAndCounterTest.java index 7734245321..2e3f49ec7a 100644 --- a/rxjava-core/src/test/java/rx/observers/SerializedObserverViaQueueAndCounterTest.java +++ b/rxjava-core/src/test/java/rx/observers/SerializedObserverViaQueueAndCounterTest.java @@ -15,614 +15,11 @@ */ package rx.observers; -import static org.junit.Assert.*; -import static org.mockito.Matchers.*; -import static org.mockito.Mockito.*; - -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; - -import org.junit.Before; -import org.junit.Test; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import rx.Observable; import rx.Observer; -import rx.Subscriber; -import rx.Subscription; - -public class SerializedObserverViaQueueAndCounterTest { - - @Mock - Subscriber observer; - - @Before - public void before() { - MockitoAnnotations.initMocks(this); - } - - @Test - public void testSingleThreadedBasic() { - Subscription s = mock(Subscription.class); - TestSingleThreadedObservable onSubscribe = new TestSingleThreadedObservable(s, "one", "two", "three"); - Observable w = Observable.create(onSubscribe); - - SerializedObserverViaQueueAndCounter aw = new SerializedObserverViaQueueAndCounter(observer); - - w.subscribe(aw); - onSubscribe.waitToFinish(); - - verify(observer, times(1)).onNext("one"); - verify(observer, times(1)).onNext("two"); - verify(observer, times(1)).onNext("three"); - verify(observer, never()).onError(any(Throwable.class)); - verify(observer, times(1)).onCompleted(); - // non-deterministic because unsubscribe happens after 'waitToFinish' releases - // so commenting out for now as this is not a critical thing to test here - // verify(s, times(1)).unsubscribe(); - } - - @Test - public void testMultiThreadedBasic() { - Subscription s = mock(Subscription.class); - TestMultiThreadedObservable onSubscribe = new TestMultiThreadedObservable(s, "one", "two", "three"); - Observable w = Observable.create(onSubscribe); - - BusyObserver busyObserver = new BusyObserver(); - SerializedObserverViaQueueAndCounter aw = new SerializedObserverViaQueueAndCounter(busyObserver); - - w.subscribe(aw); - onSubscribe.waitToFinish(); - - assertEquals(3, busyObserver.onNextCount.get()); - assertFalse(busyObserver.onError); - assertTrue(busyObserver.onCompleted); - // non-deterministic because unsubscribe happens after 'waitToFinish' releases - // so commenting out for now as this is not a critical thing to test here - // verify(s, times(1)).unsubscribe(); - - // we can have concurrency ... - assertTrue(onSubscribe.maxConcurrentThreads.get() > 1); - // ... but the onNext execution should be single threaded - assertEquals(1, busyObserver.maxConcurrentThreads.get()); - } - - @Test(timeout=1000) - public void testMultiThreadedWithNPE() throws InterruptedException { - Subscription s = mock(Subscription.class); - TestMultiThreadedObservable onSubscribe = new TestMultiThreadedObservable(s, "one", "two", "three", null); - Observable w = Observable.create(onSubscribe); - - BusyObserver busyObserver = new BusyObserver(); - SerializedObserverViaQueueAndCounter aw = new SerializedObserverViaQueueAndCounter(busyObserver); - - w.subscribe(aw); - onSubscribe.waitToFinish(); - busyObserver.terminalEvent.await(); - - System.out.println("OnSubscribe maxConcurrentThreads: " + onSubscribe.maxConcurrentThreads.get() + " Observer maxConcurrentThreads: " + busyObserver.maxConcurrentThreads.get()); - - // we can't know how many onNext calls will occur since they each run on a separate thread - // that depends on thread scheduling so 0, 1, 2 and 3 are all valid options - // assertEquals(3, busyObserver.onNextCount.get()); - assertTrue(busyObserver.onNextCount.get() < 4); - assertTrue(busyObserver.onError); - // no onCompleted because onError was invoked - assertFalse(busyObserver.onCompleted); - // non-deterministic because unsubscribe happens after 'waitToFinish' releases - // so commenting out for now as this is not a critical thing to test here - //verify(s, times(1)).unsubscribe(); - - // we can have concurrency ... - assertTrue(onSubscribe.maxConcurrentThreads.get() > 1); - // ... but the onNext execution should be single threaded - assertEquals(1, busyObserver.maxConcurrentThreads.get()); - } - - @Test - public void testMultiThreadedWithNPEinMiddle() { - Subscription s = mock(Subscription.class); - TestMultiThreadedObservable onSubscribe = new TestMultiThreadedObservable(s, "one", "two", "three", null, "four", "five", "six", "seven", "eight", "nine"); - Observable w = Observable.create(onSubscribe); - - BusyObserver busyObserver = new BusyObserver(); - SerializedObserverViaQueueAndCounter aw = new SerializedObserverViaQueueAndCounter(busyObserver); - - w.subscribe(aw); - onSubscribe.waitToFinish(); - - System.out.println("OnSubscribe maxConcurrentThreads: " + onSubscribe.maxConcurrentThreads.get() + " Observer maxConcurrentThreads: " + busyObserver.maxConcurrentThreads.get()); - - // we can have concurrency ... - assertTrue(onSubscribe.maxConcurrentThreads.get() > 1); - // ... but the onNext execution should be single threaded - assertEquals(1, busyObserver.maxConcurrentThreads.get()); - - // this should not be the full number of items since the error should stop it before it completes all 9 - System.out.println("onNext count: " + busyObserver.onNextCount.get()); - assertTrue(busyObserver.onNextCount.get() < 9); - assertTrue(busyObserver.onError); - // no onCompleted because onError was invoked - assertFalse(busyObserver.onCompleted); - // non-deterministic because unsubscribe happens after 'waitToFinish' releases - // so commenting out for now as this is not a critical thing to test here - // verify(s, times(1)).unsubscribe(); - } - - /** - * A non-realistic use case that tries to expose thread-safety issues by throwing lots of out-of-order - * events on many threads. - * - * @param w - * @param tw - */ - @Test - public void runOutOfOrderConcurrencyTest() { - ExecutorService tp = Executors.newFixedThreadPool(20); - try { - TestConcurrencyObserver tw = new TestConcurrencyObserver(); - // we need Synchronized + SafeSubscriber to handle synchronization plus life-cycle - SerializedObserverViaQueueAndCounter w = new SerializedObserverViaQueueAndCounter(new SafeSubscriber(tw)); - - Future f1 = tp.submit(new OnNextThread(w, 12000)); - Future f2 = tp.submit(new OnNextThread(w, 5000)); - Future f3 = tp.submit(new OnNextThread(w, 75000)); - Future f4 = tp.submit(new OnNextThread(w, 13500)); - Future f5 = tp.submit(new OnNextThread(w, 22000)); - Future f6 = tp.submit(new OnNextThread(w, 15000)); - Future f7 = tp.submit(new OnNextThread(w, 7500)); - Future f8 = tp.submit(new OnNextThread(w, 23500)); - - Future f10 = tp.submit(new CompletionThread(w, TestConcurrencyObserverEvent.onCompleted, f1, f2, f3, f4)); - try { - Thread.sleep(1); - } catch (InterruptedException e) { - // ignore - } - Future f11 = tp.submit(new CompletionThread(w, TestConcurrencyObserverEvent.onCompleted, f4, f6, f7)); - Future f12 = tp.submit(new CompletionThread(w, TestConcurrencyObserverEvent.onCompleted, f4, f6, f7)); - Future f13 = tp.submit(new CompletionThread(w, TestConcurrencyObserverEvent.onCompleted, f4, f6, f7)); - Future f14 = tp.submit(new CompletionThread(w, TestConcurrencyObserverEvent.onCompleted, f4, f6, f7)); - // // the next 4 onError events should wait on same as f10 - Future f15 = tp.submit(new CompletionThread(w, TestConcurrencyObserverEvent.onError, f1, f2, f3, f4)); - Future f16 = tp.submit(new CompletionThread(w, TestConcurrencyObserverEvent.onError, f1, f2, f3, f4)); - Future f17 = tp.submit(new CompletionThread(w, TestConcurrencyObserverEvent.onError, f1, f2, f3, f4)); - Future f18 = tp.submit(new CompletionThread(w, TestConcurrencyObserverEvent.onError, f1, f2, f3, f4)); - - waitOnThreads(f1, f2, f3, f4, f5, f6, f7, f8, f10, f11, f12, f13, f14, f15, f16, f17, f18); - @SuppressWarnings("unused") - int numNextEvents = tw.assertEvents(null); // no check of type since we don't want to test barging results here, just interleaving behavior - // System.out.println("Number of events executed: " + numNextEvents); - } catch (Throwable e) { - fail("Concurrency test failed: " + e.getMessage()); - e.printStackTrace(); - } finally { - tp.shutdown(); - try { - tp.awaitTermination(5000, TimeUnit.MILLISECONDS); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - } - - /** - * - * @param w - * @param tw - */ - @Test - public void runConcurrencyTest() { - ExecutorService tp = Executors.newFixedThreadPool(20); - try { - TestConcurrencyObserver tw = new TestConcurrencyObserver(); - // we need Synchronized + SafeSubscriber to handle synchronization plus life-cycle - SerializedObserverViaQueueAndCounter w = new SerializedObserverViaQueueAndCounter(new SafeSubscriber(tw)); - - Future f1 = tp.submit(new OnNextThread(w, 12000)); - Future f2 = tp.submit(new OnNextThread(w, 5000)); - Future f3 = tp.submit(new OnNextThread(w, 75000)); - Future f4 = tp.submit(new OnNextThread(w, 13500)); - Future f5 = tp.submit(new OnNextThread(w, 22000)); - Future f6 = tp.submit(new OnNextThread(w, 15000)); - Future f7 = tp.submit(new OnNextThread(w, 7500)); - Future f8 = tp.submit(new OnNextThread(w, 23500)); - - // 12000 + 5000 + 75000 + 13500 + 22000 + 15000 + 7500 + 23500 = 173500 - - Future f10 = tp.submit(new CompletionThread(w, TestConcurrencyObserverEvent.onCompleted, f1, f2, f3, f4, f5, f6, f7, f8)); - try { - Thread.sleep(1); - } catch (InterruptedException e) { - // ignore - } - - waitOnThreads(f1, f2, f3, f4, f5, f6, f7, f8, f10); - @SuppressWarnings("unused") - int numNextEvents = tw.assertEvents(null); // no check of type since we don't want to test barging results here, just interleaving behavior - assertEquals(173500, numNextEvents); - // System.out.println("Number of events executed: " + numNextEvents); - } catch (Throwable e) { - fail("Concurrency test failed: " + e.getMessage()); - e.printStackTrace(); - } finally { - tp.shutdown(); - try { - tp.awaitTermination(25000, TimeUnit.MILLISECONDS); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - } - - private static void waitOnThreads(Future... futures) { - for (Future f : futures) { - try { - f.get(20, TimeUnit.SECONDS); - } catch (Throwable e) { - System.err.println("Failed while waiting on future."); - e.printStackTrace(); - } - } - } - - /** - * A thread that will pass data to onNext - */ - public static class OnNextThread implements Runnable { - - private final Observer Observer; - private final int numStringsToSend; - - OnNextThread(Observer Observer, int numStringsToSend) { - this.Observer = Observer; - this.numStringsToSend = numStringsToSend; - } - - @Override - public void run() { - for (int i = 0; i < numStringsToSend; i++) { - Observer.onNext(Thread.currentThread().getId() + "-" + i); - } - } - } - - /** - * A thread that will call onError or onNext - */ - public static class CompletionThread implements Runnable { - - private final Observer observer; - private final TestConcurrencyObserverEvent event; - private final Future[] waitOnThese; - - CompletionThread(Observer Observer, TestConcurrencyObserverEvent event, Future... waitOnThese) { - this.observer = Observer; - this.event = event; - this.waitOnThese = waitOnThese; - } - - @Override - public void run() { - /* if we have 'waitOnThese' futures, we'll wait on them before proceeding */ - if (waitOnThese != null) { - for (Future f : waitOnThese) { - try { - f.get(); - } catch (Throwable e) { - System.err.println("Error while waiting on future in CompletionThread"); - } - } - } - - /* send the event */ - if (event == TestConcurrencyObserverEvent.onError) { - observer.onError(new RuntimeException("mocked exception")); - } else if (event == TestConcurrencyObserverEvent.onCompleted) { - observer.onCompleted(); - - } else { - throw new IllegalArgumentException("Expecting either onError or onCompleted"); - } - } - } - - private static enum TestConcurrencyObserverEvent { - onCompleted, onError, onNext - } - - private static class TestConcurrencyObserver extends Subscriber { - - /** - * used to store the order and number of events received - */ - private final LinkedBlockingQueue events = new LinkedBlockingQueue(); - private final int waitTime; - - @SuppressWarnings("unused") - public TestConcurrencyObserver(int waitTimeInNext) { - this.waitTime = waitTimeInNext; - } - - public TestConcurrencyObserver() { - this.waitTime = 0; - } - - @Override - public void onCompleted() { - events.add(TestConcurrencyObserverEvent.onCompleted); - } - - @Override - public void onError(Throwable e) { - events.add(TestConcurrencyObserverEvent.onError); - } - - @Override - public void onNext(String args) { - events.add(TestConcurrencyObserverEvent.onNext); - // do some artificial work to make the thread scheduling/timing vary - int s = 0; - for (int i = 0; i < 20; i++) { - s += s * i; - } - - if (waitTime > 0) { - try { - Thread.sleep(waitTime); - } catch (InterruptedException e) { - // ignore - } - } - } - - /** - * Assert the order of events is correct and return the number of onNext executions. - * - * @param expectedEndingEvent - * @return int count of onNext calls - * @throws IllegalStateException - * If order of events was invalid. - */ - public int assertEvents(TestConcurrencyObserverEvent expectedEndingEvent) throws IllegalStateException { - int nextCount = 0; - boolean finished = false; - for (TestConcurrencyObserverEvent e : events) { - if (e == TestConcurrencyObserverEvent.onNext) { - if (finished) { - // already finished, we shouldn't get this again - throw new IllegalStateException("Received onNext but we're already finished."); - } - nextCount++; - } else if (e == TestConcurrencyObserverEvent.onError) { - if (finished) { - // already finished, we shouldn't get this again - throw new IllegalStateException("Received onError but we're already finished."); - } - if (expectedEndingEvent != null && TestConcurrencyObserverEvent.onError != expectedEndingEvent) { - throw new IllegalStateException("Received onError ending event but expected " + expectedEndingEvent); - } - finished = true; - } else if (e == TestConcurrencyObserverEvent.onCompleted) { - if (finished) { - // already finished, we shouldn't get this again - throw new IllegalStateException("Received onCompleted but we're already finished."); - } - if (expectedEndingEvent != null && TestConcurrencyObserverEvent.onCompleted != expectedEndingEvent) { - throw new IllegalStateException("Received onCompleted ending event but expected " + expectedEndingEvent); - } - finished = true; - } - } - - return nextCount; - } - - } - - /** - * This spawns a single thread for the subscribe execution - */ - private static class TestSingleThreadedObservable implements Observable.OnSubscribeFunc { - - final Subscription s; - final String[] values; - private Thread t = null; - - public TestSingleThreadedObservable(final Subscription s, final String... values) { - this.s = s; - this.values = values; - - } - - public Subscription onSubscribe(final Observer observer) { - System.out.println("TestSingleThreadedObservable subscribed to ..."); - t = new Thread(new Runnable() { - - @Override - public void run() { - try { - System.out.println("running TestSingleThreadedObservable thread"); - for (String s : values) { - System.out.println("TestSingleThreadedObservable onNext: " + s); - observer.onNext(s); - } - observer.onCompleted(); - } catch (Throwable e) { - throw new RuntimeException(e); - } - } - - }); - System.out.println("starting TestSingleThreadedObservable thread"); - t.start(); - System.out.println("done starting TestSingleThreadedObservable thread"); - return s; - } - - public void waitToFinish() { - try { - t.join(); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - } - - } - - /** - * This spawns a thread for the subscription, then a separate thread for each onNext call. - */ - private static class TestMultiThreadedObservable implements Observable.OnSubscribeFunc { - - final Subscription s; - final String[] values; - Thread t = null; - AtomicInteger threadsRunning = new AtomicInteger(); - AtomicInteger maxConcurrentThreads = new AtomicInteger(); - ExecutorService threadPool; - - public TestMultiThreadedObservable(Subscription s, String... values) { - this.s = s; - this.values = values; - this.threadPool = Executors.newCachedThreadPool(); - } - - @Override - public Subscription onSubscribe(final Observer observer) { - System.out.println("TestMultiThreadedObservable subscribed to ..."); - t = new Thread(new Runnable() { - - @Override - public void run() { - try { - System.out.println("running TestMultiThreadedObservable thread"); - for (final String s : values) { - threadPool.execute(new Runnable() { - - @Override - public void run() { - threadsRunning.incrementAndGet(); - try { - // perform onNext call - System.out.println("TestMultiThreadedObservable onNext: " + s); - if (s == null) { - // force an error - throw new NullPointerException(); - } - observer.onNext(s); - // capture 'maxThreads' - int concurrentThreads = threadsRunning.get(); - int maxThreads = maxConcurrentThreads.get(); - if (concurrentThreads > maxThreads) { - maxConcurrentThreads.compareAndSet(maxThreads, concurrentThreads); - } - } catch (Throwable e) { - observer.onError(e); - } finally { - threadsRunning.decrementAndGet(); - } - } - }); - } - // we are done spawning threads - threadPool.shutdown(); - } catch (Throwable e) { - throw new RuntimeException(e); - } - - // wait until all threads are done, then mark it as COMPLETED - try { - // wait for all the threads to finish - threadPool.awaitTermination(2, TimeUnit.SECONDS); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - observer.onCompleted(); - } - }); - System.out.println("starting TestMultiThreadedObservable thread"); - t.start(); - System.out.println("done starting TestMultiThreadedObservable thread"); - return s; - } - - public void waitToFinish() { - try { - t.join(); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - } - } - - private static class BusyObserver extends Subscriber { - volatile boolean onCompleted = false; - volatile boolean onError = false; - AtomicInteger onNextCount = new AtomicInteger(); - AtomicInteger threadsRunning = new AtomicInteger(); - AtomicInteger maxConcurrentThreads = new AtomicInteger(); - final CountDownLatch terminalEvent = new CountDownLatch(1); - - @Override - public void onCompleted() { - threadsRunning.incrementAndGet(); - try { - onCompleted = true; - } finally { - captureMaxThreads(); - threadsRunning.decrementAndGet(); - terminalEvent.countDown(); - } - } - - @Override - public void onError(Throwable e) { - System.out.println(">>>>>>>>>>>>>>>>>>>> onError received: " + e); - threadsRunning.incrementAndGet(); - try { - onError = true; - } finally { - captureMaxThreads(); - threadsRunning.decrementAndGet(); - terminalEvent.countDown(); - } - } - - @Override - public void onNext(String args) { - threadsRunning.incrementAndGet(); - try { - onNextCount.incrementAndGet(); - try { - // simulate doing something computational - Thread.sleep(200); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } finally { - // capture 'maxThreads' - captureMaxThreads(); - threadsRunning.decrementAndGet(); - } - } - - protected void captureMaxThreads() { - int concurrentThreads = threadsRunning.get(); - int maxThreads = maxConcurrentThreads.get(); - if (concurrentThreads > maxThreads) { - maxConcurrentThreads.compareAndSet(maxThreads, concurrentThreads); - if (concurrentThreads > 1) { - new RuntimeException("should not be greater than 1").printStackTrace(); - } - } - } +public class SerializedObserverViaQueueAndCounterTest extends SerializedObserverTest { + @Override + protected Observer serializedObserver(Observer o) { + return new SerializedObserverViaQueueAndCounter(o); } } diff --git a/rxjava-core/src/test/java/rx/observers/SerializedObserverViaStateMachineTest.java b/rxjava-core/src/test/java/rx/observers/SerializedObserverViaStateMachineTest.java index 18fb4ce4aa..76582d06e0 100644 --- a/rxjava-core/src/test/java/rx/observers/SerializedObserverViaStateMachineTest.java +++ b/rxjava-core/src/test/java/rx/observers/SerializedObserverViaStateMachineTest.java @@ -1,5 +1,10 @@ package rx.observers; -public class SerializedObserverViaStateMachineTest extends SerializedObserverViaQueueAndCounterTest { +import rx.Observer; +public class SerializedObserverViaStateMachineTest extends SerializedObserverTest { + @Override + protected Observer serializedObserver(Observer o) { + return new SerializedObserverViaStateMachine(o); + } } From b6096079c17488b1232f7db942c529c7eb5f9843 Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Wed, 12 Mar 2014 11:43:37 -0700 Subject: [PATCH 123/422] Unlock in finally block --- .../SerializedObserverViaStateMachine.java | 29 +++++++++---------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/rxjava-core/src/main/java/rx/observers/SerializedObserverViaStateMachine.java b/rxjava-core/src/main/java/rx/observers/SerializedObserverViaStateMachine.java index fdd6e844ad..14c9612e07 100644 --- a/rxjava-core/src/main/java/rx/observers/SerializedObserverViaStateMachine.java +++ b/rxjava-core/src/main/java/rx/observers/SerializedObserverViaStateMachine.java @@ -63,25 +63,24 @@ public void onNext(T t) { } while (!state.compareAndSet(current, newState)); if (newState.shouldProcess()) { - if (newState == State.PROCESS_SELF) { - s.onNext(t); - - // finish processing to let this thread move on - do { - current = state.get(); - newState = current.finishProcessing(1); - } while (!state.compareAndSet(current, newState)); - } else { - // drain queue - Object[] items = newState.queue; - for (int i = 0; i < items.length; i++) { - s.onNext((T) items[i]); + int numItemsProcessed = 0; + try { + if (newState == State.PROCESS_SELF) { + s.onNext(t); + numItemsProcessed++; + } else { + // drain queue + Object[] items = newState.queue; + for (int i = 0; i < items.length; i++) { + s.onNext((T) items[i]); + numItemsProcessed++; + } } - + } finally { // finish processing to let this thread move on do { current = state.get(); - newState = current.finishProcessing(items.length); + newState = current.finishProcessing(numItemsProcessed); } while (!state.compareAndSet(current, newState)); } } From c1037968b91ee4b90c5846d3c8c9b68e42fb7442 Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Wed, 12 Mar 2014 14:06:55 -0700 Subject: [PATCH 124/422] queue and lock implementation --- .../SerializedObserverViaQueueAndLock.java | 174 ++++++++++++++++++ .../rx/observers/SerializedSubscriber.java | 2 +- .../OperatorSerializePerformance.java | 26 ++- ...SerializedObserverViaQueueAndLockTest.java | 11 ++ 4 files changed, 207 insertions(+), 6 deletions(-) create mode 100644 rxjava-core/src/main/java/rx/observers/SerializedObserverViaQueueAndLock.java create mode 100644 rxjava-core/src/test/java/rx/observers/SerializedObserverViaQueueAndLockTest.java diff --git a/rxjava-core/src/main/java/rx/observers/SerializedObserverViaQueueAndLock.java b/rxjava-core/src/main/java/rx/observers/SerializedObserverViaQueueAndLock.java new file mode 100644 index 0000000000..5e999d38fd --- /dev/null +++ b/rxjava-core/src/main/java/rx/observers/SerializedObserverViaQueueAndLock.java @@ -0,0 +1,174 @@ +package rx.observers; + +import java.util.ArrayList; + +import rx.Observer; + +public class SerializedObserverViaQueueAndLock implements Observer { + private final Observer actual; + + private boolean emitting = false; + private boolean terminated = false; + private ArrayList queue = new ArrayList(); + + private static Sentinel NULL_SENTINEL = new Sentinel(); + private static Sentinel COMPLETE_SENTINEL = new Sentinel(); + + private static class Sentinel { + + } + + private static class ErrorSentinel extends Sentinel { + final Throwable e; + + ErrorSentinel(Throwable e) { + this.e = e; + } + } + + public SerializedObserverViaQueueAndLock(Observer s) { + this.actual = s; + } + + @Override + public void onCompleted() { + boolean canEmit = false; + ArrayList list = null; + synchronized (this) { + if (terminated) { + return; + } + terminated = true; + if (!emitting) { + // emit immediately + emitting = true; + canEmit = true; + if (queue.size() > 0) { + list = queue; // copy reference + queue = new ArrayList(); // new version; + } + } else { + // someone else is already emitting so just queue it + queue.add(COMPLETE_SENTINEL); + } + } + if (canEmit) { + // we won the right to emit + try { + drainQueue(list); + actual.onCompleted(); + } finally { + synchronized (this) { + emitting = false; + } + } + } + } + + @Override + public void onError(final Throwable e) { + boolean canEmit = false; + ArrayList list = null; + synchronized (this) { + if (terminated) { + return; + } + terminated = true; + if (!emitting) { + // emit immediately + emitting = true; + canEmit = true; + if (queue.size() > 0) { + list = queue; // copy reference + queue = new ArrayList(); // new version; + } + } else { + // someone else is already emitting so just queue it ... after eliminating the queue to shortcut + queue.clear(); + queue.add(new ErrorSentinel(e)); + } + } + if (canEmit) { + // we won the right to emit + try { + drainQueue(list); + actual.onError(e); + } finally { + synchronized (this) { + emitting = false; + } + } + } + } + + @Override + public void onNext(T t) { + boolean canEmit = false; + ArrayList list = null; + synchronized (this) { + if (terminated) { + return; + } + if (!emitting) { + // emit immediately + emitting = true; + canEmit = true; + if (queue.size() > 0) { + list = queue; // copy reference + queue = new ArrayList(); // new version; + } + } else { + // someone else is already emitting so just queue it + if (t == null) { + queue.add(NULL_SENTINEL); + } else { + queue.add(t); + } + } + } + if (canEmit) { + // we won the right to emit + try { + drainQueue(list); + actual.onNext(t); + } finally { + synchronized (this) { + if (terminated) { + list = queue; // copy reference + queue = new ArrayList(); // new version; + } else { + // release this thread + emitting = false; + canEmit = false; + } + } + } + } + + // if terminated this will still be true so let's drain the rest of the queue + if (canEmit) { + drainQueue(list); + } + } + + public void drainQueue(ArrayList list) { + if (list == null || list.size() == 0) { + return; + } + for (Object v : list) { + if (v != null) { + if (v instanceof Sentinel) { + if (v == NULL_SENTINEL) { + actual.onNext(null); + } else if (v == COMPLETE_SENTINEL) { + actual.onCompleted(); + } else if (v instanceof ErrorSentinel) { + actual.onError(((ErrorSentinel) v).e); + } + } else { + actual.onNext((T) v); + } + } + } + } +} diff --git a/rxjava-core/src/main/java/rx/observers/SerializedSubscriber.java b/rxjava-core/src/main/java/rx/observers/SerializedSubscriber.java index 0eecc1e24e..5c5335a934 100644 --- a/rxjava-core/src/main/java/rx/observers/SerializedSubscriber.java +++ b/rxjava-core/src/main/java/rx/observers/SerializedSubscriber.java @@ -8,7 +8,7 @@ public class SerializedSubscriber extends Subscriber { private final Observer s; public SerializedSubscriber(Subscriber s) { - this.s = new SerializedObserverViaStateMachine(s); + this.s = new SerializedObserverViaQueueAndLock(s); } @Override diff --git a/rxjava-core/src/perf/java/rx/operators/OperatorSerializePerformance.java b/rxjava-core/src/perf/java/rx/operators/OperatorSerializePerformance.java index 3bebb580c0..f6368c9499 100644 --- a/rxjava-core/src/perf/java/rx/operators/OperatorSerializePerformance.java +++ b/rxjava-core/src/perf/java/rx/operators/OperatorSerializePerformance.java @@ -14,9 +14,9 @@ import rx.schedulers.Schedulers; public class OperatorSerializePerformance extends AbstractPerformanceTester { - static int reps = Integer.MAX_VALUE / 16384; // timeTwoStreams + // static int reps = Integer.MAX_VALUE / 16384; // timeTwoStreams - // static int reps = Integer.MAX_VALUE / 1024; // timeSingleStream + static int reps = Integer.MAX_VALUE / 1024; // timeSingleStream // static int reps = 1000; // interval streams @@ -32,8 +32,8 @@ public static void main(String args[]) { @Override public void call() { - spt.timeTwoStreams(); - // spt.timeSingleStream(); + // spt.timeTwoStreams(); + spt.timeSingleStream(); } }); } catch (Exception e) { @@ -52,13 +52,21 @@ public void call() { * Run: 13 - 35,269,946 ops/sec * Run: 14 - 34,165,013 ops/sec * - * -> using "observeOn" technique + * -> using queue and counter technique * * Run: 10 - 19,548,387 ops/sec * Run: 11 - 19,471,069 ops/sec * Run: 12 - 19,480,112 ops/sec * Run: 13 - 18,720,550 ops/sec * Run: 14 - 19,070,383 ops/sec + * + * -> using queue and lock technique + * + * Run: 10 - 51,295,152 ops/sec + * Run: 11 - 50,317,937 ops/sec + * Run: 12 - 51,126,331 ops/sec + * Run: 13 - 52,418,291 ops/sec + * Run: 14 - 51,694,710 ops/sec */ public long timeSingleStream() { @@ -119,6 +127,14 @@ public void call(Integer t1) { * Run: 12 - 4,510,978 ops/sec * Run: 13 - 3,218,915 ops/sec * Run: 14 - 3,938,549 ops/sec + * + * -> using queue and lock technique + * + * Run: 10 - 5,348,090 ops/sec + * Run: 11 - 6,458,608 ops/sec + * Run: 12 - 5,430,743 ops/sec + * Run: 13 - 5,159,666 ops/sec + * Run: 14 - 6,129,682 ops/sec */ public long timeTwoStreams() { diff --git a/rxjava-core/src/test/java/rx/observers/SerializedObserverViaQueueAndLockTest.java b/rxjava-core/src/test/java/rx/observers/SerializedObserverViaQueueAndLockTest.java new file mode 100644 index 0000000000..45cdfa1fe5 --- /dev/null +++ b/rxjava-core/src/test/java/rx/observers/SerializedObserverViaQueueAndLockTest.java @@ -0,0 +1,11 @@ +package rx.observers; + +import rx.Observer; + +public class SerializedObserverViaQueueAndLockTest extends SerializedObserverTest { + @Override + protected Observer serializedObserver(Observer o) { + return new SerializedObserverViaQueueAndLock(o); + } + +} From 76bbefcd2b0a94f1f010aad1d6a9095c6ccfd248 Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Thu, 13 Mar 2014 10:12:16 -0700 Subject: [PATCH 125/422] SerializedObserver and SerializedSubscriber Using the "queue and lock" implementation which won our performance and production testing. --- rxjava-core/src/main/java/rx/Observable.java | 2 + .../java/rx/observers/SerializedObserver.java | 42 ++++++++ .../SerializedObserverViaQueueAndCounter.java | 2 +- .../SerializedObserverViaQueueAndLock.java | 2 +- .../SerializedObserverViaStateMachine.java | 2 +- .../rx/observers/SerializedSubscriber.java | 14 ++- .../rx/observers/SynchronizedObserver.java | 2 + .../rx/observers/SynchronizedSubscriber.java | 2 + .../OperatorSerializePerformance.java | 101 +++++++++++++----- 9 files changed, 139 insertions(+), 30 deletions(-) create mode 100644 rxjava-core/src/main/java/rx/observers/SerializedObserver.java diff --git a/rxjava-core/src/main/java/rx/Observable.java b/rxjava-core/src/main/java/rx/Observable.java index 7874ff8f3f..235e1e62e4 100644 --- a/rxjava-core/src/main/java/rx/Observable.java +++ b/rxjava-core/src/main/java/rx/Observable.java @@ -7264,6 +7264,7 @@ public final Observable switchMap(Func1RxJava Wiki: synchronize() + * @deprecated Use {@link #serialize()} instead as it doesn't block threads while emitting notification. */ public final Observable synchronize() { return lift(new OperatorSynchronize()); @@ -7288,6 +7289,7 @@ public final Observable synchronize() { * @return an Observable that is a chronologically well-behaved version of the source Observable, and that * synchronously notifies its {@link Observer}s * @see RxJava Wiki: synchronize() + * @deprecated Use {@link #serialize()} instead as it doesn't block threads while emitting notification. */ public final Observable synchronize(Object lock) { return lift(new OperatorSynchronize(lock)); diff --git a/rxjava-core/src/main/java/rx/observers/SerializedObserver.java b/rxjava-core/src/main/java/rx/observers/SerializedObserver.java new file mode 100644 index 0000000000..12e46fac06 --- /dev/null +++ b/rxjava-core/src/main/java/rx/observers/SerializedObserver.java @@ -0,0 +1,42 @@ +package rx.observers; + +import rx.Observer; + +/** + * Enforce single-threaded, serialized, ordered execution of onNext, onCompleted, onError. + *

+ * When multiple threads are notifying they will be serialized by: + *

+ *

  • Allowing only one thread at a time to emit
  • + *
  • Adding notifications to a queue if another thread is already emitting
  • + *
  • Not holding any locks or blocking any threads while emitting
  • + *

    + * + * @param + */ +public class SerializedObserver implements Observer { + /* + * Facade to actual implementation until final decision is made + * on the implementation. + */ + private final SerializedObserverViaQueueAndLock actual; + + public SerializedObserver(Observer observer) { + this.actual = new SerializedObserverViaQueueAndLock(observer); + } + + @Override + public void onCompleted() { + actual.onCompleted(); + } + + @Override + public void onError(Throwable e) { + actual.onError(e); + } + + @Override + public void onNext(T t) { + actual.onNext(t); + } +} diff --git a/rxjava-core/src/main/java/rx/observers/SerializedObserverViaQueueAndCounter.java b/rxjava-core/src/main/java/rx/observers/SerializedObserverViaQueueAndCounter.java index 60e62d429d..2a9ac3c473 100644 --- a/rxjava-core/src/main/java/rx/observers/SerializedObserverViaQueueAndCounter.java +++ b/rxjava-core/src/main/java/rx/observers/SerializedObserverViaQueueAndCounter.java @@ -5,7 +5,7 @@ import rx.Observer; -public class SerializedObserverViaQueueAndCounter implements Observer { +/* package */class SerializedObserverViaQueueAndCounter implements Observer { private final Observer actual; private final AtomicInteger count = new AtomicInteger(); private final ConcurrentLinkedQueue queue = new ConcurrentLinkedQueue(); diff --git a/rxjava-core/src/main/java/rx/observers/SerializedObserverViaQueueAndLock.java b/rxjava-core/src/main/java/rx/observers/SerializedObserverViaQueueAndLock.java index 5e999d38fd..48f3c94f4b 100644 --- a/rxjava-core/src/main/java/rx/observers/SerializedObserverViaQueueAndLock.java +++ b/rxjava-core/src/main/java/rx/observers/SerializedObserverViaQueueAndLock.java @@ -4,7 +4,7 @@ import rx.Observer; -public class SerializedObserverViaQueueAndLock implements Observer { +/* package */ class SerializedObserverViaQueueAndLock implements Observer { private final Observer actual; private boolean emitting = false; diff --git a/rxjava-core/src/main/java/rx/observers/SerializedObserverViaStateMachine.java b/rxjava-core/src/main/java/rx/observers/SerializedObserverViaStateMachine.java index 14c9612e07..71756bb98d 100644 --- a/rxjava-core/src/main/java/rx/observers/SerializedObserverViaStateMachine.java +++ b/rxjava-core/src/main/java/rx/observers/SerializedObserverViaStateMachine.java @@ -4,7 +4,7 @@ import rx.Observer; -public class SerializedObserverViaStateMachine implements Observer { +/* package */class SerializedObserverViaStateMachine implements Observer { private final AtomicReference state = new AtomicReference(State.createNew()); private final Observer s; diff --git a/rxjava-core/src/main/java/rx/observers/SerializedSubscriber.java b/rxjava-core/src/main/java/rx/observers/SerializedSubscriber.java index 5c5335a934..db545ff430 100644 --- a/rxjava-core/src/main/java/rx/observers/SerializedSubscriber.java +++ b/rxjava-core/src/main/java/rx/observers/SerializedSubscriber.java @@ -3,12 +3,24 @@ import rx.Observer; import rx.Subscriber; +/** + * Enforce single-threaded, serialized, ordered execution of onNext, onCompleted, onError. + *

    + * When multiple threads are notifying they will be serialized by: + *

    + *

  • Allowing only one thread at a time to emit
  • + *
  • Adding notifications to a queue if another thread is already emitting
  • + *
  • Not holding any locks or blocking any threads while emitting
  • + *

    + * + * @param + */ public class SerializedSubscriber extends Subscriber { private final Observer s; public SerializedSubscriber(Subscriber s) { - this.s = new SerializedObserverViaQueueAndLock(s); + this.s = new SerializedObserver(s); } @Override diff --git a/rxjava-core/src/main/java/rx/observers/SynchronizedObserver.java b/rxjava-core/src/main/java/rx/observers/SynchronizedObserver.java index 25ea8c3403..dbcbb9bc35 100644 --- a/rxjava-core/src/main/java/rx/observers/SynchronizedObserver.java +++ b/rxjava-core/src/main/java/rx/observers/SynchronizedObserver.java @@ -23,7 +23,9 @@ * This ONLY does synchronization. It does not involve itself in safety or subscriptions. See SafeSubscriber for that. * * @param + * @deprecated Use SerializedObserver instead as it doesn't block threads during event notification. */ +@Deprecated public final class SynchronizedObserver implements Observer { /** diff --git a/rxjava-core/src/main/java/rx/observers/SynchronizedSubscriber.java b/rxjava-core/src/main/java/rx/observers/SynchronizedSubscriber.java index 8dbf1b20f5..9f1b3dec90 100644 --- a/rxjava-core/src/main/java/rx/observers/SynchronizedSubscriber.java +++ b/rxjava-core/src/main/java/rx/observers/SynchronizedSubscriber.java @@ -29,7 +29,9 @@ * * * @param + * @deprecated Use SerializedSubscriber instead as it doesn't block threads during event notification. */ +@Deprecated public final class SynchronizedSubscriber extends Subscriber { private final Observer observer; diff --git a/rxjava-core/src/perf/java/rx/operators/OperatorSerializePerformance.java b/rxjava-core/src/perf/java/rx/operators/OperatorSerializePerformance.java index f6368c9499..cfe70889cf 100644 --- a/rxjava-core/src/perf/java/rx/operators/OperatorSerializePerformance.java +++ b/rxjava-core/src/perf/java/rx/operators/OperatorSerializePerformance.java @@ -14,10 +14,9 @@ import rx.schedulers.Schedulers; public class OperatorSerializePerformance extends AbstractPerformanceTester { - // static int reps = Integer.MAX_VALUE / 16384; // timeTwoStreams - - static int reps = Integer.MAX_VALUE / 1024; // timeSingleStream + static int reps = Integer.MAX_VALUE / 16384; // timeTwoStreams + // static int reps = Integer.MAX_VALUE / 1024; // timeSingleStream // static int reps = 1000; // interval streams OperatorSerializePerformance() { @@ -32,8 +31,9 @@ public static void main(String args[]) { @Override public void call() { - // spt.timeTwoStreams(); - spt.timeSingleStream(); + spt.timeTwoStreams(); + // spt.timeSingleStream(); + // spt.timeTwoStreamsIntervals(); } }); } catch (Exception e) { @@ -43,8 +43,17 @@ public void call() { } /** + * 1 streams emitting in a tight loop. Testing for single-threaded overhead. + * + * -> blocking synchronization (SynchronizedObserver) * - * -> state machine technique + * Run: 10 - 58,186,310 ops/sec + * Run: 11 - 60,592,037 ops/sec + * Run: 12 - 58,099,263 ops/sec + * Run: 13 - 59,034,765 ops/sec + * Run: 14 - 58,231,548 ops/sec + * + * -> state machine technique (SerializedObserverViaStateMachine) * * Run: 10 - 34,668,810 ops/sec * Run: 11 - 32,874,312 ops/sec @@ -52,7 +61,7 @@ public void call() { * Run: 13 - 35,269,946 ops/sec * Run: 14 - 34,165,013 ops/sec * - * -> using queue and counter technique + * -> using queue and counter technique (SerializedObserverViaQueueAndCounter) * * Run: 10 - 19,548,387 ops/sec * Run: 11 - 19,471,069 ops/sec @@ -60,7 +69,7 @@ public void call() { * Run: 13 - 18,720,550 ops/sec * Run: 14 - 19,070,383 ops/sec * - * -> using queue and lock technique + * -> using queue and lock technique (SerializedObserverViaQueueAndLock) * * Run: 10 - 51,295,152 ops/sec * Run: 11 - 50,317,937 ops/sec @@ -112,29 +121,41 @@ public void call(Integer t1) { } /** - * -> state machine technique + * 2 streams emitting in tight loops so very high contention. + * + * -> blocking synchronization (SynchronizedObserver) + * + * Run: 10 - 8,361,252 ops/sec + * Run: 11 - 7,184,728 ops/sec + * Run: 12 - 8,249,685 ops/sec + * Run: 13 - 6,831,595 ops/sec + * Run: 14 - 8,003,358 ops/sec + * + * (faster because it allows each thread to be "single threaded" while blocking the other) + * + * -> state machine technique (SerializedObserverViaStateMachine) * - * Run: 10 - 3,432,256 ops/sec - * Run: 11 - 3,570,444 ops/sec - * Run: 12 - 3,791,137 ops/sec - * Run: 13 - 3,664,579 ops/sec - * Run: 14 - 5,211,156 ops/sec + * Run: 10 - 4,060,062 ops/sec + * Run: 11 - 3,561,131 ops/sec + * Run: 12 - 3,721,387 ops/sec + * Run: 13 - 3,693,909 ops/sec + * Run: 14 - 3,516,324 ops/sec * - * -> using "observeOn" technique + * -> using queue and counter technique (SerializedObserverViaQueueAndCounter) * - * Run: 10 - 3,995,336 ops/sec - * Run: 11 - 4,033,077 ops/sec - * Run: 12 - 4,510,978 ops/sec - * Run: 13 - 3,218,915 ops/sec - * Run: 14 - 3,938,549 ops/sec + * Run: 10 - 4,300,229 ops/sec + * Run: 11 - 4,395,995 ops/sec + * Run: 12 - 4,551,550 ops/sec + * Run: 13 - 4,443,235 ops/sec + * Run: 14 - 4,158,475 ops/sec * - * -> using queue and lock technique + * -> using queue and lock technique (SerializedObserverViaQueueAndLock) * - * Run: 10 - 5,348,090 ops/sec - * Run: 11 - 6,458,608 ops/sec - * Run: 12 - 5,430,743 ops/sec - * Run: 13 - 5,159,666 ops/sec - * Run: 14 - 6,129,682 ops/sec + * Run: 10 - 6,369,781 ops/sec + * Run: 11 - 6,933,872 ops/sec + * Run: 12 - 5,652,535 ops/sec + * Run: 13 - 5,503,716 ops/sec + * Run: 14 - 6,219,264 ops/sec */ public long timeTwoStreams() { @@ -198,11 +219,39 @@ public void call(Integer t1) { } /** + * 2 streams emitting once a millisecond. Slow emission so little to no contention. + * + * -> blocking synchronization (SynchronizedObserver) + * + * Run: 10 - 1,996 ops/sec + * Run: 11 - 1,996 ops/sec + * Run: 12 - 1,995 ops/sec + * Run: 13 - 1,997 ops/sec + * Run: 14 - 1,996 ops/sec + * + * -> state machine technique (SerializedObserverViaStateMachine) + * * Run: 10 - 1,996 ops/sec * Run: 11 - 1,996 ops/sec * Run: 12 - 1,996 ops/sec * Run: 13 - 1,996 ops/sec * Run: 14 - 1,996 ops/sec + * + * -> using queue and counter technique (SerializedObserverViaQueueAndCounter) + * + * Run: 10 - 1,996 ops/sec + * Run: 11 - 1,996 ops/sec + * Run: 12 - 1,996 ops/sec + * Run: 13 - 1,996 ops/sec + * Run: 14 - 1,995 ops/sec + * + * -> using queue and lock technique (SerializedObserverViaQueueAndLock) + * + * Run: 10 - 1,996 ops/sec + * Run: 11 - 1,996 ops/sec + * Run: 12 - 1,997 ops/sec + * Run: 13 - 1,996 ops/sec + * Run: 14 - 1,995 ops/sec */ public long timeTwoStreamsIntervals() { From 6926fa6d150a2ed221df051deb72a4d503de50f8 Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Thu, 13 Mar 2014 10:38:44 -0700 Subject: [PATCH 126/422] Synchronize -> Serialize - migrate all usage to Serialized instead of Synchronized - remove implementations of SerializedObserver that lost the competition (performance and testing in production) --- .../java/rx/observers/SafeSubscriber.java | 2 +- .../java/rx/observers/SerializedObserver.java | 168 +++++++++++++- .../SerializedObserverViaQueueAndCounter.java | 70 ------ .../SerializedObserverViaQueueAndLock.java | 174 -------------- .../SerializedObserverViaStateMachine.java | 214 ------------------ .../java/rx/operators/OperationDebounce.java | 3 +- .../operators/OperationMergeDelayError.java | 12 +- .../OperationMergeMaxConcurrent.java | 31 ++- .../rx/operators/OperatorTimeoutBase.java | 21 +- .../main/java/rx/operators/SafeObserver.java | 4 +- .../rx/observers/SerializedObserverTest.java | 6 +- ...ializedObserverViaQueueAndCounterTest.java | 25 -- ...SerializedObserverViaQueueAndLockTest.java | 11 - ...SerializedObserverViaStateMachineTest.java | 10 - 14 files changed, 192 insertions(+), 559 deletions(-) delete mode 100644 rxjava-core/src/main/java/rx/observers/SerializedObserverViaQueueAndCounter.java delete mode 100644 rxjava-core/src/main/java/rx/observers/SerializedObserverViaQueueAndLock.java delete mode 100644 rxjava-core/src/main/java/rx/observers/SerializedObserverViaStateMachine.java delete mode 100644 rxjava-core/src/test/java/rx/observers/SerializedObserverViaQueueAndCounterTest.java delete mode 100644 rxjava-core/src/test/java/rx/observers/SerializedObserverViaQueueAndLockTest.java delete mode 100644 rxjava-core/src/test/java/rx/observers/SerializedObserverViaStateMachineTest.java diff --git a/rxjava-core/src/main/java/rx/observers/SafeSubscriber.java b/rxjava-core/src/main/java/rx/observers/SafeSubscriber.java index 922b61de95..ff121eeb77 100644 --- a/rxjava-core/src/main/java/rx/observers/SafeSubscriber.java +++ b/rxjava-core/src/main/java/rx/observers/SafeSubscriber.java @@ -52,7 +52,7 @@ *

  • When onError or onComplete occur it will unsubscribe from the Observable (if executing asynchronously).
  • * *

    - * It will not synchronize onNext execution. Use the {@link SynchronizedObserver} to do that. + * It will not synchronize onNext execution. Use the {@link SerializedSubscriber} to do that. * * @param */ diff --git a/rxjava-core/src/main/java/rx/observers/SerializedObserver.java b/rxjava-core/src/main/java/rx/observers/SerializedObserver.java index 12e46fac06..93f1632973 100644 --- a/rxjava-core/src/main/java/rx/observers/SerializedObserver.java +++ b/rxjava-core/src/main/java/rx/observers/SerializedObserver.java @@ -1,5 +1,7 @@ package rx.observers; +import java.util.ArrayList; + import rx.Observer; /** @@ -15,28 +17,170 @@ * @param */ public class SerializedObserver implements Observer { - /* - * Facade to actual implementation until final decision is made - * on the implementation. - */ - private final SerializedObserverViaQueueAndLock actual; - - public SerializedObserver(Observer observer) { - this.actual = new SerializedObserverViaQueueAndLock(observer); + private final Observer actual; + + private boolean emitting = false; + private boolean terminated = false; + private ArrayList queue = new ArrayList(); + + private static Sentinel NULL_SENTINEL = new Sentinel(); + private static Sentinel COMPLETE_SENTINEL = new Sentinel(); + + private static class Sentinel { + + } + + private static class ErrorSentinel extends Sentinel { + final Throwable e; + + ErrorSentinel(Throwable e) { + this.e = e; + } + } + + public SerializedObserver(Observer s) { + this.actual = s; } @Override public void onCompleted() { - actual.onCompleted(); + boolean canEmit = false; + ArrayList list = null; + synchronized (this) { + if (terminated) { + return; + } + terminated = true; + if (!emitting) { + // emit immediately + emitting = true; + canEmit = true; + if (queue.size() > 0) { + list = queue; // copy reference + queue = new ArrayList(); // new version; + } + } else { + // someone else is already emitting so just queue it + queue.add(COMPLETE_SENTINEL); + } + } + if (canEmit) { + // we won the right to emit + try { + drainQueue(list); + actual.onCompleted(); + } finally { + synchronized (this) { + emitting = false; + } + } + } } @Override - public void onError(Throwable e) { - actual.onError(e); + public void onError(final Throwable e) { + boolean canEmit = false; + ArrayList list = null; + synchronized (this) { + if (terminated) { + return; + } + terminated = true; + if (!emitting) { + // emit immediately + emitting = true; + canEmit = true; + if (queue.size() > 0) { + list = queue; // copy reference + queue = new ArrayList(); // new version; + } + } else { + // someone else is already emitting so just queue it ... after eliminating the queue to shortcut + queue.clear(); + queue.add(new ErrorSentinel(e)); + } + } + if (canEmit) { + // we won the right to emit + try { + drainQueue(list); + actual.onError(e); + } finally { + synchronized (this) { + emitting = false; + } + } + } } @Override public void onNext(T t) { - actual.onNext(t); + boolean canEmit = false; + ArrayList list = null; + synchronized (this) { + if (terminated) { + return; + } + if (!emitting) { + // emit immediately + emitting = true; + canEmit = true; + if (queue.size() > 0) { + list = queue; // copy reference + queue = new ArrayList(); // new version; + } + } else { + // someone else is already emitting so just queue it + if (t == null) { + queue.add(NULL_SENTINEL); + } else { + queue.add(t); + } + } + } + if (canEmit) { + // we won the right to emit + try { + drainQueue(list); + actual.onNext(t); + } finally { + synchronized (this) { + if (terminated) { + list = queue; // copy reference + queue = new ArrayList(); // new version; + } else { + // release this thread + emitting = false; + canEmit = false; + } + } + } + } + + // if terminated this will still be true so let's drain the rest of the queue + if (canEmit) { + drainQueue(list); + } + } + + public void drainQueue(ArrayList list) { + if (list == null || list.size() == 0) { + return; + } + for (Object v : list) { + if (v != null) { + if (v instanceof Sentinel) { + if (v == NULL_SENTINEL) { + actual.onNext(null); + } else if (v == COMPLETE_SENTINEL) { + actual.onCompleted(); + } else if (v instanceof ErrorSentinel) { + actual.onError(((ErrorSentinel) v).e); + } + } else { + actual.onNext((T) v); + } + } + } } } diff --git a/rxjava-core/src/main/java/rx/observers/SerializedObserverViaQueueAndCounter.java b/rxjava-core/src/main/java/rx/observers/SerializedObserverViaQueueAndCounter.java deleted file mode 100644 index 2a9ac3c473..0000000000 --- a/rxjava-core/src/main/java/rx/observers/SerializedObserverViaQueueAndCounter.java +++ /dev/null @@ -1,70 +0,0 @@ -package rx.observers; - -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.atomic.AtomicInteger; - -import rx.Observer; - -/* package */class SerializedObserverViaQueueAndCounter implements Observer { - private final Observer actual; - private final AtomicInteger count = new AtomicInteger(); - private final ConcurrentLinkedQueue queue = new ConcurrentLinkedQueue(); - - private static Sentinel NULL_SENTINEL = new Sentinel(); - private static Sentinel COMPLETE_SENTINEL = new Sentinel(); - - private static class Sentinel { - - } - - private static class ErrorSentinel extends Sentinel { - final Throwable e; - - ErrorSentinel(Throwable e) { - this.e = e; - } - } - - public SerializedObserverViaQueueAndCounter(Observer s) { - this.actual = s; - } - - @Override - public void onCompleted() { - queue.add(COMPLETE_SENTINEL); - doIt(); - } - - @Override - public void onError(final Throwable e) { - queue.add(new ErrorSentinel(e)); - doIt(); - } - - @Override - public void onNext(T t) { - queue.add(t); - doIt(); - } - - public void doIt() { - if (count.getAndIncrement() == 0) { - do { - Object v = queue.poll(); - if (v != null) { - if (v instanceof Sentinel) { - if (v == NULL_SENTINEL) { - actual.onNext(null); - } else if (v == COMPLETE_SENTINEL) { - actual.onCompleted(); - } else if (v instanceof ErrorSentinel) { - actual.onError(((ErrorSentinel) v).e); - } - } else { - actual.onNext((T) v); - } - } - } while (count.decrementAndGet() > 0); - } - } -} diff --git a/rxjava-core/src/main/java/rx/observers/SerializedObserverViaQueueAndLock.java b/rxjava-core/src/main/java/rx/observers/SerializedObserverViaQueueAndLock.java deleted file mode 100644 index 48f3c94f4b..0000000000 --- a/rxjava-core/src/main/java/rx/observers/SerializedObserverViaQueueAndLock.java +++ /dev/null @@ -1,174 +0,0 @@ -package rx.observers; - -import java.util.ArrayList; - -import rx.Observer; - -/* package */ class SerializedObserverViaQueueAndLock implements Observer { - private final Observer actual; - - private boolean emitting = false; - private boolean terminated = false; - private ArrayList queue = new ArrayList(); - - private static Sentinel NULL_SENTINEL = new Sentinel(); - private static Sentinel COMPLETE_SENTINEL = new Sentinel(); - - private static class Sentinel { - - } - - private static class ErrorSentinel extends Sentinel { - final Throwable e; - - ErrorSentinel(Throwable e) { - this.e = e; - } - } - - public SerializedObserverViaQueueAndLock(Observer s) { - this.actual = s; - } - - @Override - public void onCompleted() { - boolean canEmit = false; - ArrayList list = null; - synchronized (this) { - if (terminated) { - return; - } - terminated = true; - if (!emitting) { - // emit immediately - emitting = true; - canEmit = true; - if (queue.size() > 0) { - list = queue; // copy reference - queue = new ArrayList(); // new version; - } - } else { - // someone else is already emitting so just queue it - queue.add(COMPLETE_SENTINEL); - } - } - if (canEmit) { - // we won the right to emit - try { - drainQueue(list); - actual.onCompleted(); - } finally { - synchronized (this) { - emitting = false; - } - } - } - } - - @Override - public void onError(final Throwable e) { - boolean canEmit = false; - ArrayList list = null; - synchronized (this) { - if (terminated) { - return; - } - terminated = true; - if (!emitting) { - // emit immediately - emitting = true; - canEmit = true; - if (queue.size() > 0) { - list = queue; // copy reference - queue = new ArrayList(); // new version; - } - } else { - // someone else is already emitting so just queue it ... after eliminating the queue to shortcut - queue.clear(); - queue.add(new ErrorSentinel(e)); - } - } - if (canEmit) { - // we won the right to emit - try { - drainQueue(list); - actual.onError(e); - } finally { - synchronized (this) { - emitting = false; - } - } - } - } - - @Override - public void onNext(T t) { - boolean canEmit = false; - ArrayList list = null; - synchronized (this) { - if (terminated) { - return; - } - if (!emitting) { - // emit immediately - emitting = true; - canEmit = true; - if (queue.size() > 0) { - list = queue; // copy reference - queue = new ArrayList(); // new version; - } - } else { - // someone else is already emitting so just queue it - if (t == null) { - queue.add(NULL_SENTINEL); - } else { - queue.add(t); - } - } - } - if (canEmit) { - // we won the right to emit - try { - drainQueue(list); - actual.onNext(t); - } finally { - synchronized (this) { - if (terminated) { - list = queue; // copy reference - queue = new ArrayList(); // new version; - } else { - // release this thread - emitting = false; - canEmit = false; - } - } - } - } - - // if terminated this will still be true so let's drain the rest of the queue - if (canEmit) { - drainQueue(list); - } - } - - public void drainQueue(ArrayList list) { - if (list == null || list.size() == 0) { - return; - } - for (Object v : list) { - if (v != null) { - if (v instanceof Sentinel) { - if (v == NULL_SENTINEL) { - actual.onNext(null); - } else if (v == COMPLETE_SENTINEL) { - actual.onCompleted(); - } else if (v instanceof ErrorSentinel) { - actual.onError(((ErrorSentinel) v).e); - } - } else { - actual.onNext((T) v); - } - } - } - } -} diff --git a/rxjava-core/src/main/java/rx/observers/SerializedObserverViaStateMachine.java b/rxjava-core/src/main/java/rx/observers/SerializedObserverViaStateMachine.java deleted file mode 100644 index 71756bb98d..0000000000 --- a/rxjava-core/src/main/java/rx/observers/SerializedObserverViaStateMachine.java +++ /dev/null @@ -1,214 +0,0 @@ -package rx.observers; - -import java.util.concurrent.atomic.AtomicReference; - -import rx.Observer; - -/* package */class SerializedObserverViaStateMachine implements Observer { - - private final AtomicReference state = new AtomicReference(State.createNew()); - private final Observer s; - - public SerializedObserverViaStateMachine(Observer s) { - this.s = s; - } - - @Override - public void onCompleted() { - State current = null; - State newState = null; - do { - current = state.get(); - if (current.isTerminated()) { - // already received terminal state - return; - } - newState = current.complete(); - } while (!state.compareAndSet(current, newState)); - terminateIfNecessary(newState); - } - - @Override - public void onError(Throwable e) { - State current = null; - State newState = null; - do { - current = state.get(); - if (current.isTerminated()) { - // already received terminal state - return; - } - newState = current.error(e); - } while (!state.compareAndSet(current, newState)); - terminateIfNecessary(newState); - } - - @SuppressWarnings("unchecked") - @Override - public void onNext(T t) { - State current = null; - State newState = null; - - State orig = null; - do { - current = state.get(); - if (orig == null) { - orig = current; - } - if (current.isTerminated()) { - // already received terminal state - return; - } - newState = current.offerItem(t); - } while (!state.compareAndSet(current, newState)); - - if (newState.shouldProcess()) { - int numItemsProcessed = 0; - try { - if (newState == State.PROCESS_SELF) { - s.onNext(t); - numItemsProcessed++; - } else { - // drain queue - Object[] items = newState.queue; - for (int i = 0; i < items.length; i++) { - s.onNext((T) items[i]); - numItemsProcessed++; - } - } - } finally { - // finish processing to let this thread move on - do { - current = state.get(); - newState = current.finishProcessing(numItemsProcessed); - } while (!state.compareAndSet(current, newState)); - } - } - terminateIfNecessary(newState); - } - - @SuppressWarnings("unchecked") - private void terminateIfNecessary(State current) { - if (current.isTerminated()) { - State newState = null; - do { - current = state.get(); - newState = current.startTermination(); - } while (!state.compareAndSet(current, newState)); - - if (newState.shouldProcess()) { - // drain any items left - for (int i = 0; i < newState.queue.length; i++) { - s.onNext((T) newState.queue[i]); - } - - // now terminate - if (newState.onComplete) { - s.onCompleted(); - } else { - s.onError(newState.onError); - } - } - } - } - - public static class State { - final boolean shouldProcess; - final boolean isSomeoneProcessing; - final int queueSize; - final Object[] queue; - final boolean onComplete; - final Throwable onError; - - private final static Object[] EMPTY = new Object[0]; - private final static Object[] PROCESS_SELF_QUEUE = new Object[1]; - - private final static State NON_TERMINATED_EMPTY = new State(false, false, 0, false, null, EMPTY); - private final static State PROCESS_SELF = new State(true, true, 1, false, null, PROCESS_SELF_QUEUE); - - public State(boolean shouldProcess, boolean isSomeoneProcessing, int queueSize, boolean onComplete, Throwable onError, Object[] queue) { - this.shouldProcess = shouldProcess; - this.isSomeoneProcessing = isSomeoneProcessing; - this.queueSize = queueSize; - this.queue = queue; - this.onComplete = onComplete; - this.onError = onError; - } - - public static State createNew() { - return new State(false, false, 0, false, null, EMPTY); - } - - public boolean shouldProcess() { - return shouldProcess; - } - - public boolean isTerminated() { - return onComplete || onError != null; - } - - public State complete() { - return new State(false, isSomeoneProcessing, queueSize, true, onError, queue); - } - - public State error(Throwable e) { - // immediately empty the queue and emit error as soon as possible - return new State(false, isSomeoneProcessing, 0, onComplete, e, EMPTY); - } - - public State startTermination() { - if (isSomeoneProcessing) { - return new State(false, isSomeoneProcessing, queueSize, onComplete, onError, queue); - } else { - return new State(true, true, queueSize, onComplete, onError, queue); - } - } - - public State offerItem(Object item) { - if (queueSize == 0) { - if (isTerminated()) { - // return count of 0 meaning don't emit as we are terminated - return new State(false, isSomeoneProcessing, 0, onComplete, onError, EMPTY); - } else { - // no concurrent requests so don't queue, we'll process immediately - return PROCESS_SELF; - } - } else { - // there are items queued so we need to queue - int idx = queue.length; - Object[] newQueue = new Object[idx + 1]; - System.arraycopy(queue, 0, newQueue, 0, idx); - newQueue[idx] = item; - - if (isSomeoneProcessing) { - // we just add to queue - return new State(false, true, queueSize + 1, onComplete, onError, newQueue); - } else { - // we add to queue and claim work - return new State(true, true, queueSize + 1, onComplete, onError, newQueue); - } - } - } - - public State finishProcessing(int numOnNextSent) { - int size = queueSize - numOnNextSent; - if (size > 0 || isTerminated()) { - // if size == 0 but we are terminated then it's an empty queue - Object[] newQueue = EMPTY; - if (size > 0) { - newQueue = new Object[queue.length - numOnNextSent]; - System.arraycopy(queue, numOnNextSent, newQueue, 0, newQueue.length); - } - return new State(false, false, size, onComplete, onError, newQueue); - } else { - return NON_TERMINATED_EMPTY; - } - } - - @Override - public String toString() { - return "State => shouldProcess: " + shouldProcess + " processing: " + isSomeoneProcessing + " queueSize: " + queueSize + " queue: " + queue.length + " terminated: " + isTerminated(); - } - - } -} diff --git a/rxjava-core/src/main/java/rx/operators/OperationDebounce.java b/rxjava-core/src/main/java/rx/operators/OperationDebounce.java index 5a7da4a1bb..e970176c8b 100644 --- a/rxjava-core/src/main/java/rx/operators/OperationDebounce.java +++ b/rxjava-core/src/main/java/rx/operators/OperationDebounce.java @@ -26,6 +26,7 @@ import rx.Subscription; import rx.functions.Action1; import rx.functions.Func1; +import rx.observers.SerializedObserver; import rx.observers.SynchronizedObserver; import rx.schedulers.Schedulers; import rx.subscriptions.CompositeSubscription; @@ -111,7 +112,7 @@ private static class DebounceObserver implements Observer { public DebounceObserver(Observer observer, long timeout, TimeUnit unit, Scheduler scheduler) { // we need to synchronize the observer since the on* events can be coming from different // threads and are thus non-deterministic and could be interleaved - this.observer = new SynchronizedObserver(observer); + this.observer = new SerializedObserver(observer); this.timeout = timeout; this.unit = unit; this.scheduler = scheduler; diff --git a/rxjava-core/src/main/java/rx/operators/OperationMergeDelayError.java b/rxjava-core/src/main/java/rx/operators/OperationMergeDelayError.java index b2498df6c2..4c97223a87 100644 --- a/rxjava-core/src/main/java/rx/operators/OperationMergeDelayError.java +++ b/rxjava-core/src/main/java/rx/operators/OperationMergeDelayError.java @@ -25,7 +25,7 @@ import rx.Observer; import rx.Subscription; import rx.exceptions.CompositeException; -import rx.observers.SynchronizedObserver; +import rx.observers.SerializedObserver; import rx.subscriptions.BooleanSubscription; import rx.subscriptions.CompositeSubscription; @@ -141,15 +141,7 @@ private MergeDelayErrorObservable(Observable> public Subscription onSubscribe(Observer actualObserver) { CompositeSubscription completeSubscription = new CompositeSubscription(); - - /** - * We must synchronize a merge because we subscribe to multiple sequences in parallel that will each be emitting. - *

    - * The calls from each sequence must be serialized. - *

    - * Bug report: https://github.com/Netflix/RxJava/issues/614 - */ - SynchronizedObserver synchronizedObserver = new SynchronizedObserver(actualObserver); + SerializedObserver synchronizedObserver = new SerializedObserver(actualObserver); /** * Subscribe to the parent Observable to get to the children Observables diff --git a/rxjava-core/src/main/java/rx/operators/OperationMergeMaxConcurrent.java b/rxjava-core/src/main/java/rx/operators/OperationMergeMaxConcurrent.java index ec27002d4a..d0c296b06d 100644 --- a/rxjava-core/src/main/java/rx/operators/OperationMergeMaxConcurrent.java +++ b/rxjava-core/src/main/java/rx/operators/OperationMergeMaxConcurrent.java @@ -21,7 +21,7 @@ import rx.Observable.OnSubscribeFunc; import rx.Observer; import rx.Subscription; -import rx.observers.SynchronizedObserver; +import rx.observers.SerializedObserver; import rx.subscriptions.CompositeSubscription; /** @@ -85,9 +85,8 @@ public Subscription onSubscribe(Observer actualObserver) { * Bug report: https://github.com/Netflix/RxJava/issues/200 */ SafeObservableSubscription subscription = new SafeObservableSubscription(ourSubscription); - SynchronizedObserver synchronizedObserver = new SynchronizedObserver( - new SafeObserver(subscription, actualObserver), // Create a SafeObserver as SynchronizedObserver does not automatically unsubscribe - subscription); + SerializedObserver synchronizedObserver = new SerializedObserver( + new SafeObserver(subscription, actualObserver)); // Create a SafeObserver as SynchronizedObserver does not automatically unsubscribe /** * Subscribe to the parent Observable to get to the children Observables @@ -103,10 +102,10 @@ public Subscription onSubscribe(Observer actualObserver) { * @param */ private class ParentObserver implements Observer> { - private final SynchronizedObserver synchronizedObserver; + private final SerializedObserver serializedObserver; - public ParentObserver(SynchronizedObserver synchronizedObserver) { - this.synchronizedObserver = synchronizedObserver; + public ParentObserver(SerializedObserver serializedObserver) { + this.serializedObserver = serializedObserver; } @Override @@ -119,13 +118,13 @@ public void onCompleted() { // but will let the child worry about it // if however this completes and there are no children processing, then we will send onCompleted if (isStopped()) { - synchronizedObserver.onCompleted(); + serializedObserver.onCompleted(); } } @Override public void onError(Throwable e) { - synchronizedObserver.onError(e); + serializedObserver.onError(e); } @Override @@ -151,7 +150,7 @@ public void onNext(Observable childObservable) { } if (observable != null) { ourSubscription.add(observable.subscribe(new ChildObserver( - synchronizedObserver))); + serializedObserver))); } } } @@ -162,10 +161,10 @@ public void onNext(Observable childObservable) { */ private class ChildObserver implements Observer { - private final SynchronizedObserver synchronizedObserver; + private final SerializedObserver serializedObserver; - public ChildObserver(SynchronizedObserver synchronizedObserver) { - this.synchronizedObserver = synchronizedObserver; + public ChildObserver(SerializedObserver serializedObserver) { + this.serializedObserver = serializedObserver; } @Override @@ -192,19 +191,19 @@ public void onCompleted() { } else { // No pending observable. Need to check if it's necessary to emit an onCompleted if (isStopped()) { - synchronizedObserver.onCompleted(); + serializedObserver.onCompleted(); } } } @Override public void onError(Throwable e) { - synchronizedObserver.onError(e); + serializedObserver.onError(e); } @Override public void onNext(T args) { - synchronizedObserver.onNext(args); + serializedObserver.onNext(args); } } diff --git a/rxjava-core/src/main/java/rx/operators/OperatorTimeoutBase.java b/rxjava-core/src/main/java/rx/operators/OperatorTimeoutBase.java index 3d6dc97998..7a6ea8e8d8 100644 --- a/rxjava-core/src/main/java/rx/operators/OperatorTimeoutBase.java +++ b/rxjava-core/src/main/java/rx/operators/OperatorTimeoutBase.java @@ -25,7 +25,7 @@ import rx.Subscription; import rx.functions.Func2; import rx.functions.Func3; -import rx.observers.SynchronizedSubscriber; +import rx.observers.SerializedSubscriber; import rx.subscriptions.SerialSubscription; class OperatorTimeoutBase implements Operator { @@ -67,8 +67,7 @@ public Subscriber call(Subscriber subscriber) { // Use SynchronizedSubscriber for safe memory access // as the subscriber will be accessed in the current thread or the // scheduler or other Observables. - final SynchronizedSubscriber synchronizedSubscriber = new SynchronizedSubscriber( - subscriber); + final SerializedSubscriber synchronizedSubscriber = new SerializedSubscriber(subscriber); TimeoutSubscriber timeoutSubscriber = new TimeoutSubscriber( synchronizedSubscriber, timeoutStub, serial, other); @@ -84,17 +83,17 @@ public Subscriber call(Subscriber subscriber) { private final SerialSubscription serial; private final Object gate = new Object(); - private final SynchronizedSubscriber synchronizedSubscriber; + private final SerializedSubscriber serializedSubscriber; private final TimeoutStub timeoutStub; private final Observable other; private TimeoutSubscriber( - SynchronizedSubscriber synchronizedSubscriber, + SerializedSubscriber serializedSubscriber, TimeoutStub timeoutStub, SerialSubscription serial, Observable other) { - this.synchronizedSubscriber = synchronizedSubscriber; + this.serializedSubscriber = serializedSubscriber; this.timeoutStub = timeoutStub; this.serial = serial; this.other = other; @@ -110,7 +109,7 @@ public void onNext(T value) { } } if (onNextWins) { - synchronizedSubscriber.onNext(value); + serializedSubscriber.onNext(value); serial.set(timeoutStub.call(this, actual.get(), value)); } } @@ -125,7 +124,7 @@ public void onError(Throwable error) { } if (onErrorWins) { serial.unsubscribe(); - synchronizedSubscriber.onError(error); + serializedSubscriber.onError(error); } } @@ -139,7 +138,7 @@ public void onCompleted() { } if (onCompletedWins) { serial.unsubscribe(); - synchronizedSubscriber.onCompleted(); + serializedSubscriber.onCompleted(); } } @@ -153,9 +152,9 @@ public void onTimeout(long seqId) { } if (timeoutWins) { if (other == null) { - synchronizedSubscriber.onError(new TimeoutException()); + serializedSubscriber.onError(new TimeoutException()); } else { - serial.set(other.subscribe(synchronizedSubscriber)); + serial.set(other.subscribe(serializedSubscriber)); } } } diff --git a/rxjava-core/src/main/java/rx/operators/SafeObserver.java b/rxjava-core/src/main/java/rx/operators/SafeObserver.java index cf632b4dc5..08984deae3 100644 --- a/rxjava-core/src/main/java/rx/operators/SafeObserver.java +++ b/rxjava-core/src/main/java/rx/operators/SafeObserver.java @@ -22,7 +22,7 @@ import rx.Subscription; import rx.exceptions.CompositeException; import rx.exceptions.OnErrorNotImplementedException; -import rx.observers.SynchronizedObserver; +import rx.observers.SerializedObserver; import rx.plugins.RxJavaPlugins; import rx.subscriptions.Subscriptions; @@ -55,7 +55,7 @@ *

  • When onError or onComplete occur it will unsubscribe from the Observable (if executing asynchronously).
  • * *

    - * It will not synchronize onNext execution. Use the {@link SynchronizedObserver} to do that. + * It will not synchronize onNext execution. Use the {@link SerializedObserver} to do that. * * @param * @deprecated replaced by SafeSubscriber diff --git a/rxjava-core/src/test/java/rx/observers/SerializedObserverTest.java b/rxjava-core/src/test/java/rx/observers/SerializedObserverTest.java index 9b7656be29..e6043edd39 100644 --- a/rxjava-core/src/test/java/rx/observers/SerializedObserverTest.java +++ b/rxjava-core/src/test/java/rx/observers/SerializedObserverTest.java @@ -37,7 +37,7 @@ import rx.Subscriber; import rx.Subscription; -public abstract class SerializedObserverTest { +public class SerializedObserverTest { @Mock Subscriber observer; @@ -47,7 +47,9 @@ public void before() { MockitoAnnotations.initMocks(this); } - protected abstract Observer serializedObserver(Observer o); + private Observer serializedObserver(Observer o) { + return new SerializedObserver(o); + } @Test public void testSingleThreadedBasic() { diff --git a/rxjava-core/src/test/java/rx/observers/SerializedObserverViaQueueAndCounterTest.java b/rxjava-core/src/test/java/rx/observers/SerializedObserverViaQueueAndCounterTest.java deleted file mode 100644 index 2e3f49ec7a..0000000000 --- a/rxjava-core/src/test/java/rx/observers/SerializedObserverViaQueueAndCounterTest.java +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Copyright 2014 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package rx.observers; - -import rx.Observer; - -public class SerializedObserverViaQueueAndCounterTest extends SerializedObserverTest { - @Override - protected Observer serializedObserver(Observer o) { - return new SerializedObserverViaQueueAndCounter(o); - } -} diff --git a/rxjava-core/src/test/java/rx/observers/SerializedObserverViaQueueAndLockTest.java b/rxjava-core/src/test/java/rx/observers/SerializedObserverViaQueueAndLockTest.java deleted file mode 100644 index 45cdfa1fe5..0000000000 --- a/rxjava-core/src/test/java/rx/observers/SerializedObserverViaQueueAndLockTest.java +++ /dev/null @@ -1,11 +0,0 @@ -package rx.observers; - -import rx.Observer; - -public class SerializedObserverViaQueueAndLockTest extends SerializedObserverTest { - @Override - protected Observer serializedObserver(Observer o) { - return new SerializedObserverViaQueueAndLock(o); - } - -} diff --git a/rxjava-core/src/test/java/rx/observers/SerializedObserverViaStateMachineTest.java b/rxjava-core/src/test/java/rx/observers/SerializedObserverViaStateMachineTest.java deleted file mode 100644 index 76582d06e0..0000000000 --- a/rxjava-core/src/test/java/rx/observers/SerializedObserverViaStateMachineTest.java +++ /dev/null @@ -1,10 +0,0 @@ -package rx.observers; - -import rx.Observer; - -public class SerializedObserverViaStateMachineTest extends SerializedObserverTest { - @Override - protected Observer serializedObserver(Observer o) { - return new SerializedObserverViaStateMachine(o); - } -} From c9a983ae3da03b52031d637af5d97fab50b83a84 Mon Sep 17 00:00:00 2001 From: Bob T Builder Date: Thu, 13 Mar 2014 18:45:09 +0000 Subject: [PATCH 127/422] [Gradle Release Plugin] - pre tag commit: '0.17.1'. --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 641a9d1d9b..beefc2ce98 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -version=0.17.1-SNAPSHOT +version=0.17.1 From 27bf21cb27cfc6b2dece44fae5bc807eeeb8acd0 Mon Sep 17 00:00:00 2001 From: Bob T Builder Date: Thu, 13 Mar 2014 18:45:13 +0000 Subject: [PATCH 128/422] [Gradle Release Plugin] - new version commit: '0.17.2-SNAPSHOT'. --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index beefc2ce98..bc0a89d83b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -version=0.17.1 +version=0.17.2-SNAPSHOT From 2052da32e8596431da58eb6d70113c2fadfb5c75 Mon Sep 17 00:00:00 2001 From: Gleb Smirnov Date: Thu, 13 Mar 2014 23:44:35 +0400 Subject: [PATCH 129/422] Updated gradle build scripts so there is a separate benchmarks task --- build.gradle | 80 +++++++++++++++++++++++++--------------- rxjava-core/build.gradle | 10 +---- 2 files changed, 52 insertions(+), 38 deletions(-) diff --git a/build.gradle b/build.gradle index 304a26643c..366a6f94dc 100644 --- a/build.gradle +++ b/build.gradle @@ -9,8 +9,20 @@ apply from: file('gradle/release.gradle') buildscript { repositories { mavenLocal() - mavenCentral() // maven { url 'http://jcenter.bintray.com' } + mavenCentral() + maven { + //FIXME: waiting for https://github.com/johnrengelman/shadow/pull/38 to merge + name 'Shadow' + url 'http://dl.bintray.com/content/gvsmirnov/gradle-plugins' + } + jcenter() + } + + dependencies { + // Required for benchmarks + classpath 'com.github.jengelman.gradle.plugins:shadow:0.8.1' } + apply from: file('gradle/buildscript.gradle'), to: buildscript } @@ -19,7 +31,7 @@ allprojects { apply plugin: 'idea' repositories { mavenLocal() - mavenCentral() // maven { url: 'http://jcenter.bintray.com' } + mavenCentral() } } @@ -31,8 +43,6 @@ subprojects { configurations { examplesCompile.extendsFrom compile examplesRuntime.extendsFrom runtime - perfCompile.extendsFrom compile - perfRuntime.extendsFrom runtime } @@ -40,48 +50,60 @@ subprojects { it.classpath = sourceSets.main.compileClasspath } - sourceSets { - //include /src/examples folder + sourceSets { examples - //include /src/perf folder - perf { - java { - srcDir 'src/perf/java' - compileClasspath += main.output - runtimeClasspath += main.output - } - } - } - - dependencies { - perfCompile 'org.openjdk.jmh:jmh-core:0.2' + perf } tasks.build { - //include 'examples' in build task + //include 'examples' in build task dependsOn(examplesClasses) - //include 'perf' in build task - // dependsOn(perfClasses) //-> Not working so commented out + } + + dependencies { + perfCompile 'org.openjdk.jmh:jmh-core:0.5.3' + perfCompile 'org.openjdk.jmh:jmh-generator-annprocess:0.5.3' + + perfCompile project } eclipse { - classpath { - // include 'provided' dependencies on the classpath - plusConfigurations += configurations.provided + classpath { plusConfigurations += configurations.perfCompile downloadSources = true downloadJavadoc = true } } - + idea { module { - // include 'provided' dependencies on the classpath - scopes.PROVIDED.plus += configurations.provided - // TODO not sure what to add it to - //scopes.PROVIDED.plus += configurations.perfCompile + scopes.PROVIDED.plus += configurations.perfCompile + scopes.PROVIDED.minus += configurations.compile + } + } + + task perfJar(type: Jar, dependsOn: perfClasses) { + from sourceSets.perf.output + sourceSets.main.output + } + + task benchmarks(dependsOn: perfJar) { + + apply plugin: "shadow" + + shadow { + classifier = "benchmarks" + includeDependenciesFor = ["runtime", "perfRuntime"] + + transformer(com.github.jengelman.gradle.plugins.shadow.transformers.ManifestResourceTransformer) { + mainClass = "org.openjdk.jmh.Main" + } } + + doLast { + shadowJar.execute() + } + } } diff --git a/rxjava-core/build.gradle b/rxjava-core/build.gradle index 00420d0f74..d03c5653a4 100644 --- a/rxjava-core/build.gradle +++ b/rxjava-core/build.gradle @@ -1,6 +1,5 @@ apply plugin: 'maven' apply plugin: 'osgi' -apply plugin:'application' sourceCompatibility = JavaVersion.VERSION_1_6 targetCompatibility = JavaVersion.VERSION_1_6 @@ -13,7 +12,7 @@ dependencies { javadoc { // we do not want the org.rx.operations package include exclude '**/operations/**' - + options { doclet = "org.benjchristensen.doclet.DocletExclude" docletpath = [rootProject.file('./gradle/doclet-exclude.jar')] @@ -31,11 +30,4 @@ jar { instruction 'Import-Package', '!org.junit,!junit.framework,!org.mockito.*,*' instruction 'Eclipse-ExtensibleAPI', 'true' } -} - -task time(type:JavaExec) { - classpath = sourceSets.perf.runtimeClasspath - group 'Application' - description 'Execute the calipser benchmark timing of Rx' - main 'rx.operators.ObservableBenchmark' } \ No newline at end of file From 2fa35e5569d0919c98178dbf4d26f1fb07b8d7d7 Mon Sep 17 00:00:00 2001 From: Gleb Smirnov Date: Fri, 14 Mar 2014 00:26:05 +0400 Subject: [PATCH 130/422] Updated ObservableBenchmark to follow better benchmarking practices --- .../rx/operators/ObservableBenchmark.java | 149 ++++++++---------- 1 file changed, 62 insertions(+), 87 deletions(-) diff --git a/rxjava-core/src/perf/java/rx/operators/ObservableBenchmark.java b/rxjava-core/src/perf/java/rx/operators/ObservableBenchmark.java index 9602cb797d..5de2a022d7 100644 --- a/rxjava-core/src/perf/java/rx/operators/ObservableBenchmark.java +++ b/rxjava-core/src/perf/java/rx/operators/ObservableBenchmark.java @@ -1,14 +1,12 @@ package rx.operators; +import java.util.ArrayList; +import java.util.Collection; import java.util.concurrent.CountDownLatch; -import java.util.concurrent.atomic.AtomicInteger; -import org.openjdk.jmh.annotations.GenerateMicroBenchmark; -import org.openjdk.jmh.runner.Runner; -import org.openjdk.jmh.runner.RunnerException; -import org.openjdk.jmh.runner.options.Options; -import org.openjdk.jmh.runner.options.OptionsBuilder; +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.logic.BlackHole; import rx.Observable; import rx.Observable.OnSubscribe; import rx.Observable.Operator; @@ -19,107 +17,84 @@ public class ObservableBenchmark { @GenerateMicroBenchmark - public void timeBaseline() { - observableOfInts.subscribe(newObserver()); - awaitAllObservers(); - } - - @GenerateMicroBenchmark - public int timeMapIterate() { - int x = 0; - for (int j = 0; j < intValues.length; j++) { - // use hash code to make sure the JIT doesn't optimize too much and remove all of - // our code. - x |= ident.call(intValues[j]).hashCode(); + public void measureBaseline(BlackHole bh, Input input) { + for (Integer value : input.values) { + bh.consume(IDENTITY_FUNCTION.call(value)); } - return x; } @GenerateMicroBenchmark - public void timeMap() { - timeOperator(new OperatorMap(ident)); + public void measureMap(Input input) throws InterruptedException { + input.observable.lift(MAP_OPERATOR).subscribe(input.observer); + + input.awaitCompletion(); } - /************************************************************************** - * Below is internal stuff to avoid object allocation and time overhead of anything that isn't - * being tested. - * - * @throws RunnerException - **************************************************************************/ + private static final Func1 IDENTITY_FUNCTION = new Func1() { + @Override + public Integer call(Integer value) { + return value; + } + }; - public static void main(String[] args) throws RunnerException { - Options opt = new OptionsBuilder() - .include(ObservableBenchmark.class.getName()+".*") - .forks(1) - .build(); + private static final Operator MAP_OPERATOR = new OperatorMap(IDENTITY_FUNCTION); - new Runner(opt).run(); - } + @State(Scope.Thread) + public static class Input { - private void timeOperator(Operator op) { - observableOfInts.lift(op).subscribe(newObserver()); - awaitAllObservers(); - } + @Param({"1", "1024", "1048576"}) + public int size; - private final static AtomicInteger outstanding = new AtomicInteger(0); - private final static CountDownLatch latch = new CountDownLatch(1); + public Collection values; + public Observable observable; + public Observer observer; - private static Observer newObserver() { - outstanding.incrementAndGet(); - return new Observer() { - @Override - public void onCompleted() { - int left = outstanding.decrementAndGet(); - if (left == 0) { - latch.countDown(); - } + private CountDownLatch latch; + + @Setup + public void setup() { + values = new ArrayList(); + for(int i = 0; i < size; i ++) { + values.add(i); } - @Override - public void onError(Throwable e) { - int left = outstanding.decrementAndGet(); - if (left == 0) { + observable = Observable.create(new OnSubscribe() { + @Override + public void call(Subscriber o) { + for (Integer value : values) { + if (o.isUnsubscribed()) + return; + o.onNext(value); + } + o.onCompleted(); + } + }); + + final BlackHole bh = new BlackHole(); + latch = new CountDownLatch(1); + + observer = new Observer() { + @Override + public void onCompleted() { latch.countDown(); } - } - @Override - public void onNext(T t) { - // do nothing - } - }; - } + @Override + public void onError(Throwable e) { + throw new RuntimeException(e); + } + + @Override + public void onNext(Integer value) { + bh.consume(value); + } + }; - private static void awaitAllObservers() { - try { - latch.await(); - } catch (InterruptedException e) { - return; } - } - private static final Integer[] intValues = new Integer[1000]; - static { - for (int i = 0; i < intValues.length; i++) { - intValues[i] = i; + public void awaitCompletion() throws InterruptedException { + latch.await(); } } - private static final Observable observableOfInts = Observable.create(new OnSubscribe() { - @Override - public void call(Subscriber o) { - for (int i = 0; i < intValues.length; i++) { - if (o.isUnsubscribed()) - return; - o.onNext(intValues[i]); - } - o.onCompleted(); - } - }); - private static final Func1 ident = new Func1() { - @Override - public Object call(Integer t) { - return t; - } - }; } From 2c82d48d5f12ca890e33a41073dd6b3191ca84dd Mon Sep 17 00:00:00 2001 From: akarnokd Date: Thu, 13 Mar 2014 23:05:12 +0100 Subject: [PATCH 131/422] SubjectSubscriptionManager fix. --- .../subjects/SubjectSubscriptionManager.java | 27 ++++++++--- .../java/rx/subjects/BehaviorSubjectTest.java | 42 +++++++++++++++++ .../java/rx/subjects/PublishSubjectTest.java | 42 ++++++++++++++++- .../java/rx/subjects/ReplaySubjectTest.java | 45 ++++++++++++++++++- 4 files changed, 147 insertions(+), 9 deletions(-) diff --git a/rxjava-core/src/main/java/rx/subjects/SubjectSubscriptionManager.java b/rxjava-core/src/main/java/rx/subjects/SubjectSubscriptionManager.java index e6626d434d..6f049eb38a 100644 --- a/rxjava-core/src/main/java/rx/subjects/SubjectSubscriptionManager.java +++ b/rxjava-core/src/main/java/rx/subjects/SubjectSubscriptionManager.java @@ -92,7 +92,10 @@ public void call() { } } })); - + if (subscription.isUnsubscribed()) { + addedObserver = false; + break; + } // on subscribe add it to the map of outbound observers to notify newState = current.addObserver(subscription, observer); } @@ -202,15 +205,21 @@ public State removeObserver(Subscription s) { // we are empty, nothing to remove if (this.observers.length == 0) { return this; + } else + if (this.observers.length == 1) { + if (this.subscriptions[0].equals(s)) { + return createNewWith(EMPTY_S, EMPTY_O); + } + return this; } - int n = Math.max(this.observers.length - 1, 1); + int n = this.observers.length - 1; int copied = 0; - Subscription[] newsubscriptions = Arrays.copyOf(this.subscriptions, n); - SubjectObserver[] newobservers = Arrays.copyOf(this.observers, n); + Subscription[] newsubscriptions = new Subscription[n]; + SubjectObserver[] newobservers = new SubjectObserver[n]; for (int i = 0; i < this.subscriptions.length; i++) { Subscription s0 = this.subscriptions[i]; - if (s0 != s) { + if (!s0.equals(s)) { if (copied == n) { // if s was not found till the end of the iteration // we return ourselves since no modification should @@ -229,7 +238,13 @@ public State removeObserver(Subscription s) { // if somehow copied less than expected, truncate the arrays // if s is unique, this should never happen if (copied < n) { - return createNewWith(Arrays.copyOf(newsubscriptions, copied), Arrays.copyOf(newobservers, copied)); + Subscription[] newsubscriptions2 = new Subscription[copied]; + System.arraycopy(newsubscriptions, 0, newsubscriptions2, 0, copied); + + SubjectObserver[] newobservers2 = new SubjectObserver[copied]; + System.arraycopy(newobservers, 0, newobservers2, 0, copied); + + return createNewWith(newsubscriptions2, newobservers2); } return createNewWith(newsubscriptions, newobservers); } diff --git a/rxjava-core/src/test/java/rx/subjects/BehaviorSubjectTest.java b/rxjava-core/src/test/java/rx/subjects/BehaviorSubjectTest.java index 646e1acc26..c3e6974f71 100644 --- a/rxjava-core/src/test/java/rx/subjects/BehaviorSubjectTest.java +++ b/rxjava-core/src/test/java/rx/subjects/BehaviorSubjectTest.java @@ -21,9 +21,11 @@ import org.junit.Test; import org.mockito.InOrder; import org.mockito.Mockito; +import rx.Observable; import rx.Observer; import rx.Subscription; +import rx.util.functions.Func1; public class BehaviorSubjectTest { @@ -237,4 +239,44 @@ public void testCompletedAfterErrorIsNotSent3() { verify(o2, never()).onNext(any()); verify(observer, never()).onError(any(Throwable.class)); } + @Test(timeout = 1000) + public void testUnsubscriptionCase() { + BehaviorSubject src = BehaviorSubject.create((String)null); + + for (int i = 0; i < 10; i++) { + @SuppressWarnings("unchecked") + final Observer o = mock(Observer.class); + InOrder inOrder = inOrder(o); + String v = "" + i; + src.onNext(v); + System.out.printf("Turn: %d%n", i); + src.first() + .flatMap(new Func1>() { + + @Override + public Observable call(String t1) { + return Observable.from(t1 + ", " + t1); + } + }) + .subscribe(new Observer() { + @Override + public void onNext(String t) { + o.onNext(t); + } + + @Override + public void onError(Throwable e) { + o.onError(e); + } + + @Override + public void onCompleted() { + o.onCompleted(); + } + }); + inOrder.verify(o).onNext(v + ", " + v); + inOrder.verify(o).onCompleted(); + verify(o, never()).onError(any(Throwable.class)); + } + } } diff --git a/rxjava-core/src/test/java/rx/subjects/PublishSubjectTest.java b/rxjava-core/src/test/java/rx/subjects/PublishSubjectTest.java index 9c71d4aa5c..a5bd5d7579 100644 --- a/rxjava-core/src/test/java/rx/subjects/PublishSubjectTest.java +++ b/rxjava-core/src/test/java/rx/subjects/PublishSubjectTest.java @@ -16,7 +16,6 @@ package rx.subjects; import static org.junit.Assert.*; -import static org.mockito.Matchers.*; import static org.mockito.Mockito.*; import java.util.ArrayList; @@ -299,4 +298,45 @@ public void testReSubscribe() { private final Throwable testException = new Throwable(); + @Test(timeout = 1000) + public void testUnsubscriptionCase() { + PublishSubject src = PublishSubject.create(); + + for (int i = 0; i < 10; i++) { + @SuppressWarnings("unchecked") + final Observer o = mock(Observer.class); + InOrder inOrder = inOrder(o); + String v = "" + i; + System.out.printf("Turn: %d%n", i); + src.first() + .flatMap(new rx.util.functions.Func1>() { + + @Override + public Observable call(String t1) { + return Observable.from(t1 + ", " + t1); + } + }) + .subscribe(new Observer() { + @Override + public void onNext(String t) { + o.onNext(t); + } + + @Override + public void onError(Throwable e) { + o.onError(e); + } + + @Override + public void onCompleted() { + o.onCompleted(); + } + }); + src.onNext(v); + + inOrder.verify(o).onNext(v + ", " + v); + inOrder.verify(o).onCompleted(); + verify(o, never()).onError(any(Throwable.class)); + } + } } diff --git a/rxjava-core/src/test/java/rx/subjects/ReplaySubjectTest.java b/rxjava-core/src/test/java/rx/subjects/ReplaySubjectTest.java index 5a963eba02..9cb0e81cfd 100644 --- a/rxjava-core/src/test/java/rx/subjects/ReplaySubjectTest.java +++ b/rxjava-core/src/test/java/rx/subjects/ReplaySubjectTest.java @@ -16,7 +16,6 @@ package rx.subjects; import static org.junit.Assert.*; -import static org.mockito.Matchers.*; import static org.mockito.Mockito.*; import java.util.concurrent.CountDownLatch; @@ -25,10 +24,12 @@ import org.junit.Test; import org.mockito.InOrder; import org.mockito.Mockito; +import rx.Observable; import rx.Observer; import rx.Subscriber; import rx.Subscription; +import rx.functions.Func1; import rx.schedulers.Schedulers; public class ReplaySubjectTest { @@ -356,4 +357,44 @@ public void testSubscriptionLeak() { assertEquals(0, replaySubject.subscriberCount()); } -} + @Test(timeout = 1000) + public void testUnsubscriptionCase() { + ReplaySubject src = ReplaySubject.create(); + + for (int i = 0; i < 10; i++) { + @SuppressWarnings("unchecked") + final Observer o = mock(Observer.class); + InOrder inOrder = inOrder(o); + String v = "" + i; + src.onNext(v); + System.out.printf("Turn: %d%n", i); + src.first() + .flatMap(new Func1>() { + + @Override + public Observable call(String t1) { + return Observable.from(t1 + ", " + t1); + } + }) + .subscribe(new Observer() { + @Override + public void onNext(String t) { + System.out.println(t); + o.onNext(t); + } + + @Override + public void onError(Throwable e) { + o.onError(e); + } + + @Override + public void onCompleted() { + o.onCompleted(); + } + }); + inOrder.verify(o).onNext("0, 0"); + inOrder.verify(o).onCompleted(); + verify(o, never()).onError(any(Throwable.class)); + } + }} From d3ead3a7748fa8245b39725ba9534cbaa4d3ee36 Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Thu, 13 Mar 2014 15:52:13 -0700 Subject: [PATCH 132/422] Version 0.17.1 --- CHANGES.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 1c7481a9bb..881656798b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,20 @@ # RxJava Releases # +### Version 0.17.1 ([Maven Central](http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22com.netflix.rxjava%22%20AND%20v%3A%220.17.1%22)) ### + +* [Pull 953](https://github.com/Netflix/RxJava/pull/953) Make ObserveOnTest.testNonBlockingOuterWhileBlockingOnNext deterministic +* [Pull 930](https://github.com/Netflix/RxJava/pull/930) Initial commit of the Android samples module +* [Pull 938](https://github.com/Netflix/RxJava/pull/938) OperatorWeakBinding (deprecates OperatorObserveFromAndroidComponent) +* [Pull 952](https://github.com/Netflix/RxJava/pull/952) rxjava-scala improvements and reimplemented the `amb` operator +* [Pull 955](https://github.com/Netflix/RxJava/pull/955) Fixed ReplaySubject leak +* [Pull 956](https://github.com/Netflix/RxJava/pull/956) Fixed byLine test to use line.separator system property instead of \n. +* [Pull 958](https://github.com/Netflix/RxJava/pull/958) OperatorSkipWhile +* [Pull 959](https://github.com/Netflix/RxJava/pull/959) OperationToFuture must throw CancellationException on get() if cancelled +* [Pull 928](https://github.com/Netflix/RxJava/pull/928) Fix deadlock in SubscribeOnBounded +* [Pull 960](https://github.com/Netflix/RxJava/pull/960) Unit test for "Cannot subscribe to a Retry observable once all subscribers unsubscribed" +* [Pull 962](https://github.com/Netflix/RxJava/pull/962) Migrate from SynchronizedObserver to SerializedObserver + + ### Version 0.17.0 ([Maven Central](http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22com.netflix.rxjava%22%20AND%20v%3A%220.17.0%22)) ### From 7ce4c84f97c9cff52361fe5362cb5473a5226a1b Mon Sep 17 00:00:00 2001 From: zsxwing Date: Sun, 16 Mar 2014 21:13:19 +0800 Subject: [PATCH 133/422] Reimplement the 'single' operator --- rxjava-core/src/main/java/rx/Observable.java | 6 +- .../java/rx/operators/OperationSingle.java | 96 ------------------- .../java/rx/operators/OperatorSingle.java | 90 +++++++++++++++++ ...ingleTest.java => OperatorSingleTest.java} | 2 +- 4 files changed, 94 insertions(+), 100 deletions(-) delete mode 100644 rxjava-core/src/main/java/rx/operators/OperationSingle.java create mode 100644 rxjava-core/src/main/java/rx/operators/OperatorSingle.java rename rxjava-core/src/test/java/rx/operators/{OperationSingleTest.java => OperatorSingleTest.java} (99%) diff --git a/rxjava-core/src/main/java/rx/Observable.java b/rxjava-core/src/main/java/rx/Observable.java index 235e1e62e4..5df7b3bba9 100644 --- a/rxjava-core/src/main/java/rx/Observable.java +++ b/rxjava-core/src/main/java/rx/Observable.java @@ -86,7 +86,7 @@ import rx.operators.OperationReplay; import rx.operators.OperationSample; import rx.operators.OperationSequenceEqual; -import rx.operators.OperationSingle; +import rx.operators.OperatorSingle; import rx.operators.OperationSkip; import rx.operators.OperationSkipLast; import rx.operators.OperationSkipUntil; @@ -6216,7 +6216,7 @@ public final Observable serialize() { * @see "MSDN: Observable.singleAsync()" */ public final Observable single() { - return create(OperationSingle. single(this)); + return lift(new OperatorSingle()); } /** @@ -6257,7 +6257,7 @@ public final Observable single(Func1 predicate) { * @see "MSDN: Observable.singleOrDefaultAsync()" */ public final Observable singleOrDefault(T defaultValue) { - return create(OperationSingle. singleOrDefault(this, defaultValue)); + return lift(new OperatorSingle(defaultValue)); } /** diff --git a/rxjava-core/src/main/java/rx/operators/OperationSingle.java b/rxjava-core/src/main/java/rx/operators/OperationSingle.java deleted file mode 100644 index 871d565092..0000000000 --- a/rxjava-core/src/main/java/rx/operators/OperationSingle.java +++ /dev/null @@ -1,96 +0,0 @@ -/** - * Copyright 2014 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package rx.operators; - -import rx.Observable; -import rx.Observable.OnSubscribeFunc; -import rx.Observer; -import rx.Subscription; - -/** - * If the Observable completes after emitting a single item that matches a - * predicate, return an Observable containing that item. If it emits more than - * one such item or no item, throw an IllegalArgumentException. - */ -public class OperationSingle { - - public static OnSubscribeFunc single( - final Observable source) { - return single(source, false, null); - } - - public static OnSubscribeFunc singleOrDefault( - final Observable source, final T defaultValue) { - return single(source, true, defaultValue); - } - - private static OnSubscribeFunc single( - final Observable source, - final boolean hasDefaultValue, final T defaultValue) { - return new OnSubscribeFunc() { - - @Override - public Subscription onSubscribe(final Observer observer) { - final SafeObservableSubscription subscription = new SafeObservableSubscription(); - subscription.wrap(source.subscribe(new Observer() { - - private T value; - private boolean isEmpty = true; - private boolean hasTooManyElemenets; - - @Override - public void onCompleted() { - if (hasTooManyElemenets) { - // We has already sent an onError message - } else { - if (isEmpty) { - if (hasDefaultValue) { - observer.onNext(defaultValue); - observer.onCompleted(); - } else { - observer.onError(new IllegalArgumentException( - "Sequence contains no elements")); - } - } else { - observer.onNext(value); - observer.onCompleted(); - } - } - } - - @Override - public void onError(Throwable e) { - observer.onError(e); - } - - @Override - public void onNext(T value) { - if (isEmpty) { - this.value = value; - isEmpty = false; - } else { - hasTooManyElemenets = true; - observer.onError(new IllegalArgumentException( - "Sequence contains too many elements")); - subscription.unsubscribe(); - } - } - })); - return subscription; - } - }; - } -} diff --git a/rxjava-core/src/main/java/rx/operators/OperatorSingle.java b/rxjava-core/src/main/java/rx/operators/OperatorSingle.java new file mode 100644 index 0000000000..6a63b025ce --- /dev/null +++ b/rxjava-core/src/main/java/rx/operators/OperatorSingle.java @@ -0,0 +1,90 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package rx.operators; + +import rx.Observable.Operator; +import rx.Subscriber; + +/** + * If the Observable completes after emitting a single item that matches a + * predicate, return an Observable containing that item. If it emits more than + * one such item or no item, throw an IllegalArgumentException. + */ +public final class OperatorSingle implements Operator { + + private final boolean hasDefaultValue; + private final T defaultValue; + + public OperatorSingle() { + this(false, null); + } + + public OperatorSingle(T defaultValue) { + this(true, defaultValue); + } + + private OperatorSingle(boolean hasDefaultValue, final T defaultValue) { + this.hasDefaultValue = hasDefaultValue; + this.defaultValue = defaultValue; + } + + @Override + public Subscriber call(final Subscriber subscriber) { + return new Subscriber(subscriber) { + + private T value; + private boolean isNonEmpty = false; + private boolean hasTooManyElemenets = false; + + @Override + public void onNext(T value) { + if (isNonEmpty) { + hasTooManyElemenets = true; + subscriber.onError(new IllegalArgumentException("Sequence contains too many elements")); + } else { + this.value = value; + isNonEmpty = true; + } + } + + @Override + public void onCompleted() { + if (hasTooManyElemenets) { + // We has already sent an onError message + } else { + if (isNonEmpty) { + subscriber.onNext(value); + subscriber.onCompleted(); + } else { + if (hasDefaultValue) { + subscriber.onNext(defaultValue); + subscriber.onCompleted(); + } else { + subscriber.onError(new IllegalArgumentException("Sequence contains no elements")); + } + } + } + } + + @Override + public void onError(Throwable e) { + subscriber.onError(e); + } + + }; + } + +} diff --git a/rxjava-core/src/test/java/rx/operators/OperationSingleTest.java b/rxjava-core/src/test/java/rx/operators/OperatorSingleTest.java similarity index 99% rename from rxjava-core/src/test/java/rx/operators/OperationSingleTest.java rename to rxjava-core/src/test/java/rx/operators/OperatorSingleTest.java index dee0f1eb88..baf83929ec 100644 --- a/rxjava-core/src/test/java/rx/operators/OperationSingleTest.java +++ b/rxjava-core/src/test/java/rx/operators/OperatorSingleTest.java @@ -25,7 +25,7 @@ import rx.Observer; import rx.functions.Func1; -public class OperationSingleTest { +public class OperatorSingleTest { @Test public void testSingle() { From 531449e4050d1046034992dad2043374ae2e97a4 Mon Sep 17 00:00:00 2001 From: zsxwing Date: Mon, 17 Mar 2014 15:45:40 +0800 Subject: [PATCH 134/422] Add first, last, single to rxjava-scala --- .../rx/lang/scala/examples/RxScalaDemo.scala | 12 ++++ .../main/scala/rx/lang/scala/Observable.scala | 70 +++++++++++++------ .../observables/BlockingObservable.scala | 43 ++++++++++++ 3 files changed, 104 insertions(+), 21 deletions(-) diff --git a/language-adaptors/rxjava-scala/src/examples/scala/rx/lang/scala/examples/RxScalaDemo.scala b/language-adaptors/rxjava-scala/src/examples/scala/rx/lang/scala/examples/RxScalaDemo.scala index e09e6a7ec9..4ef20263a3 100644 --- a/language-adaptors/rxjava-scala/src/examples/scala/rx/lang/scala/examples/RxScalaDemo.scala +++ b/language-adaptors/rxjava-scala/src/examples/scala/rx/lang/scala/examples/RxScalaDemo.scala @@ -388,6 +388,18 @@ class RxScalaDemo extends JUnitSuite { assertEquals(10, List(-1, 0, 1).toObservable.filter(condition).firstOrElse(10).toBlockingObservable.single) } + @Test def firstLastSingleExample() { + assertEquals(1, List(1, 2, 3, 4).toObservable.head.toBlockingObservable.single) + assertEquals(1, List(1, 2, 3, 4).toObservable.first.toBlockingObservable.single) + assertEquals(4, List(1, 2, 3, 4).toObservable.last.toBlockingObservable.single) + assertEquals(1, List(1).toObservable.single.toBlockingObservable.single) + + assertEquals(1, List(1, 2, 3, 4).toObservable.toBlockingObservable.head) + assertEquals(1, List(1, 2, 3, 4).toObservable.toBlockingObservable.first) + assertEquals(4, List(1, 2, 3, 4).toObservable.toBlockingObservable.last) + assertEquals(1, List(1).toObservable.toBlockingObservable.single) + } + def square(x: Int): Int = { println(s"$x*$x is being calculated on thread ${Thread.currentThread().getId}") Thread.sleep(100) // calculating a square is heavy work :) diff --git a/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/Observable.scala b/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/Observable.scala index 8dafa30e2b..73abed3d8a 100644 --- a/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/Observable.scala +++ b/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/Observable.scala @@ -1949,36 +1949,64 @@ trait Observable[+T] def headOrElse[U >: T](default: => U): Observable[U] = firstOrElse(default) /** - * Returns an Observable that emits only the very first item emitted by the source Observable. - * This is just a shorthand for `take(1)`. - * + * Returns an Observable that emits only the very first item emitted by the source Observable, or raises an + * `IllegalArgumentException` if the source Observable is empty. + *

    * - * - * @return an Observable that emits only the very first item from the source, or none if the - * source Observable completes without emitting a single item. + * + * @return an Observable that emits only the very first item emitted by the source Observable, or raises an + * `IllegalArgumentException` if the source Observable is empty + * @see RxJava Wiki: first() + * @see "MSDN: Observable.firstAsync()" */ - def first: Observable[T] = take(1) + def first: Observable[T] = { + toScalaObservable[T](asJavaObservable.first) + } - /* - - TODO once https://github.com/Netflix/RxJava/issues/417 is fixed, we can add head and tail methods + /** + * Returns an Observable that emits only the very first item emitted by the source Observable, or raises an + * `IllegalArgumentException` if the source Observable is empty. + *

    + * + * + * @return an Observable that emits only the very first item emitted by the source Observable, or raises an + * `IllegalArgumentException` if the source Observable is empty + * @see RxJava Wiki: first() + * @see "MSDN: Observable.firstAsync()" + * @see [[Observable.first]] + */ + def head: Observable[T] = first /** - * emits NoSuchElementException("head of empty Observable") if empty + * Returns an Observable that emits the last item emitted by the source Observable or notifies observers of + * an `IllegalArgumentException` if the source Observable is empty. + * + * + * + * @return an Observable that emits the last item from the source Observable or notifies observers of an + * error + * @see RxJava Wiki: last() + * @see "MSDN: Observable.lastAsync()" */ - def head: Observable[T] = { - this.take(1).fold[Option[T]](None)((v: Option[T], e: T) => Some(e)).map({ - case Some(element) => element - case None => throw new NoSuchElementException("head of empty Observable") - }) + def last: Observable[T] = { + toScalaObservable[T](asJavaObservable.last) } - + /** - * emits an UnsupportedOperationException("tail of empty list") if empty + * If the source Observable completes after emitting a single item, return an Observable that emits that + * item. If the source Observable emits more than one item or no items, throw an `IllegalArgumentException`. + * + * + * + * @return an Observable that emits the single item emitted by the source Observable + * @throws IllegalArgumentException + * if the source emits more than one item or no items + * @see RxJava Wiki: single() + * @see "MSDN: Observable.singleAsync()" */ - def tail: Observable[T] = ??? - - */ + def single: Observable[T] = { + toScalaObservable[T](asJavaObservable.single) + } /** * Returns an Observable that forwards all sequentially distinct items emitted from the source Observable. diff --git a/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/observables/BlockingObservable.scala b/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/observables/BlockingObservable.scala index c1ce913095..fb2c2764f8 100644 --- a/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/observables/BlockingObservable.scala +++ b/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/observables/BlockingObservable.scala @@ -53,6 +53,49 @@ class BlockingObservable[+T] private[scala] (val asJava: rx.observables.Blocking new WithFilter[T](p, asJava) } + /** + * Returns the last item emitted by a specified [[Observable]], or + * throws `IllegalArgumentException` if it emits no items. + * + * + * + * @return the last item emitted by the source [[Observable]] + * @throws IllegalArgumentException + * if source contains no elements + * @see RxJava Wiki: last() + * @see MSDN: Observable.Last + */ + def last : T = { + asJava.last : T + } + + /** + * Returns the first item emitted by a specified [[Observable]], or + * `IllegalArgumentException` if source contains no elements. + * + * @return the first item emitted by the source [[Observable]] + * @throws IllegalArgumentException + * if source contains no elements + * @see RxJava Wiki: first() + * @see MSDN: Observable.First + */ + def first : T = { + asJava.first : T + } + + /** + * Returns the first item emitted by a specified [[Observable]], or + * `IllegalArgumentException` if source contains no elements. + * + * @return the first item emitted by the source [[Observable]] + * @throws IllegalArgumentException + * if source contains no elements + * @see RxJava Wiki: first() + * @see MSDN: Observable.First + * @see [[BlockingObservable.first]] + */ + def head : T = first + // last -> use toIterable.last // lastOrDefault -> use toIterable.lastOption // first -> use toIterable.head From c1ed51d10be2a2a134d4eb1d8401010b99b9bf1b Mon Sep 17 00:00:00 2001 From: zsxwing Date: Mon, 17 Mar 2014 16:55:33 +0800 Subject: [PATCH 135/422] Throw NoSuchElementException in the first, last and single operators when the Observable is empty --- .../main/scala/rx/lang/scala/Observable.scala | 14 +++++----- .../observables/BlockingObservable.scala | 12 ++++----- rxjava-core/src/main/java/rx/Observable.java | 27 ++++++++++--------- .../rx/observables/BlockingObservable.java | 20 +++++++------- .../java/rx/operators/OperatorSingle.java | 4 ++- .../src/test/java/rx/ObservableTests.java | 9 ++++--- rxjava-core/src/test/java/rx/ZipTests.java | 5 ++-- .../observables/BlockingObservableTest.java | 8 +++--- .../OperationFirstOrDefaultTest.java | 6 +++-- .../java/rx/operators/OperationLastTest.java | 16 ++++++----- .../rx/operators/OperationMinMaxTest.java | 9 ++++--- .../java/rx/operators/OperatorMapTest.java | 5 ++-- .../java/rx/operators/OperatorSingleTest.java | 6 +++-- .../java/rx/operators/OperatorZipTest.java | 5 ++-- 14 files changed, 82 insertions(+), 64 deletions(-) diff --git a/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/Observable.scala b/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/Observable.scala index 73abed3d8a..1d17b3ebee 100644 --- a/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/Observable.scala +++ b/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/Observable.scala @@ -1950,12 +1950,12 @@ trait Observable[+T] /** * Returns an Observable that emits only the very first item emitted by the source Observable, or raises an - * `IllegalArgumentException` if the source Observable is empty. + * `NoSuchElementException` if the source Observable is empty. *

    * * * @return an Observable that emits only the very first item emitted by the source Observable, or raises an - * `IllegalArgumentException` if the source Observable is empty + * `NoSuchElementException` if the source Observable is empty * @see RxJava Wiki: first() * @see "MSDN: Observable.firstAsync()" */ @@ -1965,12 +1965,12 @@ trait Observable[+T] /** * Returns an Observable that emits only the very first item emitted by the source Observable, or raises an - * `IllegalArgumentException` if the source Observable is empty. + * `NoSuchElementException` if the source Observable is empty. *

    * * * @return an Observable that emits only the very first item emitted by the source Observable, or raises an - * `IllegalArgumentException` if the source Observable is empty + * `NoSuchElementException` if the source Observable is empty * @see RxJava Wiki: first() * @see "MSDN: Observable.firstAsync()" * @see [[Observable.first]] @@ -1979,7 +1979,7 @@ trait Observable[+T] /** * Returns an Observable that emits the last item emitted by the source Observable or notifies observers of - * an `IllegalArgumentException` if the source Observable is empty. + * an `NoSuchElementException` if the source Observable is empty. * * * @@ -1994,12 +1994,12 @@ trait Observable[+T] /** * If the source Observable completes after emitting a single item, return an Observable that emits that - * item. If the source Observable emits more than one item or no items, throw an `IllegalArgumentException`. + * item. If the source Observable emits more than one item or no items, throw an `NoSuchElementException`. * * * * @return an Observable that emits the single item emitted by the source Observable - * @throws IllegalArgumentException + * @throws NoSuchElementException * if the source emits more than one item or no items * @see RxJava Wiki: single() * @see "MSDN: Observable.singleAsync()" diff --git a/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/observables/BlockingObservable.scala b/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/observables/BlockingObservable.scala index fb2c2764f8..4ae6a54ce7 100644 --- a/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/observables/BlockingObservable.scala +++ b/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/observables/BlockingObservable.scala @@ -55,12 +55,12 @@ class BlockingObservable[+T] private[scala] (val asJava: rx.observables.Blocking /** * Returns the last item emitted by a specified [[Observable]], or - * throws `IllegalArgumentException` if it emits no items. + * throws `NoSuchElementException` if it emits no items. * * * * @return the last item emitted by the source [[Observable]] - * @throws IllegalArgumentException + * @throws NoSuchElementException * if source contains no elements * @see RxJava Wiki: last() * @see MSDN: Observable.Last @@ -71,10 +71,10 @@ class BlockingObservable[+T] private[scala] (val asJava: rx.observables.Blocking /** * Returns the first item emitted by a specified [[Observable]], or - * `IllegalArgumentException` if source contains no elements. + * `NoSuchElementException` if source contains no elements. * * @return the first item emitted by the source [[Observable]] - * @throws IllegalArgumentException + * @throws NoSuchElementException * if source contains no elements * @see RxJava Wiki: first() * @see MSDN: Observable.First @@ -85,10 +85,10 @@ class BlockingObservable[+T] private[scala] (val asJava: rx.observables.Blocking /** * Returns the first item emitted by a specified [[Observable]], or - * `IllegalArgumentException` if source contains no elements. + * `NoSuchElementException` if source contains no elements. * * @return the first item emitted by the source [[Observable]] - * @throws IllegalArgumentException + * @throws NoSuchElementException * if source contains no elements * @see RxJava Wiki: first() * @see MSDN: Observable.First diff --git a/rxjava-core/src/main/java/rx/Observable.java b/rxjava-core/src/main/java/rx/Observable.java index 5df7b3bba9..17eb5af99d 100644 --- a/rxjava-core/src/main/java/rx/Observable.java +++ b/rxjava-core/src/main/java/rx/Observable.java @@ -4545,12 +4545,12 @@ public final Observable finallyDo(Action0 action) { /** * Returns an Observable that emits only the very first item emitted by the source Observable, or raises an - * {@code IllegalArgumentException} if the source Observable is empty. + * {@code NoSuchElementException} if the source Observable is empty. *

    * * * @return an Observable that emits only the very first item emitted by the source Observable, or raises an - * {@code IllegalArgumentException} if the source Observable is empty + * {@code NoSuchElementException} if the source Observable is empty * @see RxJava Wiki: first() * @see "MSDN: Observable.firstAsync()" */ @@ -4560,14 +4560,14 @@ public final Observable first() { /** * Returns an Observable that emits only the very first item emitted by the source Observable that satisfies - * a specified condition, or raises an {@code IllegalArgumentException} if no such items are emitted. + * a specified condition, or raises an {@code NoSuchElementException} if no such items are emitted. *

    * * * @param predicate * the condition that an item emitted by the source Observable has to satisfy * @return an Observable that emits only the very first item emitted by the source Observable that satisfies - * the {@code predicate}, or raises an {@code IllegalArgumentException} if no such items are emitted + * the {@code predicate}, or raises an {@code NoSuchElementException} if no such items are emitted * @see RxJava Wiki: first() * @see "MSDN: Observable.firstAsync()" */ @@ -4778,7 +4778,7 @@ public final Observable join(Obser /** * Returns an Observable that emits the last item emitted by the source Observable or notifies observers of - * an {@code IllegalArgumentException} if the source Observable is empty. + * an {@code NoSuchElementException} if the source Observable is empty. *

    * * @@ -4793,14 +4793,14 @@ public final Observable last() { /** * Returns an Observable that emits only the last item emitted by the source Observable that satisfies a - * given condition, or an {@code IllegalArgumentException} if no such items are emitted. + * given condition, or an {@code NoSuchElementException} if no such items are emitted. *

    * * * @param predicate * the condition any source emitted item has to satisfy * @return an Observable that emits only the last item satisfying the given condition from the source, or an - * {@code IllegalArgumentException} if no such items are emitted + * {@code NoSuchElementException} if no such items are emitted * @throws IllegalArgumentException * if no items that match the predicate are emitted by the source Observable * @see RxJava Wiki: last() @@ -6205,13 +6205,15 @@ public final Observable serialize() { /** * If the source Observable completes after emitting a single item, return an Observable that emits that * item. If the source Observable emits more than one item or no items, throw an - * {@code IllegalArgumentException}. + * {@code NoSuchElementException}. *

    * * * @return an Observable that emits the single item emitted by the source Observable * @throws IllegalArgumentException - * if the source emits more than one item or no items + * if the source emits more than one item + * @throws NoSuchElementException + * if the source emits no items * @see RxJava Wiki: single() * @see "MSDN: Observable.singleAsync()" */ @@ -6222,7 +6224,7 @@ public final Observable single() { /** * If the Observable completes after emitting a single item that matches a specified predicate, return an * Observable that emits that item. If the source Observable emits more than one such item or no such items, - * throw an {@code IllegalArgumentException}. + * throw an {@code NoSuchElementException}. *

    * * @@ -6231,8 +6233,9 @@ public final Observable single() { * @return an Observable that emits the single item emitted by the source Observable that matches the * predicate * @throws IllegalArgumentException - * if the source Observable emits either more than one item that matches the predicate or no - * items that match the predicate + * if the source Observable emits more than one item that matches the predicate + * @throws NoSuchElementException + * if the source Observable emits no item that matches the predicate * @see RxJava Wiki: single() * @see "MSDN: Observable.singleAsync()" */ diff --git a/rxjava-core/src/main/java/rx/observables/BlockingObservable.java b/rxjava-core/src/main/java/rx/observables/BlockingObservable.java index fdd8db73ed..15957dea4e 100644 --- a/rxjava-core/src/main/java/rx/observables/BlockingObservable.java +++ b/rxjava-core/src/main/java/rx/observables/BlockingObservable.java @@ -163,10 +163,10 @@ public Iterator getIterator() { /** * Returns the first item emitted by a specified {@link Observable}, or - * IllegalArgumentException if source contains no elements. + * NoSuchElementException if source contains no elements. * * @return the first item emitted by the source {@link Observable} - * @throws IllegalArgumentException + * @throws NoSuchElementException * if source contains no elements * @see RxJava Wiki: first() * @see MSDN: Observable.First @@ -177,14 +177,14 @@ public T first() { /** * Returns the first item emitted by a specified {@link Observable} that - * matches a predicate, or IllegalArgumentException if no such + * matches a predicate, or NoSuchElementException if no such * item is emitted. * * @param predicate * a predicate function to evaluate items emitted by the {@link Observable} * @return the first item emitted by the {@link Observable} that matches the * predicate - * @throws IllegalArgumentException + * @throws NoSuchElementException * if no such items are emitted * @see RxJava Wiki: first() * @see MSDN: Observable.First @@ -227,12 +227,12 @@ public T firstOrDefault(T defaultValue, Func1 predicate) { /** * Returns the last item emitted by a specified {@link Observable}, or - * throws IllegalArgumentException if it emits no items. + * throws NoSuchElementException if it emits no items. *

    * * * @return the last item emitted by the source {@link Observable} - * @throws IllegalArgumentException + * @throws NoSuchElementException * if source contains no elements * @see RxJava Wiki: last() * @see MSDN: Observable.Last @@ -243,7 +243,7 @@ public T last() { /** * Returns the last item emitted by a specified {@link Observable} that - * matches a predicate, or throws IllegalArgumentException if + * matches a predicate, or throws NoSuchElementException if * it emits no such items. *

    * @@ -252,7 +252,7 @@ public T last() { * a predicate function to evaluate items emitted by the {@link Observable} * @return the last item emitted by the {@link Observable} that matches the * predicate - * @throws IllegalArgumentException + * @throws NoSuchElementException * if no such items are emitted * @see RxJava Wiki: last() * @see MSDN: Observable.Last @@ -349,7 +349,7 @@ public Iterable latest() { /** * If the {@link Observable} completes after emitting a single item, return - * that item, otherwise throw an IllegalArgumentException. + * that item, otherwise throw an NoSuchElementException. *

    * * @@ -364,7 +364,7 @@ public T single() { /** * If the {@link Observable} completes after emitting a single item that * matches a given predicate, return that item, otherwise throw an - * IllegalArgumentException. + * NoSuchElementException. *

    * * diff --git a/rxjava-core/src/main/java/rx/operators/OperatorSingle.java b/rxjava-core/src/main/java/rx/operators/OperatorSingle.java index 6a63b025ce..e00aa87d82 100644 --- a/rxjava-core/src/main/java/rx/operators/OperatorSingle.java +++ b/rxjava-core/src/main/java/rx/operators/OperatorSingle.java @@ -15,6 +15,8 @@ */ package rx.operators; +import java.util.NoSuchElementException; + import rx.Observable.Operator; import rx.Subscriber; @@ -73,7 +75,7 @@ public void onCompleted() { subscriber.onNext(defaultValue); subscriber.onCompleted(); } else { - subscriber.onError(new IllegalArgumentException("Sequence contains no elements")); + subscriber.onError(new NoSuchElementException("Sequence contains no elements")); } } } diff --git a/rxjava-core/src/test/java/rx/ObservableTests.java b/rxjava-core/src/test/java/rx/ObservableTests.java index e4a84c15ac..d75af529dc 100644 --- a/rxjava-core/src/test/java/rx/ObservableTests.java +++ b/rxjava-core/src/test/java/rx/ObservableTests.java @@ -23,6 +23,7 @@ import java.util.Arrays; import java.util.LinkedList; import java.util.List; +import java.util.NoSuchElementException; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; @@ -204,7 +205,7 @@ public void testFirstOfNone() { observable.first().subscribe(w); verify(w, never()).onNext(anyInt()); verify(w, never()).onCompleted(); - verify(w, times(1)).onError(isA(IllegalArgumentException.class)); + verify(w, times(1)).onError(isA(NoSuchElementException.class)); } @Test @@ -213,7 +214,7 @@ public void testFirstWithPredicateOfNoneMatchingThePredicate() { observable.first(IS_EVEN).subscribe(w); verify(w, never()).onNext(anyInt()); verify(w, never()).onCompleted(); - verify(w, times(1)).onError(isA(IllegalArgumentException.class)); + verify(w, times(1)).onError(isA(NoSuchElementException.class)); } @Test @@ -233,9 +234,9 @@ public Integer call(Integer t1, Integer t2) { } /** - * A reduce should fail with an IllegalArgumentException if done on an empty Observable. + * A reduce should fail with an NoSuchElementException if done on an empty Observable. */ - @Test(expected = IllegalArgumentException.class) + @Test(expected = NoSuchElementException.class) public void testReduceWithEmptyObservable() { Observable observable = Observable.range(1, 0); observable.reduce(new Func2() { diff --git a/rxjava-core/src/test/java/rx/ZipTests.java b/rxjava-core/src/test/java/rx/ZipTests.java index 8735cb2e66..e8cd9a90dd 100644 --- a/rxjava-core/src/test/java/rx/ZipTests.java +++ b/rxjava-core/src/test/java/rx/ZipTests.java @@ -21,6 +21,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.Map; +import java.util.NoSuchElementException; import org.junit.Test; @@ -100,9 +101,9 @@ public void testCovarianceOfZip() { * Occasionally zip may be invoked with 0 observables. Test that we don't block indefinitely instead * of immediately invoking zip with 0 argument. * - * We now expect an IllegalArgumentException since last() requires at least one value and nothing will be emitted. + * We now expect an NoSuchElementException since last() requires at least one value and nothing will be emitted. */ - @Test(expected = IllegalArgumentException.class) + @Test(expected = NoSuchElementException.class) public void nonBlockingObservable() { final Object invoked = new Object(); diff --git a/rxjava-core/src/test/java/rx/observables/BlockingObservableTest.java b/rxjava-core/src/test/java/rx/observables/BlockingObservableTest.java index 50b3d8f161..663f5d95f6 100644 --- a/rxjava-core/src/test/java/rx/observables/BlockingObservableTest.java +++ b/rxjava-core/src/test/java/rx/observables/BlockingObservableTest.java @@ -52,7 +52,7 @@ public void testLast() { assertEquals("three", obs.last()); } - @Test(expected = IllegalArgumentException.class) + @Test(expected = NoSuchElementException.class) public void testLastEmptyObservable() { BlockingObservable obs = BlockingObservable.from(Observable.empty()); obs.last(); @@ -175,7 +175,7 @@ public void testSingleWrong() { observable.single(); } - @Test(expected = IllegalArgumentException.class) + @Test(expected = NoSuchElementException.class) public void testSingleWrongPredicate() { BlockingObservable observable = BlockingObservable.from(Observable.from(-1)); observable.single(new Func1() { @@ -319,7 +319,7 @@ public void testFirst() { assertEquals("one", observable.first()); } - @Test(expected = IllegalArgumentException.class) + @Test(expected = NoSuchElementException.class) public void testFirstWithEmpty() { BlockingObservable.from(Observable. empty()).first(); } @@ -336,7 +336,7 @@ public Boolean call(String args) { assertEquals("three", first); } - @Test(expected = IllegalArgumentException.class) + @Test(expected = NoSuchElementException.class) public void testFirstWithPredicateAndEmpty() { BlockingObservable observable = BlockingObservable.from(Observable.from("one", "two", "three")); observable.first(new Func1() { diff --git a/rxjava-core/src/test/java/rx/operators/OperationFirstOrDefaultTest.java b/rxjava-core/src/test/java/rx/operators/OperationFirstOrDefaultTest.java index 84f1b6aee0..369b29ce7e 100644 --- a/rxjava-core/src/test/java/rx/operators/OperationFirstOrDefaultTest.java +++ b/rxjava-core/src/test/java/rx/operators/OperationFirstOrDefaultTest.java @@ -19,6 +19,8 @@ import static org.mockito.Mockito.*; import static org.mockito.MockitoAnnotations.*; +import java.util.NoSuchElementException; + import org.junit.Before; import org.junit.Test; import org.mockito.InOrder; @@ -127,7 +129,7 @@ public void testFirstWithEmpty() { InOrder inOrder = inOrder(observer); inOrder.verify(observer, times(1)).onError( - isA(IllegalArgumentException.class)); + isA(NoSuchElementException.class)); inOrder.verifyNoMoreInteractions(); } @@ -189,7 +191,7 @@ public Boolean call(Integer t1) { InOrder inOrder = inOrder(observer); inOrder.verify(observer, times(1)).onError( - isA(IllegalArgumentException.class)); + isA(NoSuchElementException.class)); inOrder.verifyNoMoreInteractions(); } diff --git a/rxjava-core/src/test/java/rx/operators/OperationLastTest.java b/rxjava-core/src/test/java/rx/operators/OperationLastTest.java index 6cb6d44ff5..7f9009625a 100644 --- a/rxjava-core/src/test/java/rx/operators/OperationLastTest.java +++ b/rxjava-core/src/test/java/rx/operators/OperationLastTest.java @@ -15,9 +15,13 @@ */ package rx.operators; -import static org.junit.Assert.*; -import static org.mockito.Matchers.*; -import static org.mockito.Mockito.*; +import static org.junit.Assert.assertEquals; +import static org.mockito.Matchers.isA; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; + +import java.util.NoSuchElementException; import org.junit.Test; import org.mockito.InOrder; @@ -34,7 +38,7 @@ public void testLastWithElements() { assertEquals(3, last.toBlockingObservable().single().intValue()); } - @Test(expected = IllegalArgumentException.class) + @Test(expected = NoSuchElementException.class) public void testLastWithNoElements() { Observable last = Observable.empty().last(); last.toBlockingObservable().single(); @@ -90,7 +94,7 @@ public void testLastWithEmpty() { InOrder inOrder = inOrder(observer); inOrder.verify(observer, times(1)).onError( - isA(IllegalArgumentException.class)); + isA(NoSuchElementException.class)); inOrder.verifyNoMoreInteractions(); } @@ -152,7 +156,7 @@ public Boolean call(Integer t1) { InOrder inOrder = inOrder(observer); inOrder.verify(observer, times(1)).onError( - isA(IllegalArgumentException.class)); + isA(NoSuchElementException.class)); inOrder.verifyNoMoreInteractions(); } diff --git a/rxjava-core/src/test/java/rx/operators/OperationMinMaxTest.java b/rxjava-core/src/test/java/rx/operators/OperationMinMaxTest.java index cad4805c7c..bf0ebe59a0 100644 --- a/rxjava-core/src/test/java/rx/operators/OperationMinMaxTest.java +++ b/rxjava-core/src/test/java/rx/operators/OperationMinMaxTest.java @@ -23,6 +23,7 @@ import java.util.Arrays; import java.util.Comparator; import java.util.List; +import java.util.NoSuchElementException; import org.junit.Test; import org.mockito.InOrder; @@ -56,7 +57,7 @@ public void testMinWithEmpty() { observable.subscribe(observer); InOrder inOrder = inOrder(observer); inOrder.verify(observer, times(1)).onError( - isA(IllegalArgumentException.class)); + isA(NoSuchElementException.class)); inOrder.verifyNoMoreInteractions(); } @@ -96,7 +97,7 @@ public int compare(Integer o1, Integer o2) { observable.subscribe(observer); InOrder inOrder = inOrder(observer); inOrder.verify(observer, times(1)).onError( - isA(IllegalArgumentException.class)); + isA(NoSuchElementException.class)); inOrder.verifyNoMoreInteractions(); } @@ -216,7 +217,7 @@ public void testMaxWithEmpty() { observable.subscribe(observer); InOrder inOrder = inOrder(observer); inOrder.verify(observer, times(1)).onError( - isA(IllegalArgumentException.class)); + isA(NoSuchElementException.class)); inOrder.verifyNoMoreInteractions(); } @@ -256,7 +257,7 @@ public int compare(Integer o1, Integer o2) { observable.subscribe(observer); InOrder inOrder = inOrder(observer); inOrder.verify(observer, times(1)).onError( - isA(IllegalArgumentException.class)); + isA(NoSuchElementException.class)); inOrder.verifyNoMoreInteractions(); } diff --git a/rxjava-core/src/test/java/rx/operators/OperatorMapTest.java b/rxjava-core/src/test/java/rx/operators/OperatorMapTest.java index fd091bdaef..111f694637 100644 --- a/rxjava-core/src/test/java/rx/operators/OperatorMapTest.java +++ b/rxjava-core/src/test/java/rx/operators/OperatorMapTest.java @@ -20,6 +20,7 @@ import java.util.HashMap; import java.util.Map; +import java.util.NoSuchElementException; import org.junit.Before; import org.junit.Test; @@ -211,9 +212,9 @@ public String call(String arg0) { } /** - * While mapping over range(1,1).last() we expect IllegalArgumentException since the sequence is empty. + * While mapping over range(1,0).last() we expect NoSuchElementException since the sequence is empty. */ - @Test(expected = IllegalArgumentException.class) + @Test(expected = NoSuchElementException.class) public void testErrorPassesThruMap() { Observable.range(1, 0).last().map(new Func1() { diff --git a/rxjava-core/src/test/java/rx/operators/OperatorSingleTest.java b/rxjava-core/src/test/java/rx/operators/OperatorSingleTest.java index baf83929ec..54673db194 100644 --- a/rxjava-core/src/test/java/rx/operators/OperatorSingleTest.java +++ b/rxjava-core/src/test/java/rx/operators/OperatorSingleTest.java @@ -18,6 +18,8 @@ import static org.mockito.Matchers.*; import static org.mockito.Mockito.*; +import java.util.NoSuchElementException; + import org.junit.Test; import org.mockito.InOrder; @@ -65,7 +67,7 @@ public void testSingleWithEmpty() { InOrder inOrder = inOrder(observer); inOrder.verify(observer, times(1)).onError( - isA(IllegalArgumentException.class)); + isA(NoSuchElementException.class)); inOrder.verifyNoMoreInteractions(); } @@ -127,7 +129,7 @@ public Boolean call(Integer t1) { InOrder inOrder = inOrder(observer); inOrder.verify(observer, times(1)).onError( - isA(IllegalArgumentException.class)); + isA(NoSuchElementException.class)); inOrder.verifyNoMoreInteractions(); } diff --git a/rxjava-core/src/test/java/rx/operators/OperatorZipTest.java b/rxjava-core/src/test/java/rx/operators/OperatorZipTest.java index 7862c9f49a..51e1395e3d 100644 --- a/rxjava-core/src/test/java/rx/operators/OperatorZipTest.java +++ b/rxjava-core/src/test/java/rx/operators/OperatorZipTest.java @@ -24,6 +24,7 @@ import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.NoSuchElementException; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; @@ -1034,10 +1035,10 @@ public Object call(final Object... args) { } /** - * Expect IllegalArgumentException instead of blocking forever as zip should emit onCompleted and no onNext + * Expect NoSuchElementException instead of blocking forever as zip should emit onCompleted and no onNext * and last() expects at least a single response. */ - @Test(expected = IllegalArgumentException.class) + @Test(expected = NoSuchElementException.class) public void testZipEmptyListBlocking() { final Object invoked = new Object(); From badadd14064f58e6f687e34eca519de31bda13aa Mon Sep 17 00:00:00 2001 From: zsxwing Date: Mon, 17 Mar 2014 21:41:20 +0800 Subject: [PATCH 136/422] Rename OperationFirstOrDefaultTest and OperationLastTest to OperatorFirstOrDefaultTest and OperatorLastTest --- ...{OperationFirstOrDefaultTest.java => OperatorFirstTest.java} | 2 +- .../operators/{OperationLastTest.java => OperatorLastTest.java} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename rxjava-core/src/test/java/rx/operators/{OperationFirstOrDefaultTest.java => OperatorFirstTest.java} (99%) rename rxjava-core/src/test/java/rx/operators/{OperationLastTest.java => OperatorLastTest.java} (99%) diff --git a/rxjava-core/src/test/java/rx/operators/OperationFirstOrDefaultTest.java b/rxjava-core/src/test/java/rx/operators/OperatorFirstTest.java similarity index 99% rename from rxjava-core/src/test/java/rx/operators/OperationFirstOrDefaultTest.java rename to rxjava-core/src/test/java/rx/operators/OperatorFirstTest.java index 369b29ce7e..2408a6004d 100644 --- a/rxjava-core/src/test/java/rx/operators/OperationFirstOrDefaultTest.java +++ b/rxjava-core/src/test/java/rx/operators/OperatorFirstTest.java @@ -30,7 +30,7 @@ import rx.Observer; import rx.functions.Func1; -public class OperationFirstOrDefaultTest { +public class OperatorFirstTest { @Mock Observer w; diff --git a/rxjava-core/src/test/java/rx/operators/OperationLastTest.java b/rxjava-core/src/test/java/rx/operators/OperatorLastTest.java similarity index 99% rename from rxjava-core/src/test/java/rx/operators/OperationLastTest.java rename to rxjava-core/src/test/java/rx/operators/OperatorLastTest.java index 7f9009625a..5eee929fbd 100644 --- a/rxjava-core/src/test/java/rx/operators/OperationLastTest.java +++ b/rxjava-core/src/test/java/rx/operators/OperatorLastTest.java @@ -30,7 +30,7 @@ import rx.Observer; import rx.functions.Func1; -public class OperationLastTest { +public class OperatorLastTest { @Test public void testLastWithElements() { From b8524108ac3f6a486d32e0a333e88ce9aa5946a5 Mon Sep 17 00:00:00 2001 From: zsxwing Date: Mon, 17 Mar 2014 23:26:56 +0800 Subject: [PATCH 137/422] Fix clojure test --- .../src/test/clojure/rx/lang/clojure/blocking_test.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/blocking_test.clj b/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/blocking_test.clj index df8e9fae1e..260e6a3dc5 100644 --- a/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/blocking_test.clj +++ b/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/blocking_test.clj @@ -36,7 +36,7 @@ (testing "returns one element" (is (= 1 (b/single (rx/return 1))))) (testing "throw if empty" - (is (thrown? java.lang.IllegalArgumentException (b/single (rx/empty))))) + (is (thrown? java.util.NoSuchElementException (b/single (rx/empty))))) (testing "throw if many" (is (thrown? java.lang.IllegalArgumentException (b/single (rx/seq->o [1 2]))))) (testing "rethrows errors" From 88679528fa96c0b0dbeb0da14f35566279fde8ff Mon Sep 17 00:00:00 2001 From: zsxwing Date: Mon, 17 Mar 2014 23:48:22 +0800 Subject: [PATCH 138/422] Fix tests in rxjava-math --- .../test/java/rx/math/operators/OperationMinMaxTest.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/rxjava-contrib/rxjava-math/src/test/java/rx/math/operators/OperationMinMaxTest.java b/rxjava-contrib/rxjava-math/src/test/java/rx/math/operators/OperationMinMaxTest.java index 63eee7b28f..64d0a38f4d 100644 --- a/rxjava-contrib/rxjava-math/src/test/java/rx/math/operators/OperationMinMaxTest.java +++ b/rxjava-contrib/rxjava-math/src/test/java/rx/math/operators/OperationMinMaxTest.java @@ -23,6 +23,7 @@ import java.util.Arrays; import java.util.Comparator; import java.util.List; +import java.util.NoSuchElementException; import org.junit.Test; import org.mockito.InOrder; @@ -56,7 +57,7 @@ public void testMinWithEmpty() { observable.subscribe(observer); InOrder inOrder = inOrder(observer); inOrder.verify(observer, times(1)).onError( - isA(IllegalArgumentException.class)); + isA(NoSuchElementException.class)); inOrder.verifyNoMoreInteractions(); } @@ -96,7 +97,7 @@ public int compare(Integer o1, Integer o2) { observable.subscribe(observer); InOrder inOrder = inOrder(observer); inOrder.verify(observer, times(1)).onError( - isA(IllegalArgumentException.class)); + isA(NoSuchElementException.class)); inOrder.verifyNoMoreInteractions(); } @@ -216,7 +217,7 @@ public void testMaxWithEmpty() { observable.subscribe(observer); InOrder inOrder = inOrder(observer); inOrder.verify(observer, times(1)).onError( - isA(IllegalArgumentException.class)); + isA(NoSuchElementException.class)); inOrder.verifyNoMoreInteractions(); } @@ -256,7 +257,7 @@ public int compare(Integer o1, Integer o2) { observable.subscribe(observer); InOrder inOrder = inOrder(observer); inOrder.verify(observer, times(1)).onError( - isA(IllegalArgumentException.class)); + isA(NoSuchElementException.class)); inOrder.verifyNoMoreInteractions(); } From 00703cfffdc6687f460445eecd3d6b8a801acf17 Mon Sep 17 00:00:00 2001 From: George Campbell Date: Wed, 19 Mar 2014 23:04:01 -0700 Subject: [PATCH 139/422] Notifications for the allocation adverse. --- .../java/rx/observers/SerializedObserver.java | 44 +---- .../rx/operators/BufferUntilSubscriber.java | 39 +--- .../java/rx/operators/NotificationLite.java | 167 ++++++++++++++++++ .../java/rx/operators/OperatorObserveOn.java | 39 +--- .../operators/OperatorObserveOnBounded.java | 58 ++---- .../main/java/rx/operators/OperatorZip.java | 42 +++-- 6 files changed, 226 insertions(+), 163 deletions(-) create mode 100644 rxjava-core/src/main/java/rx/operators/NotificationLite.java diff --git a/rxjava-core/src/main/java/rx/observers/SerializedObserver.java b/rxjava-core/src/main/java/rx/observers/SerializedObserver.java index 93f1632973..8c87a7b855 100644 --- a/rxjava-core/src/main/java/rx/observers/SerializedObserver.java +++ b/rxjava-core/src/main/java/rx/observers/SerializedObserver.java @@ -2,7 +2,11 @@ import java.util.ArrayList; +import javax.management.NotificationListener; + +import rx.Notification; import rx.Observer; +import rx.operators.NotificationLite; /** * Enforce single-threaded, serialized, ordered execution of onNext, onCompleted, onError. @@ -22,21 +26,7 @@ public class SerializedObserver implements Observer { private boolean emitting = false; private boolean terminated = false; private ArrayList queue = new ArrayList(); - - private static Sentinel NULL_SENTINEL = new Sentinel(); - private static Sentinel COMPLETE_SENTINEL = new Sentinel(); - - private static class Sentinel { - - } - - private static class ErrorSentinel extends Sentinel { - final Throwable e; - - ErrorSentinel(Throwable e) { - this.e = e; - } - } + private NotificationLite on = NotificationLite.instance(); public SerializedObserver(Observer s) { this.actual = s; @@ -61,7 +51,7 @@ public void onCompleted() { } } else { // someone else is already emitting so just queue it - queue.add(COMPLETE_SENTINEL); + queue.add(on.completed()); } } if (canEmit) { @@ -97,7 +87,7 @@ public void onError(final Throwable e) { } else { // someone else is already emitting so just queue it ... after eliminating the queue to shortcut queue.clear(); - queue.add(new ErrorSentinel(e)); + queue.add(on.error(e)); } } if (canEmit) { @@ -131,11 +121,7 @@ public void onNext(T t) { } } else { // someone else is already emitting so just queue it - if (t == null) { - queue.add(NULL_SENTINEL); - } else { - queue.add(t); - } + queue.add(on.next(t)); } } if (canEmit) { @@ -168,19 +154,7 @@ public void drainQueue(ArrayList list) { return; } for (Object v : list) { - if (v != null) { - if (v instanceof Sentinel) { - if (v == NULL_SENTINEL) { - actual.onNext(null); - } else if (v == COMPLETE_SENTINEL) { - actual.onCompleted(); - } else if (v instanceof ErrorSentinel) { - actual.onError(((ErrorSentinel) v).e); - } - } else { - actual.onNext((T) v); - } - } + on.accept(actual, v); } } } diff --git a/rxjava-core/src/main/java/rx/operators/BufferUntilSubscriber.java b/rxjava-core/src/main/java/rx/operators/BufferUntilSubscriber.java index 7526674636..c849421ee5 100644 --- a/rxjava-core/src/main/java/rx/operators/BufferUntilSubscriber.java +++ b/rxjava-core/src/main/java/rx/operators/BufferUntilSubscriber.java @@ -17,6 +17,7 @@ import java.util.LinkedList; import java.util.Queue; + import rx.Subscriber; import rx.subscriptions.CompositeSubscription; @@ -36,21 +37,8 @@ public class BufferUntilSubscriber extends Subscriber { private final Queue queue = new LinkedList(); /** The queue capacity. */ private final int capacity; - /** Null sentinel (in case queue type is changed). */ - private static final Object NULL_SENTINEL = new Object(); - /** Complete sentinel. */ - private static final Object COMPLETE_SENTINEL = new Object(); - /** - * Container for an onError event. - */ - private static final class ErrorSentinel { - final Throwable t; + private final NotificationLite on = NotificationLite.instance(); - public ErrorSentinel(Throwable t) { - this.t = t; - } - - } /** * Constructor that wraps the actual subscriber and shares its subscription. * @param capacity the queue capacity to accept before blocking, negative value indicates an unbounded queue @@ -85,22 +73,7 @@ public void enterPassthroughMode() { while (!queue.isEmpty()) { Object o = queue.poll(); if (!actual.isUnsubscribed()) { - if (o == NULL_SENTINEL) { - actual.onNext(null); - } else - if (o == COMPLETE_SENTINEL) { - actual.onCompleted(); - } else - if (o instanceof ErrorSentinel) { - actual.onError(((ErrorSentinel)o).t); - } else - if (o != null) { - @SuppressWarnings("unchecked") - T v = (T)o; - actual.onNext(v); - } else { - throw new NullPointerException(); - } + on.accept(actual, o); } } passthroughMode = true; @@ -115,7 +88,7 @@ public void onNext(T t) { synchronized (gate) { if (!passthroughMode) { if (capacity < 0 || queue.size() < capacity) { - queue.offer(t != null ? t : NULL_SENTINEL); + queue.offer(on.next(t)); return; } try { @@ -142,7 +115,7 @@ public void onError(Throwable e) { synchronized (gate) { if (!passthroughMode) { if (capacity < 0 || queue.size() < capacity) { - queue.offer(new ErrorSentinel(e)); + queue.offer(on.error(e)); return; } try { @@ -169,7 +142,7 @@ public void onCompleted() { synchronized (gate) { if (!passthroughMode) { if (capacity < 0 || queue.size() < capacity) { - queue.offer(COMPLETE_SENTINEL); + queue.offer(on.completed()); return; } try { diff --git a/rxjava-core/src/main/java/rx/operators/NotificationLite.java b/rxjava-core/src/main/java/rx/operators/NotificationLite.java new file mode 100644 index 0000000000..54e807855e --- /dev/null +++ b/rxjava-core/src/main/java/rx/operators/NotificationLite.java @@ -0,0 +1,167 @@ +package rx.operators; + +import java.io.ObjectStreamException; +import java.io.Serializable; + +import rx.Notification; +import rx.Notification.Kind; +import rx.Observer; + +/** + * For use in internal operators that need something like materialize and dematerialize wholly + * within the implementation of the operator but don't want to incur the allocation cost of actually + * creating {@link rx.Notification} objects for every onNext and onComplete. + * + * An object is allocated inside {@link #error(Throwable)} to wrap the {@link Throwable} but this + * shouldn't effect performance because exceptions should be exceptionally rare. + * + * It's implemented as a singleton to maintain some semblance of type safety that is completely + * non-existent. + * + * @author gscampbell + * + * @param + */ +public final class NotificationLite { + private NotificationLite() { + } + + private static final NotificationLite INSTANCE = new NotificationLite(); + + @SuppressWarnings("unchecked") + public static NotificationLite instance() { + return INSTANCE; + } + + private static final Object ON_COMPLETED_SENTINEL = new Serializable() { + private static final long serialVersionUID = 1; + }; + + private static final Object ON_NEXT_NULL_SENTINEL = new Serializable() { + private static final long serialVersionUID = 2; + }; + + private static class OnErrorSentinel implements Serializable { + private static final long serialVersionUID = 3; + private final Throwable e; + + public OnErrorSentinel(Throwable e) { + this.e = e; + } + } + + /** + * Creates a lite onNext notification for the value passed in without doing any allocation. Can + * be unwrapped and sent with the {@link #accept} method. + * + * @param t + * @return + */ + public Object next(T t) { + if (t == null) + return ON_NEXT_NULL_SENTINEL; + else + return t; + } + + /** + * Creates a lite onComplete notification without doing any allocation. Can be unwrapped and + * sent with the {@link #accept} method. + * + * @return + */ + public Object completed() { + return ON_COMPLETED_SENTINEL; + } + + /** + * Create a lite onError notification. This call does new up an object to wrap the + * {@link Throwable} but since there should only be one of these the performance impact should + * be small. Can be unwrapped and sent with the {@link #accept} method. + * + * @param e + * @return + */ + public Object error(Throwable e) { + return new OnErrorSentinel(e); + } + + /** + * Unwraps the lite notification and calls the appropriate method on the {@link Observer}. + * + * @param o + * the {@link Observer} to call onNext, onCompleted or onError. + * @param n + * @throws IllegalArgumentException + * if the notification is null. + * @throws NullPointerException + * if the {@link Observer} is null. + */ + @SuppressWarnings("unchecked") + public void accept(Observer o, Object n) { + switch (kind(n)) { + case OnNext: + o.onNext(getValue(n)); + break; + case OnCompleted: + o.onCompleted(); + break; + case OnError: + o.onError(getError(n)); + break; + } + } + + public boolean isCompleted(Object n) { + return n == ON_COMPLETED_SENTINEL; + } + + public boolean isError(Object n) { + return n instanceof OnErrorSentinel; + } + + /** + * If there is custom logic that isn't as simple as call the right method on an {@link Observer} + * then this method can be used to get the {@link Notification.Kind} + * + * @param n + * @return + */ + public Kind kind(Object n) { + if (n == null) + throw new IllegalArgumentException("The lite notification can not be null"); + else if (n == ON_COMPLETED_SENTINEL) + return Kind.OnCompleted; + else if (n instanceof OnErrorSentinel) + return Kind.OnError; + else + // value or ON_NEXT_NULL_SENTINEL but either way it's an OnNext + return Kind.OnNext; + } + + /** + * returns value passed in {@link #next(Object)} method call. Bad things happen if you call this + * the onComplete or onError notification type. For performance you are expected to use this + * when it is appropriate. + * + * @param n + * @return + */ + @SuppressWarnings("unchecked") + public T getValue(Object n) { + return n == ON_NEXT_NULL_SENTINEL ? null : (T) n; + } + + /** + * returns {@link Throwable} passed in {@link #error(Throwable)} method call. Bad things happen + * if you + * call this the onComplete or onNext notification type. For performance you are expected to use + * this when it is appropriate. + * + * @param n + * @return The {@link Throwable} wrapped inside n + */ + public Throwable getError(Object n) { + return ((OnErrorSentinel) n).e; + } +} diff --git a/rxjava-core/src/main/java/rx/operators/OperatorObserveOn.java b/rxjava-core/src/main/java/rx/operators/OperatorObserveOn.java index c2457dd8d2..9a3b6937f5 100644 --- a/rxjava-core/src/main/java/rx/operators/OperatorObserveOn.java +++ b/rxjava-core/src/main/java/rx/operators/OperatorObserveOn.java @@ -55,20 +55,7 @@ public Subscriber call(Subscriber child) { } } - private static class Sentinel { - - } - - private static Sentinel NULL_SENTINEL = new Sentinel(); - private static Sentinel COMPLETE_SENTINEL = new Sentinel(); - - private static class ErrorSentinel extends Sentinel { - final Throwable e; - - ErrorSentinel(Throwable e) { - this.e = e; - } - } + private final NotificationLite on = NotificationLite.instance(); /** Observe through individual queue per observer. */ private class ObserveOnSubscriber extends Subscriber { @@ -85,23 +72,19 @@ public ObserveOnSubscriber(Subscriber observer) { @Override public void onNext(final T t) { - if (t == null) { - queue.offer(NULL_SENTINEL); - } else { - queue.offer(t); - } + queue.offer(on.next(t)); schedule(); } @Override public void onCompleted() { - queue.offer(COMPLETE_SENTINEL); + queue.offer(on.completed()); schedule(); } @Override public void onError(final Throwable e) { - queue.offer(new ErrorSentinel(e)); + queue.offer(on.error(e)); schedule(); } @@ -134,19 +117,7 @@ public void call(Inner inner) { private void pollQueue() { do { Object v = queue.poll(); - if (v != null) { - if (v instanceof Sentinel) { - if (v == NULL_SENTINEL) { - observer.onNext(null); - } else if (v == COMPLETE_SENTINEL) { - observer.onCompleted(); - } else if (v instanceof ErrorSentinel) { - observer.onError(((ErrorSentinel) v).e); - } - } else { - observer.onNext((T) v); - } - } + on.accept(observer, v); } while (counter.decrementAndGet() > 0); } diff --git a/rxjava-core/src/main/java/rx/operators/OperatorObserveOnBounded.java b/rxjava-core/src/main/java/rx/operators/OperatorObserveOnBounded.java index 97c53ae431..7901a86f4f 100644 --- a/rxjava-core/src/main/java/rx/operators/OperatorObserveOnBounded.java +++ b/rxjava-core/src/main/java/rx/operators/OperatorObserveOnBounded.java @@ -32,17 +32,18 @@ /** * Delivers events on the specified Scheduler. *

    - * This provides backpressure by blocking the incoming onNext when there is already one in the queue. + * This provides backpressure by blocking the incoming onNext when there is already one in the + * queue. *

    - * This means that at any given time the max number of "onNext" in flight is 3: - * -> 1 being delivered on the Scheduler - * -> 1 in the queue waiting for the Scheduler - * -> 1 blocking on the queue waiting to deliver it + * This means that at any given time the max number of "onNext" in flight is 3: -> 1 being delivered + * on the Scheduler -> 1 in the queue waiting for the Scheduler -> 1 blocking on the queue waiting + * to deliver it * - * I have chosen to allow 1 in the queue rather than using an Exchanger style process so that the Scheduler - * can loop and have something to do each time around to optimize for avoiding rescheduling when it - * can instead just loop. I'm avoiding having the Scheduler thread ever block as it could be an event-loop - * thus if the queue is empty it exits and next time something is added it will reschedule. + * I have chosen to allow 1 in the queue rather than using an Exchanger style process so that the + * Scheduler can loop and have something to do each time around to optimize for avoiding + * rescheduling when it can instead just loop. I'm avoiding having the Scheduler thread ever block + * as it could be an event-loop thus if the queue is empty it exits and next time something is added + * it will reschedule. * * */ @@ -97,16 +98,7 @@ public Subscriber call(Subscriber child) { } } - private static Object NULL_SENTINEL = new Object(); - private static Object COMPLETE_SENTINEL = new Object(); - - private static class ErrorSentinel { - final Throwable e; - - ErrorSentinel(Throwable e) { - this.e = e; - } - } + private final NotificationLite on = NotificationLite.instance(); /** Observe through individual queue per observer. */ private class ObserveOnSubscriber extends Subscriber { @@ -126,11 +118,7 @@ public void onNext(final T t) { try { // we want to block for natural back-pressure // so that the producer waits for each value to be consumed - if (t == null) { - queue.addBlocking(NULL_SENTINEL); - } else { - queue.addBlocking(t); - } + queue.addBlocking(on.next(t)); schedule(); } catch (InterruptedException e) { if (!isUnsubscribed()) { @@ -144,7 +132,7 @@ public void onCompleted() { try { // we want to block for natural back-pressure // so that the producer waits for each value to be consumed - queue.addBlocking(COMPLETE_SENTINEL); + queue.addBlocking(on.completed()); schedule(); } catch (InterruptedException e) { onError(e); @@ -156,7 +144,7 @@ public void onError(final Throwable e) { try { // we want to block for natural back-pressure // so that the producer waits for each value to be consumed - queue.addBlocking(new ErrorSentinel(e)); + queue.addBlocking(on.error(e)); schedule(); } catch (InterruptedException e2) { // call directly if we can't schedule @@ -205,26 +193,18 @@ public void call(Inner inner) { private void pollQueue() { do { Object v = queue.poll(); - if (v != null) { - if (v == NULL_SENTINEL) { - observer.onNext(null); - } else if (v == COMPLETE_SENTINEL) { - observer.onCompleted(); - } else if (v instanceof ErrorSentinel) { - observer.onError(((ErrorSentinel) v).e); - } else { - observer.onNext((T) v); - } - } + on.accept(observer, v); } while (counter.decrementAndGet() > 0); } } /** - * Single-producer-single-consumer queue (only thread-safe for 1 producer thread with 1 consumer thread). + * Single-producer-single-consumer queue (only thread-safe for 1 producer thread with 1 consumer + * thread). * - * This supports an interrupt() being called externally rather than needing to interrupt the thread. This allows + * This supports an interrupt() being called externally rather than needing to interrupt the + * thread. This allows * unsubscribe behavior when this queue is being used. * * @param diff --git a/rxjava-core/src/main/java/rx/operators/OperatorZip.java b/rxjava-core/src/main/java/rx/operators/OperatorZip.java index 21da75d417..9ecc99b376 100644 --- a/rxjava-core/src/main/java/rx/operators/OperatorZip.java +++ b/rxjava-core/src/main/java/rx/operators/OperatorZip.java @@ -134,6 +134,7 @@ public void onNext(Observable[] observables) { }; } + static final NotificationLite on = NotificationLite.instance(); private static class Zip { @SuppressWarnings("rawtypes") final Observable[] os; @@ -141,9 +142,7 @@ private static class Zip { final Observer observer; final FuncN zipFunction; final CompositeSubscription childSubscription = new CompositeSubscription(); - - static Object NULL_SENTINEL = new Object(); - static Object COMPLETE_SENTINEL = new Object(); + @SuppressWarnings("rawtypes") public Zip(Observable[] os, final Subscriber observer, FuncN zipFunction) { @@ -180,23 +179,26 @@ public void zip() { void tick() { if (counter.getAndIncrement() == 0) { do { - Object[] vs = new Object[observers.length]; + final Object[] vs = new Object[observers.length]; boolean allHaveValues = true; for (int i = 0; i < observers.length; i++) { - vs[i] = ((InnerObserver) observers[i]).items.peek(); - if (vs[i] == NULL_SENTINEL) { - // special handling for null - vs[i] = null; - } else if (vs[i] == COMPLETE_SENTINEL) { - // special handling for onComplete + Object n = ((InnerObserver) observers[i]).items.peek(); + + if (n == null) { + allHaveValues = false; + continue; + } + + switch (on.kind(n)) { + case OnNext: + vs[i] = on.getValue(n); + break; + case OnCompleted: observer.onCompleted(); - // we need to unsubscribe from all children since children are independently subscribed + // we need to unsubscribe from all children since children are + // independently subscribed childSubscription.unsubscribe(); return; - } else if (vs[i] == null) { - allHaveValues = false; - // we continue as there may be an onCompleted on one of the others - continue; } } if (allHaveValues) { @@ -211,7 +213,7 @@ void tick() { for (int i = 0; i < observers.length; i++) { ((InnerObserver) observers[i]).items.poll(); // eagerly check if the next item on this queue is an onComplete - if (((InnerObserver) observers[i]).items.peek() == COMPLETE_SENTINEL) { + if (on.isCompleted(((InnerObserver) observers[i]).items.peek())) { // it is an onComplete so shut down observer.onCompleted(); // we need to unsubscribe from all children since children are independently subscribed @@ -235,7 +237,7 @@ final class InnerObserver extends Subscriber { @SuppressWarnings("unchecked") @Override public void onCompleted() { - items.add(COMPLETE_SENTINEL); + items.add(on.completed()); tick(); } @@ -248,11 +250,7 @@ public void onError(Throwable e) { @SuppressWarnings("unchecked") @Override public void onNext(Object t) { - if (t == null) { - items.add(NULL_SENTINEL); - } else { - items.add(t); - } + items.add(on.next(t)); tick(); } }; From ceff938a5b31dc992d2db0e8aa5e4b1735b0468e Mon Sep 17 00:00:00 2001 From: akarnokd Date: Thu, 20 Mar 2014 13:59:28 +0100 Subject: [PATCH 140/422] Fixed deadlock in Subjects + OperatorCache. --- rxjava-core/src/main/java/rx/Observable.java | 4 +- ...OperationCache.java => OperatorCache.java} | 52 +++--- .../main/java/rx/subjects/AsyncSubject.java | 23 ++- .../java/rx/subjects/BehaviorSubject.java | 25 +-- .../main/java/rx/subjects/PublishSubject.java | 20 ++- .../main/java/rx/subjects/ReplaySubject.java | 26 +-- .../subjects/SubjectSubscriptionManager.java | 8 +- .../java/rx/operators/OperationCacheTest.java | 87 --------- .../java/rx/operators/OperatorCacheTest.java | 168 ++++++++++++++++++ 9 files changed, 262 insertions(+), 151 deletions(-) rename rxjava-core/src/main/java/rx/operators/{OperationCache.java => OperatorCache.java} (56%) delete mode 100644 rxjava-core/src/test/java/rx/operators/OperationCacheTest.java create mode 100644 rxjava-core/src/test/java/rx/operators/OperatorCacheTest.java diff --git a/rxjava-core/src/main/java/rx/Observable.java b/rxjava-core/src/main/java/rx/Observable.java index 235e1e62e4..bc20108ade 100644 --- a/rxjava-core/src/main/java/rx/Observable.java +++ b/rxjava-core/src/main/java/rx/Observable.java @@ -56,7 +56,6 @@ import rx.operators.OperationAsObservable; import rx.operators.OperationAverage; import rx.operators.OperationBuffer; -import rx.operators.OperationCache; import rx.operators.OperationCombineLatest; import rx.operators.OperationConcat; import rx.operators.OperationDebounce; @@ -105,6 +104,7 @@ import rx.operators.OperationUsing; import rx.operators.OperationWindow; import rx.operators.OperatorAmb; +import rx.operators.OperatorCache; import rx.operators.OperatorCast; import rx.operators.OperatorDoOnEach; import rx.operators.OperatorFilter; @@ -3863,7 +3863,7 @@ public final Observable> buffer(Observable boundary, int initialC * @see RxJava Wiki: cache() */ public final Observable cache() { - return create(OperationCache.cache(this)); + return create(new OperatorCache(this)); } /** diff --git a/rxjava-core/src/main/java/rx/operators/OperationCache.java b/rxjava-core/src/main/java/rx/operators/OperatorCache.java similarity index 56% rename from rxjava-core/src/main/java/rx/operators/OperationCache.java rename to rxjava-core/src/main/java/rx/operators/OperatorCache.java index eb10bc9ab6..6f3385f1de 100644 --- a/rxjava-core/src/main/java/rx/operators/OperationCache.java +++ b/rxjava-core/src/main/java/rx/operators/OperatorCache.java @@ -18,10 +18,13 @@ import java.util.concurrent.atomic.AtomicBoolean; import rx.Observable; +import rx.Observable.OnSubscribe; import rx.Observable.OnSubscribeFunc; import rx.Observer; +import rx.Subscriber; import rx.Subscription; import rx.subjects.ReplaySubject; +import rx.subjects.Subject; /** * This method has similar behavior to {@link Observable#replay()} except that this auto-subscribes @@ -35,30 +38,35 @@ * NOTE: You sacrifice the ability to unsubscribe from the origin when you use this operator, so be * careful not to use this operator on Observables that emit infinite or very large numbers of * items, as this will use up memory. + * + * @param the cached value type */ -public class OperationCache { - - public static OnSubscribeFunc cache(final Observable source) { - return new OnSubscribeFunc() { - - final AtomicBoolean subscribed = new AtomicBoolean(false); - private final ReplaySubject cache = ReplaySubject.create(); +public final class OperatorCache implements OnSubscribe { + protected final Observable source; + protected final Subject cache; + protected final AtomicBoolean sourceSubscribed; - @Override - public Subscription onSubscribe(Observer observer) { - if (subscribed.compareAndSet(false, true)) { - // subscribe to the source once - source.subscribe(cache); - /* - * Note that we will never unsubscribe from 'source' as we want to receive and cache all of its values. - * - * This means this should never be used on an infinite or very large sequence, similar to toList(). - */ - } - - return cache.subscribe(observer); - } + public OperatorCache(Observable source) { + this(source, ReplaySubject.create()); + } + + /** Test support. */ + public OperatorCache(Observable source, Subject cache) { + this.source = source; + this.cache = cache; + this.sourceSubscribed = new AtomicBoolean(); + } - }; + @Override + public void call(Subscriber t1) { + if (sourceSubscribed.compareAndSet(false, true)) { + source.subscribe(cache); + /* + * Note that we will never unsubscribe from 'source' as we want to receive and cache all of its values. + * + * This means this should never be used on an infinite or very large sequence, similar to toList(). + */ + } + cache.subscribe(t1); } } diff --git a/rxjava-core/src/main/java/rx/subjects/AsyncSubject.java b/rxjava-core/src/main/java/rx/subjects/AsyncSubject.java index 7b50cd9468..c2302d9890 100644 --- a/rxjava-core/src/main/java/rx/subjects/AsyncSubject.java +++ b/rxjava-core/src/main/java/rx/subjects/AsyncSubject.java @@ -105,35 +105,40 @@ protected AsyncSubject(OnSubscribe onSubscribe, SubjectSubscriptionManager @Override public void onCompleted() { + Collection> observers = subscriptionManager.terminate(new Action1>>() { @Override public void call(Collection> observers) { - for (Observer o : observers) { - emitValueToObserver(lastNotification.get(), o); - } } }); + if (observers != null) { + for (Observer o : observers) { + emitValueToObserver(lastNotification.get(), o); + } + } } @Override public void onError(final Throwable e) { + Collection> observers = subscriptionManager.terminate(new Action1>>() { - @Override public void call(Collection> observers) { - lastNotification.set(new Notification(e)); - for (Observer o : observers) { - emitValueToObserver(lastNotification.get(), o); - } + lastNotification.set(Notification.createOnError(e)); } }); + if (observers != null) { + for (Observer o : observers) { + emitValueToObserver(lastNotification.get(), o); + } + } } @Override public void onNext(T v) { - lastNotification.set(new Notification(v)); + lastNotification.set(Notification.createOnNext(v)); } } \ No newline at end of file diff --git a/rxjava-core/src/main/java/rx/subjects/BehaviorSubject.java b/rxjava-core/src/main/java/rx/subjects/BehaviorSubject.java index f59dc03133..5f2010fd85 100644 --- a/rxjava-core/src/main/java/rx/subjects/BehaviorSubject.java +++ b/rxjava-core/src/main/java/rx/subjects/BehaviorSubject.java @@ -144,31 +144,36 @@ protected BehaviorSubject(OnSubscribe onSubscribe, SubjectSubscriptionManager @Override public void onCompleted() { + Collection> observers = subscriptionManager.terminate(new Action1>>() { @Override public void call(Collection> observers) { - lastNotification.set(new Notification()); - for (Observer o : observers) { - o.onCompleted(); - } + lastNotification.set(Notification.createOnCompleted()); } }); + if (observers != null) { + for (Observer o : observers) { + o.onCompleted(); + } + } } @Override public void onError(final Throwable e) { + Collection> observers = subscriptionManager.terminate(new Action1>>() { @Override public void call(Collection> observers) { - lastNotification.set(new Notification(e)); - for (Observer o : observers) { - o.onError(e); - } + lastNotification.set(Notification.createOnError(e)); } }); - + if (observers != null) { + for (Observer o : observers) { + o.onError(e); + } + } } @Override @@ -176,7 +181,7 @@ public void onNext(T v) { // do not overwrite a terminal notification // so new subscribers can get them if (lastNotification.get().isOnNext()) { - lastNotification.set(new Notification(v)); + lastNotification.set(Notification.createOnNext(v)); for (Observer o : subscriptionManager.rawSnapshot()) { o.onNext(v); } diff --git a/rxjava-core/src/main/java/rx/subjects/PublishSubject.java b/rxjava-core/src/main/java/rx/subjects/PublishSubject.java index 4d94a087fd..687d039e6a 100644 --- a/rxjava-core/src/main/java/rx/subjects/PublishSubject.java +++ b/rxjava-core/src/main/java/rx/subjects/PublishSubject.java @@ -95,31 +95,35 @@ protected PublishSubject(OnSubscribe onSubscribe, SubjectSubscriptionManager< @Override public void onCompleted() { + Collection> observers = subscriptionManager.terminate(new Action1>>() { - @Override public void call(Collection> observers) { lastNotification.set(Notification. createOnCompleted()); - for (Observer o : observers) { - o.onCompleted(); - } } }); + if (observers != null) { + for (Observer o : observers) { + o.onCompleted(); + } + } } @Override public void onError(final Throwable e) { + Collection> observers = subscriptionManager.terminate(new Action1>>() { @Override public void call(Collection> observers) { lastNotification.set(Notification.createOnError(e)); - for (Observer o : observers) { - o.onError(e); - } } }); - + if (observers != null) { + for (Observer o : observers) { + o.onError(e); + } + } } @Override diff --git a/rxjava-core/src/main/java/rx/subjects/ReplaySubject.java b/rxjava-core/src/main/java/rx/subjects/ReplaySubject.java index 7d659822c2..01107efa20 100644 --- a/rxjava-core/src/main/java/rx/subjects/ReplaySubject.java +++ b/rxjava-core/src/main/java/rx/subjects/ReplaySubject.java @@ -122,34 +122,40 @@ protected ReplaySubject(OnSubscribe onSubscribe, SubjectSubscriptionManager> observers = subscriptionManager.terminate(new Action1>>() { @Override public void call(Collection> observers) { state.history.complete(Notification.createOnCompleted()); - for (SubjectObserver o : observers) { - if (caughtUp(o)) { - o.onCompleted(); - } - } } }); + if (observers != null) { + for (SubjectObserver o : observers) { + if (caughtUp(o)) { + o.onCompleted(); + } + } + } } @Override public void onError(final Throwable e) { + Collection> observers = subscriptionManager.terminate(new Action1>>() { @Override public void call(Collection> observers) { state.history.complete(Notification.createOnError(e)); - for (SubjectObserver o : observers) { - if (caughtUp(o)) { - o.onError(e); - } - } } }); + if (observers != null) { + for (SubjectObserver o : observers) { + if (caughtUp(o)) { + o.onError(e); + } + } + } } @Override diff --git a/rxjava-core/src/main/java/rx/subjects/SubjectSubscriptionManager.java b/rxjava-core/src/main/java/rx/subjects/SubjectSubscriptionManager.java index 6f049eb38a..8022f9723d 100644 --- a/rxjava-core/src/main/java/rx/subjects/SubjectSubscriptionManager.java +++ b/rxjava-core/src/main/java/rx/subjects/SubjectSubscriptionManager.java @@ -113,19 +113,20 @@ public void call() { } @SuppressWarnings({ "unchecked", "rawtypes" }) - protected void terminate(Action1>> onTerminate) { + protected Collection> terminate(Action1>> onTerminate) { State current; State newState = null; do { current = state.get(); if (current.terminated) { // already terminated so do nothing - return; + return null; } else { newState = current.terminate(); } } while (!state.compareAndSet(current, newState)); + Collection> observerCollection = (Collection)Arrays.asList(newState.observers); /* * if we get here then we won setting the state to terminated * and have a deterministic set of Observers to emit to (concurrent subscribes @@ -134,11 +135,12 @@ protected void terminate(Action1>> onTermi */ try { // had to circumvent type check, we know what the array contains - onTerminate.call((Collection) Arrays.asList(newState.observers)); + onTerminate.call(observerCollection); } finally { // mark that termination is completed newState.terminationLatch.countDown(); } + return observerCollection; } /** diff --git a/rxjava-core/src/test/java/rx/operators/OperationCacheTest.java b/rxjava-core/src/test/java/rx/operators/OperationCacheTest.java deleted file mode 100644 index 8637eb8521..0000000000 --- a/rxjava-core/src/test/java/rx/operators/OperationCacheTest.java +++ /dev/null @@ -1,87 +0,0 @@ -/** - * Copyright 2014 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package rx.operators; - -import static org.junit.Assert.*; -import static rx.operators.OperationCache.*; - -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; - -import org.junit.Test; - -import rx.Observable; -import rx.Observer; -import rx.Subscription; -import rx.functions.Action1; -import rx.subscriptions.BooleanSubscription; - -public class OperationCacheTest { - - @Test - public void testCache() throws InterruptedException { - final AtomicInteger counter = new AtomicInteger(); - Observable o = Observable.create(cache(Observable.create(new Observable.OnSubscribeFunc() { - - @Override - public Subscription onSubscribe(final Observer observer) { - final BooleanSubscription subscription = new BooleanSubscription(); - new Thread(new Runnable() { - - @Override - public void run() { - counter.incrementAndGet(); - System.out.println("published observable being executed"); - observer.onNext("one"); - observer.onCompleted(); - } - }).start(); - return subscription; - } - }))); - - // we then expect the following 2 subscriptions to get that same value - final CountDownLatch latch = new CountDownLatch(2); - - // subscribe once - o.subscribe(new Action1() { - - @Override - public void call(String v) { - assertEquals("one", v); - System.out.println("v: " + v); - latch.countDown(); - } - }); - - // subscribe again - o.subscribe(new Action1() { - - @Override - public void call(String v) { - assertEquals("one", v); - System.out.println("v: " + v); - latch.countDown(); - } - }); - - if (!latch.await(1000, TimeUnit.MILLISECONDS)) { - fail("subscriptions did not receive values"); - } - assertEquals(1, counter.get()); - } -} diff --git a/rxjava-core/src/test/java/rx/operators/OperatorCacheTest.java b/rxjava-core/src/test/java/rx/operators/OperatorCacheTest.java new file mode 100644 index 0000000000..3a6c170c56 --- /dev/null +++ b/rxjava-core/src/test/java/rx/operators/OperatorCacheTest.java @@ -0,0 +1,168 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package rx.operators; + +import java.util.Arrays; +import static org.junit.Assert.*; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Test; + +import rx.Observable; +import rx.Observer; +import rx.Subscription; +import rx.functions.Action1; +import rx.functions.Func1; +import rx.functions.Func2; +import rx.observers.TestObserver; +import rx.schedulers.Schedulers; +import rx.subjects.AsyncSubject; +import rx.subjects.BehaviorSubject; +import rx.subjects.PublishSubject; +import rx.subjects.ReplaySubject; +import rx.subjects.Subject; +import rx.subscriptions.BooleanSubscription; + +public class OperatorCacheTest { + + @Test + public void testCache() throws InterruptedException { + final AtomicInteger counter = new AtomicInteger(); + Observable o = Observable.create(new Observable.OnSubscribeFunc() { + + @Override + public Subscription onSubscribe(final Observer observer) { + final BooleanSubscription subscription = new BooleanSubscription(); + new Thread(new Runnable() { + + @Override + public void run() { + counter.incrementAndGet(); + System.out.println("published observable being executed"); + observer.onNext("one"); + observer.onCompleted(); + } + }).start(); + return subscription; + } + }).cache(); + + // we then expect the following 2 subscriptions to get that same value + final CountDownLatch latch = new CountDownLatch(2); + + // subscribe once + o.subscribe(new Action1() { + + @Override + public void call(String v) { + assertEquals("one", v); + System.out.println("v: " + v); + latch.countDown(); + } + }); + + // subscribe again + o.subscribe(new Action1() { + + @Override + public void call(String v) { + assertEquals("one", v); + System.out.println("v: " + v); + latch.countDown(); + } + }); + + if (!latch.await(1000, TimeUnit.MILLISECONDS)) { + fail("subscriptions did not receive values"); + } + assertEquals(1, counter.get()); + } + void testWithCustomSubjectAndRepeat(Subject subject, Integer... expected) { + Observable source0 = Observable.from(1, 2, 3) + .subscribeOn(Schedulers.io()) + .flatMap(new Func1>() { + @Override + public Observable call(final Integer i) { + return Observable.timer(i * 20, TimeUnit.MILLISECONDS).map(new Func1() { + @Override + public Integer call(Long t1) { + return i; + } + }); + } + }); + + Observable source1 = Observable.create(new OperatorCache(source0, subject)); + + Observable source2 = source1 + .repeat(4) + .zip(Observable.timer(0, 10, TimeUnit.MILLISECONDS, Schedulers.newThread()), new Func2() { + @Override + public Integer call(Integer t1, Long t2) { + return t1; + } + + }); + final CountDownLatch cdl = new CountDownLatch(1); + TestObserver test = new TestObserver(new Observer() { + @Override + public void onNext(Integer t) { + } + + @Override + public void onError(Throwable e) { + cdl.countDown(); + } + + @Override + public void onCompleted() { + cdl.countDown(); + } + }); + source2.subscribe(test); + + try { + cdl.await(20, TimeUnit.SECONDS); + } catch (InterruptedException ex) { + fail("Interrupted"); + } + + test.assertReceivedOnNext(Arrays.asList(expected)); + test.assertTerminalEvent(); + assertTrue(test.getOnErrorEvents().isEmpty()); + } + @Test(timeout = 10000) + public void testWithAsyncSubjectAndRepeat() { + testWithCustomSubjectAndRepeat(AsyncSubject.create(), 3, 3, 3, 3); + } + @Test(timeout = 10000) + public void testWithBehaviorSubjectAndRepeat() { + // BehaviorSubject just completes when repeated + testWithCustomSubjectAndRepeat(BehaviorSubject.create(0), 0, 1, 2, 3); + } + @Test(timeout = 10000) + public void testWithPublishSubjectAndRepeat() { + // PublishSubject just completes when repeated + testWithCustomSubjectAndRepeat(PublishSubject.create(), 1, 2, 3); + } + @Test(timeout = 10000) + public void testWithReplaySubjectAndRepeat() { + testWithCustomSubjectAndRepeat(ReplaySubject.create(), 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3); + } +} From 352e7fa5b44b586b3cd26f067975384e5494ff06 Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Thu, 20 Mar 2014 09:53:56 -0700 Subject: [PATCH 141/422] Merge with Serialize - concurrency unit tests - handle Observable that emits onComplete multiple times --- .../main/java/rx/operators/OperatorMerge.java | 13 +- .../java/rx/operators/OperatorMergeTest.java | 120 +++++++++++++++++- 2 files changed, 127 insertions(+), 6 deletions(-) diff --git a/rxjava-core/src/main/java/rx/operators/OperatorMerge.java b/rxjava-core/src/main/java/rx/operators/OperatorMerge.java index b3d822ca84..15843ff19f 100644 --- a/rxjava-core/src/main/java/rx/operators/OperatorMerge.java +++ b/rxjava-core/src/main/java/rx/operators/OperatorMerge.java @@ -68,15 +68,22 @@ public void onNext(Observable innerObservable) { final class InnerObserver extends Subscriber { + private boolean innerCompleted = false; + public InnerObserver() { } @Override public void onCompleted() { - if (runningCount.decrementAndGet() == 0 && completed) { - o.onCompleted(); + if (!innerCompleted) { + // we check if already completed otherwise a misbehaving Observable that emits onComplete more than once + // will cause the runningCount to decrement multiple times. + innerCompleted = true; + if (runningCount.decrementAndGet() == 0 && completed) { + o.onCompleted(); + } + cleanup(); } - cleanup(); } @Override diff --git a/rxjava-core/src/test/java/rx/operators/OperatorMergeTest.java b/rxjava-core/src/test/java/rx/operators/OperatorMergeTest.java index 4afd9c07b8..934513d0db 100644 --- a/rxjava-core/src/test/java/rx/operators/OperatorMergeTest.java +++ b/rxjava-core/src/test/java/rx/operators/OperatorMergeTest.java @@ -15,9 +15,14 @@ */ package rx.operators; -import static org.junit.Assert.*; -import static org.mockito.Matchers.*; -import static org.mockito.Mockito.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import java.util.ArrayList; import java.util.Arrays; @@ -37,6 +42,7 @@ import rx.Observable.OnSubscribe; import rx.Observer; import rx.Scheduler; +import rx.Scheduler.Inner; import rx.Subscriber; import rx.Subscription; import rx.functions.Action0; @@ -472,4 +478,112 @@ public void call() { }); } + @Test + public void testConcurrency() { + + Observable o = Observable.create(new OnSubscribe() { + + @Override + public void call(final Subscriber s) { + Schedulers.newThread().schedule(new Action1() { + + @Override + public void call(Inner inner) { + for (int i = 0; i < 10000; i++) { + s.onNext(1); + } + s.onCompleted(); + } + + }); + } + }); + + for (int i = 0; i < 10; i++) { + Observable merge = Observable.merge(o, o, o); + TestSubscriber ts = new TestSubscriber(); + merge.subscribe(ts); + + ts.awaitTerminalEvent(); + assertEquals(1, ts.getOnCompletedEvents().size()); + assertEquals(30000, ts.getOnNextEvents().size()); + List onNextEvents = ts.getOnNextEvents(); + // System.out.println("onNext: " + onNextEvents.size() + " onCompleted: " + ts.getOnCompletedEvents().size()); + } + } + + @Test + public void testConcurrencyWithSleeping() { + + Observable o = Observable.create(new OnSubscribe() { + + @Override + public void call(final Subscriber s) { + Schedulers.newThread().schedule(new Action1() { + + @Override + public void call(Inner inner) { + for (int i = 0; i < 100; i++) { + s.onNext(1); + try { + Thread.sleep(1); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + s.onCompleted(); + } + + }); + } + }); + + for (int i = 0; i < 10; i++) { + Observable merge = Observable.merge(o, o, o); + TestSubscriber ts = new TestSubscriber(); + merge.subscribe(ts); + + ts.awaitTerminalEvent(); + assertEquals(1, ts.getOnCompletedEvents().size()); + assertEquals(300, ts.getOnNextEvents().size()); + List onNextEvents = ts.getOnNextEvents(); + // System.out.println("onNext: " + onNextEvents.size() + " onCompleted: " + ts.getOnCompletedEvents().size()); + } + } + + @Test + public void testConcurrencyWithBrokenOnCompleteContract() { + + Observable o = Observable.create(new OnSubscribe() { + + @Override + public void call(final Subscriber s) { + Schedulers.newThread().schedule(new Action1() { + + @Override + public void call(Inner inner) { + for (int i = 0; i < 10000; i++) { + s.onNext(1); + } + s.onCompleted(); + s.onCompleted(); + s.onCompleted(); + } + + }); + } + }); + + for (int i = 0; i < 100; i++) { + Observable merge = Observable.merge(o, o, o); + TestSubscriber ts = new TestSubscriber(); + merge.subscribe(ts); + + ts.awaitTerminalEvent(); + assertEquals(1, ts.getOnCompletedEvents().size()); + assertEquals(30000, ts.getOnNextEvents().size()); + List onNextEvents = ts.getOnNextEvents(); + // System.out.println("onNext: " + onNextEvents.size() + " onCompleted: " + ts.getOnCompletedEvents().size()); + } + } } From 9785ada2e5825b5882e225fc25ec34d073485f1f Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Thu, 20 Mar 2014 10:38:58 -0700 Subject: [PATCH 142/422] TestSubject, TestObserver and TestScheduler Improvements --- .../main/java/rx/observers/TestObserver.java | 4 +- .../java/rx/schedulers/TestScheduler.java | 8 +- .../main/java/rx/subjects/TestSubject.java | 182 ++++++++++++++++++ .../java/rx/observers/TestObserverTest.java | 2 +- 4 files changed, 191 insertions(+), 5 deletions(-) create mode 100644 rxjava-core/src/main/java/rx/subjects/TestSubject.java diff --git a/rxjava-core/src/main/java/rx/observers/TestObserver.java b/rxjava-core/src/main/java/rx/observers/TestObserver.java index f4e278118d..dd546359c8 100644 --- a/rxjava-core/src/main/java/rx/observers/TestObserver.java +++ b/rxjava-core/src/main/java/rx/observers/TestObserver.java @@ -27,7 +27,6 @@ */ public class TestObserver implements Observer { - private final Observer delegate; private final ArrayList onNextEvents = new ArrayList(); private final ArrayList onErrorEvents = new ArrayList(); @@ -91,7 +90,8 @@ public void assertReceivedOnNext(List items) { throw new AssertionError("Value at index: " + i + " expected to be [null] but was: [" + onNextEvents.get(i) + "]"); } } else if (!items.get(i).equals(onNextEvents.get(i))) { - throw new AssertionError("Value at index: " + i + " expected to be [" + items.get(i) + "] but was: [" + onNextEvents.get(i) + "]"); + throw new AssertionError("Value at index: " + i + " expected to be [" + items.get(i) + "] (" + items.get(i).getClass().getSimpleName() + ") but was: [" + onNextEvents.get(i) + "] (" + onNextEvents.get(i).getClass().getSimpleName() + ")"); + } } diff --git a/rxjava-core/src/main/java/rx/schedulers/TestScheduler.java b/rxjava-core/src/main/java/rx/schedulers/TestScheduler.java index 2bb9a11d40..1231c71fa4 100644 --- a/rxjava-core/src/main/java/rx/schedulers/TestScheduler.java +++ b/rxjava-core/src/main/java/rx/schedulers/TestScheduler.java @@ -97,9 +97,13 @@ private void triggerActions(long targetTimeInNanos) { time = targetTimeInNanos; } + public Inner createInnerScheduler() { + return new InnerTestScheduler(); + } + @Override public Subscription schedule(Action1 action, long delayTime, TimeUnit unit) { - InnerTestScheduler inner = new InnerTestScheduler(); + Inner inner = createInnerScheduler(); final TimedAction timedAction = new TimedAction(inner, time + unit.toNanos(delayTime), action); queue.add(timedAction); return inner; @@ -107,7 +111,7 @@ public Subscription schedule(Action1 action, long delayTime, TimeUnit uni @Override public Subscription schedule(Action1 action) { - InnerTestScheduler inner = new InnerTestScheduler(); + Inner inner = createInnerScheduler(); final TimedAction timedAction = new TimedAction(inner, 0, action); queue.add(timedAction); return inner; diff --git a/rxjava-core/src/main/java/rx/subjects/TestSubject.java b/rxjava-core/src/main/java/rx/subjects/TestSubject.java new file mode 100644 index 0000000000..af3a02f3ee --- /dev/null +++ b/rxjava-core/src/main/java/rx/subjects/TestSubject.java @@ -0,0 +1,182 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package rx.subjects; + +import java.util.Collection; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import rx.Notification; +import rx.Observer; +import rx.Scheduler; +import rx.Scheduler.Inner; +import rx.functions.Action1; +import rx.schedulers.TestScheduler; +import rx.subjects.SubjectSubscriptionManager.SubjectObserver; + +/** + * Subject that, once and {@link Observer} has subscribed, publishes all subsequent events to the subscriber. + *

    + * + *

    + * Example usage: + *

    + *

     {@code
    +
    + * PublishSubject subject = PublishSubject.create();
    +  // observer1 will receive all onNext and onCompleted events
    +  subject.subscribe(observer1);
    +  subject.onNext("one");
    +  subject.onNext("two");
    +  // observer2 will only receive "three" and onCompleted
    +  subject.subscribe(observer2);
    +  subject.onNext("three");
    +  subject.onCompleted();
    +
    +  } 
    + * 
    + * @param 
    + */
    +public final class TestSubject extends Subject {
    +
    +    public static  TestSubject create(TestScheduler scheduler) {
    +        final SubjectSubscriptionManager subscriptionManager = new SubjectSubscriptionManager();
    +        // set a default value so subscriptions will immediately receive this until a new notification is received
    +        final AtomicReference> lastNotification = new AtomicReference>();
    +
    +        OnSubscribe onSubscribe = subscriptionManager.getOnSubscribeFunc(
    +                /**
    +                 * This function executes at beginning of subscription.
    +                 * 
    +                 * This will always run, even if Subject is in terminal state.
    +                 */
    +                new Action1>() {
    +
    +                    @Override
    +                    public void call(SubjectObserver o) {
    +                        // nothing onSubscribe unless in terminal state which is the next function
    +                    }
    +                },
    +                /**
    +                 * This function executes if the Subject is terminated before subscription occurs.
    +                 */
    +                new Action1>() {
    +
    +                    @Override
    +                    public void call(SubjectObserver o) {
    +                        /*
    +                         * If we are already terminated, or termination happens while trying to subscribe
    +                         * this will be invoked and we emit whatever the last terminal value was.
    +                         */
    +                        lastNotification.get().accept(o);
    +                    }
    +                });
    +
    +        return new TestSubject(onSubscribe, subscriptionManager, lastNotification, scheduler);
    +    }
    +
    +    private final SubjectSubscriptionManager subscriptionManager;
    +    private final AtomicReference> lastNotification;
    +    private final Scheduler.Inner innerScheduler;
    +
    +    protected TestSubject(OnSubscribe onSubscribe, SubjectSubscriptionManager subscriptionManager, AtomicReference> lastNotification, TestScheduler scheduler) {
    +        super(onSubscribe);
    +        this.subscriptionManager = subscriptionManager;
    +        this.lastNotification = lastNotification;
    +        this.innerScheduler = scheduler.createInnerScheduler();
    +    }
    +
    +    @Override
    +    public void onCompleted() {
    +        onCompleted(innerScheduler.now());
    +    }
    +
    +    private void _onCompleted() {
    +        subscriptionManager.terminate(new Action1>>() {
    +
    +            @Override
    +            public void call(Collection> observers) {
    +                lastNotification.set(Notification. createOnCompleted());
    +                for (Observer o : observers) {
    +                    o.onCompleted();
    +                }
    +            }
    +        });
    +    }
    +
    +    public void onCompleted(long timeInMilliseconds) {
    +        innerScheduler.schedule(new Action1() {
    +
    +            @Override
    +            public void call(Inner t1) {
    +                _onCompleted();
    +            }
    +
    +        }, timeInMilliseconds, TimeUnit.MILLISECONDS);
    +    }
    +
    +    @Override
    +    public void onError(final Throwable e) {
    +        onError(e, innerScheduler.now());
    +    }
    +
    +    private void _onError(final Throwable e) {
    +        subscriptionManager.terminate(new Action1>>() {
    +
    +            @Override
    +            public void call(Collection> observers) {
    +                lastNotification.set(Notification. createOnError(e));
    +                for (Observer o : observers) {
    +                    o.onError(e);
    +                }
    +            }
    +        });
    +
    +    }
    +
    +    public void onError(final Throwable e, long timeInMilliseconds) {
    +        innerScheduler.schedule(new Action1() {
    +
    +            @Override
    +            public void call(Inner t1) {
    +                _onError(e);
    +            }
    +
    +        }, timeInMilliseconds, TimeUnit.MILLISECONDS);
    +    }
    +
    +    @Override
    +    public void onNext(T v) {
    +        onNext(v, innerScheduler.now());
    +    }
    +
    +    private void _onNext(T v) {
    +        for (Observer o : subscriptionManager.rawSnapshot()) {
    +            o.onNext(v);
    +        }
    +    }
    +
    +    public void onNext(final T v, long timeInMilliseconds) {
    +        innerScheduler.schedule(new Action1() {
    +
    +            @Override
    +            public void call(Inner t1) {
    +                _onNext(v);
    +            }
    +
    +        }, timeInMilliseconds, TimeUnit.MILLISECONDS);
    +    }
    +}
    \ No newline at end of file
    diff --git a/rxjava-core/src/test/java/rx/observers/TestObserverTest.java b/rxjava-core/src/test/java/rx/observers/TestObserverTest.java
    index 70e22d8fb2..67883043bd 100644
    --- a/rxjava-core/src/test/java/rx/observers/TestObserverTest.java
    +++ b/rxjava-core/src/test/java/rx/observers/TestObserverTest.java
    @@ -66,7 +66,7 @@ public void testAssertNotMatchValue() {
             oi.subscribe(o);
     
             thrown.expect(AssertionError.class);
    -        thrown.expectMessage("Value at index: 1 expected to be [3] but was: [2]");
    +        thrown.expectMessage("Value at index: 1 expected to be [3] (Integer) but was: [2] (Integer)");
     
             o.assertReceivedOnNext(Arrays.asList(1, 3));
             assertEquals(2, o.getOnNextEvents().size());
    
    From cbb6fda7364048c12fe931150eeb5afc8032cc24 Mon Sep 17 00:00:00 2001
    From: Ben Christensen 
    Date: Thu, 20 Mar 2014 10:45:56 -0700
    Subject: [PATCH 143/422] GroupBy & Time Gap Fixes
    
    I ran head-on into the "time gap" (https://github.com/Netflix/RxJava/issues/844) issue while working on a stream processing use case (and new 'pivot' operator I'm writing).
    This is a solution. It's still not ideal as the Javadocs of BufferUntilSubscriber mention, but this is working better than nothing and does not require blocking threads.
    A better solution will come as part of the back pressure work where BufferUntilSubscriber will evolve to have a bounded buffer.
    ---
     .../rx/operators/BufferUntilSubscriber.java   | 271 ++++++------
     .../java/rx/operators/OperatorGroupBy.java    |  56 +--
     .../operators/OperatorSubscribeOnBounded.java | 112 -----
     .../rx/operators/OperatorGroupByTest.java     |  37 +-
     .../OperatorSubscribeOnBoundedTest.java       | 416 ------------------
     5 files changed, 211 insertions(+), 681 deletions(-)
     delete mode 100644 rxjava-core/src/main/java/rx/operators/OperatorSubscribeOnBounded.java
     delete mode 100644 rxjava-core/src/test/java/rx/operators/OperatorSubscribeOnBoundedTest.java
    
    diff --git a/rxjava-core/src/main/java/rx/operators/BufferUntilSubscriber.java b/rxjava-core/src/main/java/rx/operators/BufferUntilSubscriber.java
    index c849421ee5..d26f84debc 100644
    --- a/rxjava-core/src/main/java/rx/operators/BufferUntilSubscriber.java
    +++ b/rxjava-core/src/main/java/rx/operators/BufferUntilSubscriber.java
    @@ -15,152 +15,175 @@
      */
     package rx.operators;
     
    -import java.util.LinkedList;
    -import java.util.Queue;
    +import java.util.concurrent.ConcurrentLinkedQueue;
    +import java.util.concurrent.atomic.AtomicReference;
     
    +import rx.Observable;
    +import rx.Observer;
     import rx.Subscriber;
    -import rx.subscriptions.CompositeSubscription;
     
     /**
    - * Buffers the incoming events until notified, then replays the
    - * buffered events and continues as a simple pass-through subscriber.
    - * @param  the streamed value type
    + * A solution to the "time gap" problem that occurs with `groupBy` and `pivot` => https://github.com/Netflix/RxJava/issues/844
    + * 
    + * This currently has temporary unbounded buffers. It needs to become bounded and then do one of two things:
    + * 
    + * 1) blow up and make the user do something about it
    + * 2) work with the backpressure solution ... still to be implemented (such as co-routines)
    + * 
    + * Generally the buffer should be very short lived (milliseconds) and then stops being involved.
    + * It can become a memory leak though if a GroupedObservable backed by this class is emitted but never subscribed to (such as filtered out).
    + * In that case, either a time-bomb to throw away the buffer, or just blowing up and making the user do something about it is needed.
    + * 
    + * For example, to filter out GroupedObservables, perhaps they need a silent `subscribe()` on them to just blackhole the data.
    + * 
    + * This is an initial start at solving this problem and solves the immediate problem of `groupBy` and `pivot` and trades off the possibility of memory leak for deterministic functionality.
    + *
    + * @param 
      */
    -public class BufferUntilSubscriber extends Subscriber {
    -    /** The actual subscriber. */
    -    private final Subscriber actual;
    -    /** Indicate the pass-through mode. */
    -    private volatile boolean passthroughMode;
    -    /** Protect mode transition. */
    -    private final Object gate = new Object();
    -    /** The buffered items. */
    -    private final Queue queue = new LinkedList();
    -    /** The queue capacity. */
    -    private final int capacity;
    -    private final NotificationLite on = NotificationLite.instance();
    +public class BufferUntilSubscriber extends Observable implements Observer {
     
    -    /**
    -     * Constructor that wraps the actual subscriber and shares its subscription.
    -     * @param capacity the queue capacity to accept before blocking, negative value indicates an unbounded queue
    -     * @param actual
    -     */
    -    public BufferUntilSubscriber(int capacity, Subscriber actual) {
    -        super(actual);
    -        this.actual = actual;
    -        this.capacity = capacity;
    -    }
    -    /**
    -     * Constructor that wraps the actual subscriber and uses the given composite
    -     * subscription.
    -     * @param capacity the queue capacity to accept before blocking, negative value indicates an unbounded queue
    -     * @param actual
    -     * @param cs 
    -     */
    -    public BufferUntilSubscriber(int capacity, Subscriber actual, CompositeSubscription cs) {
    -        super(cs);
    -        this.actual = actual;
    -        this.capacity = capacity;
    +    public static  BufferUntilSubscriber create() {
    +        return new BufferUntilSubscriber(new AtomicReference>(new BufferedObserver()));
         }
    -    
    -    /**
    -     * Call this method to replay the buffered events and continue as a pass-through subscriber.
    -     * If already in pass-through mode, this method is a no-op.
    -     */
    -    public void enterPassthroughMode() {
    -        if (!passthroughMode) {
    -            synchronized (gate) {
    -                if (!passthroughMode) {
    -                    while (!queue.isEmpty()) {
    -                        Object o = queue.poll();
    -                        if (!actual.isUnsubscribed()) {
    -                            on.accept(actual, o);
    -                        }
    -                    }
    -                    passthroughMode = true;
    -                    gate.notifyAll();
    +
    +    private final AtomicReference> observerRef;
    +
    +    private BufferUntilSubscriber(final AtomicReference> observerRef) {
    +        super(new OnSubscribe() {
    +
    +            @Override
    +            public void call(Subscriber s) {
    +                // drain queued notifications before subscription
    +                // we do this here before PassThruObserver so the consuming thread can do this before putting itself in the line of the producer
    +                BufferedObserver buffered = (BufferedObserver) observerRef.get();
    +                Object o = null;
    +                while ((o = buffered.buffer.poll()) != null) {
    +                    emit(s, o);
                     }
    +                // register real observer for pass-thru ... and drain any further events received on first notification
    +                observerRef.set(new PassThruObserver(s, buffered.buffer, observerRef));
                 }
    -        }
    +
    +        });
    +        this.observerRef = observerRef;
    +    }
    +
    +    @Override
    +    public void onCompleted() {
    +        observerRef.get().onCompleted();
         }
    +
    +    @Override
    +    public void onError(Throwable e) {
    +        observerRef.get().onError(e);
    +    }
    +
         @Override
         public void onNext(T t) {
    -        if (!passthroughMode) {
    -            synchronized (gate) {
    -                if (!passthroughMode) {
    -                    if (capacity < 0 || queue.size() < capacity) {
    -                        queue.offer(on.next(t));
    -                        return;
    -                    }
    -                    try {
    -                        while (!passthroughMode) {
    -                            gate.wait();
    -                        }
    -                        if (actual.isUnsubscribed()) {
    -                            return;
    -                        }
    -                    } catch (InterruptedException ex) {
    -                        Thread.currentThread().interrupt();
    -                        actual.onError(ex);
    -                        return;
    -                    }
    -                }
    +        observerRef.get().onNext(t);
    +    }
    +
    +    /**
    +     * This is a temporary observer between buffering and the actual that gets into the line of notifications
    +     * from the producer and will drain the queue of any items received during the race of the initial drain and
    +     * switching this.
    +     * 
    +     * It will then immediately swap itself out for the actual (after a single notification), but since this is now
    +     * being done on the same producer thread no further buffering will occur.
    +     */
    +    private static class PassThruObserver implements Observer {
    +
    +        private final Observer actual;
    +        // this assumes single threaded synchronous notifications (the Rx contract for a single Observer)
    +        private final ConcurrentLinkedQueue buffer;
    +        private final AtomicReference> observerRef;
    +
    +        PassThruObserver(Observer actual, ConcurrentLinkedQueue buffer, AtomicReference> observerRef) {
    +            this.actual = actual;
    +            this.buffer = buffer;
    +            this.observerRef = observerRef;
    +        }
    +
    +        @Override
    +        public void onCompleted() {
    +            drainIfNeededAndSwitchToActual();
    +            actual.onCompleted();
    +        }
    +
    +        @Override
    +        public void onError(Throwable e) {
    +            drainIfNeededAndSwitchToActual();
    +            actual.onError(e);
    +        }
    +
    +        @Override
    +        public void onNext(T t) {
    +            drainIfNeededAndSwitchToActual();
    +            actual.onNext(t);
    +        }
    +
    +        private void drainIfNeededAndSwitchToActual() {
    +            Object o = null;
    +            while ((o = buffer.poll()) != null) {
    +                emit(this, o);
                 }
    +            // now we can safely change over to the actual and get rid of the pass-thru
    +            observerRef.set(actual);
             }
    -        actual.onNext(t);
    +
         }
     
    -    @Override
    -    public void onError(Throwable e) {
    -        if (!passthroughMode) {
    -            synchronized (gate) {
    -                if (!passthroughMode) {
    -                    if (capacity < 0 || queue.size() < capacity) {
    -                        queue.offer(on.error(e));
    -                        return;
    -                    }
    -                    try {
    -                        while (!passthroughMode) {
    -                            gate.wait();
    -                        }
    -                        if (actual.isUnsubscribed()) {
    -                            return;
    -                        }
    -                    } catch (InterruptedException ex) {
    -                        Thread.currentThread().interrupt();
    -                        actual.onError(ex);
    -                        return;
    -                    }
    -                }
    +    private static class BufferedObserver implements Observer {
    +        private final ConcurrentLinkedQueue buffer = new ConcurrentLinkedQueue();
    +
    +        @Override
    +        public void onCompleted() {
    +            buffer.add(COMPLETE_SENTINEL);
    +        }
    +
    +        @Override
    +        public void onError(Throwable e) {
    +            buffer.add(new ErrorSentinel(e));
    +        }
    +
    +        @Override
    +        public void onNext(T t) {
    +            if (t == null) {
    +                buffer.add(NULL_SENTINEL);
    +            } else {
    +                buffer.add(t);
                 }
             }
    -        actual.onError(e);
    +
         }
     
    -    @Override
    -    public void onCompleted() {
    -        if (!passthroughMode) {
    -            synchronized (gate) {
    -                if (!passthroughMode) {
    -                    if (capacity < 0 || queue.size() < capacity) {
    -                        queue.offer(on.completed());
    -                        return;
    -                    }
    -                    try {
    -                        while (!passthroughMode) {
    -                            gate.wait();
    -                        }
    -                        if (actual.isUnsubscribed()) {
    -                            return;
    -                        }
    -                    } catch (InterruptedException ex) {
    -                        Thread.currentThread().interrupt();
    -                        actual.onError(ex);
    -                        return;
    -                    }
    -                }
    +    private final static  void emit(Observer s, Object v) {
    +        if (v instanceof Sentinel) {
    +            if (v == NULL_SENTINEL) {
    +                s.onNext(null);
    +            } else if (v == COMPLETE_SENTINEL) {
    +                s.onCompleted();
    +            } else if (v instanceof ErrorSentinel) {
    +                s.onError(((ErrorSentinel) v).e);
                 }
    +        } else {
    +            s.onNext((T) v);
    +        }
    +    }
    +
    +    private static class Sentinel {
    +
    +    }
    +
    +    private static Sentinel NULL_SENTINEL = new Sentinel();
    +    private static Sentinel COMPLETE_SENTINEL = new Sentinel();
    +
    +    private static class ErrorSentinel extends Sentinel {
    +        final Throwable e;
    +
    +        ErrorSentinel(Throwable e) {
    +            this.e = e;
             }
    -        actual.onCompleted();
         }
     
     }
    diff --git a/rxjava-core/src/main/java/rx/operators/OperatorGroupBy.java b/rxjava-core/src/main/java/rx/operators/OperatorGroupBy.java
    index a8e5ffb53f..2451939581 100644
    --- a/rxjava-core/src/main/java/rx/operators/OperatorGroupBy.java
    +++ b/rxjava-core/src/main/java/rx/operators/OperatorGroupBy.java
    @@ -27,8 +27,6 @@
     import rx.functions.Action0;
     import rx.functions.Func1;
     import rx.observables.GroupedObservable;
    -import rx.subjects.PublishSubject;
    -import rx.subjects.Subject;
     import rx.subscriptions.CompositeSubscription;
     import rx.subscriptions.Subscriptions;
     
    @@ -51,43 +49,50 @@ public Subscriber call(final Subscriber(new CompositeSubscription()) {
    -            private final Map> groups = new HashMap>();
    +            private final Map> groups = new HashMap>();
                 private final AtomicInteger completionCounter = new AtomicInteger(0);
    -            private final AtomicBoolean completed = new AtomicBoolean(false);
    +            private final AtomicBoolean completionEmitted = new AtomicBoolean(false);
    +            private final AtomicBoolean terminated = new AtomicBoolean(false);
     
                 @Override
                 public void onCompleted() {
    -                completed.set(true);
    -                // if we receive onCompleted from our parent we onComplete children
    -                for (Subject ps : groups.values()) {
    -                    ps.onCompleted();
    -                }
    +                if (terminated.compareAndSet(false, true)) {
    +                    // if we receive onCompleted from our parent we onComplete children
    +                    for (BufferUntilSubscriber ps : groups.values()) {
    +                        ps.onCompleted();
    +                    }
     
    -                // special case for empty (no groups emitted)
    -                if (completionCounter.get() == 0) {
    -                    childObserver.onCompleted();
    +                    // special case for empty (no groups emitted)
    +                    if (completionCounter.get() == 0) {
    +                        // we must track 'completionEmitted' seperately from 'completed' since `completeInner` can result in childObserver.onCompleted() being emitted 
    +                        if (completionEmitted.compareAndSet(false, true)) {
    +                            childObserver.onCompleted();
    +                        }
    +                    }
                     }
                 }
     
                 @Override
                 public void onError(Throwable e) {
    -                // we immediately tear everything down if we receive an error
    -                childObserver.onError(e);
    +                if (terminated.compareAndSet(false, true)) {
    +                    // we immediately tear everything down if we receive an error
    +                    childObserver.onError(e);
    +                }
                 }
     
                 @Override
                 public void onNext(T t) {
                     try {
                         final K key = keySelector.call(t);
    -                    Subject gps = groups.get(key);
    +                    BufferUntilSubscriber gps = groups.get(key);
                         if (gps == null) {
                             // this group doesn't exist
                             if (childObserver.isUnsubscribed()) {
                                 // we have been unsubscribed on the outer so won't send any  more groups 
                                 return;
                             }
    -                        gps = PublishSubject.create();
    -                        final Subject _gps = gps;
    +                        gps = BufferUntilSubscriber.create();
    +                        final BufferUntilSubscriber _gps = gps;
     
                             GroupedObservable go = new GroupedObservable(key, new OnSubscribe() {
     
    @@ -136,15 +141,16 @@ public void onNext(T t) {
                 }
     
                 private void completeInner() {
    -                if (completionCounter.decrementAndGet() == 0 && (completed.get() || childObserver.isUnsubscribed())) {
    -                    if (childObserver.isUnsubscribed()) {
    -                        // if the entire groupBy has been unsubscribed and children are completed we will propagate the unsubscribe up.
    -                        unsubscribe();
    -                    }
    -                    for (Subject ps : groups.values()) {
    -                        ps.onCompleted();
    +                // count can be < 0 because unsubscribe also calls this
    +                if (completionCounter.decrementAndGet() <= 0 && (terminated.get() || childObserver.isUnsubscribed())) {
    +                    // completionEmitted ensures we only emit onCompleted once
    +                    if (completionEmitted.compareAndSet(false, true)) {
    +                        if (childObserver.isUnsubscribed()) {
    +                            // if the entire groupBy has been unsubscribed and children are completed we will propagate the unsubscribe up.
    +                            unsubscribe();
    +                        }
    +                        childObserver.onCompleted();
                         }
    -                    childObserver.onCompleted();
                     }
                 }
     
    diff --git a/rxjava-core/src/main/java/rx/operators/OperatorSubscribeOnBounded.java b/rxjava-core/src/main/java/rx/operators/OperatorSubscribeOnBounded.java
    deleted file mode 100644
    index f5a459ef5d..0000000000
    --- a/rxjava-core/src/main/java/rx/operators/OperatorSubscribeOnBounded.java
    +++ /dev/null
    @@ -1,112 +0,0 @@
    -/**
    - * Copyright 2014 Netflix, Inc.
    - * 
    - * Licensed under the Apache License, Version 2.0 (the "License");
    - * you may not use this file except in compliance with the License.
    - * You may obtain a copy of the License at
    - * 
    - * http://www.apache.org/licenses/LICENSE-2.0
    - * 
    - * Unless required by applicable law or agreed to in writing, software
    - * distributed under the License is distributed on an "AS IS" BASIS,
    - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    - * See the License for the specific language governing permissions and
    - * limitations under the License.
    - */
    -package rx.operators;
    -
    -import rx.Observable;
    -import rx.Observable.Operator;
    -import rx.Scheduler;
    -import rx.Scheduler.Inner;
    -import rx.Subscriber;
    -import rx.functions.Action1;
    -
    -/**
    - * Subscribes and unsubscribes Observers on the specified Scheduler.
    - * 

    - * Will occur asynchronously except when subscribing to `GroupedObservable`, `PublishSubject` and possibly other "hot" Observables - * in which case it will subscribe synchronously and buffer/block onNext calls until the subscribe has occurred. - *

    - * See https://github.com/Netflix/RxJava/issues/844 for more information on the "time gap" issue that the synchronous - * subscribe is solving. - * - * - */ -public class OperatorSubscribeOnBounded implements Operator> { - - private final Scheduler scheduler; - /** - * Indicate that events fired between the original subscription time and - * the actual subscription time should not get lost. - */ - private final boolean dontLoseEvents; - /** The buffer size to avoid flooding. Negative value indicates an unbounded buffer. */ - private final int bufferSize; - - public OperatorSubscribeOnBounded(Scheduler scheduler) { - this.scheduler = scheduler; - this.dontLoseEvents = false; - this.bufferSize = -1; - } - - /** - * Construct a SubscribeOn operator. - * - * @param scheduler - * the target scheduler - * @param bufferSize - * if dontLoseEvents == true, this indicates the buffer size. Filling the buffer will - * block the source. -1 indicates an unbounded buffer - */ - public OperatorSubscribeOnBounded(Scheduler scheduler, int bufferSize) { - this.scheduler = scheduler; - this.dontLoseEvents = true; - this.bufferSize = bufferSize; - } - - @Override - public Subscriber> call(final Subscriber subscriber) { - return new Subscriber>(subscriber) { - - @Override - public void onCompleted() { - // ignore - } - - @Override - public void onError(Throwable e) { - subscriber.onError(e); - } - - boolean checkNeedBuffer(Observable o) { - return dontLoseEvents; - } - - @Override - public void onNext(final Observable o) { - if (checkNeedBuffer(o)) { - // use buffering (possibly blocking) for a possibly synchronous subscribe - final BufferUntilSubscriber bus = new BufferUntilSubscriber(bufferSize, subscriber); - subscriber.add(scheduler.schedule(new Action1() { - @Override - public void call(final Inner inner) { - bus.enterPassthroughMode(); - } - })); - o.subscribe(bus); - } else { - // no buffering (async subscribe) - subscriber.add(scheduler.schedule(new Action1() { - - @Override - public void call(final Inner inner) { - o.subscribe(subscriber); - } - })); - } - } - - }; - } -} diff --git a/rxjava-core/src/test/java/rx/operators/OperatorGroupByTest.java b/rxjava-core/src/test/java/rx/operators/OperatorGroupByTest.java index d79ff8cb99..13afb43a93 100644 --- a/rxjava-core/src/test/java/rx/operators/OperatorGroupByTest.java +++ b/rxjava-core/src/test/java/rx/operators/OperatorGroupByTest.java @@ -15,8 +15,14 @@ */ package rx.operators; -import static org.junit.Assert.*; -import static org.mockito.Mockito.*; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; import java.util.ArrayList; import java.util.Arrays; @@ -32,6 +38,7 @@ import org.junit.Test; import org.mockito.Matchers; +import rx.Notification; import rx.Observable; import rx.Observable.OnSubscribe; import rx.Observer; @@ -650,6 +657,7 @@ public void call(String s) { @Test public void testFirstGroupsCompleteAndParentSlowToThenEmitFinalGroupsWhichThenSubscribesOnAndDelaysAndThenCompletes() throws InterruptedException { + System.err.println("----------------------------------------------------------------------------------------------"); final CountDownLatch first = new CountDownLatch(2); // there are two groups to first complete final ArrayList results = new ArrayList(); Observable.create(new OnSubscribe() { @@ -701,17 +709,31 @@ public void call() { }); } else { - return group.nest().lift(new OperatorSubscribeOnBounded(Schedulers.newThread(), 1)).delay(400, TimeUnit.MILLISECONDS).map(new Func1() { + return group.subscribeOn(Schedulers.newThread()).delay(400, TimeUnit.MILLISECONDS).map(new Func1() { @Override public String call(Integer t1) { return "last group: " + t1; } + }).doOnEach(new Action1>() { + + @Override + public void call(Notification t1) { + System.err.println("subscribeOn notification => " + t1); + } + }); } } + }).doOnEach(new Action1>() { + + @Override + public void call(Notification t1) { + System.err.println("outer notification => " + t1); + } + }).toBlockingObservable().forEach(new Action1() { @Override @@ -827,7 +849,7 @@ public Integer call(Integer t) { @Override public Observable call(final GroupedObservable group) { - return group.nest().lift(new OperatorSubscribeOnBounded(Schedulers.newThread(), 0)).map(new Func1() { + return group.subscribeOn(Schedulers.newThread()).map(new Func1() { @Override public String call(Integer t1) { @@ -838,6 +860,13 @@ public String call(Integer t1) { }); } + }).doOnEach(new Action1>() { + + @Override + public void call(Notification t1) { + System.out.println("notification => " + t1); + } + }).toBlockingObservable().forEach(new Action1() { @Override diff --git a/rxjava-core/src/test/java/rx/operators/OperatorSubscribeOnBoundedTest.java b/rxjava-core/src/test/java/rx/operators/OperatorSubscribeOnBoundedTest.java deleted file mode 100644 index 7a38bcd8da..0000000000 --- a/rxjava-core/src/test/java/rx/operators/OperatorSubscribeOnBoundedTest.java +++ /dev/null @@ -1,416 +0,0 @@ -/** - * Copyright 2014 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package rx.operators; - -import static org.junit.Assert.*; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; - -import org.junit.Test; - -import rx.Observable; -import rx.Observable.OnSubscribe; -import rx.Scheduler; -import rx.Subscriber; -import rx.Subscription; -import rx.functions.Action0; -import rx.functions.Action1; -import rx.functions.Func1; -import rx.observables.GroupedObservable; -import rx.observers.TestObserver; -import rx.observers.TestSubscriber; -import rx.schedulers.Schedulers; -import rx.schedulers.Timestamped; -import rx.subjects.PublishSubject; -import rx.subscriptions.Subscriptions; - -public class OperatorSubscribeOnBoundedTest { - - private static class ThreadSubscription implements Subscription { - private volatile Thread thread; - - private final CountDownLatch latch = new CountDownLatch(1); - - private final Subscription s = Subscriptions.create(new Action0() { - - @Override - public void call() { - thread = Thread.currentThread(); - latch.countDown(); - } - - }); - - @Override - public void unsubscribe() { - s.unsubscribe(); - } - - @Override - public boolean isUnsubscribed() { - return s.isUnsubscribed(); - } - - public Thread getThread() throws InterruptedException { - latch.await(); - return thread; - } - } - - @Test - public void testSubscribeOnAndVerifySubscribeAndUnsubscribeThreads() - throws InterruptedException { - final ThreadSubscription subscription = new ThreadSubscription(); - final AtomicReference subscribeThread = new AtomicReference(); - Observable w = Observable.create(new OnSubscribe() { - - @Override - public void call(Subscriber t1) { - subscribeThread.set(Thread.currentThread()); - t1.add(subscription); - t1.onNext(1); - t1.onNext(2); - t1.onCompleted(); - } - }); - - TestObserver observer = new TestObserver(); - w.nest().lift(new OperatorSubscribeOnBounded(Schedulers.newThread())).subscribe(observer); - - Thread unsubscribeThread = subscription.getThread(); - - assertNotNull(unsubscribeThread); - assertNotSame(Thread.currentThread(), unsubscribeThread); - - assertNotNull(subscribeThread.get()); - assertNotSame(Thread.currentThread(), subscribeThread.get()); - // True for Schedulers.newThread() - assertTrue(unsubscribeThread == subscribeThread.get()); - - observer.assertReceivedOnNext(Arrays.asList(1, 2)); - observer.assertTerminalEvent(); - } - - @Test(timeout = 2000) - public void testIssue813() throws InterruptedException { - // https://github.com/Netflix/RxJava/issues/813 - final CountDownLatch scheduled = new CountDownLatch(1); - final CountDownLatch latch = new CountDownLatch(1); - final CountDownLatch doneLatch = new CountDownLatch(1); - - TestObserver observer = new TestObserver(); - final ThreadSubscription s = new ThreadSubscription(); - - final Subscription subscription = Observable - .create(new Observable.OnSubscribe() { - @Override - public void call( - final Subscriber subscriber) { - subscriber.add(s); - scheduled.countDown(); - try { - latch.await(); - - // this should not run because the await above will be interrupted by the unsubscribe - subscriber.onCompleted(); - } catch (InterruptedException e) { - System.out.println("Interrupted because it is unsubscribed"); - Thread.currentThread().interrupt(); - } catch (Throwable e) { - subscriber.onError(e); - } finally { - doneLatch.countDown(); - } - } - }).nest().lift(new OperatorSubscribeOnBounded(Schedulers.computation())).subscribe(observer); - - // wait for scheduling - scheduled.await(); - // trigger unsubscribe - subscription.unsubscribe(); - // As unsubscribe is called in other thread, we need to wait for it. - s.getThread(); - latch.countDown(); - doneLatch.await(); - assertEquals(0, observer.getOnErrorEvents().size()); - // 0 because the unsubscribe interrupts and prevents onCompleted from being executed - assertEquals(0, observer.getOnCompletedEvents().size()); - } - - public static class SlowScheduler extends Scheduler { - final Scheduler actual; - final long delay; - final TimeUnit unit; - - public SlowScheduler() { - this(Schedulers.computation(), 2, TimeUnit.SECONDS); - } - - public SlowScheduler(Scheduler actual, long delay, TimeUnit unit) { - this.actual = actual; - this.delay = delay; - this.unit = unit; - } - - @Override - public Subscription schedule(final Action1 action) { - return actual.schedule(action, delay, unit); - } - - @Override - public Subscription schedule(final Action1 action, final long delayTime, final TimeUnit delayUnit) { - TimeUnit common = delayUnit.compareTo(unit) < 0 ? delayUnit : unit; - long t = common.convert(delayTime, delayUnit) + common.convert(delay, unit); - return actual.schedule(action, t, common); - } - } - - @Test - public void testSubscribeOnPublishSubjectWithSlowScheduler() { - PublishSubject ps = PublishSubject.create(); - TestSubscriber ts = new TestSubscriber(); - ps.nest().lift(new OperatorSubscribeOnBounded(new SlowScheduler(), 0)).subscribe(ts); - ps.onNext(1); - ps.onNext(2); - ps.onCompleted(); - - ts.awaitTerminalEvent(); - ts.assertReceivedOnNext(Arrays.asList(1, 2)); - } - - @Test - public void testGroupsWithNestedSubscribeOn() throws InterruptedException { - final ArrayList results = new ArrayList(); - Observable.create(new OnSubscribe() { - - @Override - public void call(Subscriber sub) { - sub.onNext(1); - sub.onNext(2); - sub.onNext(1); - sub.onNext(2); - sub.onCompleted(); - } - - }).groupBy(new Func1() { - - @Override - public Integer call(Integer t) { - return t; - } - - }).flatMap(new Func1, Observable>() { - - @Override - public Observable call(final GroupedObservable group) { - return group.nest().lift(new OperatorSubscribeOnBounded(Schedulers.newThread(), 0)).map(new Func1() { - - @Override - public String call(Integer t1) { - System.out.println("Received: " + t1 + " on group : " + group.getKey()); - return "first groups: " + t1; - } - - }); - } - - }).toBlockingObservable().forEach(new Action1() { - - @Override - public void call(String s) { - results.add(s); - } - - }); - - System.out.println("Results: " + results); - assertEquals(4, results.size()); - } - - @Test - public void testFirstGroupsCompleteAndParentSlowToThenEmitFinalGroupsWhichThenSubscribesOnAndDelaysAndThenCompletes() throws InterruptedException { - final CountDownLatch first = new CountDownLatch(2); // there are two groups to first complete - final ArrayList results = new ArrayList(); - Observable.create(new OnSubscribe() { - - @Override - public void call(Subscriber sub) { - sub.onNext(1); - sub.onNext(2); - sub.onNext(1); - sub.onNext(2); - try { - first.await(); - } catch (InterruptedException e) { - sub.onError(e); - return; - } - sub.onNext(3); - sub.onNext(3); - sub.onCompleted(); - } - - }).groupBy(new Func1() { - - @Override - public Integer call(Integer t) { - return t; - } - - }).flatMap(new Func1, Observable>() { - - @Override - public Observable call(final GroupedObservable group) { - if (group.getKey() < 3) { - return group.map(new Func1() { - - @Override - public String call(Integer t1) { - return "first groups: " + t1; - } - - }) - // must take(2) so an onCompleted + unsubscribe happens on these first 2 groups - .take(2).doOnCompleted(new Action0() { - - @Override - public void call() { - first.countDown(); - } - - }); - } else { - return group.nest().lift(new OperatorSubscribeOnBounded(Schedulers.newThread(), 0)) - .delay(400, TimeUnit.MILLISECONDS).map(new Func1() { - - @Override - public String call(Integer t1) { - return "last group: " + t1; - } - - }); - } - } - - }).toBlockingObservable().forEach(new Action1() { - - @Override - public void call(String s) { - results.add(s); - } - - }); - - System.out.println("Results: " + results); - assertEquals(6, results.size()); - } - - void testBoundedBufferingWithSize(int size) throws Exception { - Observable timer = Observable.timer(100, 100, TimeUnit.MILLISECONDS); - - final List deltas = Collections.synchronizedList(new ArrayList()); - - Subscription s = timer.timestamp().nest().lift(new OperatorSubscribeOnBounded>( - new SlowScheduler(Schedulers.computation(), 1, TimeUnit.SECONDS), size)).map(new Func1, Long>() { - @Override - public Long call(Timestamped t1) { - long v = System.currentTimeMillis() - t1.getTimestampMillis(); - return v; - } - }).doOnNext(new Action1() { - @Override - public void call(Long t1) { - deltas.add(t1); - } - }).subscribe(); - - Thread.sleep(2050); - - s.unsubscribe(); - - if (deltas.size() < size + 1) { - fail("To few items in deltas: " + deltas); - } - for (int i = 0; i < size + 1; i++) { - if (deltas.get(i) < 500) { - fail(i + "th item arrived too early: " + deltas); - } - } - for (int i = size + 1; i < deltas.size(); i++) { - if (deltas.get(i) >= 500) { - fail(i + "th item arrived too late: " + deltas); - } - } - } - - @Test(timeout = 5000) - public void testBoundedBufferingOfZero() throws Exception { - testBoundedBufferingWithSize(0); - } - - @Test(timeout = 5000) - public void testBoundedBufferingOfOne() throws Exception { - testBoundedBufferingWithSize(1); - } - - @Test(timeout = 5000) - public void testBoundedBufferingOfTwo() throws Exception { - testBoundedBufferingWithSize(2); - } - - @Test(timeout = 5000) - public void testUnsubscribeInfiniteStream() throws InterruptedException { - TestSubscriber ts = new TestSubscriber(); - final AtomicInteger count = new AtomicInteger(); - Observable.create(new OnSubscribe() { - - @Override - public void call(Subscriber sub) { - for (int i = 1; !sub.isUnsubscribed(); i++) { - count.incrementAndGet(); - sub.onNext(i); - } - } - - }).nest().lift(new OperatorSubscribeOnBounded(Schedulers.newThread())).take(10).subscribe(ts); - - ts.awaitTerminalEventAndUnsubscribeOnTimeout(1000, TimeUnit.MILLISECONDS); - Thread.sleep(200); // give time for the loop to continue - ts.assertReceivedOnNext(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)); - assertEquals(10, count.get()); - } - @Test(timeout = 2000) - public void testNoDeadlock() { - List data = Arrays.asList(1, 2, 3, 4, 5); - Observable source = Observable.from(data); - - Observable result = source.nest().lift(new OperatorSubscribeOnBounded(Schedulers.newThread(), 1)); - - TestSubscriber ts = new TestSubscriber(); - - result.subscribe(ts); - - ts.awaitTerminalEvent(); - ts.assertReceivedOnNext(data); - } -} From 2087f2346a39461f610195851dfdabdc457aa30b Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Thu, 20 Mar 2014 11:21:36 -0700 Subject: [PATCH 144/422] Compilation Fix --- rxjava-core/src/main/java/rx/subjects/TestSubject.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rxjava-core/src/main/java/rx/subjects/TestSubject.java b/rxjava-core/src/main/java/rx/subjects/TestSubject.java index af3a02f3ee..7904886c75 100644 --- a/rxjava-core/src/main/java/rx/subjects/TestSubject.java +++ b/rxjava-core/src/main/java/rx/subjects/TestSubject.java @@ -83,7 +83,7 @@ public void call(SubjectObserver o) { */ lastNotification.get().accept(o); } - }); + }, null); return new TestSubject(onSubscribe, subscriptionManager, lastNotification, scheduler); } @@ -179,4 +179,4 @@ public void call(Inner t1) { }, timeInMilliseconds, TimeUnit.MILLISECONDS); } -} \ No newline at end of file +} From e68f0b20a5d324032a4989265278dbac4cad9b63 Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Thu, 20 Mar 2014 11:22:08 -0700 Subject: [PATCH 145/422] Fix Unit Test Assertion --- rxjava-core/src/test/java/rx/observers/TestSubscriberTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rxjava-core/src/test/java/rx/observers/TestSubscriberTest.java b/rxjava-core/src/test/java/rx/observers/TestSubscriberTest.java index d3f0830128..1cda136fe6 100644 --- a/rxjava-core/src/test/java/rx/observers/TestSubscriberTest.java +++ b/rxjava-core/src/test/java/rx/observers/TestSubscriberTest.java @@ -66,7 +66,8 @@ public void testAssertNotMatchValue() { oi.subscribe(o); thrown.expect(AssertionError.class); - thrown.expectMessage("Value at index: 1 expected to be [3] but was: [2]"); + thrown.expectMessage("Value at index: 1 expected to be [3] (Integer) but was: [2] (Integer)"); + o.assertReceivedOnNext(Arrays.asList(1, 3)); assertEquals(2, o.getOnNextEvents().size()); From 3013ec902084d2573d37e673331ffa0ae02f9c9d Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Thu, 20 Mar 2014 11:31:56 -0700 Subject: [PATCH 146/422] parallel-merge unit test assertions Using serialize for merge allows less threads to be used under contention instead of blocking and using them all. This changes the assertion to be <= 3 instead of == 3 because of that. --- .../src/main/java/rx/operators/OperationParallelMerge.java | 2 +- .../test/java/rx/operators/OperationParallelMergeTest.java | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/rxjava-core/src/main/java/rx/operators/OperationParallelMerge.java b/rxjava-core/src/main/java/rx/operators/OperationParallelMerge.java index b71aaf5efa..7c06082c41 100644 --- a/rxjava-core/src/main/java/rx/operators/OperationParallelMerge.java +++ b/rxjava-core/src/main/java/rx/operators/OperationParallelMerge.java @@ -26,7 +26,7 @@ public class OperationParallelMerge { public static Observable> parallelMerge(final Observable> source, final int parallelObservables) { - return parallelMerge(source, parallelObservables, Schedulers.currentThread()); + return parallelMerge(source, parallelObservables, Schedulers.immediate()); } public static Observable> parallelMerge(final Observable> source, final int parallelObservables, final Scheduler scheduler) { diff --git a/rxjava-core/src/test/java/rx/operators/OperationParallelMergeTest.java b/rxjava-core/src/test/java/rx/operators/OperationParallelMergeTest.java index 8a0b4b6770..b126189c37 100644 --- a/rxjava-core/src/test/java/rx/operators/OperationParallelMergeTest.java +++ b/rxjava-core/src/test/java/rx/operators/OperationParallelMergeTest.java @@ -15,7 +15,8 @@ */ package rx.operators; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import java.util.List; import java.util.concurrent.ConcurrentHashMap; @@ -79,7 +80,7 @@ public void call(String o) { } }); - assertEquals(3, threads.keySet().size()); + assertTrue(threads.keySet().size() <= 3); // can be less than since merge doesn't block threads and may not use all of them } @Test @@ -98,7 +99,7 @@ public void call(String o) { } }); - assertEquals(3, threads.keySet().size()); + assertTrue(threads.keySet().size() <= 3); // can be less than since merge doesn't block threads and may not use all of them } private static Observable> getStreams() { From 75b012448976579e91468a9beb587fe0f829f1b8 Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Thu, 20 Mar 2014 14:34:03 -0700 Subject: [PATCH 147/422] Dematerialize - handle non-materialized terminal events --- .../rx/operators/OperationDematerialize.java | 2 + .../operators/OperationDematerializeTest.java | 41 +++++++++++++++++-- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/rxjava-core/src/main/java/rx/operators/OperationDematerialize.java b/rxjava-core/src/main/java/rx/operators/OperationDematerialize.java index 9bfd901f7e..2d937aa47a 100644 --- a/rxjava-core/src/main/java/rx/operators/OperationDematerialize.java +++ b/rxjava-core/src/main/java/rx/operators/OperationDematerialize.java @@ -57,10 +57,12 @@ public Subscription onSubscribe(final Observer observer) { return sequence.subscribe(new Observer>() { @Override public void onCompleted() { + observer.onCompleted(); } @Override public void onError(Throwable e) { + observer.onError(e); } @Override diff --git a/rxjava-core/src/test/java/rx/operators/OperationDematerializeTest.java b/rxjava-core/src/test/java/rx/operators/OperationDematerializeTest.java index 147ec2ea1c..c3f9954a48 100644 --- a/rxjava-core/src/test/java/rx/operators/OperationDematerializeTest.java +++ b/rxjava-core/src/test/java/rx/operators/OperationDematerializeTest.java @@ -15,15 +15,19 @@ */ package rx.operators; -import static org.mockito.Matchers.*; -import static org.mockito.Mockito.*; -import static rx.operators.OperationDematerialize.*; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static rx.operators.OperationDematerialize.dematerialize; import org.junit.Test; import rx.Notification; import rx.Observable; import rx.Observer; +import rx.observers.TestSubscriber; public class OperationDematerializeTest { @@ -71,4 +75,35 @@ public void testDematerialize3() { verify(observer, times(0)).onCompleted(); verify(observer, times(0)).onNext(any(Integer.class)); } + + @Test + public void testErrorPassThru() { + Exception exception = new Exception("test"); + Observable observable = Observable.error(exception); + Observable dematerialize = observable.dematerialize(); + + Observer observer = mock(Observer.class); + dematerialize.subscribe(observer); + + verify(observer, times(1)).onError(exception); + verify(observer, times(0)).onCompleted(); + verify(observer, times(0)).onNext(any(Integer.class)); + } + + @Test + public void testCompletePassThru() { + Observable observable = Observable.empty(); + Observable dematerialize = observable.dematerialize(); + + Observer observer = mock(Observer.class); + TestSubscriber ts = new TestSubscriber(observer); + dematerialize.subscribe(ts); + + System.out.println(ts.getOnErrorEvents()); + + verify(observer, never()).onError(any(Throwable.class)); + verify(observer, times(1)).onCompleted(); + verify(observer, times(0)).onNext(any(Integer.class)); + } + } From 3d45dac73b183523594f0f812294b214af6a291f Mon Sep 17 00:00:00 2001 From: zsxwing Date: Sat, 15 Mar 2014 22:03:15 +0800 Subject: [PATCH 148/422] Chain Subscription in TimeoutSubscriber and SerializedSubscriber --- .../rx/observers/SerializedSubscriber.java | 1 + .../rx/operators/OperatorTimeoutBase.java | 1 + .../rx/operators/OperatorTimeoutTests.java | 86 +++++++++++++++++++ 3 files changed, 88 insertions(+) diff --git a/rxjava-core/src/main/java/rx/observers/SerializedSubscriber.java b/rxjava-core/src/main/java/rx/observers/SerializedSubscriber.java index db545ff430..b42fd04440 100644 --- a/rxjava-core/src/main/java/rx/observers/SerializedSubscriber.java +++ b/rxjava-core/src/main/java/rx/observers/SerializedSubscriber.java @@ -20,6 +20,7 @@ public class SerializedSubscriber extends Subscriber { private final Observer s; public SerializedSubscriber(Subscriber s) { + super(s); this.s = new SerializedObserver(s); } diff --git a/rxjava-core/src/main/java/rx/operators/OperatorTimeoutBase.java b/rxjava-core/src/main/java/rx/operators/OperatorTimeoutBase.java index 7a6ea8e8d8..73bb966277 100644 --- a/rxjava-core/src/main/java/rx/operators/OperatorTimeoutBase.java +++ b/rxjava-core/src/main/java/rx/operators/OperatorTimeoutBase.java @@ -93,6 +93,7 @@ private TimeoutSubscriber( SerializedSubscriber serializedSubscriber, TimeoutStub timeoutStub, SerialSubscription serial, Observable other) { + super(serializedSubscriber); this.serializedSubscriber = serializedSubscriber; this.timeoutStub = timeoutStub; this.serial = serial; diff --git a/rxjava-core/src/test/java/rx/operators/OperatorTimeoutTests.java b/rxjava-core/src/test/java/rx/operators/OperatorTimeoutTests.java index c371d90680..6702024636 100644 --- a/rxjava-core/src/test/java/rx/operators/OperatorTimeoutTests.java +++ b/rxjava-core/src/test/java/rx/operators/OperatorTimeoutTests.java @@ -18,6 +18,7 @@ import static org.mockito.Matchers.*; import static org.mockito.Mockito.*; +import java.io.IOException; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -266,4 +267,89 @@ public void call(Subscriber subscriber) { exit.countDown(); // exit the thread } + + @Test + public void shouldUnsubscribeFromUnderlyingSubscriptionOnTimeout() throws InterruptedException { + // From https://github.com/Netflix/RxJava/pull/951 + final Subscription s = mock(Subscription.class); + + Observable never = Observable.create(new OnSubscribe() { + public void call(Subscriber subscriber) { + subscriber.add(s); + } + }); + + TestScheduler testScheduler = new TestScheduler(); + Observable observableWithTimeout = never.timeout(1000, TimeUnit.MILLISECONDS, testScheduler); + + @SuppressWarnings("unchecked") + Observer observer = mock(Observer.class); + observableWithTimeout.subscribe(observer); + + testScheduler.advanceTimeBy(2000, TimeUnit.MILLISECONDS); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer).onError(isA(TimeoutException.class)); + inOrder.verifyNoMoreInteractions(); + + verify(s, times(1)).unsubscribe(); + } + + @Test + public void shouldUnsubscribeFromUnderlyingSubscriptionOnImmediatelyComplete() { + // From https://github.com/Netflix/RxJava/pull/951 + final Subscription s = mock(Subscription.class); + + Observable immediatelyComplete = Observable.create(new OnSubscribe() { + public void call(Subscriber subscriber) { + subscriber.add(s); + subscriber.onCompleted(); + } + }); + + TestScheduler testScheduler = new TestScheduler(); + Observable observableWithTimeout = immediatelyComplete.timeout(1000, TimeUnit.MILLISECONDS, + testScheduler); + + @SuppressWarnings("unchecked") + Observer observer = mock(Observer.class); + observableWithTimeout.subscribe(observer); + + testScheduler.advanceTimeBy(2000, TimeUnit.MILLISECONDS); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer).onCompleted(); + inOrder.verifyNoMoreInteractions(); + + verify(s, times(1)).unsubscribe(); + } + + @Test + public void shouldUnsubscribeFromUnderlyingSubscriptionOnImmediatelyErrored() throws InterruptedException { + // From https://github.com/Netflix/RxJava/pull/951 + final Subscription s = mock(Subscription.class); + + Observable immediatelyError = Observable.create(new OnSubscribe() { + public void call(Subscriber subscriber) { + subscriber.add(s); + subscriber.onError(new IOException("Error")); + } + }); + + TestScheduler testScheduler = new TestScheduler(); + Observable observableWithTimeout = immediatelyError.timeout(1000, TimeUnit.MILLISECONDS, + testScheduler); + + @SuppressWarnings("unchecked") + Observer observer = mock(Observer.class); + observableWithTimeout.subscribe(observer); + + testScheduler.advanceTimeBy(2000, TimeUnit.MILLISECONDS); + + InOrder inOrder = inOrder(observer); + inOrder.verify(observer).onError(isA(IOException.class)); + inOrder.verifyNoMoreInteractions(); + + verify(s, times(1)).unsubscribe(); + } } From 25555b4f6f9e83acaf6e0f169b6041975718e7d3 Mon Sep 17 00:00:00 2001 From: zsxwing Date: Sun, 16 Mar 2014 00:30:51 +0800 Subject: [PATCH 149/422] Reimplement the ElementAt operator --- rxjava-core/src/main/java/rx/Observable.java | 6 +- .../java/rx/operators/OperationElementAt.java | 137 ------------------ .../java/rx/operators/OperatorElementAt.java | 84 +++++++++++ .../rx/operators/OperationElementAtTest.java | 127 ---------------- .../rx/operators/OperatorElementAtTest.java | 60 ++++++++ 5 files changed, 147 insertions(+), 267 deletions(-) delete mode 100644 rxjava-core/src/main/java/rx/operators/OperationElementAt.java create mode 100644 rxjava-core/src/main/java/rx/operators/OperatorElementAt.java delete mode 100644 rxjava-core/src/test/java/rx/operators/OperationElementAtTest.java create mode 100644 rxjava-core/src/test/java/rx/operators/OperatorElementAtTest.java diff --git a/rxjava-core/src/main/java/rx/Observable.java b/rxjava-core/src/main/java/rx/Observable.java index 235e1e62e4..98885a765c 100644 --- a/rxjava-core/src/main/java/rx/Observable.java +++ b/rxjava-core/src/main/java/rx/Observable.java @@ -66,7 +66,7 @@ import rx.operators.OperationDematerialize; import rx.operators.OperationDistinct; import rx.operators.OperationDistinctUntilChanged; -import rx.operators.OperationElementAt; +import rx.operators.OperatorElementAt; import rx.operators.OperationFinally; import rx.operators.OperationFlatMap; import rx.operators.OperationGroupByUntil; @@ -4466,7 +4466,7 @@ public final void onNext(T args) { * @see RxJava Wiki: elementAt() */ public final Observable elementAt(int index) { - return create(OperationElementAt.elementAt(this, index)); + return create(new OperatorElementAt(this, index)); } /** @@ -4486,7 +4486,7 @@ public final Observable elementAt(int index) { * @see RxJava Wiki: elementAtOrDefault() */ public final Observable elementAtOrDefault(int index, T defaultValue) { - return create(OperationElementAt.elementAtOrDefault(this, index, defaultValue)); + return create(new OperatorElementAt(this, index, defaultValue)); } /** diff --git a/rxjava-core/src/main/java/rx/operators/OperationElementAt.java b/rxjava-core/src/main/java/rx/operators/OperationElementAt.java deleted file mode 100644 index 729258e734..0000000000 --- a/rxjava-core/src/main/java/rx/operators/OperationElementAt.java +++ /dev/null @@ -1,137 +0,0 @@ -/** - * Copyright 2014 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package rx.operators; - -import java.util.concurrent.atomic.AtomicInteger; - -import rx.Observable; -import rx.Observable.OnSubscribeFunc; -import rx.Observer; -import rx.Subscription; - -/** - * Returns the element at a specified index in a sequence. - */ -public class OperationElementAt { - - /** - * Returns the element at a specified index in a sequence. - * - * @param source - * Observable sequence to return the element from. - * @param index - * The zero-based index of the element to retrieve. - * - * @return An observable sequence that produces the element at the specified - * position in the source sequence. - * - * @throws IndexOutOfBoundsException - * Index is greater than or equal to the number of elements in - * the source sequence. - * @throws IndexOutOfBoundsException - * Index is less than 0. - */ - public static OnSubscribeFunc elementAt(Observable source, int index) { - return new ElementAt(source, index, null, false); - } - - /** - * Returns the element at a specified index in a sequence or the default - * value if the index is out of range. - * - * @param source - * Observable sequence to return the element from. - * @param index - * The zero-based index of the element to retrieve. - * @param defaultValue - * The default value. - * - * @return An observable sequence that produces the element at the specified - * position in the source sequence, or the default value if the - * index is outside the bounds of the source sequence. - * - * @throws IndexOutOfBoundsException - * Index is less than 0. - */ - public static OnSubscribeFunc elementAtOrDefault(Observable source, int index, T defaultValue) { - return new ElementAt(source, index, defaultValue, true); - } - - private static class ElementAt implements OnSubscribeFunc { - - private final Observable source; - private final int index; - private final boolean hasDefault; - private final T defaultValue; - - private ElementAt(Observable source, int index, - T defaultValue, boolean hasDefault) { - this.source = source; - this.index = index; - this.defaultValue = defaultValue; - this.hasDefault = hasDefault; - } - - @Override - public Subscription onSubscribe(final Observer observer) { - final SafeObservableSubscription subscription = new SafeObservableSubscription(); - return subscription.wrap(source.subscribe(new Observer() { - - private AtomicInteger counter = new AtomicInteger(); - - @Override - public void onNext(T value) { - try { - int currentIndex = counter.getAndIncrement(); - if (currentIndex == index) { - observer.onNext(value); - observer.onCompleted(); - } else if (currentIndex > index) { - // this will work if the sequence is asynchronous, - // it will have no effect on a synchronous observable - subscription.unsubscribe(); - } - } catch (Throwable ex) { - observer.onError(ex); - // this will work if the sequence is asynchronous, it - // will have no effect on a synchronous observable - subscription.unsubscribe(); - } - - } - - @Override - public void onError(Throwable ex) { - observer.onError(ex); - } - - @Override - public void onCompleted() { - if (index < 0) { - observer.onError(new IndexOutOfBoundsException(index + " is out of bounds")); - } else if (counter.get() <= index) { - if (hasDefault) { - observer.onNext(defaultValue); - observer.onCompleted(); - } else { - observer.onError(new IndexOutOfBoundsException(index + " is out of bounds")); - } - } - } - })); - } - } -} diff --git a/rxjava-core/src/main/java/rx/operators/OperatorElementAt.java b/rxjava-core/src/main/java/rx/operators/OperatorElementAt.java new file mode 100644 index 0000000000..fe7d94c518 --- /dev/null +++ b/rxjava-core/src/main/java/rx/operators/OperatorElementAt.java @@ -0,0 +1,84 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package rx.operators; + +import rx.Observable; +import rx.Observable.OnSubscribe; +import rx.Subscriber; + +/** + * Returns the element at a specified index in a sequence. + */ +public final class OperatorElementAt implements OnSubscribe { + + private final Observable source; + private final int index; + private final boolean hasDefault; + private final T defaultValue; + + public OperatorElementAt(Observable source, int index) { + this(source, index, null, false); + } + + public OperatorElementAt(Observable source, int index, T defaultValue) { + this(source, index, defaultValue, true); + } + + private OperatorElementAt(Observable source, int index, T defaultValue, boolean hasDefault) { + if (index < 0) { + throw new IndexOutOfBoundsException(index + " is out of bounds"); + } + this.source = source; + this.index = index; + this.defaultValue = defaultValue; + this.hasDefault = hasDefault; + } + + @Override + public void call(final Subscriber subscriber) { + source.subscribe(new Subscriber(subscriber) { + + private int currentIndex = 0; + + @Override + public void onNext(T value) { + if (currentIndex == index) { + subscriber.onNext(value); + subscriber.onCompleted(); + } + currentIndex++; + } + + @Override + public void onError(Throwable e) { + subscriber.onError(e); + } + + @Override + public void onCompleted() { + if (currentIndex <= index) { + // If "subscriber.onNext(value)" is called, currentIndex must be greater than index + if (hasDefault) { + subscriber.onNext(defaultValue); + subscriber.onCompleted(); + } else { + subscriber.onError(new IndexOutOfBoundsException(index + " is out of bounds")); + } + } + } + }); + } +} diff --git a/rxjava-core/src/test/java/rx/operators/OperationElementAtTest.java b/rxjava-core/src/test/java/rx/operators/OperationElementAtTest.java deleted file mode 100644 index 7728c8791b..0000000000 --- a/rxjava-core/src/test/java/rx/operators/OperationElementAtTest.java +++ /dev/null @@ -1,127 +0,0 @@ -/** - * Copyright 2014 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package rx.operators; - -import static org.junit.Assert.*; -import static org.mockito.Matchers.*; -import static org.mockito.Mockito.*; -import static rx.operators.OperationElementAt.*; - -import java.util.Iterator; -import java.util.concurrent.ExecutionException; - -import org.junit.Test; - -import rx.Observable; -import rx.Observer; - -public class OperationElementAtTest { - - @Test - public void testElementAt() { - Observable w = Observable.from(1, 2); - Observable observable = Observable.create(elementAt(w, 1)); - - @SuppressWarnings("unchecked") - Observer observer = mock(Observer.class); - observable.subscribe(observer); - verify(observer, never()).onNext(1); - verify(observer, times(1)).onNext(2); - verify(observer, never()).onError( - any(Throwable.class)); - verify(observer, times(1)).onCompleted(); - } - - @Test - public void testElementAtWithMinusIndex() { - Observable w = Observable.from(1, 2); - Observable observable = Observable - .create(elementAt(w, -1)); - - try { - Iterator iter = OperationToIterator - .toIterator(observable); - assertTrue(iter.hasNext()); - iter.next(); - fail("expect an IndexOutOfBoundsException when index is out of bounds"); - } catch (IndexOutOfBoundsException e) { - } - } - - @Test - public void testElementAtWithIndexOutOfBounds() - throws InterruptedException, ExecutionException { - Observable w = Observable.from(1, 2); - Observable observable = Observable.create(elementAt(w, 2)); - try { - Iterator iter = OperationToIterator - .toIterator(observable); - assertTrue(iter.hasNext()); - iter.next(); - fail("expect an IndexOutOfBoundsException when index is out of bounds"); - } catch (IndexOutOfBoundsException e) { - } - } - - @Test - public void testElementAtOrDefault() throws InterruptedException, - ExecutionException { - Observable w = Observable.from(1, 2); - Observable observable = Observable - .create(elementAtOrDefault(w, 1, 0)); - - @SuppressWarnings("unchecked") - Observer observer = mock(Observer.class); - observable.subscribe(observer); - verify(observer, never()).onNext(1); - verify(observer, times(1)).onNext(2); - verify(observer, never()).onError(any(Throwable.class)); - verify(observer, times(1)).onCompleted(); - } - - @Test - public void testElementAtOrDefaultWithIndexOutOfBounds() - throws InterruptedException, ExecutionException { - Observable w = Observable.from(1, 2); - Observable observable = Observable - .create(elementAtOrDefault(w, 2, 0)); - - @SuppressWarnings("unchecked") - Observer observer = mock(Observer.class); - observable.subscribe(observer); - verify(observer, never()).onNext(1); - verify(observer, never()).onNext(2); - verify(observer, times(1)).onNext(0); - verify(observer, never()).onError(any(Throwable.class)); - verify(observer, times(1)).onCompleted(); - } - - @Test - public void testElementAtOrDefaultWithMinusIndex() { - Observable w = Observable.from(1, 2); - Observable observable = Observable - .create(elementAtOrDefault(w, -1, 0)); - - try { - Iterator iter = OperationToIterator - .toIterator(observable); - assertTrue(iter.hasNext()); - iter.next(); - fail("expect an IndexOutOfBoundsException when index is out of bounds"); - } catch (IndexOutOfBoundsException e) { - } - } -} diff --git a/rxjava-core/src/test/java/rx/operators/OperatorElementAtTest.java b/rxjava-core/src/test/java/rx/operators/OperatorElementAtTest.java new file mode 100644 index 0000000000..a9a21f0087 --- /dev/null +++ b/rxjava-core/src/test/java/rx/operators/OperatorElementAtTest.java @@ -0,0 +1,60 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package rx.operators; + +import static org.junit.Assert.assertEquals; + +import java.util.Arrays; + +import org.junit.Test; + +import rx.Observable; + +public class OperatorElementAtTest { + + @Test + public void testElementAt() { + assertEquals(2, Observable.from(Arrays.asList(1, 2)).elementAt(1).toBlockingObservable().single() + .intValue()); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void testElementAtWithMinusIndex() { + Observable.from(Arrays.asList(1, 2)).elementAt(-1); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void testElementAtWithIndexOutOfBounds() { + Observable.from(Arrays.asList(1, 2)).elementAt(2).toBlockingObservable().single(); + } + + @Test + public void testElementAtOrDefault() { + assertEquals(2, Observable.from(Arrays.asList(1, 2)).elementAtOrDefault(1, 0).toBlockingObservable() + .single().intValue()); + } + + @Test + public void testElementAtOrDefaultWithIndexOutOfBounds() { + assertEquals(0, Observable.from(Arrays.asList(1, 2)).elementAtOrDefault(2, 0).toBlockingObservable() + .single().intValue()); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void testElementAtOrDefaultWithMinusIndex() { + Observable.from(Arrays.asList(1, 2)).elementAtOrDefault(-1, 0); + } +} From 11a18aaa68031e1503edf054855b90a6a41aeb94 Mon Sep 17 00:00:00 2001 From: zsxwing Date: Sun, 16 Mar 2014 00:31:09 +0800 Subject: [PATCH 150/422] Add ElementAt and ElementAtOrDefault to rxjava-scala --- .../rx/lang/scala/examples/RxScalaDemo.scala | 9 +++ .../main/scala/rx/lang/scala/Observable.scala | 59 +++++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/language-adaptors/rxjava-scala/src/examples/scala/rx/lang/scala/examples/RxScalaDemo.scala b/language-adaptors/rxjava-scala/src/examples/scala/rx/lang/scala/examples/RxScalaDemo.scala index e09e6a7ec9..d0ee67838e 100644 --- a/language-adaptors/rxjava-scala/src/examples/scala/rx/lang/scala/examples/RxScalaDemo.scala +++ b/language-adaptors/rxjava-scala/src/examples/scala/rx/lang/scala/examples/RxScalaDemo.scala @@ -564,4 +564,13 @@ class RxScalaDemo extends JUnitSuite { println(result) } + @Test def elementAtExample(): Unit = { + val o = List("red", "green", "blue").toObservable + println(o(2).toBlockingObservable.single) + } + + @Test def elementAtOrDefaultExample(): Unit = { + val o : Observable[Seq[Char]] = List("red".toList, "green".toList, "blue".toList).toObservable.elementAtOrDefault(3, "black".toSeq) + println(o.toBlockingObservable.single) + } } diff --git a/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/Observable.scala b/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/Observable.scala index 8dafa30e2b..d185c1af8b 100644 --- a/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/Observable.scala +++ b/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/Observable.scala @@ -2305,6 +2305,65 @@ trait Observable[+T] def delaySubscription(delay: Duration, scheduler: Scheduler): Observable[T] = { toScalaObservable[T](asJavaObservable.delaySubscription(delay.length, delay.unit, scheduler)) } + + /** + * Returns an Observable that emits the single item at a specified index in a sequence of emissions from a + * source Observbable. + * + * + * + * @param index + * the zero-based index of the item to retrieve + * @return an Observable that emits a single item: the item at the specified position in the sequence of + * those emitted by the source Observable + * @throws IndexOutOfBoundsException + * if index is greater than or equal to the number of items emitted by the source + * Observable + * @throws IndexOutOfBoundsException + * if index is less than 0 + * @see `Observable.elementAt` + */ + def apply(index: Int): Observable[T] = elementAt(index) + + /** + * Returns an Observable that emits the single item at a specified index in a sequence of emissions from a + * source Observbable. + * + * + * + * @param index + * the zero-based index of the item to retrieve + * @return an Observable that emits a single item: the item at the specified position in the sequence of + * those emitted by the source Observable + * @throws IndexOutOfBoundsException + * if index is greater than or equal to the number of items emitted by the source + * Observable + * @throws IndexOutOfBoundsException + * if index is less than 0 + */ + def elementAt(index: Int): Observable[T] = { + toScalaObservable[T](asJavaObservable.elementAt(index)) + } + + /** + * Returns an Observable that emits the item found at a specified index in a sequence of emissions from a + * source Observable, or a default item if that index is out of range. + * + * + * + * @param index + * the zero-based index of the item to retrieve + * @param defaultValue + * the default item + * @return an Observable that emits the item at the specified position in the sequence emitted by the source + * Observable, or the default item if that index is outside the bounds of the source sequence + * @throws IndexOutOfBoundsException + * if {@code index} is less than 0 + */ + def elementAtOrDefault[U >: T](index: Int, default: U): Observable[U] = { + val thisJava = asJavaObservable.asInstanceOf[rx.Observable[U]] + toScalaObservable[U](thisJava.elementAtOrDefault(index, default)) + } } /** From f0fea5fa67bc9ed770ddfa55845e276d4a766e69 Mon Sep 17 00:00:00 2001 From: zsxwing Date: Sun, 16 Mar 2014 11:28:43 +0800 Subject: [PATCH 151/422] Implement ElementAt as Operator --- rxjava-core/src/main/java/rx/Observable.java | 4 ++-- .../java/rx/operators/OperatorElementAt.java | 24 +++++++++---------- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/rxjava-core/src/main/java/rx/Observable.java b/rxjava-core/src/main/java/rx/Observable.java index 98885a765c..1745f148ea 100644 --- a/rxjava-core/src/main/java/rx/Observable.java +++ b/rxjava-core/src/main/java/rx/Observable.java @@ -4466,7 +4466,7 @@ public final void onNext(T args) { * @see RxJava Wiki: elementAt() */ public final Observable elementAt(int index) { - return create(new OperatorElementAt(this, index)); + return lift(new OperatorElementAt(index)); } /** @@ -4486,7 +4486,7 @@ public final Observable elementAt(int index) { * @see RxJava Wiki: elementAtOrDefault() */ public final Observable elementAtOrDefault(int index, T defaultValue) { - return create(new OperatorElementAt(this, index, defaultValue)); + return lift(new OperatorElementAt(index, defaultValue)); } /** diff --git a/rxjava-core/src/main/java/rx/operators/OperatorElementAt.java b/rxjava-core/src/main/java/rx/operators/OperatorElementAt.java index fe7d94c518..6c92c49009 100644 --- a/rxjava-core/src/main/java/rx/operators/OperatorElementAt.java +++ b/rxjava-core/src/main/java/rx/operators/OperatorElementAt.java @@ -15,41 +15,38 @@ */ package rx.operators; -import rx.Observable; -import rx.Observable.OnSubscribe; +import rx.Observable.Operator; import rx.Subscriber; /** * Returns the element at a specified index in a sequence. */ -public final class OperatorElementAt implements OnSubscribe { +public final class OperatorElementAt implements Operator { - private final Observable source; private final int index; private final boolean hasDefault; private final T defaultValue; - public OperatorElementAt(Observable source, int index) { - this(source, index, null, false); + public OperatorElementAt(int index) { + this(index, null, false); } - public OperatorElementAt(Observable source, int index, T defaultValue) { - this(source, index, defaultValue, true); + public OperatorElementAt(int index, T defaultValue) { + this(index, defaultValue, true); } - private OperatorElementAt(Observable source, int index, T defaultValue, boolean hasDefault) { + private OperatorElementAt(int index, T defaultValue, boolean hasDefault) { if (index < 0) { throw new IndexOutOfBoundsException(index + " is out of bounds"); } - this.source = source; this.index = index; this.defaultValue = defaultValue; this.hasDefault = hasDefault; } @Override - public void call(final Subscriber subscriber) { - source.subscribe(new Subscriber(subscriber) { + public Subscriber call(final Subscriber subscriber) { + return new Subscriber(subscriber) { private int currentIndex = 0; @@ -79,6 +76,7 @@ public void onCompleted() { } } } - }); + }; } + } From 682f4bd73fb942cd70116dd3aa4d49953bc3c66e Mon Sep 17 00:00:00 2001 From: zsxwing Date: Mon, 24 Mar 2014 10:56:41 +0800 Subject: [PATCH 152/422] Fix typos --- .../src/main/java/rx/operators/OperatorSingle.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/rxjava-core/src/main/java/rx/operators/OperatorSingle.java b/rxjava-core/src/main/java/rx/operators/OperatorSingle.java index e00aa87d82..f633354817 100644 --- a/rxjava-core/src/main/java/rx/operators/OperatorSingle.java +++ b/rxjava-core/src/main/java/rx/operators/OperatorSingle.java @@ -49,12 +49,12 @@ public Subscriber call(final Subscriber subscriber) { private T value; private boolean isNonEmpty = false; - private boolean hasTooManyElemenets = false; + private boolean hasTooManyElements = false; @Override public void onNext(T value) { if (isNonEmpty) { - hasTooManyElemenets = true; + hasTooManyElements = true; subscriber.onError(new IllegalArgumentException("Sequence contains too many elements")); } else { this.value = value; @@ -64,8 +64,8 @@ public void onNext(T value) { @Override public void onCompleted() { - if (hasTooManyElemenets) { - // We has already sent an onError message + if (hasTooManyElements) { + // We have already sent an onError message } else { if (isNonEmpty) { subscriber.onNext(value); From 07e27697dc51ffa4c26f9e20a427d9900bf6b0b9 Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Thu, 20 Mar 2014 11:00:04 -0700 Subject: [PATCH 153/422] Pivot Operator --- rxjava-core/src/main/java/rx/Observable.java | 31 +- .../rx/observables/GroupedObservable.java | 11 + .../java/rx/observers/SerializedObserver.java | 4 +- .../main/java/rx/operators/OperatorPivot.java | 497 ++++++++++++++++++ .../java/rx/operators/OperatorPivotTest.java | 201 +++++++ 5 files changed, 736 insertions(+), 8 deletions(-) create mode 100644 rxjava-core/src/main/java/rx/operators/OperatorPivot.java create mode 100644 rxjava-core/src/test/java/rx/operators/OperatorPivotTest.java diff --git a/rxjava-core/src/main/java/rx/Observable.java b/rxjava-core/src/main/java/rx/Observable.java index 235e1e62e4..9048a888c2 100644 --- a/rxjava-core/src/main/java/rx/Observable.java +++ b/rxjava-core/src/main/java/rx/Observable.java @@ -12,7 +12,7 @@ */ package rx; -import static rx.functions.Functions.*; +import static rx.functions.Functions.alwaysFalse; import java.util.ArrayList; import java.util.Arrays; @@ -115,6 +115,7 @@ import rx.operators.OperatorOnErrorFlatMap; import rx.operators.OperatorOnErrorResumeNextViaFunction; import rx.operators.OperatorParallel; +import rx.operators.OperatorPivot; import rx.operators.OperatorRepeat; import rx.operators.OperatorRetry; import rx.operators.OperatorScan; @@ -1640,8 +1641,18 @@ public final static Observable interval(long interval, TimeUnit unit, Sche * @return an Observable that emits {@code value} as a single item and then completes * @see RxJava Wiki: just() */ - public final static Observable just(T value) { - return from(Arrays.asList(value)); + public final static Observable just(final T value) { + return Observable.create(new OnSubscribe() { + + @Override + public void call(Subscriber s) { + if (!s.isUnsubscribed()) { + s.onNext(value); + s.onCompleted(); + } + } + + }); } /** @@ -1664,7 +1675,7 @@ public final static Observable just(T value) { */ @Deprecated public final static Observable just(T value, Scheduler scheduler) { - return from(Arrays.asList((value)), scheduler); + return just(value).subscribeOn(scheduler); } /** @@ -2410,7 +2421,7 @@ public final static > Observable min(Observab * @return an Observable that emits a single item: the source Observable */ public final Observable> nest() { - return from(this); + return just(this); } /** @@ -2477,6 +2488,16 @@ public final static Observable> parallelMerge(Observable Observable>> pivot(Observable>> groups) { + return groups.lift(new OperatorPivot()); + } + /** * Returns an Observable that emits a sequence of Integers within a specified range. *

    diff --git a/rxjava-core/src/main/java/rx/observables/GroupedObservable.java b/rxjava-core/src/main/java/rx/observables/GroupedObservable.java index 120f742e77..13b36edd82 100644 --- a/rxjava-core/src/main/java/rx/observables/GroupedObservable.java +++ b/rxjava-core/src/main/java/rx/observables/GroupedObservable.java @@ -16,6 +16,7 @@ package rx.observables; import rx.Observable; +import rx.Subscriber; import rx.functions.Func1; /** @@ -31,6 +32,16 @@ public class GroupedObservable extends Observable { private final K key; + public static GroupedObservable from(K key, final Observable o) { + return new GroupedObservable(key, new OnSubscribe() { + + @Override + public void call(Subscriber s) { + o.subscribe(s); + } + }); + } + public GroupedObservable(K key, OnSubscribe onSubscribe) { super(onSubscribe); this.key = key; diff --git a/rxjava-core/src/main/java/rx/observers/SerializedObserver.java b/rxjava-core/src/main/java/rx/observers/SerializedObserver.java index 8c87a7b855..6be8d7a23e 100644 --- a/rxjava-core/src/main/java/rx/observers/SerializedObserver.java +++ b/rxjava-core/src/main/java/rx/observers/SerializedObserver.java @@ -2,9 +2,6 @@ import java.util.ArrayList; -import javax.management.NotificationListener; - -import rx.Notification; import rx.Observer; import rx.operators.NotificationLite; @@ -54,6 +51,7 @@ public void onCompleted() { queue.add(on.completed()); } } + if (canEmit) { // we won the right to emit try { diff --git a/rxjava-core/src/main/java/rx/operators/OperatorPivot.java b/rxjava-core/src/main/java/rx/operators/OperatorPivot.java new file mode 100644 index 0000000000..c341cca1b3 --- /dev/null +++ b/rxjava-core/src/main/java/rx/operators/OperatorPivot.java @@ -0,0 +1,497 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package rx.operators; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Map.Entry; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +import rx.Observable.OnSubscribe; +import rx.Observable.Operator; +import rx.Subscriber; +import rx.functions.Action0; +import rx.observables.GroupedObservable; +import rx.subscriptions.CompositeSubscription; +import rx.subscriptions.Subscriptions; + +public class OperatorPivot implements Operator>, GroupedObservable>> { + + @Override + public Subscriber>> call(final Subscriber>> child) { + final AtomicReference state = new AtomicReference(State.create()); + final OperatorPivot.PivotSubscriber pivotSubscriber = new PivotSubscriber(new CompositeSubscription(), child, state); + child.add(Subscriptions.create(new Action0() { + + @Override + public void call() { + State current; + State newState = null; + do { + current = state.get(); + newState = current.unsubscribe(); + } while (!state.compareAndSet(current, newState)); + + // If all outer/inner groups are completed/unsubscribed then we can shut it all down + if (newState.shouldComplete()) { + pivotSubscriber.groups.completeAll(newState); + } + // otherwise it is just marked as unsubscribed and groups being completed/unsubscribed will allow it to cleanup + } + + })); + + return pivotSubscriber; + } + + private final class PivotSubscriber extends Subscriber>> { + /* + * needs to decouple the subscription as the inner subscriptions need a separate lifecycle + * and will unsubscribe on this parent if they are all unsubscribed + */ + private final CompositeSubscription parentSubscription; + private final Subscriber>> child; + private final AtomicReference state; + private final GroupState groups; + + private PivotSubscriber(CompositeSubscription parentSubscription, Subscriber>> child, AtomicReference state) { + super(parentSubscription); + this.parentSubscription = parentSubscription; + this.child = child; + this.state = state; + this.groups = new GroupState(parentSubscription, child); + } + + @Override + public void onCompleted() { + State current; + State newState = null; + do { + current = state.get(); + newState = current.complete(); + } while (!state.compareAndSet(current, newState)); + + // special case for empty (no groups emitted) or all groups already done + if (newState.shouldComplete()) { + groups.completeAll(newState); + } + } + + @Override + public void onError(Throwable e) { + // we immediately tear everything down if we receive an error + child.onError(e); + } + + @Override + public void onNext(final GroupedObservable> k1Group) { + groups.startK1Group(state, k1Group.getKey()); + k1Group.subscribe(new Subscriber>(parentSubscription) { + + @Override + public void onCompleted() { + groups.completeK1Group(state, k1Group.getKey()); + } + + @Override + public void onError(Throwable e) { + child.onError(e); + } + + @Override + public void onNext(final GroupedObservable k2Group) { + /* + * In this scope once pivoted, k2 == outer and k2.k1 == inner + */ + final Inner inner = groups.getOrCreateFor(state, child, k1Group.getKey(), k2Group.getKey()); + if (inner == null) { + // we have been unsubscribed + return; + } + k2Group.subscribe(new Subscriber(parentSubscription) { + + @Override + public void onCompleted() { + /* + * we don't actually propagate onCompleted to the 'inner.subscriber' here + * since multiple upstream groups will be sent to a single downstream + * and a single upstream group completing does not indicate total completion + */ + } + + @Override + public void onError(Throwable e) { + inner.subscriber.onError(e); + } + + @Override + public void onNext(T t) { + inner.subscriber.onNext(t); + } + + }); + + } + + }); + + } + + } + + private static class GroupState { + private final ConcurrentHashMap, Inner> innerSubjects = new ConcurrentHashMap, Inner>(); + private final ConcurrentHashMap> outerSubjects = new ConcurrentHashMap>(); + private final AtomicBoolean completeEmitted = new AtomicBoolean(); + private final CompositeSubscription parentSubscription; + private final Subscriber>> child; + + public GroupState(CompositeSubscription parentSubscription, Subscriber>> child) { + this.parentSubscription = parentSubscription; + this.child = child; + } + + public void startK1Group(AtomicReference state, K1 key) { + State current; + State newState = null; + do { + current = state.get(); + newState = current.addK1(key); + } while (!state.compareAndSet(current, newState)); + } + + public void completeK1Group(AtomicReference state, K1 key) { + State current; + State newState = null; + do { + current = state.get(); + newState = current.removeK1(key); + } while (!state.compareAndSet(current, newState)); + + if (newState.shouldComplete()) { + completeAll(newState); + } + } + + public void startK1K2Group(AtomicReference state, KeyPair keyPair) { + State current; + State newState = null; + do { + current = state.get(); + newState = current.addK1k2(keyPair); + } while (!state.compareAndSet(current, newState)); + } + + public void completeK1K2Group(AtomicReference state, KeyPair keyPair) { + State current; + State newState = null; + do { + current = state.get(); + newState = current.removeK1k2(keyPair); + } while (!state.compareAndSet(current, newState)); + + if (newState.shouldComplete()) { + completeAll(newState); + } + } + + public void completeAll(State state) { + if (completeEmitted.compareAndSet(false, true)) { + /* + * after we are completely done emitting we can now shut down the groups + */ + for (Entry> outer : outerSubjects.entrySet()) { + outer.getValue().subscriber.onCompleted(); + } + for (Entry, Inner> inner : innerSubjects.entrySet()) { + inner.getValue().subscriber.onCompleted(); + } + // unsubscribe eagerly + if (state.unsubscribed) { + parentSubscription.unsubscribe(); // unsubscribe from parent + } + child.onCompleted(); + } + } + + private Inner getOrCreateFor(final AtomicReference state, final Subscriber>> child, K1 key1, K2 key2) { + Outer outer = getOrCreateOuter(state, child, key2); + if (outer == null) { + // we have been unsubscribed + return null; + } + + Inner orCreateInnerSubject = getOrCreateInnerSubject(state, outer, key1, key2); + return orCreateInnerSubject; + } + + private Inner getOrCreateInnerSubject(final AtomicReference state, final Outer outer, final K1 key1, final K2 key2) { + KeyPair keyPair = new KeyPair(key1, key2); + Inner innerSubject = innerSubjects.get(keyPair); + if (innerSubject != null) { + return innerSubject; + } else { + Inner newInner = Inner.create(this, state, outer, keyPair); + Inner existing = innerSubjects.putIfAbsent(keyPair, newInner); + if (existing != null) { + // we lost the race to create so return the one that did + return existing; + } else { + startK1K2Group(state, keyPair); + outer.subscriber.onNext(newInner.group); + return newInner; + } + } + } + + private Outer getOrCreateOuter(final AtomicReference state, final Subscriber>> child, final K2 key2) { + Outer outerSubject = outerSubjects.get(key2); + if (outerSubject != null) { + return outerSubject; + } else { + // this group doesn't exist + if (child.isUnsubscribed()) { + // we have been unsubscribed on the outer so won't send any more groups + return null; + } + + Outer newOuter = Outer. create(this, state, key2); + Outer existing = outerSubjects.putIfAbsent(key2, newOuter); + if (existing != null) { + // we lost the race to create so return the one that did + return existing; + } else { + child.onNext(newOuter.group); + return newOuter; + } + } + } + } + + private static class Inner { + + private final BufferUntilSubscriber subscriber; + private final GroupedObservable group; + + private Inner(BufferUntilSubscriber subscriber, GroupedObservable group) { + this.subscriber = subscriber; + this.group = group; + } + + public static Inner create(final GroupState groupState, final AtomicReference state, final Outer outer, final KeyPair keyPair) { + final BufferUntilSubscriber subject = BufferUntilSubscriber.create(); + GroupedObservable group = new GroupedObservable(keyPair.k1, new OnSubscribe() { + + @Override + public void call(final Subscriber o) { + o.add(Subscriptions.create(new Action0() { + + @Override + public void call() { + groupState.completeK1K2Group(state, keyPair); + } + + })); + subject.subscribe(new Subscriber(o) { + + @Override + public void onCompleted() { + groupState.completeK1K2Group(state, keyPair); + o.onCompleted(); + } + + @Override + public void onError(Throwable e) { + o.onError(e); + } + + @Override + public void onNext(T t) { + if (!isUnsubscribed()) { + o.onNext(t); + } + } + + }); + } + + }); + return new Inner(subject, group); + } + } + + private static class Outer { + + private final BufferUntilSubscriber> subscriber; + private final GroupedObservable> group; + + private Outer(BufferUntilSubscriber> subscriber, GroupedObservable> group) { + this.subscriber = subscriber; + this.group = group; + } + + public static Outer create(final GroupState groups, final AtomicReference state, final K2 key2) { + final BufferUntilSubscriber> subject = BufferUntilSubscriber.create(); + GroupedObservable> group = new GroupedObservable>(key2, new OnSubscribe>() { + + @Override + public void call(final Subscriber> o) { + subject.subscribe(new Subscriber>(o) { + + @Override + public void onCompleted() { + o.onCompleted(); + } + + @Override + public void onError(Throwable e) { + o.onError(e); + } + + @Override + public void onNext(GroupedObservable t) { + if (!isUnsubscribed()) { + o.onNext(t); + } + } + + }); + } + + }); + + return new Outer(subject, group); + } + } + + private static class State { + private final boolean unsubscribed; + private final boolean completed; + private final Set k1Keys; + private final Set> k1k2Keys; + + private State(boolean completed, boolean unsubscribed, Set k1Keys, Set> k1k2Keys) { + this.completed = completed; + this.unsubscribed = unsubscribed; + this.k1Keys = k1Keys; + this.k1k2Keys = k1k2Keys; + } + + public static State create() { + return new State(false, false, Collections.emptySet(), Collections.> emptySet()); + } + + public State addK1(Object key) { + Set newKeys = new HashSet(k1Keys); + newKeys.add(key); + return new State(completed, unsubscribed, newKeys, k1k2Keys); + } + + public State removeK1(Object key) { + Set newKeys = new HashSet(k1Keys); + newKeys.remove(key); + return new State(completed, unsubscribed, newKeys, k1k2Keys); + } + + public State addK1k2(KeyPair key) { + Set> newKeys = new HashSet>(k1k2Keys); + newKeys.add(key); + return new State(completed, unsubscribed, k1Keys, newKeys); + } + + public State removeK1k2(KeyPair key) { + Set> newKeys = new HashSet>(k1k2Keys); + newKeys.remove(key); + return new State(completed, unsubscribed, k1Keys, newKeys); + } + + public State complete() { + return new State(true, unsubscribed, k1Keys, k1k2Keys); + } + + public State unsubscribe() { + return new State(completed, true, k1Keys, k1k2Keys); + } + + public boolean shouldComplete() { + if (k1Keys.size() == 0 && completed) { + return true; + } else if (unsubscribed) { + // if unsubscribed and all groups are completed/unsubscribed we can complete + return k1k2Keys.size() == 0; + } else { + return false; + } + } + + @Override + public String toString() { + return "State => k1: " + k1Keys.size() + " k1k2: " + k1k2Keys.size() + " completed: " + completed + " unsubscribed: " + unsubscribed; + } + } + + private static class KeyPair { + private final K1 k1; + private final K2 k2; + + KeyPair(K1 k1, K2 k2) { + this.k1 = k1; + this.k2 = k2; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((k1 == null) ? 0 : k1.hashCode()); + result = prime * result + ((k2 == null) ? 0 : k2.hashCode()); + return result; + } + + @SuppressWarnings("rawtypes") + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + KeyPair other = (KeyPair) obj; + if (k1 == null) { + if (other.k1 != null) + return false; + } else if (!k1.equals(other.k1)) + return false; + if (k2 == null) { + if (other.k2 != null) + return false; + } else if (!k2.equals(other.k2)) + return false; + return true; + } + + @Override + public String toString() { + return k2 + "." + k1; + } + + } + +} diff --git a/rxjava-core/src/test/java/rx/operators/OperatorPivotTest.java b/rxjava-core/src/test/java/rx/operators/OperatorPivotTest.java new file mode 100644 index 0000000000..306252db3f --- /dev/null +++ b/rxjava-core/src/test/java/rx/operators/OperatorPivotTest.java @@ -0,0 +1,201 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package rx.operators; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Test; + +import rx.Observable; +import rx.Observable.OnSubscribe; +import rx.Observer; +import rx.Subscriber; +import rx.functions.Func1; +import rx.observables.GroupedObservable; +import rx.observers.TestSubscriber; +import rx.schedulers.Schedulers; + +public class OperatorPivotTest { + + @Test + public void testPivotEvenAndOdd() throws InterruptedException { + Observable> o1 = Observable.range(1, 10).groupBy(modKeySelector).subscribeOn(Schedulers.newThread()); + Observable> o2 = Observable.range(11, 10).groupBy(modKeySelector).subscribeOn(Schedulers.newThread()); + Observable>> groups = Observable.from(GroupedObservable.from("o1", o1), GroupedObservable.from("o2", o2)); + Observable>> pivoted = Observable.pivot(groups); + + final AtomicInteger count = new AtomicInteger(); + + final CountDownLatch latch = new CountDownLatch(1); + + pivoted.flatMap(new Func1>, Observable>() { + + @Override + public Observable call(final GroupedObservable> outerGroup) { + return outerGroup.flatMap(new Func1, Observable>() { + + @Override + public Observable call(final GroupedObservable innerGroup) { + return innerGroup.map(new Func1() { + + @Override + public String call(Integer i) { + return (outerGroup.getKey() ? "Even" : "Odd ") + " => from source: " + innerGroup.getKey() + " Value: " + i; + } + + }); + } + + }); + } + + }).subscribe(new Observer() { + + @Override + public void onCompleted() { + System.out.println("============> OnCompleted"); + System.out.println("-------------------------------------------------------------------------------------"); + latch.countDown(); + } + + @Override + public void onError(Throwable e) { + System.out.println("============> Error: " + e); + System.out.println("-------------------------------------------------------------------------------------"); + latch.countDown(); + } + + @Override + public void onNext(String t) { + System.out.println("============> OnNext: " + t); + count.incrementAndGet(); + } + + }); + + if (!latch.await(800, TimeUnit.MILLISECONDS)) { + System.out.println("xxxxxxxxxxxxxxxxxx> TIMED OUT > o1 = getSource(2000, counter1).subscribeOn(Schedulers.newThread()).groupBy(modKeySelector); + Observable> o2 = getSource(4000, counter2).subscribeOn(Schedulers.newThread()).groupBy(modKeySelector); + Observable>> groups = Observable.from(GroupedObservable.from("o1", o1), GroupedObservable.from("o2", o2)); + Observable>> pivoted = Observable.pivot(groups); + TestSubscriber ts = new TestSubscriber(); + pivoted.take(2).flatMap(new Func1>, Observable>() { + + @Override + public Observable call(final GroupedObservable> outerGroup) { + return outerGroup.flatMap(new Func1, Observable>() { + + @Override + public Observable call(final GroupedObservable innerGroup) { + return innerGroup.take(10).map(new Func1() { + + @Override + public String call(Integer i) { + return (outerGroup.getKey() ? "Even" : "Odd ") + " => from source: " + innerGroup.getKey() + " Value: " + i; + } + + }); + } + + }); + } + + }).subscribe(ts); + + ts.awaitTerminalEvent(); + System.out.println("onNext [" + ts.getOnNextEvents().size() + "]: " + ts.getOnNextEvents()); + assertEquals(40, ts.getOnNextEvents().size()); // 2 (o1 + o2) + 2 (odd + even) * take(10) on each + + int c1 = counter1.get(); + int c2 = counter2.get(); + + System.out.println("Counter1: " + c1); + System.out.println("Counter2: " + c2); + + Thread.sleep(200); + + System.out.println("Counter1: " + counter1.get()); + System.out.println("Counter2: " + counter2.get()); + + // after time it should be same if unsubscribed + assertEquals(c1, counter1.get(), 1); // delta of 1 for race to unsubscribe + assertEquals(c2, counter2.get(), 1); // delta of 1 for race to unsubscribe + + assertTrue(counter1.get() < 50000); // should be much smaller (< 1000) but this will be non-deterministic + assertTrue(counter2.get() < 50000); // should be much smaller (< 1000) but this will be non-deterministic + } + + private static Observable getSource(final int start, final AtomicInteger counter) { + return Observable.create(new OnSubscribe() { + + @Override + public void call(Subscriber s) { + for (int i = start; i < 1000000; i++) { + if (s.isUnsubscribed()) { + System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> unsubscribed so quitting in source: " + start); + return; + } + counter.incrementAndGet(); + s.onNext(i); + try { + // slow it down so it's not just emitting it all into a buffer (merge) + Thread.sleep(1); + } catch (InterruptedException e) { + } + } + s.onCompleted(); + } + + }); + } + + final static Func1 modKeySelector = new Func1() { + + @Override + public Boolean call(Integer i) { + return i % 2 == 0; + } + + }; +} From eb0839b7bb93fb009ee74e49190a5bac05a55e9f Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Tue, 25 Mar 2014 11:16:10 -0700 Subject: [PATCH 154/422] Pivot Unit Test --- .../java/rx/operators/OperatorPivotTest.java | 136 +++++++++++++++++- 1 file changed, 135 insertions(+), 1 deletion(-) diff --git a/rxjava-core/src/test/java/rx/operators/OperatorPivotTest.java b/rxjava-core/src/test/java/rx/operators/OperatorPivotTest.java index 306252db3f..85ff2a995c 100644 --- a/rxjava-core/src/test/java/rx/operators/OperatorPivotTest.java +++ b/rxjava-core/src/test/java/rx/operators/OperatorPivotTest.java @@ -60,7 +60,7 @@ public Observable call(final GroupedObservable innerGro @Override public String call(Integer i) { - return (outerGroup.getKey() ? "Even" : "Odd ") + " => from source: " + innerGroup.getKey() + " Value: " + i; + return (outerGroup.getKey() ? "Even" : "Odd ") + " => from source: " + outerGroup.getKey() + "." + innerGroup.getKey() + " Value: " + i + " Thread: " + Thread.currentThread(); } }); @@ -166,6 +166,140 @@ public String call(Integer i) { assertTrue(counter2.get() < 50000); // should be much smaller (< 1000) but this will be non-deterministic } + /** + * The pivot operator does not need to add any serialization but this is confirming the expected behavior. + * + * It does not need serializing as it never merges groups, it just re-arranges them. + * + * For example, a simple 2-stream case with odd/even: + * + * Observable> o1 = Observable.range(1, 10).groupBy(modKeySelector).subscribeOn(Schedulers.newThread()); // thread 1 + * Observable> o2 = Observable.range(11, 10).groupBy(modKeySelector).subscribeOn(Schedulers.newThread()); // thread 2 + * Observable>> groups = Observable.from(GroupedObservable.from("o1", o1), GroupedObservable.from("o2", o2)); + * Observable>> pivoted = Observable.pivot(groups); + * + * ============> OnNext: Odd => from source: false.o1 Value: 1 Thread: Thread[RxNewThreadScheduler-1,5,main] + * ============> OnNext: Even => from source: true.o2 Value: 12 Thread: Thread[RxNewThreadScheduler-2,5,main] + * ============> OnNext: Even => from source: true.o1 Value: 2 Thread: Thread[RxNewThreadScheduler-1,5,main] + * ============> OnNext: Odd => from source: false.o2 Value: 11 Thread: Thread[RxNewThreadScheduler-2,5,main] + * ============> OnNext: Odd => from source: false.o2 Value: 13 Thread: Thread[RxNewThreadScheduler-2,5,main] + * ============> OnNext: Odd => from source: false.o2 Value: 15 Thread: Thread[RxNewThreadScheduler-2,5,main] + * ============> OnNext: Odd => from source: false.o2 Value: 17 Thread: Thread[RxNewThreadScheduler-2,5,main] + * ============> OnNext: Even => from source: true.o2 Value: 14 Thread: Thread[RxNewThreadScheduler-2,5,main] + * ============> OnNext: Odd => from source: false.o1 Value: 3 Thread: Thread[RxNewThreadScheduler-1,5,main] + * ============> OnNext: Odd => from source: false.o1 Value: 5 Thread: Thread[RxNewThreadScheduler-1,5,main] + * ============> OnNext: Odd => from source: false.o1 Value: 7 Thread: Thread[RxNewThreadScheduler-1,5,main] + * ============> OnNext: Odd => from source: false.o1 Value: 9 Thread: Thread[RxNewThreadScheduler-1,5,main] + * ============> OnNext: Even => from source: true.o2 Value: 16 Thread: Thread[RxNewThreadScheduler-2,5,main] + * ============> OnNext: Even => from source: true.o2 Value: 18 Thread: Thread[RxNewThreadScheduler-2,5,main] + * ============> OnNext: Odd => from source: false.o2 Value: 19 Thread: Thread[RxNewThreadScheduler-2,5,main] + * ============> OnNext: Even => from source: true.o1 Value: 4 Thread: Thread[RxNewThreadScheduler-1,5,main] + * ============> OnNext: Even => from source: true.o1 Value: 6 Thread: Thread[RxNewThreadScheduler-1,5,main] + * ============> OnNext: Even => from source: true.o1 Value: 8 Thread: Thread[RxNewThreadScheduler-1,5,main] + * ============> OnNext: Even => from source: true.o1 Value: 10 Thread: Thread[RxNewThreadScheduler-1,5,main] + * ============> OnNext: Even => from source: true.o2 Value: 20 Thread: Thread[RxNewThreadScheduler-2,5,main] + * ============> OnCompleted + * + * This starts as: + * + * => Observable>>: + * + * o1.odd: 1, 3, 5, 7, 9 on Thread 1 + * o1.even: 2, 4, 6, 8, 10 on Thread 1 + * o2.odd: 11, 13, 15, 17, 19 on Thread 2 + * o2.even: 12, 14, 16, 18, 20 on Thread 2 + * + * It pivots to become: + * + * => Observable>>: + * + * odd.o1: 1, 3, 5, 7, 9 on Thread 1 + * odd.o2: 11, 13, 15, 17, 19 on Thread 2 + * even.o1: 2, 4, 6, 8, 10 on Thread 1 + * even.o2: 12, 14, 16, 18, 20 on Thread 2 + * + * Then a subsequent step can merge them if desired and add serialization, such as merge(even.o1, even.o2) to become a serialized "even" + */ + @Test + public void testConcurrencyAndSerialization() throws InterruptedException { + final AtomicInteger maxOuterConcurrency = new AtomicInteger(); + final AtomicInteger maxGroupConcurrency = new AtomicInteger(); + Observable> o1 = getSource(2000).subscribeOn(Schedulers.newThread()).groupBy(modKeySelector); + Observable> o2 = getSource(4000).subscribeOn(Schedulers.newThread()).groupBy(modKeySelector); + Observable> o3 = getSource(6000).subscribeOn(Schedulers.newThread()).groupBy(modKeySelector); + Observable> o4 = getSource(8000).subscribeOn(Schedulers.newThread()).groupBy(modKeySelector); + Observable>> groups = Observable.from(GroupedObservable.from("o1", o1), GroupedObservable.from("o2", o2), + GroupedObservable.from("o3", o3), GroupedObservable.from("o4", o4)); + Observable>> pivoted = Observable.pivot(groups); + TestSubscriber ts = new TestSubscriber(); + pivoted.take(2).flatMap(new Func1>, Observable>() { + + final AtomicInteger outerThreads = new AtomicInteger(); + + @Override + public Observable call(final GroupedObservable> outerGroup) { + return outerGroup.flatMap(new Func1, Observable>() { + + @Override + public Observable call(final GroupedObservable innerGroup) { + final AtomicInteger threadsPerGroup = new AtomicInteger(); + return innerGroup.take(100).map(new Func1() { + + @Override + public String call(Integer i) { + int outerThreadCount = outerThreads.incrementAndGet(); + setMaxConcurrency(maxOuterConcurrency, outerThreadCount); + int innerThreadCount = threadsPerGroup.incrementAndGet(); + setMaxConcurrency(maxGroupConcurrency, innerThreadCount); + if (innerThreadCount > 1) { + System.err.println("more than 1 thread for this group [" + innerGroup.getKey() + "]: " + innerThreadCount + " (before)"); + throw new RuntimeException("more than 1 thread for this group [" + innerGroup.getKey() + "]: " + innerThreadCount + " (before)"); + } + try { + return (outerGroup.getKey() ? "Even" : "Odd ") + " => from source: " + innerGroup.getKey() + " Value: " + i; + } finally { + int outerThreadCountAfter = outerThreads.decrementAndGet(); + setMaxConcurrency(maxOuterConcurrency, outerThreadCountAfter); + int innerThreadCountAfter = threadsPerGroup.decrementAndGet(); + setMaxConcurrency(maxGroupConcurrency, innerThreadCountAfter); + if (innerThreadCountAfter > 0) { + System.err.println("more than 1 thread for this group [" + innerGroup.getKey() + "]: " + innerThreadCount + " (after)"); + throw new RuntimeException("more than 1 thread for this group [" + innerGroup.getKey() + "]: " + innerThreadCountAfter + " (after)"); + } + } + } + + private void setMaxConcurrency(final AtomicInteger maxOuterConcurrency, int outerThreadCount) { + int max = maxOuterConcurrency.get(); + if (outerThreadCount > max) { + maxOuterConcurrency.compareAndSet(max, outerThreadCount); + } + } + + }); + } + + }); + } + + }).subscribe(ts); + + ts.awaitTerminalEvent(); + + System.out.println("onNext [" + ts.getOnNextEvents().size() + "]: " + ts.getOnNextEvents()); + System.out.println("max outer concurrency: " + maxOuterConcurrency.get()); + assertTrue(maxOuterConcurrency.get() > 2); // should be 4 since we have 4 threads running but setting at 3 as this is somewhat non-deterministic + System.out.println("max group concurrency: " + maxGroupConcurrency.get()); + assertTrue(maxGroupConcurrency.get() == 1); // should always be 1 + + assertEquals(800, ts.getOnNextEvents().size()); + + } + + private static Observable getSource(final int start) { + return getSource(start, new AtomicInteger()); + } + private static Observable getSource(final int start, final AtomicInteger counter) { return Observable.create(new OnSubscribe() { From b1c14f3a178e9f65c10add4a39ae78f10e2d630f Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Tue, 25 Mar 2014 11:17:29 -0700 Subject: [PATCH 155/422] Add Javadoc to Pivot --- rxjava-core/src/main/java/rx/Observable.java | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/rxjava-core/src/main/java/rx/Observable.java b/rxjava-core/src/main/java/rx/Observable.java index 9048a888c2..172e3962bd 100644 --- a/rxjava-core/src/main/java/rx/Observable.java +++ b/rxjava-core/src/main/java/rx/Observable.java @@ -2489,7 +2489,25 @@ public final static Observable> parallelMerge(Observable + * + * Observable>>: + * + * o1.odd: 1, 3, 5, 7, 9 on Thread 1 + * o1.even: 2, 4, 6, 8, 10 on Thread 1 + * o2.odd: 11, 13, 15, 17, 19 on Thread 2 + * o2.even: 12, 14, 16, 18, 20 on Thread 2 + * + * is pivoted to become this => + * + * Observable>>: + * + * odd.o1: 1, 3, 5, 7, 9 on Thread 1 + * odd.o2: 11, 13, 15, 17, 19 on Thread 2 + * even.o1: 2, 4, 6, 8, 10 on Thread 1 + * even.o2: 12, 14, 16, 18, 20 on Thread 2 * * @param groups * @return From d1ac686cfc8895365b38084e0857eb316b15d4eb Mon Sep 17 00:00:00 2001 From: David Gross Date: Tue, 25 Mar 2014 13:00:34 -0700 Subject: [PATCH 156/422] add marble diagrams to pivot() javadocs --- rxjava-core/src/main/java/rx/Observable.java | 35 +++++++++++--------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/rxjava-core/src/main/java/rx/Observable.java b/rxjava-core/src/main/java/rx/Observable.java index 172e3962bd..8f72aaea7a 100644 --- a/rxjava-core/src/main/java/rx/Observable.java +++ b/rxjava-core/src/main/java/rx/Observable.java @@ -2490,24 +2490,29 @@ public final static Observable> parallelMerge(Observable + * * - * For example an Observable such as this => - * - * Observable>>: - * - * o1.odd: 1, 3, 5, 7, 9 on Thread 1 - * o1.even: 2, 4, 6, 8, 10 on Thread 1 - * o2.odd: 11, 13, 15, 17, 19 on Thread 2 - * o2.even: 12, 14, 16, 18, 20 on Thread 2 + * For example an Observable such as this => * - * is pivoted to become this => - * - * Observable>>: + * {@code Observable>>}: + *
      + *
    • o1.odd: 1, 3, 5, 7, 9 on Thread 1
    • + *
    • o1.even: 2, 4, 6, 8, 10 on Thread 1
    • + *
    • o2.odd: 11, 13, 15, 17, 19 on Thread 2
    • + *
    • o2.even: 12, 14, 16, 18, 20 on Thread 2
    • + *
    + * is pivoted to become this => * - * odd.o1: 1, 3, 5, 7, 9 on Thread 1 - * odd.o2: 11, 13, 15, 17, 19 on Thread 2 - * even.o1: 2, 4, 6, 8, 10 on Thread 1 - * even.o2: 12, 14, 16, 18, 20 on Thread 2 + * {@code Observable>>}: + *
      + *
    • odd.o1: 1, 3, 5, 7, 9 on Thread 1
    • + *
    • odd.o2: 11, 13, 15, 17, 19 on Thread 2
    • + *
    • even.o1: 2, 4, 6, 8, 10 on Thread 1
    • + *
    • even.o2: 12, 14, 16, 18, 20 on Thread 2
    • + *
    + *

    + * * * @param groups * @return From 792fffe44742158030730035c05c55231e93a27d Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Tue, 25 Mar 2014 14:31:49 -0700 Subject: [PATCH 157/422] Fix SynchronizedObserver.runConcurrencyTest It wasn't waiting on all threads before emitting onCompleted. --- .../src/test/java/rx/observers/SynchronizedObserverTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rxjava-core/src/test/java/rx/observers/SynchronizedObserverTest.java b/rxjava-core/src/test/java/rx/observers/SynchronizedObserverTest.java index aad021d2e3..e3b7a6d9e5 100644 --- a/rxjava-core/src/test/java/rx/observers/SynchronizedObserverTest.java +++ b/rxjava-core/src/test/java/rx/observers/SynchronizedObserverTest.java @@ -361,14 +361,14 @@ public void runConcurrencyTest() { // 12000 + 5000 + 75000 + 13500 + 22000 + 15000 + 7500 + 23500 = 173500 - Future f10 = tp.submit(new CompletionThread(w, TestConcurrencyObserverEvent.onCompleted, f1, f2, f3, f4)); + Future f10 = tp.submit(new CompletionThread(w, TestConcurrencyObserverEvent.onCompleted, f1, f2, f3, f4, f5, f6, f7, f8)); try { Thread.sleep(1); } catch (InterruptedException e) { // ignore } - waitOnThreads(f1, f2, f3, f4, f5, f6, f7, f8, f10); + waitOnThreads(f10); @SuppressWarnings("unused") int numNextEvents = tw.assertEvents(null); // no check of type since we don't want to test barging results here, just interleaving behavior assertEquals(173500, numNextEvents); From 7298e763b34ca5bfd5537407a74fb6c63032ee3f Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Tue, 25 Mar 2014 14:37:35 -0700 Subject: [PATCH 158/422] Fix Non-Deterministic Pivot Test This failed on build servers (small, slow, single or dual-core machines). --- .../src/test/java/rx/operators/OperatorPivotTest.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/rxjava-core/src/test/java/rx/operators/OperatorPivotTest.java b/rxjava-core/src/test/java/rx/operators/OperatorPivotTest.java index 85ff2a995c..6293974272 100644 --- a/rxjava-core/src/test/java/rx/operators/OperatorPivotTest.java +++ b/rxjava-core/src/test/java/rx/operators/OperatorPivotTest.java @@ -287,8 +287,10 @@ private void setMaxConcurrency(final AtomicInteger maxOuterConcurrency, int oute ts.awaitTerminalEvent(); System.out.println("onNext [" + ts.getOnNextEvents().size() + "]: " + ts.getOnNextEvents()); - System.out.println("max outer concurrency: " + maxOuterConcurrency.get()); - assertTrue(maxOuterConcurrency.get() > 2); // should be 4 since we have 4 threads running but setting at 3 as this is somewhat non-deterministic + if (Runtime.getRuntime().availableProcessors() >= 4) { + System.out.println("max outer concurrency: " + maxOuterConcurrency.get()); + assertTrue(maxOuterConcurrency.get() > 1); // should be 4 since we have 4 threads and cores running but setting at just > 1 as this is non-deterministic + } System.out.println("max group concurrency: " + maxGroupConcurrency.get()); assertTrue(maxGroupConcurrency.get() == 1); // should always be 1 From fb47ac80792cbaa029a31a6755d7e82d06e787a5 Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Tue, 25 Mar 2014 14:55:50 -0700 Subject: [PATCH 159/422] OnErrorFailedException Re-throw when onError throws an Exception. This fixes https://github.com/Netflix/RxJava/issues/969 --- .../main/java/rx/exceptions/Exceptions.java | 7 ++++ .../rx/exceptions/OnErrorFailedException.java | 35 +++++++++++++++++++ .../java/rx/observers/SafeSubscriber.java | 7 ++-- .../java/rx/exceptions/ExceptionsTest.java | 33 +++++++++++++++++ .../java/rx/observers/SafeObserverTest.java | 10 ++++-- 5 files changed, 87 insertions(+), 5 deletions(-) create mode 100644 rxjava-core/src/main/java/rx/exceptions/OnErrorFailedException.java diff --git a/rxjava-core/src/main/java/rx/exceptions/Exceptions.java b/rxjava-core/src/main/java/rx/exceptions/Exceptions.java index 5fccfb2a61..86679c61b2 100644 --- a/rxjava-core/src/main/java/rx/exceptions/Exceptions.java +++ b/rxjava-core/src/main/java/rx/exceptions/Exceptions.java @@ -44,6 +44,13 @@ public static RuntimeException propagate(Throwable t) { public static void throwIfFatal(Throwable t) { if (t instanceof OnErrorNotImplementedException) { throw (OnErrorNotImplementedException) t; + } else if (t instanceof OnErrorFailedException) { + Throwable cause = ((OnErrorFailedException) t).getCause(); + if (cause instanceof RuntimeException) { + throw (RuntimeException) cause; + } else { + throw (OnErrorFailedException) t; + } } // values here derived from https://github.com/Netflix/RxJava/issues/748#issuecomment-32471495 else if (t instanceof StackOverflowError) { diff --git a/rxjava-core/src/main/java/rx/exceptions/OnErrorFailedException.java b/rxjava-core/src/main/java/rx/exceptions/OnErrorFailedException.java new file mode 100644 index 0000000000..be75ac82af --- /dev/null +++ b/rxjava-core/src/main/java/rx/exceptions/OnErrorFailedException.java @@ -0,0 +1,35 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package rx.exceptions; + +import rx.Subscriber; + +/** + * Used for re-throwing errors thrown from {@link Subscriber#onError(Throwable)}. + * + * https://github.com/Netflix/RxJava/issues/969 + */ +public class OnErrorFailedException extends RuntimeException { + private static final long serialVersionUID = -419289748403337611L; + + public OnErrorFailedException(String message, Throwable e) { + super(message, e); + } + + public OnErrorFailedException(Throwable e) { + super(e.getMessage(), e); + } +} \ No newline at end of file diff --git a/rxjava-core/src/main/java/rx/observers/SafeSubscriber.java b/rxjava-core/src/main/java/rx/observers/SafeSubscriber.java index ff121eeb77..1bdbcd8272 100644 --- a/rxjava-core/src/main/java/rx/observers/SafeSubscriber.java +++ b/rxjava-core/src/main/java/rx/observers/SafeSubscriber.java @@ -21,6 +21,7 @@ import rx.Subscriber; import rx.exceptions.CompositeException; import rx.exceptions.Exceptions; +import rx.exceptions.OnErrorFailedException; import rx.exceptions.OnErrorNotImplementedException; import rx.plugins.RxJavaPlugins; @@ -165,10 +166,10 @@ protected void _onError(Throwable e) { } catch (Throwable pluginException) { handlePluginException(pluginException); } - throw new RuntimeException("Error occurred when trying to propagate error to Observer.onError and during unsubscription.", new CompositeException(Arrays.asList(e, e2, unsubscribeException))); + throw new OnErrorFailedException("Error occurred when trying to propagate error to Observer.onError and during unsubscription.", new CompositeException(Arrays.asList(e, e2, unsubscribeException))); } - throw new RuntimeException("Error occurred when trying to propagate error to Observer.onError", new CompositeException(Arrays.asList(e, e2))); + throw new OnErrorFailedException("Error occurred when trying to propagate error to Observer.onError", new CompositeException(Arrays.asList(e, e2))); } } // if we did not throw above we will unsubscribe here, if onError failed then unsubscribe happens in the catch @@ -180,7 +181,7 @@ protected void _onError(Throwable e) { } catch (Throwable pluginException) { handlePluginException(pluginException); } - throw unsubscribeException; + throw new OnErrorFailedException(unsubscribeException); } } diff --git a/rxjava-core/src/test/java/rx/exceptions/ExceptionsTest.java b/rxjava-core/src/test/java/rx/exceptions/ExceptionsTest.java index 6880553736..75ce52c27e 100644 --- a/rxjava-core/src/test/java/rx/exceptions/ExceptionsTest.java +++ b/rxjava-core/src/test/java/rx/exceptions/ExceptionsTest.java @@ -15,6 +15,9 @@ */ package rx.exceptions; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + import org.junit.Test; import rx.Observable; @@ -134,4 +137,34 @@ public void onNext(Integer t) { }); } + /** + * https://github.com/Netflix/RxJava/issues/969 + */ + @Test + public void testOnErrorExceptionIsThrown() { + try { + Observable.error(new IllegalArgumentException("original exception")).subscribe(new Observer() { + @Override + public void onCompleted() { + + } + + @Override + public void onError(Throwable e) { + throw new IllegalStateException("This should be thrown"); + } + + @Override + public void onNext(Object o) { + + } + }); + fail("expecting an exception to be thrown"); + } catch (CompositeException t) { + CompositeException ce = (CompositeException) t; + assertTrue(ce.getExceptions().get(0) instanceof IllegalArgumentException); + assertTrue(ce.getExceptions().get(1) instanceof IllegalStateException); + } + } + } diff --git a/rxjava-core/src/test/java/rx/observers/SafeObserverTest.java b/rxjava-core/src/test/java/rx/observers/SafeObserverTest.java index 89b835ce00..f4316d4698 100644 --- a/rxjava-core/src/test/java/rx/observers/SafeObserverTest.java +++ b/rxjava-core/src/test/java/rx/observers/SafeObserverTest.java @@ -15,7 +15,11 @@ */ package rx.observers; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import java.util.concurrent.atomic.AtomicReference; @@ -23,6 +27,7 @@ import rx.Subscriber; import rx.exceptions.CompositeException; +import rx.exceptions.OnErrorFailedException; import rx.exceptions.OnErrorNotImplementedException; import rx.functions.Action0; import rx.subscriptions.Subscriptions; @@ -215,7 +220,8 @@ public void call() { assertEquals("failed", onError.get().getMessage()); // now assert the exception that was thrown - assertTrue(e instanceof SafeObserverTestException); + OnErrorFailedException onErrorFailedException = (OnErrorFailedException) e; + assertTrue(onErrorFailedException.getCause() instanceof SafeObserverTestException); assertEquals("failure from unsubscribe", e.getMessage()); } } From 007774ba6207ffaa2157a98ac684e37894c71308 Mon Sep 17 00:00:00 2001 From: Bob T Builder Date: Tue, 25 Mar 2014 22:24:04 +0000 Subject: [PATCH 160/422] [Gradle Release Plugin] - pre tag commit: '0.17.2'. --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index bc0a89d83b..b7d2c2b03a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -version=0.17.2-SNAPSHOT +version=0.17.2 From 156132a32e76bc8a9a1a5eeb8e9247758aabd9af Mon Sep 17 00:00:00 2001 From: Bob T Builder Date: Tue, 25 Mar 2014 22:24:08 +0000 Subject: [PATCH 161/422] [Gradle Release Plugin] - new version commit: '0.17.3-SNAPSHOT'. --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index b7d2c2b03a..6d1856974e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -version=0.17.2 +version=0.17.3-SNAPSHOT From f999255e8733275b07a7e0baec289f923191dc76 Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Tue, 25 Mar 2014 15:30:06 -0700 Subject: [PATCH 162/422] 0.17.2 --- CHANGES.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 881656798b..c510b7abab 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,24 @@ # RxJava Releases # +### Version 0.17.2 ([Maven Central](http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22com.netflix.rxjava%22%20AND%20v%3A%220.17.2%22)) ### + +* [Pull 963](https://github.com/Netflix/RxJava/pull/963) A more robust JMH benchmarking set-up +* [Pull 964](https://github.com/Netflix/RxJava/pull/964) SubjectSubscriptionManager fix. +* [Pull 970](https://github.com/Netflix/RxJava/pull/970) Notifications for the allocation averse. +* [Pull 973](https://github.com/Netflix/RxJava/pull/973) Merge - Handle Bad Observables +* [Pull 974](https://github.com/Netflix/RxJava/pull/974) TestSubject, TestObserver and TestScheduler Improvements +* [Pull 975](https://github.com/Netflix/RxJava/pull/975) GroupBy & Time Gap Fixes +* [Pull 976](https://github.com/Netflix/RxJava/pull/976) parallel-merge unit test assertions +* [Pull 977](https://github.com/Netflix/RxJava/pull/977) Dematerialize - handle non-materialized terminal events +* [Pull 982](https://github.com/Netflix/RxJava/pull/982) Pivot Operator +* [Pull 984](https://github.com/Netflix/RxJava/pull/984) Tests and Javadoc for Pivot +* [Pull 966](https://github.com/Netflix/RxJava/pull/966) Reimplement the ElementAt operator and add it to rxjava-scala +* [Pull 965](https://github.com/Netflix/RxJava/pull/965) BugFix: Chain Subscription in TimeoutSubscriber and SerializedSubscriber +* [Pull 986](https://github.com/Netflix/RxJava/pull/986) Fix SynchronizedObserver.runConcurrencyTest +* [Pull 987](https://github.com/Netflix/RxJava/pull/987) Fix Non-Deterministic Pivot Test +* [Pull 988](https://github.com/Netflix/RxJava/pull/988) OnErrorFailedException + + ### Version 0.17.1 ([Maven Central](http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22com.netflix.rxjava%22%20AND%20v%3A%220.17.1%22)) ### * [Pull 953](https://github.com/Netflix/RxJava/pull/953) Make ObserveOnTest.testNonBlockingOuterWhileBlockingOnNext deterministic From 770240d008880861edf804911c05c260c56e0314 Mon Sep 17 00:00:00 2001 From: pron Date: Wed, 26 Mar 2014 22:19:30 +0200 Subject: [PATCH 163/422] Use Quasar 0.5.0 release --- .gitignore | 2 ++ rxjava-contrib/rxjava-quasar/build.gradle | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 606575380d..3889cb7d51 100644 --- a/.gitignore +++ b/.gitignore @@ -63,6 +63,8 @@ bin/ # NetBeans specific files/directories .nbattrs /.nb-gradle/profiles/private/ +.nb-gradle-properties # Scala build *.cache +/.nb-gradle/private/ \ No newline at end of file diff --git a/rxjava-contrib/rxjava-quasar/build.gradle b/rxjava-contrib/rxjava-quasar/build.gradle index 72586db6a1..fdcac9663f 100644 --- a/rxjava-contrib/rxjava-quasar/build.gradle +++ b/rxjava-contrib/rxjava-quasar/build.gradle @@ -10,13 +10,13 @@ configurations { repositories { mavenLocal() mavenCentral() - maven { url "https://oss.sonatype.org/content/repositories/snapshots" } + // maven { url "https://oss.sonatype.org/content/repositories/snapshots" } } dependencies { compile project(':rxjava-core') - compile 'co.paralleluniverse:quasar-core:0.5.0-SNAPSHOT' - quasar 'co.paralleluniverse:quasar-core:0.5.0-SNAPSHOT' + compile 'co.paralleluniverse:quasar-core:0.5.0' + quasar 'co.paralleluniverse:quasar-core:0.5.0' testCompile project(":rxjava-core").sourceSets.test.output provided 'junit:junit-dep:4.10' provided 'org.mockito:mockito-core:1.8.5' From bcb44e0990135fd70c7a20906dc84126eecf456a Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Wed, 26 Mar 2014 15:31:11 -0700 Subject: [PATCH 164/422] fix jmh build config This is now working from command-line and Eclipse --- build.gradle | 46 +++++++++++----------------------------- rxjava-core/build.gradle | 4 ++-- 2 files changed, 14 insertions(+), 36 deletions(-) diff --git a/build.gradle b/build.gradle index 366a6f94dc..3cafa5b42c 100644 --- a/build.gradle +++ b/build.gradle @@ -10,19 +10,9 @@ buildscript { repositories { mavenLocal() mavenCentral() - maven { - //FIXME: waiting for https://github.com/johnrengelman/shadow/pull/38 to merge - name 'Shadow' - url 'http://dl.bintray.com/content/gvsmirnov/gradle-plugins' - } jcenter() } - dependencies { - // Required for benchmarks - classpath 'com.github.jengelman.gradle.plugins:shadow:0.8.1' - } - apply from: file('gradle/buildscript.gradle'), to: buildscript } @@ -43,6 +33,8 @@ subprojects { configurations { examplesCompile.extendsFrom compile examplesRuntime.extendsFrom runtime + perfCompile.extendsFrom compile + perfRuntime.extendsFrom runtime } @@ -52,19 +44,22 @@ subprojects { sourceSets { examples - perf + perf { + compileClasspath += sourceSets.main.output + } } tasks.build { //include 'examples' in build task dependsOn(examplesClasses) + dependsOn(perfClasses) } dependencies { perfCompile 'org.openjdk.jmh:jmh-core:0.5.3' perfCompile 'org.openjdk.jmh:jmh-generator-annprocess:0.5.3' - perfCompile project + //perfCompile project } eclipse { @@ -83,28 +78,11 @@ subprojects { } } - task perfJar(type: Jar, dependsOn: perfClasses) { - from sourceSets.perf.output + sourceSets.main.output - } - - task benchmarks(dependsOn: perfJar) { - - apply plugin: "shadow" - - shadow { - classifier = "benchmarks" - includeDependenciesFor = ["runtime", "perfRuntime"] - - transformer(com.github.jengelman.gradle.plugins.shadow.transformers.ManifestResourceTransformer) { - mainClass = "org.openjdk.jmh.Main" - } - } - - doLast { - shadowJar.execute() - } - - } + task benchmarks(type: JavaExec) { + main = 'org.openjdk.jmh.Main' + classpath = sourceSets.perf.runtimeClasspath + sourceSets.main.output + } + } project(':rxjava-core') { diff --git a/rxjava-core/build.gradle b/rxjava-core/build.gradle index d03c5653a4..6e50441495 100644 --- a/rxjava-core/build.gradle +++ b/rxjava-core/build.gradle @@ -5,8 +5,8 @@ sourceCompatibility = JavaVersion.VERSION_1_6 targetCompatibility = JavaVersion.VERSION_1_6 dependencies { - provided 'junit:junit-dep:4.10' - provided 'org.mockito:mockito-core:1.8.5' + testCompile 'junit:junit-dep:4.10' + testCompile 'org.mockito:mockito-core:1.8.5' } javadoc { From 3520445b61b7f4f483dfbd742cb973044e5fde13 Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Wed, 26 Mar 2014 16:42:22 -0700 Subject: [PATCH 165/422] Migrate Pre-JMH Tests to rx.archive --- .../ObservableCreatePerformance.java | 8 +- .../composition/RangeMapTakeOnNextPerf.java | 6 +- .../OperatorFromIterablePerformance.java | 8 +- .../operators/OperatorMapPerformance.java | 6 +- .../operators/OperatorMergePerformance.java | 8 +- .../OperatorObserveOnPerformance.java | 7 +- .../OperatorParallelPerformance.java | 6 +- .../operators/OperatorRangePerformance.java | 6 +- .../OperatorSerializePerformance.java | 6 +- .../OperatorSynchronizePerformance.java | 6 +- .../operators/OperatorTakePerformance.java | 6 +- .../operators/OperatorZipPerformance.java | 6 +- .../perf/AbstractPerformanceTester.java | 2 +- .../perf/IntegerSumObserver.java | 2 +- .../{ => archive}/perf/LongSumObserver.java | 2 +- .../performance/PerformanceTest.java | 2 +- .../performance/TestChainPerformance.java | 2 +- .../schedulers/SchedulerPerformanceTests.java | 3 +- .../schedulers/TestRecursionMemoryUsage.java | 106 ++++++++++++++++++ .../subjects/SubjectPerformanceTests.java | 3 +- .../CompositeSubscriptionAddRemovePerf.java | 6 +- ...bleBenchmark.java => OperatorMapPerf.java} | 10 +- .../schedulers/TestRecursionMemoryUsage.java | 100 ----------------- 23 files changed, 167 insertions(+), 150 deletions(-) rename rxjava-core/src/perf/java/rx/{ => archive}/ObservableCreatePerformance.java (92%) rename rxjava-core/src/perf/java/rx/{ => archive}/composition/RangeMapTakeOnNextPerf.java (92%) rename rxjava-core/src/perf/java/rx/{ => archive}/operators/OperatorFromIterablePerformance.java (96%) rename rxjava-core/src/perf/java/rx/{ => archive}/operators/OperatorMapPerformance.java (92%) rename rxjava-core/src/perf/java/rx/{ => archive}/operators/OperatorMergePerformance.java (96%) rename rxjava-core/src/perf/java/rx/{ => archive}/operators/OperatorObserveOnPerformance.java (94%) rename rxjava-core/src/perf/java/rx/{ => archive}/operators/OperatorParallelPerformance.java (92%) rename rxjava-core/src/perf/java/rx/{ => archive}/operators/OperatorRangePerformance.java (91%) rename rxjava-core/src/perf/java/rx/{ => archive}/operators/OperatorSerializePerformance.java (98%) rename rxjava-core/src/perf/java/rx/{ => archive}/operators/OperatorSynchronizePerformance.java (98%) rename rxjava-core/src/perf/java/rx/{ => archive}/operators/OperatorTakePerformance.java (91%) rename rxjava-core/src/perf/java/rx/{ => archive}/operators/OperatorZipPerformance.java (96%) rename rxjava-core/src/perf/java/rx/{ => archive}/perf/AbstractPerformanceTester.java (98%) rename rxjava-core/src/perf/java/rx/{ => archive}/perf/IntegerSumObserver.java (93%) rename rxjava-core/src/perf/java/rx/{ => archive}/perf/LongSumObserver.java (92%) rename rxjava-core/src/perf/java/rx/{ => archive}/performance/PerformanceTest.java (99%) rename rxjava-core/src/perf/java/rx/{ => archive}/performance/TestChainPerformance.java (98%) rename rxjava-core/src/perf/java/rx/{ => archive}/schedulers/SchedulerPerformanceTests.java (98%) create mode 100644 rxjava-core/src/perf/java/rx/archive/schedulers/TestRecursionMemoryUsage.java rename rxjava-core/src/perf/java/rx/{ => archive}/subjects/SubjectPerformanceTests.java (98%) rename rxjava-core/src/perf/java/rx/{ => archive}/subscriptions/CompositeSubscriptionAddRemovePerf.java (89%) rename rxjava-core/src/perf/java/rx/operators/{ObservableBenchmark.java => OperatorMapPerf.java} (91%) delete mode 100644 rxjava-core/src/perf/java/rx/schedulers/TestRecursionMemoryUsage.java diff --git a/rxjava-core/src/perf/java/rx/ObservableCreatePerformance.java b/rxjava-core/src/perf/java/rx/archive/ObservableCreatePerformance.java similarity index 92% rename from rxjava-core/src/perf/java/rx/ObservableCreatePerformance.java rename to rxjava-core/src/perf/java/rx/archive/ObservableCreatePerformance.java index 845cfeb412..5ac888a14e 100644 --- a/rxjava-core/src/perf/java/rx/ObservableCreatePerformance.java +++ b/rxjava-core/src/perf/java/rx/archive/ObservableCreatePerformance.java @@ -1,9 +1,11 @@ -package rx; +package rx.archive; +import rx.Observable; import rx.Observable.OnSubscribe; +import rx.Subscriber; +import rx.archive.perf.AbstractPerformanceTester; +import rx.archive.perf.LongSumObserver; import rx.functions.Action0; -import rx.perf.AbstractPerformanceTester; -import rx.perf.LongSumObserver; public class ObservableCreatePerformance extends AbstractPerformanceTester { diff --git a/rxjava-core/src/perf/java/rx/composition/RangeMapTakeOnNextPerf.java b/rxjava-core/src/perf/java/rx/archive/composition/RangeMapTakeOnNextPerf.java similarity index 92% rename from rxjava-core/src/perf/java/rx/composition/RangeMapTakeOnNextPerf.java rename to rxjava-core/src/perf/java/rx/archive/composition/RangeMapTakeOnNextPerf.java index 8bb8942357..3ca90131a4 100644 --- a/rxjava-core/src/perf/java/rx/composition/RangeMapTakeOnNextPerf.java +++ b/rxjava-core/src/perf/java/rx/archive/composition/RangeMapTakeOnNextPerf.java @@ -1,8 +1,8 @@ -package rx.composition; +package rx.archive.composition; import rx.Observable; -import rx.perf.AbstractPerformanceTester; -import rx.perf.IntegerSumObserver; +import rx.archive.perf.AbstractPerformanceTester; +import rx.archive.perf.IntegerSumObserver; import rx.util.functions.Action0; import rx.util.functions.Func1; diff --git a/rxjava-core/src/perf/java/rx/operators/OperatorFromIterablePerformance.java b/rxjava-core/src/perf/java/rx/archive/operators/OperatorFromIterablePerformance.java similarity index 96% rename from rxjava-core/src/perf/java/rx/operators/OperatorFromIterablePerformance.java rename to rxjava-core/src/perf/java/rx/archive/operators/OperatorFromIterablePerformance.java index 522dc15351..ff370e5c27 100644 --- a/rxjava-core/src/perf/java/rx/operators/OperatorFromIterablePerformance.java +++ b/rxjava-core/src/perf/java/rx/archive/operators/OperatorFromIterablePerformance.java @@ -1,13 +1,13 @@ -package rx.operators; +package rx.archive.operators; import java.util.Arrays; import java.util.Iterator; import rx.Observable; +import rx.archive.perf.AbstractPerformanceTester; +import rx.archive.perf.IntegerSumObserver; +import rx.archive.perf.LongSumObserver; import rx.functions.Action0; -import rx.perf.AbstractPerformanceTester; -import rx.perf.IntegerSumObserver; -import rx.perf.LongSumObserver; public class OperatorFromIterablePerformance extends AbstractPerformanceTester { diff --git a/rxjava-core/src/perf/java/rx/operators/OperatorMapPerformance.java b/rxjava-core/src/perf/java/rx/archive/operators/OperatorMapPerformance.java similarity index 92% rename from rxjava-core/src/perf/java/rx/operators/OperatorMapPerformance.java rename to rxjava-core/src/perf/java/rx/archive/operators/OperatorMapPerformance.java index b40aa72dd5..a9bfb30c7c 100644 --- a/rxjava-core/src/perf/java/rx/operators/OperatorMapPerformance.java +++ b/rxjava-core/src/perf/java/rx/archive/operators/OperatorMapPerformance.java @@ -1,10 +1,10 @@ -package rx.operators; +package rx.archive.operators; import rx.Observable; +import rx.archive.perf.AbstractPerformanceTester; +import rx.archive.perf.LongSumObserver; import rx.functions.Action0; import rx.functions.Func1; -import rx.perf.AbstractPerformanceTester; -import rx.perf.LongSumObserver; public class OperatorMapPerformance extends AbstractPerformanceTester { diff --git a/rxjava-core/src/perf/java/rx/operators/OperatorMergePerformance.java b/rxjava-core/src/perf/java/rx/archive/operators/OperatorMergePerformance.java similarity index 96% rename from rxjava-core/src/perf/java/rx/operators/OperatorMergePerformance.java rename to rxjava-core/src/perf/java/rx/archive/operators/OperatorMergePerformance.java index ee3713baf6..25d8684c29 100644 --- a/rxjava-core/src/perf/java/rx/operators/OperatorMergePerformance.java +++ b/rxjava-core/src/perf/java/rx/archive/operators/OperatorMergePerformance.java @@ -1,10 +1,10 @@ -package rx.operators; +package rx.archive.operators; import rx.Observable; +import rx.archive.perf.AbstractPerformanceTester; +import rx.archive.perf.IntegerSumObserver; +import rx.archive.perf.LongSumObserver; import rx.functions.Action0; -import rx.perf.AbstractPerformanceTester; -import rx.perf.IntegerSumObserver; -import rx.perf.LongSumObserver; import rx.schedulers.Schedulers; public class OperatorMergePerformance extends AbstractPerformanceTester { diff --git a/rxjava-core/src/perf/java/rx/operators/OperatorObserveOnPerformance.java b/rxjava-core/src/perf/java/rx/archive/operators/OperatorObserveOnPerformance.java similarity index 94% rename from rxjava-core/src/perf/java/rx/operators/OperatorObserveOnPerformance.java rename to rxjava-core/src/perf/java/rx/archive/operators/OperatorObserveOnPerformance.java index c892fe493b..e86f112990 100644 --- a/rxjava-core/src/perf/java/rx/operators/OperatorObserveOnPerformance.java +++ b/rxjava-core/src/perf/java/rx/archive/operators/OperatorObserveOnPerformance.java @@ -1,9 +1,10 @@ -package rx.operators; +package rx.archive.operators; import rx.Observable; +import rx.archive.perf.AbstractPerformanceTester; +import rx.archive.perf.IntegerSumObserver; import rx.functions.Action0; -import rx.perf.AbstractPerformanceTester; -import rx.perf.IntegerSumObserver; +import rx.operators.OperatorObserveOnBounded; import rx.schedulers.Schedulers; public class OperatorObserveOnPerformance extends AbstractPerformanceTester { diff --git a/rxjava-core/src/perf/java/rx/operators/OperatorParallelPerformance.java b/rxjava-core/src/perf/java/rx/archive/operators/OperatorParallelPerformance.java similarity index 92% rename from rxjava-core/src/perf/java/rx/operators/OperatorParallelPerformance.java rename to rxjava-core/src/perf/java/rx/archive/operators/OperatorParallelPerformance.java index 818407d7e4..41afed5c6d 100644 --- a/rxjava-core/src/perf/java/rx/operators/OperatorParallelPerformance.java +++ b/rxjava-core/src/perf/java/rx/archive/operators/OperatorParallelPerformance.java @@ -1,10 +1,10 @@ -package rx.operators; +package rx.archive.operators; import rx.Observable; +import rx.archive.perf.AbstractPerformanceTester; +import rx.archive.perf.IntegerSumObserver; import rx.functions.Action0; import rx.functions.Func1; -import rx.perf.AbstractPerformanceTester; -import rx.perf.IntegerSumObserver; public class OperatorParallelPerformance extends AbstractPerformanceTester { diff --git a/rxjava-core/src/perf/java/rx/operators/OperatorRangePerformance.java b/rxjava-core/src/perf/java/rx/archive/operators/OperatorRangePerformance.java similarity index 91% rename from rxjava-core/src/perf/java/rx/operators/OperatorRangePerformance.java rename to rxjava-core/src/perf/java/rx/archive/operators/OperatorRangePerformance.java index 034342c183..4a658b2f44 100644 --- a/rxjava-core/src/perf/java/rx/operators/OperatorRangePerformance.java +++ b/rxjava-core/src/perf/java/rx/archive/operators/OperatorRangePerformance.java @@ -1,9 +1,9 @@ -package rx.operators; +package rx.archive.operators; import rx.Observable; +import rx.archive.perf.AbstractPerformanceTester; +import rx.archive.perf.IntegerSumObserver; import rx.functions.Action0; -import rx.perf.AbstractPerformanceTester; -import rx.perf.IntegerSumObserver; public class OperatorRangePerformance extends AbstractPerformanceTester { diff --git a/rxjava-core/src/perf/java/rx/operators/OperatorSerializePerformance.java b/rxjava-core/src/perf/java/rx/archive/operators/OperatorSerializePerformance.java similarity index 98% rename from rxjava-core/src/perf/java/rx/operators/OperatorSerializePerformance.java rename to rxjava-core/src/perf/java/rx/archive/operators/OperatorSerializePerformance.java index cfe70889cf..366d23d092 100644 --- a/rxjava-core/src/perf/java/rx/operators/OperatorSerializePerformance.java +++ b/rxjava-core/src/perf/java/rx/archive/operators/OperatorSerializePerformance.java @@ -1,4 +1,4 @@ -package rx.operators; +package rx.archive.operators; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -6,11 +6,11 @@ import rx.Observable; import rx.Observable.OnSubscribe; import rx.Subscriber; +import rx.archive.perf.AbstractPerformanceTester; +import rx.archive.perf.IntegerSumObserver; import rx.functions.Action0; import rx.functions.Action1; import rx.functions.Func1; -import rx.perf.AbstractPerformanceTester; -import rx.perf.IntegerSumObserver; import rx.schedulers.Schedulers; public class OperatorSerializePerformance extends AbstractPerformanceTester { diff --git a/rxjava-core/src/perf/java/rx/operators/OperatorSynchronizePerformance.java b/rxjava-core/src/perf/java/rx/archive/operators/OperatorSynchronizePerformance.java similarity index 98% rename from rxjava-core/src/perf/java/rx/operators/OperatorSynchronizePerformance.java rename to rxjava-core/src/perf/java/rx/archive/operators/OperatorSynchronizePerformance.java index a7396693fb..dd5e2f0703 100644 --- a/rxjava-core/src/perf/java/rx/operators/OperatorSynchronizePerformance.java +++ b/rxjava-core/src/perf/java/rx/archive/operators/OperatorSynchronizePerformance.java @@ -1,4 +1,4 @@ -package rx.operators; +package rx.archive.operators; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -6,11 +6,11 @@ import rx.Observable; import rx.Observable.OnSubscribe; import rx.Subscriber; +import rx.archive.perf.AbstractPerformanceTester; +import rx.archive.perf.IntegerSumObserver; import rx.functions.Action0; import rx.functions.Action1; import rx.functions.Func1; -import rx.perf.AbstractPerformanceTester; -import rx.perf.IntegerSumObserver; import rx.schedulers.Schedulers; public class OperatorSynchronizePerformance extends AbstractPerformanceTester { diff --git a/rxjava-core/src/perf/java/rx/operators/OperatorTakePerformance.java b/rxjava-core/src/perf/java/rx/archive/operators/OperatorTakePerformance.java similarity index 91% rename from rxjava-core/src/perf/java/rx/operators/OperatorTakePerformance.java rename to rxjava-core/src/perf/java/rx/archive/operators/OperatorTakePerformance.java index 6e28f51545..85a8a09f49 100644 --- a/rxjava-core/src/perf/java/rx/operators/OperatorTakePerformance.java +++ b/rxjava-core/src/perf/java/rx/archive/operators/OperatorTakePerformance.java @@ -1,9 +1,9 @@ -package rx.operators; +package rx.archive.operators; import rx.Observable; +import rx.archive.perf.AbstractPerformanceTester; +import rx.archive.perf.IntegerSumObserver; import rx.functions.Action0; -import rx.perf.AbstractPerformanceTester; -import rx.perf.IntegerSumObserver; public class OperatorTakePerformance extends AbstractPerformanceTester { diff --git a/rxjava-core/src/perf/java/rx/operators/OperatorZipPerformance.java b/rxjava-core/src/perf/java/rx/archive/operators/OperatorZipPerformance.java similarity index 96% rename from rxjava-core/src/perf/java/rx/operators/OperatorZipPerformance.java rename to rxjava-core/src/perf/java/rx/archive/operators/OperatorZipPerformance.java index aa2c071560..6248409f8f 100644 --- a/rxjava-core/src/perf/java/rx/operators/OperatorZipPerformance.java +++ b/rxjava-core/src/perf/java/rx/archive/operators/OperatorZipPerformance.java @@ -1,10 +1,10 @@ -package rx.operators; +package rx.archive.operators; import rx.Observable; +import rx.archive.perf.AbstractPerformanceTester; +import rx.archive.perf.IntegerSumObserver; import rx.functions.Action0; import rx.functions.Func2; -import rx.perf.AbstractPerformanceTester; -import rx.perf.IntegerSumObserver; public class OperatorZipPerformance extends AbstractPerformanceTester { diff --git a/rxjava-core/src/perf/java/rx/perf/AbstractPerformanceTester.java b/rxjava-core/src/perf/java/rx/archive/perf/AbstractPerformanceTester.java similarity index 98% rename from rxjava-core/src/perf/java/rx/perf/AbstractPerformanceTester.java rename to rxjava-core/src/perf/java/rx/archive/perf/AbstractPerformanceTester.java index 32ef5c3a7b..490489ed5c 100644 --- a/rxjava-core/src/perf/java/rx/perf/AbstractPerformanceTester.java +++ b/rxjava-core/src/perf/java/rx/archive/perf/AbstractPerformanceTester.java @@ -1,4 +1,4 @@ -package rx.perf; +package rx.archive.perf; import java.util.Iterator; diff --git a/rxjava-core/src/perf/java/rx/perf/IntegerSumObserver.java b/rxjava-core/src/perf/java/rx/archive/perf/IntegerSumObserver.java similarity index 93% rename from rxjava-core/src/perf/java/rx/perf/IntegerSumObserver.java rename to rxjava-core/src/perf/java/rx/archive/perf/IntegerSumObserver.java index 28a522fbd8..30a7560311 100644 --- a/rxjava-core/src/perf/java/rx/perf/IntegerSumObserver.java +++ b/rxjava-core/src/perf/java/rx/archive/perf/IntegerSumObserver.java @@ -1,4 +1,4 @@ -package rx.perf; +package rx.archive.perf; import rx.Subscriber; diff --git a/rxjava-core/src/perf/java/rx/perf/LongSumObserver.java b/rxjava-core/src/perf/java/rx/archive/perf/LongSumObserver.java similarity index 92% rename from rxjava-core/src/perf/java/rx/perf/LongSumObserver.java rename to rxjava-core/src/perf/java/rx/archive/perf/LongSumObserver.java index 5465b326f0..c112d1598a 100644 --- a/rxjava-core/src/perf/java/rx/perf/LongSumObserver.java +++ b/rxjava-core/src/perf/java/rx/archive/perf/LongSumObserver.java @@ -1,4 +1,4 @@ -package rx.perf; +package rx.archive.perf; import rx.Subscriber; diff --git a/rxjava-core/src/perf/java/rx/performance/PerformanceTest.java b/rxjava-core/src/perf/java/rx/archive/performance/PerformanceTest.java similarity index 99% rename from rxjava-core/src/perf/java/rx/performance/PerformanceTest.java rename to rxjava-core/src/perf/java/rx/archive/performance/PerformanceTest.java index 7b69baa964..d54d3bcfa0 100644 --- a/rxjava-core/src/perf/java/rx/performance/PerformanceTest.java +++ b/rxjava-core/src/perf/java/rx/archive/performance/PerformanceTest.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package rx.performance; +package rx.archive.performance; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; diff --git a/rxjava-core/src/perf/java/rx/performance/TestChainPerformance.java b/rxjava-core/src/perf/java/rx/archive/performance/TestChainPerformance.java similarity index 98% rename from rxjava-core/src/perf/java/rx/performance/TestChainPerformance.java rename to rxjava-core/src/perf/java/rx/archive/performance/TestChainPerformance.java index 8cbf000aa5..d230df026c 100644 --- a/rxjava-core/src/perf/java/rx/performance/TestChainPerformance.java +++ b/rxjava-core/src/perf/java/rx/archive/performance/TestChainPerformance.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package rx.performance; +package rx.archive.performance; import java.util.ArrayList; import java.util.concurrent.Callable; diff --git a/rxjava-core/src/perf/java/rx/schedulers/SchedulerPerformanceTests.java b/rxjava-core/src/perf/java/rx/archive/schedulers/SchedulerPerformanceTests.java similarity index 98% rename from rxjava-core/src/perf/java/rx/schedulers/SchedulerPerformanceTests.java rename to rxjava-core/src/perf/java/rx/archive/schedulers/SchedulerPerformanceTests.java index e5f5e5a8b3..392dc50283 100644 --- a/rxjava-core/src/perf/java/rx/schedulers/SchedulerPerformanceTests.java +++ b/rxjava-core/src/perf/java/rx/archive/schedulers/SchedulerPerformanceTests.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package rx.schedulers; +package rx.archive.schedulers; import java.util.Arrays; @@ -21,6 +21,7 @@ import rx.Scheduler; import rx.Subscriber; import rx.functions.Action0; +import rx.schedulers.Schedulers; public class SchedulerPerformanceTests { diff --git a/rxjava-core/src/perf/java/rx/archive/schedulers/TestRecursionMemoryUsage.java b/rxjava-core/src/perf/java/rx/archive/schedulers/TestRecursionMemoryUsage.java new file mode 100644 index 0000000000..2e120596ee --- /dev/null +++ b/rxjava-core/src/perf/java/rx/archive/schedulers/TestRecursionMemoryUsage.java @@ -0,0 +1,106 @@ +/** + * Copyright 2014 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package rx.archive.schedulers; + +import rx.Observable; +import rx.Observable.OnSubscribe; +import rx.Scheduler; +import rx.Scheduler.Inner; +import rx.Subscriber; +import rx.functions.Action1; +import rx.schedulers.Schedulers; + +/** + * Used for manual testing of memory leaks with recursive schedulers. + * + */ +public class TestRecursionMemoryUsage { + + public static void main(String args[]) { + usingFunc2(Schedulers.newThread()); + usingAction0(Schedulers.newThread()); + + usingFunc2(Schedulers.currentThread()); + usingAction0(Schedulers.currentThread()); + + usingFunc2(Schedulers.computation()); + usingAction0(Schedulers.computation()); + + System.exit(0); + } + + protected static void usingFunc2(final Scheduler scheduler) { + System.out.println("************ usingFunc2: " + scheduler); + Observable.create(new OnSubscribe() { + + @Override + public void call(final Subscriber o) { + o.add(scheduler.schedule(new Action1() { + long i = 0; + + @Override + public void call(Inner inner) { + i++; + if (i % 500000 == 0) { + System.out.println(i + " Total Memory: " + + Runtime.getRuntime().totalMemory() + + " Free: " + + Runtime.getRuntime().freeMemory()); + o.onNext(i); + } + if (i == 100000000L) { + o.onCompleted(); + return; + } + + inner.schedule(this); + } + })); + } + }).toBlockingObservable().last(); + } + + protected static void usingAction0(final Scheduler scheduler) { + System.out.println("************ usingAction0: " + scheduler); + Observable.create(new OnSubscribe() { + + @Override + public void call(final Subscriber o) { + o.add(scheduler.schedule(new Action1() { + + private long i = 0; + + @Override + public void call(Inner inner) { + i++; + if (i % 500000 == 0) { + System.out.println(i + " Total Memory: " + + Runtime.getRuntime().totalMemory() + + " Free: " + + Runtime.getRuntime().freeMemory()); + o.onNext(i); + } + if (i == 100000000L) { + o.onCompleted(); + return; + } + inner.schedule(this); + } + })); + } + }).toBlockingObservable().last(); + } +} diff --git a/rxjava-core/src/perf/java/rx/subjects/SubjectPerformanceTests.java b/rxjava-core/src/perf/java/rx/archive/subjects/SubjectPerformanceTests.java similarity index 98% rename from rxjava-core/src/perf/java/rx/subjects/SubjectPerformanceTests.java rename to rxjava-core/src/perf/java/rx/archive/subjects/SubjectPerformanceTests.java index 4b5f3af5b1..187c63b328 100644 --- a/rxjava-core/src/perf/java/rx/subjects/SubjectPerformanceTests.java +++ b/rxjava-core/src/perf/java/rx/archive/subjects/SubjectPerformanceTests.java @@ -13,10 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package rx.subjects; +package rx.archive.subjects; import rx.Subscriber; import rx.functions.Action0; +import rx.subjects.ReplaySubject; public class SubjectPerformanceTests { diff --git a/rxjava-core/src/perf/java/rx/subscriptions/CompositeSubscriptionAddRemovePerf.java b/rxjava-core/src/perf/java/rx/archive/subscriptions/CompositeSubscriptionAddRemovePerf.java similarity index 89% rename from rxjava-core/src/perf/java/rx/subscriptions/CompositeSubscriptionAddRemovePerf.java rename to rxjava-core/src/perf/java/rx/archive/subscriptions/CompositeSubscriptionAddRemovePerf.java index 17931430d4..f1524b4769 100644 --- a/rxjava-core/src/perf/java/rx/subscriptions/CompositeSubscriptionAddRemovePerf.java +++ b/rxjava-core/src/perf/java/rx/archive/subscriptions/CompositeSubscriptionAddRemovePerf.java @@ -1,7 +1,9 @@ -package rx.subscriptions; +package rx.archive.subscriptions; +import rx.archive.perf.AbstractPerformanceTester; import rx.functions.Action0; -import rx.perf.AbstractPerformanceTester; +import rx.subscriptions.BooleanSubscription; +import rx.subscriptions.CompositeSubscription; public class CompositeSubscriptionAddRemovePerf extends AbstractPerformanceTester { diff --git a/rxjava-core/src/perf/java/rx/operators/ObservableBenchmark.java b/rxjava-core/src/perf/java/rx/operators/OperatorMapPerf.java similarity index 91% rename from rxjava-core/src/perf/java/rx/operators/ObservableBenchmark.java rename to rxjava-core/src/perf/java/rx/operators/OperatorMapPerf.java index 5de2a022d7..7ae07c68b1 100644 --- a/rxjava-core/src/perf/java/rx/operators/ObservableBenchmark.java +++ b/rxjava-core/src/perf/java/rx/operators/OperatorMapPerf.java @@ -4,9 +4,13 @@ import java.util.Collection; import java.util.concurrent.CountDownLatch; -import org.openjdk.jmh.annotations.*; - +import org.openjdk.jmh.annotations.GenerateMicroBenchmark; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; import org.openjdk.jmh.logic.BlackHole; + import rx.Observable; import rx.Observable.OnSubscribe; import rx.Observable.Operator; @@ -14,7 +18,7 @@ import rx.Subscriber; import rx.functions.Func1; -public class ObservableBenchmark { +public class OperatorMapPerf { @GenerateMicroBenchmark public void measureBaseline(BlackHole bh, Input input) { diff --git a/rxjava-core/src/perf/java/rx/schedulers/TestRecursionMemoryUsage.java b/rxjava-core/src/perf/java/rx/schedulers/TestRecursionMemoryUsage.java deleted file mode 100644 index 7453fb941d..0000000000 --- a/rxjava-core/src/perf/java/rx/schedulers/TestRecursionMemoryUsage.java +++ /dev/null @@ -1,100 +0,0 @@ -/** - * Copyright 2014 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package rx.schedulers; - -import rx.Observable; -import rx.Observable.OnSubscribeFunc; -import rx.Observer; -import rx.Scheduler; -import rx.Scheduler.Inner; -import rx.Subscription; -import rx.functions.Action1; - -/** - * Used for manual testing of memory leaks with recursive schedulers. - * - */ -public class TestRecursionMemoryUsage { - - public static void main(String args[]) { - usingFunc2(Schedulers.newThread()); - usingAction0(Schedulers.newThread()); - - usingFunc2(Schedulers.currentThread()); - usingAction0(Schedulers.currentThread()); - - usingFunc2(Schedulers.computation()); - usingAction0(Schedulers.computation()); - - System.exit(0); - } - - protected static void usingFunc2(final Scheduler scheduler) { - System.out.println("************ usingFunc2: " + scheduler); - Observable.create(new OnSubscribeFunc() { - - @Override - public Subscription onSubscribe(final Observer o) { - return scheduler.schedule(new Action1() { - long i = 0; - - @Override - public void call(Inner inner) { - i++; - if (i % 500000 == 0) { - System.out.println(i + " Total Memory: " + Runtime.getRuntime().totalMemory() + " Free: " + Runtime.getRuntime().freeMemory()); - o.onNext(i); - } - if (i == 100000000L) { - o.onCompleted(); - return; - } - - inner.schedule(this); - } - }); - } - }).toBlockingObservable().last(); - } - - protected static void usingAction0(final Scheduler scheduler) { - System.out.println("************ usingAction0: " + scheduler); - Observable.create(new OnSubscribeFunc() { - - @Override - public Subscription onSubscribe(final Observer o) { - return scheduler.schedule(new Action1() { - - private long i = 0; - - @Override - public void call(Inner inner) { - i++; - if (i % 500000 == 0) { - System.out.println(i + " Total Memory: " + Runtime.getRuntime().totalMemory() + " Free: " + Runtime.getRuntime().freeMemory()); - o.onNext(i); - } - if (i == 100000000L) { - o.onCompleted(); - return; - } - inner.schedule(this); - } - }); - } - }).toBlockingObservable().last(); - } -} From 5f3622127e8c9ae2c9ceeb2fb7cc6d0446283aff Mon Sep 17 00:00:00 2001 From: pron Date: Thu, 27 Mar 2014 02:42:01 +0200 Subject: [PATCH 166/422] Target Java 1.6 --- rxjava-contrib/rxjava-quasar/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rxjava-contrib/rxjava-quasar/build.gradle b/rxjava-contrib/rxjava-quasar/build.gradle index fdcac9663f..bb80b916d4 100644 --- a/rxjava-contrib/rxjava-quasar/build.gradle +++ b/rxjava-contrib/rxjava-quasar/build.gradle @@ -1,7 +1,7 @@ apply plugin: 'osgi' sourceCompatibility = JavaVersion.VERSION_1_6 -targetCompatibility = JavaVersion.VERSION_1_7 +targetCompatibility = JavaVersion.VERSION_1_6 configurations { quasar From 30d53a5b557e0ddac3b6ed0d955954c93a0a34e1 Mon Sep 17 00:00:00 2001 From: pron Date: Thu, 27 Mar 2014 03:50:15 +0200 Subject: [PATCH 167/422] Match updated RxJava API (rx.util.function -> rx.functions etc.) --- .../java/rx/quasar/BlockingObservable.java | 6 ++--- .../java/rx/quasar/NewFiberScheduler.java | 4 ++-- .../rx/quasar/RxSuspendableClassifier.java | 18 ++++++++++---- .../resources/META-INF/suspendable-supers | 24 +------------------ 4 files changed, 20 insertions(+), 32 deletions(-) diff --git a/rxjava-contrib/rxjava-quasar/src/main/java/rx/quasar/BlockingObservable.java b/rxjava-contrib/rxjava-quasar/src/main/java/rx/quasar/BlockingObservable.java index a161e184d8..7bffe4747f 100644 --- a/rxjava-contrib/rxjava-quasar/src/main/java/rx/quasar/BlockingObservable.java +++ b/rxjava-contrib/rxjava-quasar/src/main/java/rx/quasar/BlockingObservable.java @@ -34,10 +34,10 @@ import rx.Observer; import rx.Subscriber; import rx.Subscription; +import rx.exceptions.Exceptions; +import rx.functions.Action1; +import rx.functions.Func1; import rx.observers.SafeSubscriber; -import rx.util.Exceptions; -import rx.util.functions.Action1; -import rx.util.functions.Func1; /** * An extension of {@link Observable} that provides blocking operators, compatible with both threads and fibers. diff --git a/rxjava-contrib/rxjava-quasar/src/main/java/rx/quasar/NewFiberScheduler.java b/rxjava-contrib/rxjava-quasar/src/main/java/rx/quasar/NewFiberScheduler.java index ec6720d4b2..f5edc254f6 100644 --- a/rxjava-contrib/rxjava-quasar/src/main/java/rx/quasar/NewFiberScheduler.java +++ b/rxjava-contrib/rxjava-quasar/src/main/java/rx/quasar/NewFiberScheduler.java @@ -26,7 +26,7 @@ import rx.Subscription; import rx.subscriptions.CompositeSubscription; import rx.subscriptions.Subscriptions; -import rx.util.functions.Action1; +import rx.functions.Action1; /** * Schedules work on a new fiber. @@ -58,7 +58,7 @@ public Subscription schedule(Action1 action) { innerScheduler.schedule(action); return innerScheduler.innerSubscription; } - + @Override public Subscription schedule(Action1 action, long delayTime, TimeUnit unit) { EventLoopScheduler innerScheduler = new EventLoopScheduler(); diff --git a/rxjava-contrib/rxjava-quasar/src/main/java/rx/quasar/RxSuspendableClassifier.java b/rxjava-contrib/rxjava-quasar/src/main/java/rx/quasar/RxSuspendableClassifier.java index 14b24f294d..4905ea4bfa 100644 --- a/rxjava-contrib/rxjava-quasar/src/main/java/rx/quasar/RxSuspendableClassifier.java +++ b/rxjava-contrib/rxjava-quasar/src/main/java/rx/quasar/RxSuspendableClassifier.java @@ -25,12 +25,13 @@ public class RxSuspendableClassifier implements SuspendableClassifier { private static final Set CORE_PACKAGES = new HashSet(Arrays.asList(new String[]{ "rx", "rx.joins", "rx.observables", "rx.observers", "rx.operators", "rx.plugins", "rx.schedulers", - "rx.subjects", "rx.subscriptions", "rx.util", "rx.util.functions" + "rx.subjects", "rx.subscriptions", "rx.functions", "rx.util", "rx.util.functions" })); private static final Set EXCEPTIONS = new HashSet(Arrays.asList(new String[]{ "rx/observers/SynchronizedObserver", - "rx/schedulers/AbstractSchedulerTests$ConcurrentObserverValidator",})); + "rx/schedulers/AbstractSchedulerTests$ConcurrentObserverValidator", + })); private static final Set OBSERVER_METHODS = new HashSet(Arrays.asList(new String[]{ "onNext(Ljava/lang/Object;)V", "onCompleted()V", "onError(Ljava/lang/Throwable;)V" @@ -47,6 +48,12 @@ public MethodDatabase.SuspendableType isSuspendable(MethodDatabase db, String cl else if (isUtilFunction(db, className, superClassName, interfaces, methodName, methodDesc)) s = MethodDatabase.SuspendableType.SUSPENDABLE; } + if (s == null + && methodName.equals("call") + && (className.startsWith("rx/functions/Func") || className.startsWith("rx/functions/Action") + || className.startsWith("rx/util/functions/Func") || className.startsWith("rx/util/functions/Action"))) { + s = MethodDatabase.SuspendableType.SUSPENDABLE_SUPER; + } // System.out.println("-- " + className + "." + methodName + ": " + s); return s; } @@ -62,9 +69,12 @@ private static boolean isObserverImplementation(MethodDatabase db, String classN } private static boolean isUtilFunction(MethodDatabase db, String className, String superClassName, String[] interfaces, String methodName, String methodDesc) { - return (className.startsWith("rx/util/functions/Functions") || className.startsWith("rx/util/functions/Actions")) + return (className.startsWith("rx/functions/Functions") || className.startsWith("rx/functions/Actions") + || className.startsWith("rx/util/functions/Functions") || className.startsWith("rx/util/functions/Actions")) && methodName.equals(FUNCTION_METHOD) - && (SimpleSuspendableClassifier.extendsOrImplements("rx/util/functions/Function", db, className, superClassName, interfaces) + && (SimpleSuspendableClassifier.extendsOrImplements("rx/functions/Function", db, className, superClassName, interfaces) + || SimpleSuspendableClassifier.extendsOrImplements("rx/functions/Action", db, className, superClassName, interfaces) + || SimpleSuspendableClassifier.extendsOrImplements("rx/util/functions/Function", db, className, superClassName, interfaces) || SimpleSuspendableClassifier.extendsOrImplements("rx/util/functions/Action", db, className, superClassName, interfaces)); } diff --git a/rxjava-contrib/rxjava-quasar/src/main/resources/META-INF/suspendable-supers b/rxjava-contrib/rxjava-quasar/src/main/resources/META-INF/suspendable-supers index e547d95ce6..d79c590311 100644 --- a/rxjava-contrib/rxjava-quasar/src/main/resources/META-INF/suspendable-supers +++ b/rxjava-contrib/rxjava-quasar/src/main/resources/META-INF/suspendable-supers @@ -1,25 +1,3 @@ rx.Observer.onNext rx.Observer.onError -rx.Observer.onCompleted -rx.util.functions.Action0.call -rx.util.functions.Action1.call -rx.util.functions.Action2.call -rx.util.functions.Action3.call -rx.util.functions.Action4.call -rx.util.functions.Action5.call -rx.util.functions.Action6.call -rx.util.functions.Action7.call -rx.util.functions.Action8.call -rx.util.functions.Action9.call -rx.util.functions.ActionN.call -rx.util.functions.Func0.call -rx.util.functions.Func1.call -rx.util.functions.Func2.call -rx.util.functions.Func3.call -rx.util.functions.Func4.call -rx.util.functions.Func5.call -rx.util.functions.Func6.call -rx.util.functions.Func7.call -rx.util.functions.Func8.call -rx.util.functions.Func9.call -rx.util.functions.FuncN.call +rx.Observer.onCompleted \ No newline at end of file From 9ae36cf4fb43ab27bd5d77a75f8c4c07c7dbf2b8 Mon Sep 17 00:00:00 2001 From: pron Date: Thu, 27 Mar 2014 04:07:48 +0200 Subject: [PATCH 168/422] Change target back to 1.7 doesn't compile b/c channels implement AutoCloseable, added in JDK 7. --- rxjava-contrib/rxjava-quasar/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rxjava-contrib/rxjava-quasar/build.gradle b/rxjava-contrib/rxjava-quasar/build.gradle index bb80b916d4..fdcac9663f 100644 --- a/rxjava-contrib/rxjava-quasar/build.gradle +++ b/rxjava-contrib/rxjava-quasar/build.gradle @@ -1,7 +1,7 @@ apply plugin: 'osgi' sourceCompatibility = JavaVersion.VERSION_1_6 -targetCompatibility = JavaVersion.VERSION_1_6 +targetCompatibility = JavaVersion.VERSION_1_7 configurations { quasar From ab7f40847159baf742ea4180757a3af45350bde0 Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Wed, 26 Mar 2014 22:39:12 -0700 Subject: [PATCH 169/422] JMH Benchmarks Gradle Config - this allows running benchmarks - config is hardcoded right now - wasn't able to get uberjar/shadowjar functionality working (https://github.com/Netflix/RxJava/pull/963#issuecomment-38770680) --- build.gradle | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 3cafa5b42c..e8192208fd 100644 --- a/build.gradle +++ b/build.gradle @@ -12,7 +12,7 @@ buildscript { mavenCentral() jcenter() } - + apply from: file('gradle/buildscript.gradle'), to: buildscript } @@ -81,6 +81,28 @@ subprojects { task benchmarks(type: JavaExec) { main = 'org.openjdk.jmh.Main' classpath = sourceSets.perf.runtimeClasspath + sourceSets.main.output + maxHeapSize = "512m" +// args '-h' // help output + args '-f' // fork + args '1' + args '-tu' // time unit + args 'ns' + args '-bm' // benchmark mode + args 'avgt' + args '-wi' // warmup iterations + args '5' + args '-i' // test iterations + args '5' + args '-r' // time per execution in seconds + args '1' +// args '-prof' // profilers +// args 'HS_GC' // HotSpot (tm) memory manager (GC) profiling via implementation-specific MBeans +// args 'HS_RT' // HotSpot (tm) runtime profiling via implementation-specific MBeans +// args 'HS_THR' // HotSpot (tm) threading subsystem via implementation-specific MBeans +// args 'HS_COMP' // HotSpot (tm) JIT compiler profiling via implementation-specific MBeans +// args 'HS_CL' // HotSpot (tm) classloader profiling via implementation-specific MBeans +// args 'STACK' // Simple and naive Java stack profiler +// args '.*OperatorSerializePerf.*' // for running only a specific test } } From 5dc029274e41fdf5f761b0207e1dc96a1af2cb96 Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Wed, 26 Mar 2014 22:39:41 -0700 Subject: [PATCH 170/422] Perf Tests with JMH --- .../src/perf/java/rx/jmh/Baseline.java | 35 ++++++++ .../java/rx/operators/OperatorMapPerf.java | 21 +---- .../rx/operators/OperatorSerializePerf.java | 86 +++++++++++++++++++ 3 files changed, 124 insertions(+), 18 deletions(-) create mode 100644 rxjava-core/src/perf/java/rx/jmh/Baseline.java create mode 100644 rxjava-core/src/perf/java/rx/operators/OperatorSerializePerf.java diff --git a/rxjava-core/src/perf/java/rx/jmh/Baseline.java b/rxjava-core/src/perf/java/rx/jmh/Baseline.java new file mode 100644 index 0000000000..549118752f --- /dev/null +++ b/rxjava-core/src/perf/java/rx/jmh/Baseline.java @@ -0,0 +1,35 @@ +package rx.jmh; + +import org.openjdk.jmh.annotations.GenerateMicroBenchmark; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.logic.BlackHole; + +import rx.functions.Func1; + +public class Baseline { + + @GenerateMicroBenchmark + public void forLoopInvokingFunction(BlackHole bh, Input input) { + for (int value = 0; value < input.size; value++) { + bh.consume(IDENTITY_FUNCTION.call(value)); + } + } + + private static final Func1 IDENTITY_FUNCTION = new Func1() { + @Override + public Integer call(Integer value) { + return value; + } + }; + + @State(Scope.Thread) + public static class Input { + + @Param({ "1024", "1048576" }) + public int size; + + } + +} diff --git a/rxjava-core/src/perf/java/rx/operators/OperatorMapPerf.java b/rxjava-core/src/perf/java/rx/operators/OperatorMapPerf.java index 7ae07c68b1..70074b4b08 100644 --- a/rxjava-core/src/perf/java/rx/operators/OperatorMapPerf.java +++ b/rxjava-core/src/perf/java/rx/operators/OperatorMapPerf.java @@ -1,7 +1,5 @@ package rx.operators; -import java.util.ArrayList; -import java.util.Collection; import java.util.concurrent.CountDownLatch; import org.openjdk.jmh.annotations.GenerateMicroBenchmark; @@ -21,14 +19,7 @@ public class OperatorMapPerf { @GenerateMicroBenchmark - public void measureBaseline(BlackHole bh, Input input) { - for (Integer value : input.values) { - bh.consume(IDENTITY_FUNCTION.call(value)); - } - } - - @GenerateMicroBenchmark - public void measureMap(Input input) throws InterruptedException { + public void mapIdentityFunction(Input input) throws InterruptedException { input.observable.lift(MAP_OPERATOR).subscribe(input.observer); input.awaitCompletion(); @@ -46,10 +37,9 @@ public Integer call(Integer value) { @State(Scope.Thread) public static class Input { - @Param({"1", "1024", "1048576"}) + @Param({ "1", "1024", "1048576" }) public int size; - public Collection values; public Observable observable; public Observer observer; @@ -57,15 +47,10 @@ public static class Input { @Setup public void setup() { - values = new ArrayList(); - for(int i = 0; i < size; i ++) { - values.add(i); - } - observable = Observable.create(new OnSubscribe() { @Override public void call(Subscriber o) { - for (Integer value : values) { + for (int value = 0; value < size; value++) { if (o.isUnsubscribed()) return; o.onNext(value); diff --git a/rxjava-core/src/perf/java/rx/operators/OperatorSerializePerf.java b/rxjava-core/src/perf/java/rx/operators/OperatorSerializePerf.java new file mode 100644 index 0000000000..a130c0711c --- /dev/null +++ b/rxjava-core/src/perf/java/rx/operators/OperatorSerializePerf.java @@ -0,0 +1,86 @@ +package rx.operators; + +import java.util.concurrent.CountDownLatch; + +import org.openjdk.jmh.annotations.GenerateMicroBenchmark; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.logic.BlackHole; + +import rx.Observable; +import rx.Observable.OnSubscribe; +import rx.Observer; +import rx.Subscriber; +import rx.observers.TestSubscriber; + +public class OperatorSerializePerf { + + @GenerateMicroBenchmark + public void noSerializationSingleThreaded(Input input) { + input.observable.subscribe(input.subscriber); + } + + @GenerateMicroBenchmark + public void serializedSingleStream(Input input) { + input.observable.serialize().subscribe(input.subscriber); + } + + @GenerateMicroBenchmark + public void synchronizedSingleStream(Input input) { + input.observable.synchronize().subscribe(input.subscriber); + } + + @State(Scope.Thread) + public static class Input { + + @Param({ "1024", "1048576" }) + public int size; + + public Observable observable; + public TestSubscriber subscriber; + + private CountDownLatch latch; + + @Setup + public void setup() { + observable = Observable.create(new OnSubscribe() { + @Override + public void call(Subscriber o) { + for (int value = 0; value < size; value++) { + if (o.isUnsubscribed()) + return; + o.onNext(value); + } + o.onCompleted(); + } + }); + + final BlackHole bh = new BlackHole(); + latch = new CountDownLatch(1); + + subscriber = new TestSubscriber(new Observer() { + @Override + public void onCompleted() { + latch.countDown(); + } + + @Override + public void onError(Throwable e) { + throw new RuntimeException(e); + } + + @Override + public void onNext(Integer value) { + bh.consume(value); + } + }); + + } + + public void awaitCompletion() throws InterruptedException { + latch.await(); + } + } +} From ed392d7a0d77a22d4ad3b50bcc8e7b8a462eced8 Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Thu, 27 Mar 2014 11:04:14 -0700 Subject: [PATCH 171/422] New Implementation of SerializedObserver #### JMH Benchmarks 0.17.3 Benchmark (size) Mode Samples Mean Mean error Units r.operators.OperatorSerializePerf.noSerializationSingleThreaded 1024 avgt 5 45.504 1.710 ns/op r.operators.OperatorSerializePerf.noSerializationSingleThreaded 1048576 avgt 5 58.600 5.647 ns/op r.operators.OperatorSerializePerf.serializedSingleStream 1024 avgt 5 68.610 4.596 ns/op r.operators.OperatorSerializePerf.serializedSingleStream 1048576 avgt 5 71.313 2.318 ns/op r.operators.OperatorSerializePerf.synchronizedSingleStream 1024 avgt 5 73.322 3.666 ns/op r.operators.OperatorSerializePerf.synchronizedSingleStream 1048576 avgt 5 76.518 1.355 ns/op 0.17.2 Benchmark (size) Mode Samples Mean Mean error Units r.operators.OperatorSerializePerf.noSerializationSingleThreaded 1024 avgt 5 45.790 1.184 ns/op r.operators.OperatorSerializePerf.noSerializationSingleThreaded 1048576 avgt 5 58.518 3.788 ns/op r.operators.OperatorSerializePerf.serializedSingleStream 1024 avgt 5 72.665 7.851 ns/op r.operators.OperatorSerializePerf.serializedSingleStream 1048576 avgt 5 74.788 2.946 ns/op r.operators.OperatorSerializePerf.synchronizedSingleStream 1024 avgt 5 73.661 3.499 ns/op r.operators.OperatorSerializePerf.synchronizedSingleStream 1048576 avgt 5 78.386 5.036 ns/op #### Manual Benchmarks /** * 0.17.3: * * Run: 10 - 9,746,505 ops/sec * Run: 11 - 9,956,019 ops/sec * Run: 12 - 10,053,770 ops/sec * Run: 13 - 10,076,958 ops/sec * Run: 14 - 9,983,319 ops/sec * * 0.17.2: * * Run: 10 - 9,851,999 ops/sec * Run: 11 - 9,726,975 ops/sec * Run: 12 - 9,719,762 ops/sec * Run: 13 - 9,668,141 ops/sec * Run: 14 - 9,799,700 ops/sec * * @param input */ public void serializedSingleStream(Input input) { for (int i = 0; i < reps; i++) { input.observable.serialize().subscribe(input.subscriber); } } --- .../java/rx/observers/SerializedObserver.java | 194 ++++++++++-------- .../OperatorSerializePerformance.java | 114 +++++++++- 2 files changed, 220 insertions(+), 88 deletions(-) diff --git a/rxjava-core/src/main/java/rx/observers/SerializedObserver.java b/rxjava-core/src/main/java/rx/observers/SerializedObserver.java index 6be8d7a23e..0d397fe73f 100644 --- a/rxjava-core/src/main/java/rx/observers/SerializedObserver.java +++ b/rxjava-core/src/main/java/rx/observers/SerializedObserver.java @@ -1,9 +1,6 @@ package rx.observers; -import java.util.ArrayList; - import rx.Observer; -import rx.operators.NotificationLite; /** * Enforce single-threaded, serialized, ordered execution of onNext, onCompleted, onError. @@ -22,8 +19,40 @@ public class SerializedObserver implements Observer { private boolean emitting = false; private boolean terminated = false; - private ArrayList queue = new ArrayList(); - private NotificationLite on = NotificationLite.instance(); + private FastList queue; + + private static final int MAX_DRAIN_ITERATION = 1; + private static final Object NULL_SENTINEL = new Object(); + private static final Object COMPLETE_SENTINEL = new Object(); + + static final class FastList { + Object[] array; + int size; + + public void add(Object o) { + int s = size; + Object[] a = array; + if (a == null) { + a = new Object[16]; + array = a; + } else if (s == a.length) { + Object[] array2 = new Object[s + (s >> 2)]; + System.arraycopy(a, 0, array2, 0, s); + a = array2; + array = a; + } + a[s] = o; + size = s + 1; + } + } + + private static final class ErrorSentinel { + final Throwable e; + + ErrorSentinel(Throwable e) { + this.e = e; + } + } public SerializedObserver(Observer s) { this.actual = s; @@ -31,128 +60,119 @@ public SerializedObserver(Observer s) { @Override public void onCompleted() { - boolean canEmit = false; - ArrayList list = null; + FastList list; synchronized (this) { if (terminated) { return; } terminated = true; - if (!emitting) { - // emit immediately - emitting = true; - canEmit = true; - if (queue.size() > 0) { - list = queue; // copy reference - queue = new ArrayList(); // new version; - } - } else { - // someone else is already emitting so just queue it - queue.add(on.completed()); - } - } - - if (canEmit) { - // we won the right to emit - try { - drainQueue(list); - actual.onCompleted(); - } finally { - synchronized (this) { - emitting = false; + if (emitting) { + if (queue == null) { + queue = new FastList(); } + queue.add(COMPLETE_SENTINEL); + return; } + emitting = true; + list = queue; + queue = null; } + drainQueue(list); + actual.onCompleted(); } @Override public void onError(final Throwable e) { - boolean canEmit = false; - ArrayList list = null; + FastList list; synchronized (this) { if (terminated) { return; } terminated = true; - if (!emitting) { - // emit immediately - emitting = true; - canEmit = true; - if (queue.size() > 0) { - list = queue; // copy reference - queue = new ArrayList(); // new version; - } - } else { - // someone else is already emitting so just queue it ... after eliminating the queue to shortcut - queue.clear(); - queue.add(on.error(e)); - } - } - if (canEmit) { - // we won the right to emit - try { - drainQueue(list); - actual.onError(e); - } finally { - synchronized (this) { - emitting = false; + if (emitting) { + if (queue == null) { + queue = new FastList(); } + queue.add(new ErrorSentinel(e)); + return; } + emitting = true; + list = queue; + queue = null; } + drainQueue(list); + actual.onError(e); } @Override public void onNext(T t) { - boolean canEmit = false; - ArrayList list = null; + FastList list; + synchronized (this) { if (terminated) { return; } - if (!emitting) { - // emit immediately - emitting = true; - canEmit = true; - if (queue.size() > 0) { - list = queue; // copy reference - queue = new ArrayList(); // new version; + if (emitting) { + if (queue == null) { + queue = new FastList(); } - } else { - // someone else is already emitting so just queue it - queue.add(on.next(t)); + queue.add(t != null ? t : NULL_SENTINEL); + return; } + emitting = true; + list = queue; + queue = null; } - if (canEmit) { - // we won the right to emit - try { + + try { + int iter = MAX_DRAIN_ITERATION; + do { drainQueue(list); - actual.onNext(t); - } finally { - synchronized (this) { - if (terminated) { - list = queue; // copy reference - queue = new ArrayList(); // new version; - } else { - // release this thread - emitting = false; - canEmit = false; + if (iter == MAX_DRAIN_ITERATION) { + actual.onNext(t); + } + --iter; + if (iter > 0) { + synchronized (this) { + list = queue; + queue = null; + } + if (list == null) { + break; } } + } while (iter > 0); + } finally { + synchronized (this) { + if (terminated) { + list = queue; + queue = null; + } else { + emitting = false; + list = null; + } } - } - - // if terminated this will still be true so let's drain the rest of the queue - if (canEmit) { drainQueue(list); } } - public void drainQueue(ArrayList list) { - if (list == null || list.size() == 0) { + void drainQueue(FastList list) { + if (list == null || list.size == 0) { return; } - for (Object v : list) { - on.accept(actual, v); + for (Object v : list.array) { + if (v == null) { + break; + } + if (v == NULL_SENTINEL) { + actual.onNext(null); + } else if (v == COMPLETE_SENTINEL) { + actual.onCompleted(); + } else if (v.getClass() == ErrorSentinel.class) { + actual.onError(((ErrorSentinel) v).e); + } else { + actual.onNext((T) v); + } } } -} +} \ No newline at end of file diff --git a/rxjava-core/src/perf/java/rx/archive/operators/OperatorSerializePerformance.java b/rxjava-core/src/perf/java/rx/archive/operators/OperatorSerializePerformance.java index 366d23d092..7eb94edd3b 100644 --- a/rxjava-core/src/perf/java/rx/archive/operators/OperatorSerializePerformance.java +++ b/rxjava-core/src/perf/java/rx/archive/operators/OperatorSerializePerformance.java @@ -3,14 +3,18 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.logic.BlackHole; + import rx.Observable; import rx.Observable.OnSubscribe; +import rx.Observer; import rx.Subscriber; import rx.archive.perf.AbstractPerformanceTester; import rx.archive.perf.IntegerSumObserver; import rx.functions.Action0; import rx.functions.Action1; import rx.functions.Func1; +import rx.observers.TestSubscriber; import rx.schedulers.Schedulers; public class OperatorSerializePerformance extends AbstractPerformanceTester { @@ -26,12 +30,17 @@ public class OperatorSerializePerformance extends AbstractPerformanceTester { public static void main(String args[]) { final OperatorSerializePerformance spt = new OperatorSerializePerformance(); + final Input input = new Input(); + input.setup(); try { spt.runTest(new Action0() { @Override public void call() { - spt.timeTwoStreams(); + // spt.noSerializationSingleThreaded(input); + spt.serializedSingleStream(input); + // spt.synchronizedSingleStream(input); + // spt.timeTwoStreams(); // spt.timeSingleStream(); // spt.timeTwoStreamsIntervals(); } @@ -42,6 +51,61 @@ public void call() { } + /** + * Run: 10 - 12,186,982 ops/sec + * Run: 11 - 10,236,722 ops/sec + * Run: 12 - 11,377,690 ops/sec + * Run: 13 - 10,876,358 ops/sec + * Run: 14 - 11,383,619 ops/sec + * + * @param input + */ + public void noSerializationSingleThreaded(Input input) { + for (int i = 0; i < reps; i++) { + input.observable.subscribe(input.subscriber); + } + } + + /** + * 0.17.3: + * + * Run: 10 - 9,746,505 ops/sec + * Run: 11 - 9,956,019 ops/sec + * Run: 12 - 10,053,770 ops/sec + * Run: 13 - 10,076,958 ops/sec + * Run: 14 - 9,983,319 ops/sec + * + * 0.17.2: + * + * Run: 10 - 9,851,999 ops/sec + * Run: 11 - 9,726,975 ops/sec + * Run: 12 - 9,719,762 ops/sec + * Run: 13 - 9,668,141 ops/sec + * Run: 14 - 9,799,700 ops/sec + * + * @param input + */ + public void serializedSingleStream(Input input) { + for (int i = 0; i < reps; i++) { + input.observable.serialize().subscribe(input.subscriber); + } + } + + /** + * Run: 10 - 9,475,925 ops/sec + * Run: 11 - 9,501,341 ops/sec + * Run: 12 - 9,550,495 ops/sec + * Run: 13 - 9,510,303 ops/sec + * Run: 14 - 9,690,300 ops/sec + * + * @param input + */ + public void synchronizedSingleStream(Input input) { + for (int i = 0; i < reps; i++) { + input.observable.synchronize().subscribe(input.subscriber); + } + } + /** * 1 streams emitting in a tight loop. Testing for single-threaded overhead. * @@ -321,4 +385,52 @@ public void call(Integer t1) { return o.sum; } + public static class Input { + + public int size = 1048576; + + public Observable observable; + public TestSubscriber subscriber; + + private CountDownLatch latch; + + public void setup() { + observable = Observable.create(new OnSubscribe() { + @Override + public void call(Subscriber o) { + for (int value = 0; value < size; value++) { + if (o.isUnsubscribed()) + return; + o.onNext(value); + } + o.onCompleted(); + } + }); + + final BlackHole bh = new BlackHole(); + latch = new CountDownLatch(1); + + subscriber = new TestSubscriber(new Observer() { + @Override + public void onCompleted() { + latch.countDown(); + } + + @Override + public void onError(Throwable e) { + throw new RuntimeException(e); + } + + @Override + public void onNext(Integer value) { + bh.consume(value); + } + }); + + } + + public void awaitCompletion() throws InterruptedException { + latch.await(); + } + } } From 3d54a1792a0f05bfe13f64e0a876ea155f5b3222 Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Thu, 27 Mar 2014 14:55:30 -0700 Subject: [PATCH 172/422] Support Custom JMH Args --- build.gradle | 58 +++++++++++++++++++++++++++++++++------------------- 1 file changed, 37 insertions(+), 21 deletions(-) diff --git a/build.gradle b/build.gradle index e8192208fd..ec8a06d44b 100644 --- a/build.gradle +++ b/build.gradle @@ -78,31 +78,47 @@ subprojects { } } + /** + * By default: Run without arguments this will execute all benchmarks that are found (can take a long time). + * + * Optionally pass arguments for custom execution. Example: + * + * ../gradlew benchmarks '-Pjmh=-f 1 -tu ns -bm avgt -wi 5 -i 5 -r 1 .*OperatorSerializePerf.*' + * + * To see all options: + * + * ../gradlew benchmarks '-Pjmh=-h' + */ task benchmarks(type: JavaExec) { main = 'org.openjdk.jmh.Main' classpath = sourceSets.perf.runtimeClasspath + sourceSets.main.output maxHeapSize = "512m" -// args '-h' // help output - args '-f' // fork - args '1' - args '-tu' // time unit - args 'ns' - args '-bm' // benchmark mode - args 'avgt' - args '-wi' // warmup iterations - args '5' - args '-i' // test iterations - args '5' - args '-r' // time per execution in seconds - args '1' -// args '-prof' // profilers -// args 'HS_GC' // HotSpot (tm) memory manager (GC) profiling via implementation-specific MBeans -// args 'HS_RT' // HotSpot (tm) runtime profiling via implementation-specific MBeans -// args 'HS_THR' // HotSpot (tm) threading subsystem via implementation-specific MBeans -// args 'HS_COMP' // HotSpot (tm) JIT compiler profiling via implementation-specific MBeans -// args 'HS_CL' // HotSpot (tm) classloader profiling via implementation-specific MBeans -// args 'STACK' // Simple and naive Java stack profiler -// args '.*OperatorSerializePerf.*' // for running only a specific test + + if (project.hasProperty('jmh')) { + args(jmh.split(' ')) + } else { + //args '-h' // help output + args '-f' // fork + args '1' + args '-tu' // time unit + args 'ns' + args '-bm' // benchmark mode + args 'avgt' + args '-wi' // warmup iterations + args '5' + args '-i' // test iterations + args '5' + args '-r' // time per execution in seconds + args '5' + //args '-prof' // profilers + //args 'HS_GC' // HotSpot (tm) memory manager (GC) profiling via implementation-specific MBeans + //args 'HS_RT' // HotSpot (tm) runtime profiling via implementation-specific MBeans + //args 'HS_THR' // HotSpot (tm) threading subsystem via implementation-specific MBeans + //args 'HS_COMP' // HotSpot (tm) JIT compiler profiling via implementation-specific MBeans + //args 'HS_CL' // HotSpot (tm) classloader profiling via implementation-specific MBeans + //args 'STACK' // Simple and naive Java stack profiler + args '.*OperatorSerializePerf.*' // for running only a specific test + } } } From d48a3b29283b7e09cd93c698385f2d4e2a0246b5 Mon Sep 17 00:00:00 2001 From: Rob Spieldenner Date: Thu, 27 Mar 2014 17:11:28 -0700 Subject: [PATCH 173/422] Produce a shadow'd jar for the performance benchmarks --- build.gradle | 24 +++++++++++++++++++++--- gradle/buildscript.gradle | 6 ++++++ 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index ec8a06d44b..c4af267173 100644 --- a/build.gradle +++ b/build.gradle @@ -13,7 +13,7 @@ buildscript { jcenter() } - apply from: file('gradle/buildscript.gradle'), to: buildscript + apply from: file('gradle/buildscript.gradle'), to: buildscript } allprojects { @@ -27,6 +27,7 @@ allprojects { subprojects { apply plugin: 'java' + apply plugin: 'shadow' group = "com.netflix.rxjava" // make 'examples' use the same classpath @@ -55,12 +56,19 @@ subprojects { dependsOn(perfClasses) } + task perfJar(type: Jar, dependsOn: perfClasses) { + from sourceSets.perf.output + sourceSets.main.output + } + dependencies { perfCompile 'org.openjdk.jmh:jmh-core:0.5.3' perfCompile 'org.openjdk.jmh:jmh-generator-annprocess:0.5.3' - //perfCompile project } + + artifacts { + perfRuntime perfJar + } eclipse { classpath { @@ -120,7 +128,17 @@ subprojects { args '.*OperatorSerializePerf.*' // for running only a specific test } } - + + shadow { + classifier = "benchmarks" + includeDependenciesFor = ["runtime", "perfRuntime"] + + transformer(com.github.jengelman.gradle.plugins.shadow.transformers.ManifestResourceTransformer) { + mainClass = "org.openjdk.jmh.Main" + } + } + + shadowJar.dependsOn perfJar } project(':rxjava-core') { diff --git a/gradle/buildscript.gradle b/gradle/buildscript.gradle index 0b6da7ce84..2d8643103e 100644 --- a/gradle/buildscript.gradle +++ b/gradle/buildscript.gradle @@ -2,10 +2,16 @@ repositories { // Repo in addition to maven central repositories { maven { url 'http://dl.bintray.com/content/netflixoss/external-gradle-plugins/' } } // For gradle-release + maven { + //FIXME: waiting for https://github.com/johnrengelman/shadow/pull/38 to merge + name 'Shadow' + url 'http://dl.bintray.com/content/gvsmirnov/gradle-plugins' + } } dependencies { classpath 'nl.javadude.gradle.plugins:license-gradle-plugin:0.6.1' classpath 'com.mapvine:gradle-cobertura-plugin:0.1' classpath 'gradle-release:gradle-release:1.1.5' classpath 'org.ajoberstar:gradle-git:0.5.0' + classpath 'com.github.jengelman.gradle.plugins:shadow:0.8.1' } From 525af2289f6a8a32fbb3ed0bff3f9382d5aecb71 Mon Sep 17 00:00:00 2001 From: Rob Spieldenner Date: Thu, 27 Mar 2014 17:14:18 -0700 Subject: [PATCH 174/422] tabs to spaces --- build.gradle | 92 ++++++++++++++++++++++++++-------------------------- 1 file changed, 46 insertions(+), 46 deletions(-) diff --git a/build.gradle b/build.gradle index c4af267173..750537e93b 100644 --- a/build.gradle +++ b/build.gradle @@ -12,7 +12,7 @@ buildscript { mavenCentral() jcenter() } - + apply from: file('gradle/buildscript.gradle'), to: buildscript } @@ -46,14 +46,14 @@ subprojects { sourceSets { examples perf { - compileClasspath += sourceSets.main.output - } + compileClasspath += sourceSets.main.output + } } tasks.build { //include 'examples' in build task dependsOn(examplesClasses) - dependsOn(perfClasses) + dependsOn(perfClasses) } task perfJar(type: Jar, dependsOn: perfClasses) { @@ -86,48 +86,48 @@ subprojects { } } - /** - * By default: Run without arguments this will execute all benchmarks that are found (can take a long time). - * - * Optionally pass arguments for custom execution. Example: - * - * ../gradlew benchmarks '-Pjmh=-f 1 -tu ns -bm avgt -wi 5 -i 5 -r 1 .*OperatorSerializePerf.*' - * - * To see all options: - * - * ../gradlew benchmarks '-Pjmh=-h' - */ - task benchmarks(type: JavaExec) { - main = 'org.openjdk.jmh.Main' - classpath = sourceSets.perf.runtimeClasspath + sourceSets.main.output - maxHeapSize = "512m" - - if (project.hasProperty('jmh')) { - args(jmh.split(' ')) - } else { - //args '-h' // help output - args '-f' // fork - args '1' - args '-tu' // time unit - args 'ns' - args '-bm' // benchmark mode - args 'avgt' - args '-wi' // warmup iterations - args '5' - args '-i' // test iterations - args '5' - args '-r' // time per execution in seconds - args '5' - //args '-prof' // profilers - //args 'HS_GC' // HotSpot (tm) memory manager (GC) profiling via implementation-specific MBeans - //args 'HS_RT' // HotSpot (tm) runtime profiling via implementation-specific MBeans - //args 'HS_THR' // HotSpot (tm) threading subsystem via implementation-specific MBeans - //args 'HS_COMP' // HotSpot (tm) JIT compiler profiling via implementation-specific MBeans - //args 'HS_CL' // HotSpot (tm) classloader profiling via implementation-specific MBeans - //args 'STACK' // Simple and naive Java stack profiler - args '.*OperatorSerializePerf.*' // for running only a specific test - } - } + /** + * By default: Run without arguments this will execute all benchmarks that are found (can take a long time). + * + * Optionally pass arguments for custom execution. Example: + * + * ../gradlew benchmarks '-Pjmh=-f 1 -tu ns -bm avgt -wi 5 -i 5 -r 1 .*OperatorSerializePerf.*' + * + * To see all options: + * + * ../gradlew benchmarks '-Pjmh=-h' + */ + task benchmarks(type: JavaExec) { + main = 'org.openjdk.jmh.Main' + classpath = sourceSets.perf.runtimeClasspath + sourceSets.main.output + maxHeapSize = "512m" + + if (project.hasProperty('jmh')) { + args(jmh.split(' ')) + } else { + //args '-h' // help output + args '-f' // fork + args '1' + args '-tu' // time unit + args 'ns' + args '-bm' // benchmark mode + args 'avgt' + args '-wi' // warmup iterations + args '5' + args '-i' // test iterations + args '5' + args '-r' // time per execution in seconds + args '5' + //args '-prof' // profilers + //args 'HS_GC' // HotSpot (tm) memory manager (GC) profiling via implementation-specific MBeans + //args 'HS_RT' // HotSpot (tm) runtime profiling via implementation-specific MBeans + //args 'HS_THR' // HotSpot (tm) threading subsystem via implementation-specific MBeans + //args 'HS_COMP' // HotSpot (tm) JIT compiler profiling via implementation-specific MBeans + //args 'HS_CL' // HotSpot (tm) classloader profiling via implementation-specific MBeans + //args 'STACK' // Simple and naive Java stack profiler + args '.*OperatorSerializePerf.*' // for running only a specific test + } + } shadow { classifier = "benchmarks" From 5b317ad827f624bed6cbf5f2e04df050ebbe01d0 Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Fri, 28 Mar 2014 14:17:29 -0700 Subject: [PATCH 175/422] Update SerializedObserver to Not Allow Notification Delay Unit test showing delays. Fails when MAX_DRAIN_ITERATION set to 1, passes as currently configured. Added a thread starvation unit test and marked as ignored for now. Doesn't pass even with MAX_DRAIN_ITERATION set to 1. Probably needs backpressure solution. --- .../java/rx/observers/SerializedObserver.java | 2 +- .../rx/observers/SerializedObserverTest.java | 149 +++++++++++++++++- 2 files changed, 143 insertions(+), 8 deletions(-) diff --git a/rxjava-core/src/main/java/rx/observers/SerializedObserver.java b/rxjava-core/src/main/java/rx/observers/SerializedObserver.java index 0d397fe73f..f4a21e06f9 100644 --- a/rxjava-core/src/main/java/rx/observers/SerializedObserver.java +++ b/rxjava-core/src/main/java/rx/observers/SerializedObserver.java @@ -21,7 +21,7 @@ public class SerializedObserver implements Observer { private boolean terminated = false; private FastList queue; - private static final int MAX_DRAIN_ITERATION = 1; + private static final int MAX_DRAIN_ITERATION = Integer.MAX_VALUE; private static final Object NULL_SENTINEL = new Object(); private static final Object COMPLETE_SENTINEL = new Object(); diff --git a/rxjava-core/src/test/java/rx/observers/SerializedObserverTest.java b/rxjava-core/src/test/java/rx/observers/SerializedObserverTest.java index e6043edd39..7a82e59c06 100644 --- a/rxjava-core/src/test/java/rx/observers/SerializedObserverTest.java +++ b/rxjava-core/src/test/java/rx/observers/SerializedObserverTest.java @@ -15,9 +15,15 @@ */ package rx.observers; -import static org.junit.Assert.*; -import static org.mockito.Matchers.*; -import static org.mockito.Mockito.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; @@ -28,14 +34,17 @@ import java.util.concurrent.atomic.AtomicInteger; import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import rx.Observable; +import rx.Observable.OnSubscribe; import rx.Observer; import rx.Subscriber; import rx.Subscription; +import rx.schedulers.Schedulers; public class SerializedObserverTest { @@ -265,6 +274,111 @@ public void runConcurrencyTest() { } } + @Test + public void testNotificationDelay() { + ExecutorService tp = Executors.newFixedThreadPool(2); + + TestSubscriber to = new TestSubscriber(new Observer() { + + @Override + public void onCompleted() { + + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onNext(String t) { + // force it to take time when delivering + // so the second thread will asynchronously enqueue + try { + Thread.sleep(50); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + }); + Observer o = serializedObserver(to); + + Future f1 = tp.submit(new OnNextThread(o, 1)); + Future f2 = tp.submit(new OnNextThread(o, 1)); + + waitOnThreads(f1, f2); + // not completed yet + + assertEquals(2, to.getOnNextEvents().size()); + System.out.println(to.getOnNextEvents()); + o.onCompleted(); + System.out.println(to.getOnNextEvents()); + } + + /** + * Demonstrates thread starvation problem. + * + * No solution on this for now. Trade-off in this direction as per https://github.com/Netflix/RxJava/issues/998#issuecomment-38959474 + * Probably need backpressure for this to work + * + * When using SynchronizedObserver we get this output: + * + * p1: 18 p2: 68 => should be close to each other unless we have thread starvation + * + * When using SerializedObserver we get: + * + * p1: 1 p2: 2445261 => should be close to each other unless we have thread starvation + * + * This demonstrates how SynchronizedObserver balances back and forth better, and blocks emission. + * The real issue in this example is the async buffer-bloat, so we need backpressure. + * + * + * @throws InterruptedException + */ + @Ignore + @Test + public void testThreadStarvation() throws InterruptedException { + + TestSubscriber to = new TestSubscriber(new Observer() { + + @Override + public void onCompleted() { + + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onNext(String t) { + // force it to take time when delivering + try { + Thread.sleep(1); + } catch (InterruptedException e) { + } + } + + }); + Observer o = serializedObserver(to); + + AtomicInteger p1 = new AtomicInteger(); + AtomicInteger p2 = new AtomicInteger(); + + Subscription s1 = infinite(p1).subscribe(o); + Subscription s2 = infinite(p2).subscribe(o); + + Thread.sleep(100); + + System.out.println("p1: " + p1.get() + " p2: " + p2.get() + " => should be close to each other unless we have thread starvation"); + assertEquals(p1.get(), p2.get(), 10000); // fairly distributed within 10000 of each other + + s1.unsubscribe(); + s2.unsubscribe(); + } + private static void waitOnThreads(Future... futures) { for (Future f : futures) { try { @@ -276,23 +390,44 @@ private static void waitOnThreads(Future... futures) { } } + private static Observable infinite(final AtomicInteger produced) { + return Observable.create(new OnSubscribe() { + + @Override + public void call(Subscriber s) { + while (!s.isUnsubscribed()) { + s.onNext("onNext"); + produced.incrementAndGet(); + } + } + + }).subscribeOn(Schedulers.newThread()); + } + /** * A thread that will pass data to onNext */ public static class OnNextThread implements Runnable { - private final Observer Observer; + private final Observer observer; private final int numStringsToSend; + final AtomicInteger produced; - OnNextThread(Observer Observer, int numStringsToSend) { - this.Observer = Observer; + OnNextThread(Observer observer, int numStringsToSend, AtomicInteger produced) { + this.observer = observer; this.numStringsToSend = numStringsToSend; + this.produced = produced; + } + + OnNextThread(Observer observer, int numStringsToSend) { + this(observer, numStringsToSend, new AtomicInteger()); } @Override public void run() { for (int i = 0; i < numStringsToSend; i++) { - Observer.onNext(Thread.currentThread().getId() + "-" + i); + observer.onNext(Thread.currentThread().getId() + "-" + i); + produced.incrementAndGet(); } } } From 59ded837de3cd8347ef213cf47791f6f45cf1ab3 Mon Sep 17 00:00:00 2001 From: Rick Warren Date: Sun, 30 Mar 2014 18:06:36 -0700 Subject: [PATCH 176/422] Func0 can transparently implement java.util.concurrent.Callable. This change doesn't change the API at all for users of Func0, but it makes all Func0 objects immediately reusable with any JDK API that accepts Callables. For example, a Func0 can now be submitted directly to an ExecutorService for asynchronous execution. It also allows the elimination of a small amount of redundant code within RxJava itself. --- .../src/main/java/rx/util/async/Async.java | 12 ++++- .../operators/OperationFromFunctionals.java | 45 ++++++------------- .../OperationFromFunctionalsTest.java | 16 ++++++- .../src/main/java/rx/functions/Func0.java | 7 ++- 4 files changed, 43 insertions(+), 37 deletions(-) diff --git a/rxjava-contrib/rxjava-async-util/src/main/java/rx/util/async/Async.java b/rxjava-contrib/rxjava-async-util/src/main/java/rx/util/async/Async.java index 4559032877..d4ca377a04 100644 --- a/rxjava-contrib/rxjava-async-util/src/main/java/rx/util/async/Async.java +++ b/rxjava-contrib/rxjava-async-util/src/main/java/rx/util/async/Async.java @@ -1596,9 +1596,13 @@ public static Observable fromAction(Action0 action, R result) { * @see #start(rx.functions.Func0) * @see #fromCallable(java.util.concurrent.Callable) * @see RxJava Wiki: fromFunc0() + * + * @deprecated Unnecessary now that Func0 extends Callable. Just call + * {@link #fromCallable(Callable)} instead. */ + @Deprecated public static Observable fromFunc0(Func0 function) { - return fromFunc0(function, Schedulers.computation()); + return fromCallable(function); } /** @@ -1674,9 +1678,13 @@ public static Observable fromAction(Action0 action, R result, Scheduler s * @see #start(rx.functions.Func0) * @see #fromCallable(java.util.concurrent.Callable) * @see RxJava Wiki: fromFunc0() + * + * @deprecated Unnecessary now that Func0 extends Callable. Just call + * {@link #fromCallable(Callable, Scheduler)} instead. */ + @Deprecated public static Observable fromFunc0(Func0 function, Scheduler scheduler) { - return Observable.create(OperationFromFunctionals.fromFunc0(function)).subscribeOn(scheduler); + return fromCallable(function, scheduler); } /** diff --git a/rxjava-contrib/rxjava-async-util/src/main/java/rx/util/async/operators/OperationFromFunctionals.java b/rxjava-contrib/rxjava-async-util/src/main/java/rx/util/async/operators/OperationFromFunctionals.java index bf890c3903..57ef639a26 100644 --- a/rxjava-contrib/rxjava-async-util/src/main/java/rx/util/async/operators/OperationFromFunctionals.java +++ b/rxjava-contrib/rxjava-async-util/src/main/java/rx/util/async/operators/OperationFromFunctionals.java @@ -38,10 +38,16 @@ public final class OperationFromFunctionals { public static OnSubscribeFunc fromAction(Action0 action, R result) { return new InvokeAsync(Actions.toFunc(action, result)); } - - /** Subscriber function that invokes a function and returns its value. */ + + /** + * Subscriber function that invokes a function and returns its value. + * + * @deprecated Unnecessary now that Func0 extends Callable. Just call + * {@link #fromCallable(Callable)} instead. + */ + @Deprecated public static OnSubscribeFunc fromFunc0(Func0 function) { - return new InvokeAsync(function); + return fromCallable(function); } /** @@ -49,11 +55,11 @@ public static OnSubscribeFunc fromFunc0(Func0 function) { * propagates its checked exception. */ public static OnSubscribeFunc fromCallable(Callable callable) { - return new InvokeAsyncCallable(callable); + return new InvokeAsync(callable); } /** Subscriber function that invokes a runnable and returns the given result. */ public static OnSubscribeFunc fromRunnable(final Runnable run, final R result) { - return new InvokeAsync(new Func0() { + return new InvokeAsync(new Func0() { @Override public R call() { run.run(); @@ -62,38 +68,13 @@ public R call() { }); } - /** - * Invokes a function when an observer subscribes. - * @param the return type - */ - static final class InvokeAsync implements OnSubscribeFunc { - final Func0 function; - public InvokeAsync(Func0 function) { - if (function == null) { - throw new NullPointerException("function"); - } - this.function = function; - } - @Override - public Subscription onSubscribe(Observer t1) { - Subscription s = Subscriptions.empty(); - try { - t1.onNext(function.call()); - } catch (Throwable t) { - t1.onError(t); - return s; - } - t1.onCompleted(); - return s; - } - } /** * Invokes a java.util.concurrent.Callable when an observer subscribes. * @param the return type */ - static final class InvokeAsyncCallable implements OnSubscribeFunc { + static final class InvokeAsync implements OnSubscribeFunc { final Callable callable; - public InvokeAsyncCallable(Callable callable) { + public InvokeAsync(Callable callable) { if (callable == null) { throw new NullPointerException("function"); } diff --git a/rxjava-contrib/rxjava-async-util/src/test/java/rx/util/async/operators/OperationFromFunctionalsTest.java b/rxjava-contrib/rxjava-async-util/src/test/java/rx/util/async/operators/OperationFromFunctionalsTest.java index 7580670d8a..f89d58ccc5 100644 --- a/rxjava-contrib/rxjava-async-util/src/test/java/rx/util/async/operators/OperationFromFunctionalsTest.java +++ b/rxjava-contrib/rxjava-async-util/src/test/java/rx/util/async/operators/OperationFromFunctionalsTest.java @@ -110,6 +110,13 @@ public void call() { testRunShouldThrow(source, RuntimeException.class); } + + /** + * @deprecated {@link Func0} now extends {@link Callable}, so + * {@link Async#fromFunc0(Func0)} is unnecessary. Once it's + * removed, this test can be removed as well. + */ + @Deprecated @Test public void testFromFunc0() { Func0 func = new Func0() { @@ -139,7 +146,14 @@ public Integer call() { verify(observer, never()).onError(any(Throwable.class)); } } - + + /** + * @deprecated {@link Func0} now extends {@link Callable}, so + * {@link Async#fromFunc0(Func0, rx.Scheduler)} is + * unnecessary. Once it's removed, this test can be removed + * as well. + */ + @Deprecated @Test public void testFromFunc0Throws() { Func0 func = new Func0() { diff --git a/rxjava-core/src/main/java/rx/functions/Func0.java b/rxjava-core/src/main/java/rx/functions/Func0.java index ee8178bb33..d86215d101 100644 --- a/rxjava-core/src/main/java/rx/functions/Func0.java +++ b/rxjava-core/src/main/java/rx/functions/Func0.java @@ -15,6 +15,9 @@ */ package rx.functions; -public interface Func0 extends Function { +import java.util.concurrent.Callable; + +public interface Func0 extends Function, Callable { + @Override public R call(); -} \ No newline at end of file +} From 31cfd197ac0b83670b298cadb2331d4bc4046119 Mon Sep 17 00:00:00 2001 From: suncelesta Date: Mon, 31 Mar 2014 21:59:42 +0400 Subject: [PATCH 177/422] add toMap from Java Observable --- .../main/scala/rx/lang/scala/Observable.scala | 61 ++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/Observable.scala b/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/Observable.scala index d185c1af8b..f0f312a2ac 100644 --- a/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/Observable.scala +++ b/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/Observable.scala @@ -2353,7 +2353,7 @@ trait Observable[+T] * * @param index * the zero-based index of the item to retrieve - * @param defaultValue + * @param default * the default item * @return an Observable that emits the item at the specified position in the sequence emitted by the source * Observable, or the default item if that index is outside the bounds of the source sequence @@ -2364,6 +2364,65 @@ trait Observable[+T] val thisJava = asJavaObservable.asInstanceOf[rx.Observable[U]] toScalaObservable[U](thisJava.elementAtOrDefault(index, default)) } + + /** + * Return an Observable that emits a single Map containing all items emitted by the source Observable, + * mapped by the keys returned by a specified {@code keySelector} function. + *

    + * + *

    + * If more than one source item maps to the same key, the Map will contain the latest of those items. + * + * @param keySelector + * the function that extracts the key from a source item to be used in the Map + * @return an Observable that emits a single item: a Map containing the mapped items from the source + * Observable + */ + + def toMap[K] (keySelector: T => K): Observable[Map[K, T]]= { + toScalaObservable[Map[K,T]](asJavaObservable.toMap(keySelector)) + } + + /** + * Return an Observable that emits a single Map containing values corresponding to items emitted by the + * source Observable, mapped by the keys returned by a specified {@code keySelector} function. + *

    + * + *

    + * If more than one source item maps to the same key, the Map will contain a single entry that + * corresponds to the latest of those items. + * + * @param keySelector + * the function that extracts the key from a source item to be used in the Map + * @param valueSelector + * the function that extracts the value from a source item to be used in the Map + * @return an Observable that emits a single item: a HashMap containing the mapped items from the source + * Observable + */ + + def toMap[K, V] (keySelector: T => K, valueSelector: T => V) : Observable[Map[K, V]] = { + toScalaObservable[Map[K,V]](asJavaObservable.toMap(keySelector, valueSelector)) + } + + /** + * Return an Observable that emits a single Map, returned by a specified {@code mapFactory} function, that + * contains keys and values extracted from the items emitted by the source Observable. + *

    + * + * + * @param keySelector + * the function that extracts the key from a source item to be used in the Map + * @param valueSelector + * the function that extracts the value from the source items to be used as value in the Map + * @param mapFactory + * the function that returns a Map instance to be used + * @return an Observable that emits a single item: a Map that contains the mapped items emitted by the + * source Observable + */ + def toMap[K, V] (keySelector: T => K, valueSelector: T => V, mapFactory: () => Observable[Map[K,V]]): Observable[Map[K,V]] = { + toScalaObservable[Map[K,V]](asJavaObservable.toMap(keySelector, valueSelector, mapFactory)) + } + } /** From 69e5ffddb6b259fb7a262072df61d60d94e79410 Mon Sep 17 00:00:00 2001 From: suncelesta Date: Tue, 1 Apr 2014 13:23:41 +0400 Subject: [PATCH 178/422] add examples of toMap to RxScalaDemo Added examples for all three overloads and removed newlines between methods and docs --- .../rx/lang/scala/examples/RxScalaDemo.scala | 24 +++++++++++++++++++ .../main/scala/rx/lang/scala/Observable.scala | 4 +--- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/language-adaptors/rxjava-scala/src/examples/scala/rx/lang/scala/examples/RxScalaDemo.scala b/language-adaptors/rxjava-scala/src/examples/scala/rx/lang/scala/examples/RxScalaDemo.scala index d0ee67838e..5de6a09d62 100644 --- a/language-adaptors/rxjava-scala/src/examples/scala/rx/lang/scala/examples/RxScalaDemo.scala +++ b/language-adaptors/rxjava-scala/src/examples/scala/rx/lang/scala/examples/RxScalaDemo.scala @@ -573,4 +573,28 @@ class RxScalaDemo extends JUnitSuite { val o : Observable[Seq[Char]] = List("red".toList, "green".toList, "blue".toList).toObservable.elementAtOrDefault(3, "black".toSeq) println(o.toBlockingObservable.single) } + + @Test def toMapExample1(): Unit = { + val o : Observable[String] = List("alice", "bob", "carol").toObservable + val keySelector = (s: String) => s.head + val m = o.toMap(keySelector) + println(m.toBlockingObservable.single) + } + + @Test def toMapExample2(): Unit = { + val o : Observable[String] = List("alice", "bob", "carol").toObservable + val keySelector = (s: String) => s.head + val valueSelector = (s: String) => s.tail + val m = o.toMap(keySelector, valueSelector) + println(m.toBlockingObservable.single) + } + + @Test def toMapExample3(): Unit = { + val o : Observable[String] = List("alice", "bob", "carol").toObservable + val keySelector = (s: String) => s.head + val valueSelector = (s: String) => s.tail + val mapFactory = () => Map('s'->"tart") + val m = o.toMap(keySelector, valueSelector, mapFactory) + println(m.toBlockingObservable.single) + } } diff --git a/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/Observable.scala b/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/Observable.scala index f0f312a2ac..5f3938d38a 100644 --- a/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/Observable.scala +++ b/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/Observable.scala @@ -2378,7 +2378,6 @@ trait Observable[+T] * @return an Observable that emits a single item: a Map containing the mapped items from the source * Observable */ - def toMap[K] (keySelector: T => K): Observable[Map[K, T]]= { toScalaObservable[Map[K,T]](asJavaObservable.toMap(keySelector)) } @@ -2399,7 +2398,6 @@ trait Observable[+T] * @return an Observable that emits a single item: a HashMap containing the mapped items from the source * Observable */ - def toMap[K, V] (keySelector: T => K, valueSelector: T => V) : Observable[Map[K, V]] = { toScalaObservable[Map[K,V]](asJavaObservable.toMap(keySelector, valueSelector)) } @@ -2419,7 +2417,7 @@ trait Observable[+T] * @return an Observable that emits a single item: a Map that contains the mapped items emitted by the * source Observable */ - def toMap[K, V] (keySelector: T => K, valueSelector: T => V, mapFactory: () => Observable[Map[K,V]]): Observable[Map[K,V]] = { + def toMap[K, V] (keySelector: T => K, valueSelector: T => V, mapFactory: () => Map[K,V]): Observable[Map[K,V]] = { toScalaObservable[Map[K,V]](asJavaObservable.toMap(keySelector, valueSelector, mapFactory)) } From 16b0417ca11081d3f39546968dbaa696d7673f04 Mon Sep 17 00:00:00 2001 From: suncelesta Date: Tue, 1 Apr 2014 21:51:54 +0400 Subject: [PATCH 179/422] corrected implementation Implementation corrected to not raise exceptions --- .../rx/lang/scala/examples/RxScalaDemo.scala | 2 +- .../main/scala/rx/lang/scala/Observable.scala | 16 ++++++++++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/language-adaptors/rxjava-scala/src/examples/scala/rx/lang/scala/examples/RxScalaDemo.scala b/language-adaptors/rxjava-scala/src/examples/scala/rx/lang/scala/examples/RxScalaDemo.scala index 5de6a09d62..681d5df690 100644 --- a/language-adaptors/rxjava-scala/src/examples/scala/rx/lang/scala/examples/RxScalaDemo.scala +++ b/language-adaptors/rxjava-scala/src/examples/scala/rx/lang/scala/examples/RxScalaDemo.scala @@ -593,7 +593,7 @@ class RxScalaDemo extends JUnitSuite { val o : Observable[String] = List("alice", "bob", "carol").toObservable val keySelector = (s: String) => s.head val valueSelector = (s: String) => s.tail - val mapFactory = () => Map('s'->"tart") + val mapFactory = () => Map(('s',"tart")) val m = o.toMap(keySelector, valueSelector, mapFactory) println(m.toBlockingObservable.single) } diff --git a/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/Observable.scala b/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/Observable.scala index 5f3938d38a..88eeac4bae 100644 --- a/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/Observable.scala +++ b/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/Observable.scala @@ -20,6 +20,8 @@ import rx.functions.FuncN import rx.Observable.OnSubscribeFunc import rx.lang.scala.observables.ConnectableObservable import scala.concurrent.duration +import java.util +import collection.JavaConversions._ /** @@ -2379,7 +2381,9 @@ trait Observable[+T] * Observable */ def toMap[K] (keySelector: T => K): Observable[Map[K, T]]= { - toScalaObservable[Map[K,T]](asJavaObservable.toMap(keySelector)) + val thisJava = asJavaObservable.asInstanceOf[rx.Observable[T]] + val o: rx.Observable[util.Map[K, T]] = thisJava.toMap[K](keySelector) + toScalaObservable[util.Map[K,T]](o).map(m => m.toMap) } /** @@ -2399,7 +2403,9 @@ trait Observable[+T] * Observable */ def toMap[K, V] (keySelector: T => K, valueSelector: T => V) : Observable[Map[K, V]] = { - toScalaObservable[Map[K,V]](asJavaObservable.toMap(keySelector, valueSelector)) + val thisJava = asJavaObservable.asInstanceOf[rx.Observable[T]] + val o: rx.Observable[util.Map[K, V]] = thisJava.toMap[K, V](keySelector, valueSelector) + toScalaObservable[util.Map[K, V]](o).map(m => m.toMap) } /** @@ -2417,8 +2423,10 @@ trait Observable[+T] * @return an Observable that emits a single item: a Map that contains the mapped items emitted by the * source Observable */ - def toMap[K, V] (keySelector: T => K, valueSelector: T => V, mapFactory: () => Map[K,V]): Observable[Map[K,V]] = { - toScalaObservable[Map[K,V]](asJavaObservable.toMap(keySelector, valueSelector, mapFactory)) + def toMap[K, V] (keySelector: T => K, valueSelector: T => V, mapFactory: () => Map[K, V]): Observable[Map[K, V]] = { + val thisJava = asJavaObservable.asInstanceOf[rx.Observable[T]] + val o: rx.Observable[util.Map[K, V]] = thisJava.toMap[K, V](keySelector, valueSelector) + toScalaObservable[util.Map[K, V]](o).map(m => mapFactory() ++ m.toMap) } } From 9791c2d1e3ad84808d470f584eb4a45abf4c5cb9 Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Tue, 1 Apr 2014 13:18:25 -0700 Subject: [PATCH 180/422] Use latches instead of sleep for unit test As per suggestion at https://github.com/benjchristensen/RxJava/commit/5b317ad827f624bed6cbf5f2e04df050ebbe01d0#commitcomment-5839773 --- .../rx/observers/SerializedObserverTest.java | 32 ++++++++++++++++--- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/rxjava-core/src/test/java/rx/observers/SerializedObserverTest.java b/rxjava-core/src/test/java/rx/observers/SerializedObserverTest.java index 7a82e59c06..172c996ee0 100644 --- a/rxjava-core/src/test/java/rx/observers/SerializedObserverTest.java +++ b/rxjava-core/src/test/java/rx/observers/SerializedObserverTest.java @@ -17,6 +17,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.Matchers.any; @@ -274,10 +275,18 @@ public void runConcurrencyTest() { } } + /** + * Test that a notification does not get delayed in the queue waiting for the next event to push it through. + * + * @throws InterruptedException + */ @Test - public void testNotificationDelay() { + public void testNotificationDelay() throws InterruptedException { ExecutorService tp = Executors.newFixedThreadPool(2); + final CountDownLatch onNextCount = new CountDownLatch(1); + final CountDownLatch latch = new CountDownLatch(1); + TestSubscriber to = new TestSubscriber(new Observer() { @Override @@ -292,12 +301,12 @@ public void onError(Throwable e) { @Override public void onNext(String t) { - // force it to take time when delivering - // so the second thread will asynchronously enqueue + // know when the first thread gets in + onNextCount.countDown(); + // force it to take time when delivering so the second one is enqueued try { - Thread.sleep(50); + latch.await(); } catch (InterruptedException e) { - e.printStackTrace(); } } @@ -307,10 +316,23 @@ public void onNext(String t) { Future f1 = tp.submit(new OnNextThread(o, 1)); Future f2 = tp.submit(new OnNextThread(o, 1)); + onNextCount.await(); + + Thread t1 = to.getLastSeenThread(); + System.out.println("first onNext on thread: " + t1); + + latch.countDown(); + waitOnThreads(f1, f2); // not completed yet assertEquals(2, to.getOnNextEvents().size()); + + Thread t2 = to.getLastSeenThread(); + System.out.println("second onNext on thread: " + t2); + + assertSame(t1, t2); + System.out.println(to.getOnNextEvents()); o.onCompleted(); System.out.println(to.getOnNextEvents()); From abee009981a0610a6ee84c723bf96a59c247c27b Mon Sep 17 00:00:00 2001 From: Ben Christensen Date: Tue, 1 Apr 2014 13:50:44 -0700 Subject: [PATCH 181/422] comments to walk through logic --- .../src/main/java/rx/observers/SerializedObserver.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/rxjava-core/src/main/java/rx/observers/SerializedObserver.java b/rxjava-core/src/main/java/rx/observers/SerializedObserver.java index f4a21e06f9..94a7845bcb 100644 --- a/rxjava-core/src/main/java/rx/observers/SerializedObserver.java +++ b/rxjava-core/src/main/java/rx/observers/SerializedObserver.java @@ -117,18 +117,23 @@ public void onNext(T t) { queue = new FastList(); } queue.add(t != null ? t : NULL_SENTINEL); + // another thread is emitting so we add to the queue and return return; } + // we can emit emitting = true; + // reference to the list to drain before emitting our value list = queue; queue = null; } + // we only get here if we won the right to emit, otherwise we returned in the if(emitting) block above try { int iter = MAX_DRAIN_ITERATION; do { drainQueue(list); if (iter == MAX_DRAIN_ITERATION) { + // after the first draining we emit our own value actual.onNext(t); } --iter; @@ -152,6 +157,7 @@ public void onNext(T t) { list = null; } } + // this will only drain if terminated (done here outside of synchronized block) drainQueue(list); } } From d6afe1a9e0f0fe31c52372efa12796724620a4d6 Mon Sep 17 00:00:00 2001 From: Zachary Siegel Date: Tue, 1 Apr 2014 15:54:23 -0500 Subject: [PATCH 182/422] Adding a new RetainedFragment example --- .../samples/src/main/AndroidManifest.xml | 11 +- .../samples/RetainedFragmentActivityV2.java | 142 ++++++++++++++++++ .../layout/retained_fragment_activity_v2.xml | 14 ++ .../main/res/layout/retained_fragment_v2.xml | 19 +++ 4 files changed, 185 insertions(+), 1 deletion(-) create mode 100644 rxjava-contrib/rxjava-android-samples/samples/src/main/java/com/netflix/rxjava/android/samples/RetainedFragmentActivityV2.java create mode 100644 rxjava-contrib/rxjava-android-samples/samples/src/main/res/layout/retained_fragment_activity_v2.xml create mode 100644 rxjava-contrib/rxjava-android-samples/samples/src/main/res/layout/retained_fragment_v2.xml diff --git a/rxjava-contrib/rxjava-android-samples/samples/src/main/AndroidManifest.xml b/rxjava-contrib/rxjava-android-samples/samples/src/main/AndroidManifest.xml index f307d55bb0..bc5af1ecb8 100644 --- a/rxjava-contrib/rxjava-android-samples/samples/src/main/AndroidManifest.xml +++ b/rxjava-contrib/rxjava-android-samples/samples/src/main/AndroidManifest.xml @@ -9,7 +9,16 @@ android:theme="@style/AppTheme" android:label="@string/app_name"> + android:name=".RetainedFragmentActivityV2"> + + + + + + + + diff --git a/rxjava-contrib/rxjava-android-samples/samples/src/main/java/com/netflix/rxjava/android/samples/RetainedFragmentActivityV2.java b/rxjava-contrib/rxjava-android-samples/samples/src/main/java/com/netflix/rxjava/android/samples/RetainedFragmentActivityV2.java new file mode 100644 index 0000000000..25736d12fc --- /dev/null +++ b/rxjava-contrib/rxjava-android-samples/samples/src/main/java/com/netflix/rxjava/android/samples/RetainedFragmentActivityV2.java @@ -0,0 +1,142 @@ +package com.netflix.rxjava.android.samples; + +import android.app.Activity; +import android.app.Fragment; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.widget.Button; +import android.widget.TextView; +import org.json.JSONException; +import org.json.JSONObject; +import rx.Observable; +import rx.Subscription; +import rx.android.schedulers.AndroidSchedulers; +import rx.functions.Action1; +import rx.functions.Func1; +import rx.subscriptions.Subscriptions; + +/** + * A retained fragment whose goals are below + * + * 1) gracefully handle rotation - not losing any data + * 2) gracefully handle the user moving in and out of the app + * 3) use a button or trigger of some sort to start the observable, something more in line with typical use + * 4) ensure that the callbacks are not called if the user moves away from the fragment + * + * @author zsiegel (zsiegel87@gmail.com) + */ +public class RetainedFragmentActivityV2 extends Activity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); + setTitle("Fake API call V2"); + setContentView(R.layout.retained_fragment_activity_v2); + } + + @SuppressWarnings("ConstantConditions") + public static class RetainedFragmentV2 extends Fragment { + + private Observable observable; + private Subscription subscription = Subscriptions.empty(); + private Button startButton; + private boolean progressVisiblity; + + // in a production app, you don't want to have JSON parser code in your fragment, + // but we'll simplify a little here + private static final Func1 PARSE_JSON = new Func1() { + @Override + public String call(String json) { + try { + JSONObject jsonObject = new JSONObject(json); + return String.valueOf(jsonObject.getInt("result")); + } catch (JSONException e) { + throw new RuntimeException(e); + } + } + }; + + public RetainedFragmentV2() { + setRetainInstance(true); + } + + /** + * We un-subscribe whenever we are paused + */ + @Override + public void onPause() { + subscription.unsubscribe(); + super.onPause(); + } + + /** + * We re-subscribe whenever we are resumed + */ + @Override + public void onResume() { + super.onResume(); + subscribe(); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + return inflater.inflate(R.layout.retained_fragment_v2, container, false); + } + + @Override + public void onViewCreated(final View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + final TextView textView = (TextView)getView().findViewById(android.R.id.text1); + + startButton = (Button) view.findViewById(R.id.button); + startButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + textView.setText(""); + start(); + startButton.setEnabled(false); + } + }); + } + + private void start() { + + progressVisiblity = true; + + observable = SampleObservables + .fakeApiCall(5000) + .map(PARSE_JSON) + .observeOn(AndroidSchedulers.mainThread()) + .cache(); + + subscribe(); + } + + /** + * We subscribe/re-subscribe here + */ + private void subscribe() { + if (observable != null) { + + getActivity().setProgressBarIndeterminateVisibility(progressVisiblity); + + final TextView textView = (TextView)getView().findViewById(android.R.id.text1); + + subscription = observable.subscribe(new Action1() { + @Override + public void call(String result) { + textView.setText(result); + progressVisiblity = false; + getActivity().setProgressBarIndeterminateVisibility(progressVisiblity); + startButton.setEnabled(true); + } + }); + } + } + } +} diff --git a/rxjava-contrib/rxjava-android-samples/samples/src/main/res/layout/retained_fragment_activity_v2.xml b/rxjava-contrib/rxjava-android-samples/samples/src/main/res/layout/retained_fragment_activity_v2.xml new file mode 100644 index 0000000000..a6fe72f624 --- /dev/null +++ b/rxjava-contrib/rxjava-android-samples/samples/src/main/res/layout/retained_fragment_activity_v2.xml @@ -0,0 +1,14 @@ + + + + + diff --git a/rxjava-contrib/rxjava-android-samples/samples/src/main/res/layout/retained_fragment_v2.xml b/rxjava-contrib/rxjava-android-samples/samples/src/main/res/layout/retained_fragment_v2.xml new file mode 100644 index 0000000000..aa5e2792c7 --- /dev/null +++ b/rxjava-contrib/rxjava-android-samples/samples/src/main/res/layout/retained_fragment_v2.xml @@ -0,0 +1,19 @@ + + + + + + +