Skip to content

Commit 03bc7a2

Browse files
authored
1.x: add AsyncCompletableSubscriber that exposes unsubscribe() (#4020)
1 parent 535fb75 commit 03bc7a2

File tree

2 files changed

+226
-0
lines changed

2 files changed

+226
-0
lines changed
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
/**
2+
* Copyright 2016 Netflix, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package rx.observers;
17+
18+
import java.util.concurrent.atomic.AtomicReference;
19+
20+
import rx.Completable.CompletableSubscriber;
21+
import rx.Subscription;
22+
import rx.annotations.Experimental;
23+
import rx.internal.util.RxJavaPluginUtils;
24+
25+
/**
26+
* An abstract base class for CompletableSubscriber implementations that want to expose an unsubscription
27+
* capability.
28+
* <p>
29+
* Calling {@link #unsubscribe()} and {@link #isUnsubscribed()} is threadsafe and can happen at any time, even
30+
* before or during an active {@link rx.Completable#subscribe(CompletableSubscriber)} call.
31+
* <p>
32+
* Override the {@link #onStart()} method to execute custom logic on the very first successful onSubscribe call.
33+
* <p>
34+
* If one wants to remain consistent regarding {@link #isUnsubscribed()} and being terminated,
35+
* the {@link #clear()} method should be called from the implementing onError and onCompleted methods.
36+
* <p>
37+
* <pre><code>
38+
* public final class MyCompletableSubscriber extends AsyncCompletableSubscriber {
39+
* &#64;Override
40+
* public void onStart() {
41+
* System.out.println("Started!");
42+
* }
43+
*
44+
* &#64;Override
45+
* public void onCompleted() {
46+
* System.out.println("Completed!");
47+
* clear();
48+
* }
49+
*
50+
* &#64;Override
51+
* public void onError(Throwable e) {
52+
* e.printStackTrace();
53+
* clear();
54+
* }
55+
* }
56+
* </code></pre>
57+
* @since (if this graduates from Experimental/Beta to supported, replace this parenthetical with the release number)
58+
*/
59+
@Experimental
60+
public abstract class AsyncCompletableSubscriber implements CompletableSubscriber, Subscription {
61+
62+
/**
63+
* Holds onto a deferred subscription and allows asynchronous cancellation before the call
64+
* to onSubscribe() by the upstream.
65+
*/
66+
private final AtomicReference<Subscription> upstream = new AtomicReference<Subscription>();
67+
68+
@Override
69+
public final void onSubscribe(Subscription d) {
70+
if (!upstream.compareAndSet(null, d)) {
71+
d.unsubscribe();
72+
if (upstream.get() != UNSUBSCRIBED) {
73+
RxJavaPluginUtils.handleException(new IllegalStateException("Subscription already set!"));
74+
}
75+
} else {
76+
onStart();
77+
}
78+
}
79+
80+
/**
81+
* Called before the first onSubscribe() call succeeds.
82+
*/
83+
protected void onStart() {
84+
}
85+
86+
@Override
87+
public final boolean isUnsubscribed() {
88+
return upstream.get() == UNSUBSCRIBED;
89+
}
90+
91+
/**
92+
* Call to clear the upstream's subscription without unsubscribing it.
93+
*/
94+
protected final void clear() {
95+
upstream.set(UNSUBSCRIBED);
96+
}
97+
98+
@Override
99+
public final void unsubscribe() {
100+
Subscription current = upstream.get();
101+
if (current != UNSUBSCRIBED) {
102+
current = upstream.getAndSet(UNSUBSCRIBED);
103+
if (current != null && current != UNSUBSCRIBED) {
104+
current.unsubscribe();
105+
}
106+
}
107+
108+
}
109+
110+
/**
111+
* Indicates the unsubscribed state.
112+
*/
113+
static final Unsubscribed UNSUBSCRIBED = new Unsubscribed();
114+
115+
static final class Unsubscribed implements Subscription {
116+
117+
@Override
118+
public void unsubscribe() {
119+
// deliberately no op
120+
}
121+
122+
@Override
123+
public boolean isUnsubscribed() {
124+
return true;
125+
}
126+
127+
}
128+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/**
2+
* Copyright 2016 Netflix, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package rx.observers;
17+
18+
import java.util.*;
19+
20+
import org.junit.*;
21+
22+
import rx.Completable;
23+
import rx.Observable;
24+
import rx.exceptions.TestException;
25+
26+
public class AsyncCompletableSubscriberTest {
27+
28+
static final class TestCS extends AsyncCompletableSubscriber {
29+
int started;
30+
31+
int completions;
32+
33+
final List<Throwable> errors = new ArrayList<Throwable>();
34+
35+
@Override
36+
protected void onStart() {
37+
started++;
38+
}
39+
40+
@Override
41+
public void onCompleted() {
42+
completions++;
43+
clear();
44+
}
45+
46+
@Override
47+
public void onError(Throwable e) {
48+
errors.add(e);
49+
clear();
50+
}
51+
}
52+
53+
@Test
54+
public void normal() {
55+
TestCS ts = new TestCS();
56+
57+
Assert.assertFalse(ts.isUnsubscribed());
58+
59+
Completable.complete().subscribe(ts);
60+
61+
Assert.assertEquals(1, ts.started);
62+
Assert.assertEquals(1, ts.completions);
63+
Assert.assertEquals(ts.errors.toString(), 0, ts.errors.size());
64+
Assert.assertTrue(ts.isUnsubscribed());
65+
}
66+
67+
@Test
68+
public void error() {
69+
TestCS ts = new TestCS();
70+
71+
Assert.assertFalse(ts.isUnsubscribed());
72+
73+
Completable.error(new TestException("Forced failure")).subscribe(ts);
74+
75+
Assert.assertEquals(1, ts.started);
76+
Assert.assertEquals(0, ts.completions);
77+
Assert.assertEquals(ts.errors.toString(), 1, ts.errors.size());
78+
Assert.assertTrue(ts.errors.get(0).toString(), ts.errors.get(0) instanceof TestException);
79+
Assert.assertEquals("Forced failure", ts.errors.get(0).getMessage());
80+
Assert.assertTrue(ts.isUnsubscribed());
81+
}
82+
83+
84+
@Test
85+
public void unsubscribed() {
86+
TestCS ts = new TestCS();
87+
ts.unsubscribe();
88+
89+
Assert.assertTrue(ts.isUnsubscribed());
90+
91+
Observable.range(1, 10).toCompletable().subscribe(ts);
92+
93+
Assert.assertEquals(0, ts.started);
94+
Assert.assertEquals(0, ts.completions);
95+
Assert.assertEquals(ts.errors.toString(), 0, ts.errors.size());
96+
Assert.assertTrue(ts.isUnsubscribed());
97+
}
98+
}

0 commit comments

Comments
 (0)