Skip to content

Commit 47d6f5e

Browse files
committed
Issue monix#42 - fix switch, add SerialCancelable
1 parent d8d3174 commit 47d6f5e

File tree

9 files changed

+306
-59
lines changed

9 files changed

+306
-59
lines changed

shared/src/main/scala/monifu/concurrent/cancelables/MultiAssignmentCancelable.scala

+14-9
Original file line numberDiff line numberDiff line change
@@ -13,30 +13,35 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16-
16+
1717
package monifu.concurrent.cancelables
1818

1919
import monifu.concurrent.Cancelable
2020
import monifu.concurrent.atomic.Atomic
21-
import monifu.concurrent.cancelables.MultiAssignmentCancelable.State
22-
import monifu.concurrent.cancelables.MultiAssignmentCancelable.State.{Cancelled, Active}
2321
import scala.annotation.tailrec
2422

2523
/**
26-
* Represents a [[monifu.concurrent.Cancelable]] whose underlying cancelable reference can be swapped for another.
24+
* Represents a [[monifu.concurrent.Cancelable]] whose underlying cancelable
25+
* reference can be swapped for another.
2726
*
2827
* Example:
2928
* {{{
3029
* val s = MultiAssignmentCancelable()
31-
* s() = c1 // sets the underlying cancelable to c1
32-
* s() = c2 // swaps the underlying cancelable to c2
30+
* s := c1 // sets the underlying cancelable to c1
31+
* s := c2 // swaps the underlying cancelable to c2
3332
*
3433
* s.cancel() // also cancels c2
3534
*
36-
* s() = c3 // also cancels c3, because s is already canceled
35+
* s := c3 // also cancels c3, because s is already canceled
3736
* }}}
37+
*
38+
* Also see [[SerialCancelable]], which is similar, except that it cancels the
39+
* old cancelable upon assigning a new cancelable.
3840
*/
3941
final class MultiAssignmentCancelable private () extends BooleanCancelable {
42+
import MultiAssignmentCancelable.State
43+
import MultiAssignmentCancelable.State._
44+
4045
private[this] val state = Atomic(Active(Cancelable()) : State)
4146

4247
def isCanceled: Boolean = state.get match {
@@ -87,8 +92,8 @@ object MultiAssignmentCancelable {
8792
ms
8893
}
8994

90-
sealed trait State
91-
object State {
95+
private sealed trait State
96+
private object State {
9297
case class Active(s: Cancelable) extends State
9398
case object Cancelled extends State
9499
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/*
2+
* Copyright (c) 2014-2015 Alexandru Nedelcu
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+
17+
package monifu.concurrent.cancelables
18+
19+
import monifu.concurrent.Cancelable
20+
import monifu.concurrent.atomic.Atomic
21+
import scala.annotation.tailrec
22+
23+
/**
24+
* Represents a [[monifu.concurrent.Cancelable]] whose underlying cancelable
25+
* can be swapped for another cancelable which causes the previous underlying
26+
* cancelable to be canceled.
27+
*
28+
* Example:
29+
* {{{
30+
* val s = SerialCancelable()
31+
* s := c1 // sets the underlying cancelable to c1
32+
* s := c2 // cancels c1 and swaps the underlying cancelable to c2
33+
*
34+
* s.cancel() // also cancels c2
35+
*
36+
* s() = c3 // also cancels c3, because s is already canceled
37+
* }}}
38+
*
39+
* Also see [[MultiAssignmentCancelable]], which is similar, but doesn't cancel
40+
* the old cancelable upon assignment.
41+
*/
42+
final class SerialCancelable private () extends BooleanCancelable {
43+
import SerialCancelable.State
44+
import SerialCancelable.State._
45+
46+
private[this] val state = Atomic(Active(Cancelable()) : State)
47+
48+
def isCanceled: Boolean = state.get match {
49+
case Cancelled => true
50+
case _ => false
51+
}
52+
53+
@tailrec
54+
def cancel(): Boolean = state.get match {
55+
case Cancelled => false
56+
case current @ Active(s) =>
57+
if (state.compareAndSet(current, Cancelled)) {
58+
s.cancel()
59+
true
60+
}
61+
else
62+
cancel()
63+
}
64+
65+
/**
66+
* Swaps the underlying cancelable reference with `s`.
67+
*
68+
* In case this `SerialCancelable` is already canceled,
69+
* then the reference `value` will also be canceled on assignment.
70+
*/
71+
@tailrec
72+
def update(value: Cancelable): Unit = state.get match {
73+
case Cancelled => value.cancel()
74+
case current @ Active(s) =>
75+
if (!state.compareAndSet(current, Active(value)))
76+
update(value)
77+
else
78+
s.cancel()
79+
}
80+
81+
/**
82+
* Alias for `update(value)`
83+
*/
84+
def `:=`(value: Cancelable): Unit =
85+
update(value)
86+
}
87+
88+
object SerialCancelable {
89+
def apply(): SerialCancelable =
90+
new SerialCancelable()
91+
92+
def apply(s: Cancelable): SerialCancelable = {
93+
val ms = new SerialCancelable()
94+
ms() = s
95+
ms
96+
}
97+
98+
private sealed trait State
99+
100+
private object State {
101+
case class Active(s: Cancelable) extends State
102+
case object Cancelled extends State
103+
}
104+
}

shared/src/main/scala/monifu/reactive/Observable.scala

+18-7
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,24 @@ trait Observable[+T] { self =>
348348
(implicit ev: T <:< Observable[U]): Observable[U] =
349349
operators.flatten.merge(self, bufferPolicy, delayErrors = true)
350350

351+
/**
352+
* Convert an Observable that emits Observables into a single Observable that
353+
* emits the items emitted by the most-recently-emitted of those Observables.
354+
*/
355+
def switch[U](implicit ev: T <:< Observable[U]): Observable[U] =
356+
operators.switch(self, delayErrors = false)
357+
358+
/**
359+
* Convert an Observable that emits Observables into a single Observable that
360+
* emits the items emitted by the most-recently-emitted of those Observables.
361+
*
362+
* It's like [[Observable.switch]], except that the created observable is
363+
* reserving onError notifications until all of the Observables complete
364+
* and only then passing the error along to the observers.
365+
*/
366+
def switchDelayError[U](implicit ev: T <:< Observable[U]): Observable[U] =
367+
operators.switch(self, delayErrors = true)
368+
351369
/**
352370
* Given the source observable and another `Observable`, emits all of the items
353371
* from the first of these Observables to emit an item and cancel the other.
@@ -1168,13 +1186,6 @@ trait Observable[+T] { self =>
11681186
def lift[U](f: Observable[T] => Observable[U]): Observable[U] =
11691187
f(self)
11701188

1171-
1172-
/**
1173-
*
1174-
*/
1175-
def switch[U](implicit ev: T <:< Observable[U]): Observable[U] =
1176-
operators.switch(self)
1177-
11781189
/**
11791190
* Returns the first generated result as a Future and then cancels
11801191
* the subscription.

shared/src/main/scala/monifu/reactive/operators/switch.scala

+38-19
Original file line numberDiff line numberDiff line change
@@ -16,64 +16,78 @@
1616

1717
package monifu.reactive.operators
1818

19-
import monifu.concurrent.cancelables.{BooleanCancelable, RefCountCancelable}
19+
import monifu.concurrent.cancelables.{RefCountCancelable, SerialCancelable}
2020
import monifu.reactive.Ack.{Cancel, Continue}
2121
import monifu.reactive._
2222
import monifu.reactive.internals._
2323
import monifu.reactive.observers.SynchronousObserver
24+
import scala.collection.mutable
2425
import scala.concurrent.Future
2526

2627
object switch {
2728
/**
2829
* Implementation for [[Observable.concat]].
2930
*/
30-
def apply[T, U](source: Observable[T])(implicit ev: T <:< Observable[U]): Observable[U] =
31+
def apply[T, U](source: Observable[T], delayErrors: Boolean)
32+
(implicit ev: T <:< Observable[U]): Observable[U] = {
33+
3134
Observable.create { subscriber:Subscriber[U] =>
3235
implicit val s = subscriber.scheduler
3336
val observerU = subscriber.observer
3437

3538
source.unsafeSubscribe(new SynchronousObserver[T] { self =>
3639
// Global subscription, is canceled by the downstream
3740
// observer and if canceled all streaming is supposed to stop
38-
private[this] val upstream = BooleanCancelable()
41+
private[this] val upstream = SerialCancelable()
3942

4043
// MUST BE synchronized by `self`
4144
private[this] var ack: Future[Ack] = Continue
42-
// To be accessed only in `self.onNext`
43-
private[this] var current: BooleanCancelable = null
45+
// MUST BE synchronized by `self`
46+
private[this] val errors = if (delayErrors)
47+
mutable.ArrayBuffer.empty[Throwable] else null
4448

4549
private[this] val refCount = RefCountCancelable {
4650
self.synchronized {
47-
ack.onContinueSignalComplete(observerU)
51+
if (delayErrors && errors.nonEmpty) {
52+
ack.onContinueSignalError(observerU, CompositeException(errors))
53+
errors.clear()
54+
}
55+
else {
56+
ack.onContinueSignalComplete(observerU)
57+
}
4858
}
4959
}
5060

5161
def onNext(childObservable: T) = {
5262
if (upstream.isCanceled) Cancel else {
5363
// canceling current observable in order to
5464
// start the new stream
55-
val activeSubscription = refCount.acquire()
56-
if (current != null) current.cancel()
57-
current = activeSubscription
65+
val refID = refCount.acquire()
66+
upstream := refID
5867

5968
childObservable.unsafeSubscribe(new Observer[U] {
6069
def onNext(elem: U) = self.synchronized {
61-
if (upstream.isCanceled || activeSubscription.isCanceled)
62-
Cancel
63-
else {
70+
if (refID.isCanceled) Cancel else {
6471
ack = ack.onContinueStreamOnNext(observerU, elem)
6572
ack.ifCanceledDoCancel(upstream)
66-
ack
6773
}
6874
}
6975

70-
def onError(ex: Throwable): Unit =
71-
self.onError(ex)
76+
def onError(ex: Throwable): Unit = {
77+
if (delayErrors) self.synchronized {
78+
errors += ex
79+
onComplete()
80+
}
81+
else {
82+
self.onError(ex)
83+
}
84+
}
7285

7386
def onComplete(): Unit = {
74-
// NOTE: we aren't sending this onComplete signal downstream to our observerU
75-
// we will eventually do that after all of them are complete
76-
activeSubscription.cancel()
87+
// NOTE: we aren't sending this onComplete signal downstream to
88+
// our observerU we will eventually do that after all of them
89+
// are complete
90+
refID.cancel()
7791
}
7892
})
7993

@@ -83,7 +97,11 @@ object switch {
8397

8498
def onError(ex: Throwable): Unit =
8599
self.synchronized {
86-
if (!upstream.isCanceled) {
100+
if (delayErrors) {
101+
errors += ex
102+
onComplete()
103+
}
104+
else if (!upstream.isCanceled) {
87105
upstream.cancel()
88106
ack.onContinueSignalError(observerU, ex)
89107
}
@@ -94,4 +112,5 @@ object switch {
94112
}
95113
})
96114
}
115+
}
97116
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* Copyright (c) 2014-2015 Alexandru Nedelcu
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+
17+
package monifu.concurrent.cancelables
18+
19+
import minitest.SimpleTestSuite
20+
21+
object SerialCancelableSuite extends SimpleTestSuite {
22+
test("cancel()") {
23+
var effect = 0
24+
val sub = BooleanCancelable(effect += 1)
25+
val mSub = SerialCancelable(sub)
26+
27+
assert(effect == 0)
28+
assert(!sub.isCanceled)
29+
assert(!mSub.isCanceled)
30+
31+
mSub.cancel()
32+
assert(sub.isCanceled && mSub.isCanceled)
33+
assert(effect == 1)
34+
35+
mSub.cancel()
36+
assert(sub.isCanceled && mSub.isCanceled)
37+
assert(effect == 1)
38+
}
39+
40+
test("cancel() after second assignment") {
41+
var effect = 0
42+
val sub = BooleanCancelable(effect += 1)
43+
val mSub = SerialCancelable(sub)
44+
val sub2 = BooleanCancelable(effect += 10)
45+
mSub() = sub2
46+
47+
assert(effect == 1)
48+
assert(sub.isCanceled && !sub2.isCanceled && !mSub.isCanceled)
49+
50+
mSub.cancel()
51+
assert(sub2.isCanceled && mSub.isCanceled && sub.isCanceled)
52+
assertEquals(effect, 11)
53+
}
54+
55+
test("automatically cancel assigned") {
56+
val mSub = SerialCancelable()
57+
mSub.cancel()
58+
59+
var effect = 0
60+
val sub = BooleanCancelable(effect += 1)
61+
62+
assert(effect == 0)
63+
assert(!sub.isCanceled && mSub.isCanceled)
64+
65+
mSub() = sub
66+
assert(effect == 1)
67+
assert(sub.isCanceled)
68+
}
69+
}

0 commit comments

Comments
 (0)