monix.execution.Callback.scala Maven / Gradle / Ivy
The newest version!
/*
* Copyright (c) 2014-2021 by The Monix Project Developers.
* See the project homepage at: https://monix.io
*
* 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 monix.execution
import monix.execution.exceptions.{CallbackCalledMultipleTimesException, UncaughtErrorException}
import monix.execution.schedulers.{TrampolineExecutionContext, TrampolinedRunnable}
import scala.concurrent.{ExecutionContext, Promise}
import scala.util.control.NonFatal
import scala.util.{Failure, Success, Try}
/** Represents a callback that should be called asynchronously
* with the result of a computation.
*
* This is an `Either[E, A] => Unit` with an OOP interface that
* avoids extra boxing, along with overloads of `apply`.
*
* The `onSuccess` method should be called only once, with the successful
* result, whereas `onError` should be called if the result is an error.
*
* Obviously `Callback` describes unsafe side-effects, a fact that is
* highlighted by the usage of `Unit` as the return type. Obviously
* callbacks are unsafe to use in pure code, but are necessary for
* describing asynchronous processes.
*
* THREAD-SAFETY: callback implementations are NOT thread-safe
* by contract, this depends on the implementation. Callbacks
* can be made easily thread-safe via wrapping with:
*
* - [[monix.execution.Callback.safe Callback.safe]]
* - [[monix.execution.Callback.trampolined Callback.trampolined]]
* - [[monix.execution.Callback.forked Callback.forked]]
*
* NOTE that callbacks injected in the [[monix.eval.Task Task]] async
* builders (e.g. [[monix.eval.Task.async Task.async]]) are thread-safe.
*
* @define safetyIssues Can be called at most once by contract.
* Not necessarily thread-safe, depends on implementation.
*
* @throws CallbackCalledMultipleTimesException depending on
* implementation, when signaling via this callback is
* attempted multiple times and the protocol violation
* is detected.
*
* @define callbackCalledMultipleTimes
* [[monix.execution.exceptions.CallbackCalledMultipleTimesException CallbackCalledMultipleTimesException]]
*
* @define tryMethodDescription In case the underlying callback
* implementation protects against protocol violations, then
* this method should return `false` in case the final result
* was already signaled once via [[onSuccess]] or
* [[onError]].
*
* The default implementation relies on catching
* $callbackCalledMultipleTimes in case of violations, which
* is what thread-safe implementations of `onSuccess` or
* `onError` are usually throwing.
*
* WARNING: this method is only provided as a
* convenience. The presence of this method does not
* guarantee that the underlying callback is thread-safe or
* that it protects against protocol violations.
*
* @return `true` if the invocation completes normally or
* `false` in case another concurrent call succeeded
* first in signaling a result
*/
abstract class Callback[-E, -A] extends (Either[E, A] => Unit) {
/**
* Signals a successful value.
*
* $safetyIssues
*/
def onSuccess(value: A): Unit
/**
* Signals an error.
*
* $safetyIssues
*/
def onError(e: E): Unit
/**
* Signals a value via Scala's `Either` (`Left` is error, `Right` is
* the successful value).
*
* $safetyIssues
*/
def apply(result: Either[E, A]): Unit =
result match {
case Right(a) => onSuccess(a)
case Left(e) => onError(e)
}
/**
* Signals a value via Scala's `Try`.
*
* $safetyIssues
*/
def apply(result: Try[A])(implicit ev: Throwable <:< E): Unit =
result match {
case Success(a) => onSuccess(a)
case Failure(e) => onError(e)
}
/** Return a new callback that will apply the supplied function
* before passing the result into this callback.
*/
def contramap[B](f: B => A): Callback[E, B] =
new Callback.Contramap(this, f)
/**
* Attempts to call [[Callback.onSuccess]].
*
* $tryMethodDescription
*/
def tryOnSuccess(value: A): Boolean =
try {
onSuccess(value)
true
} catch {
case _: CallbackCalledMultipleTimesException => false
}
/**
* Attempts to call [[Callback.onError]].
*
* $tryMethodDescription
*/
def tryOnError(e: E): Boolean =
try {
onError(e)
true
} catch {
case _: CallbackCalledMultipleTimesException => false
}
/**
* Attempts to call [[Callback.apply(result:scala\.util\.Try* Callback.apply]].
*
* $tryMethodDescription
*/
def tryApply(result: Try[A])(implicit ev: Throwable <:< E): Boolean =
result match {
case Success(a) => tryOnSuccess(a)
case Failure(e) => tryOnError(e)
}
/**
* Attempts to call [[Callback.apply(result:Either* Callback.apply]].
*
* $tryMethodDescription
*/
def tryApply(result: Either[E, A]): Boolean =
result match {
case Right(a) => tryOnSuccess(a)
case Left(e) => tryOnError(e)
}
}
/**
* @define isThreadSafe '''THREAD-SAFETY''': the returned callback is
* thread-safe.
*
* In case `onSuccess` and `onError` get called multiple times,
* from multiple threads even, the implementation protects against
* access violations and throws a
* [[monix.execution.exceptions.CallbackCalledMultipleTimesException CallbackCalledMultipleTimesException]].
*/
object Callback {
/**
* For building [[Callback]] objects using the
* [[https://typelevel.org/cats/guidelines.html#partially-applied-type-params Partially-Applied Type]]
* technique.
*
* For example these are Equivalent:
*
* `Callback[Throwable, Throwable].empty[String] <-> Callback.empty[Throwable, String]`
*/
def apply[E]: Builders[E] = new Builders[E]
/** Wraps any [[Callback]] into a safer implementation that
* protects against protocol violations (e.g. `onSuccess` or `onError`
* must be called at most once).
*
* $isThreadSafe
*/
def safe[E, A](cb: Callback[E, A])(implicit r: UncaughtExceptionReporter): Callback[E, A] =
cb match {
case ref: Safe[E, A] @unchecked => ref
case _ => new Safe[E, A](cb)
}
/** Creates an empty [[Callback]], a callback that doesn't do
* anything in `onNext` and that logs errors in `onError` with
* the provided [[monix.execution.UncaughtExceptionReporter]].
*/
def empty[E, A](implicit r: UncaughtExceptionReporter): Callback[E, A] =
new Empty(r)
/** Returns a [[Callback]] instance that will complete the given
* promise.
*
* THREAD-SAFETY: the provided instance is thread-safe by virtue
* of `Promise` being thread-safe.
*/
def fromPromise[A](p: Promise[A]): Callback[Throwable, A] =
new Callback[Throwable, A] {
override def tryApply(result: Try[A])(implicit ev: Throwable <:< Throwable): Boolean =
p.tryComplete(result)
override def tryOnSuccess(value: A): Boolean =
p.trySuccess(value)
override def tryOnError(e: Throwable): Boolean =
p.tryFailure(e)
override def onSuccess(value: A): Unit =
if (!tryOnSuccess(value)) throw new CallbackCalledMultipleTimesException("onSuccess")
override def onError(e: Throwable): Unit =
if (!tryOnError(e)) throw new CallbackCalledMultipleTimesException("onError", e)
override def apply(result: Try[A])(implicit ev: Throwable <:< Throwable): Unit =
if (!tryApply(result)) throw CallbackCalledMultipleTimesException.forResult(result)
}
/** Given a [[Callback]] wraps it into an implementation that
* calls `onSuccess` and `onError` asynchronously, using the
* given [[scala.concurrent.ExecutionContext]].
*
* The async boundary created is "light", in the sense that a
* [[monix.execution.schedulers.TrampolinedRunnable TrampolinedRunnable]]
* is used and supporting schedulers can execute these using an internal
* trampoline, thus execution being faster and immediate, but still avoiding
* growing the call-stack and thus avoiding stack overflows.
*
* $isThreadSafe
*
* @see [[Callback.trampolined]]
*/
def forked[E, A](cb: Callback[E, A])(implicit ec: ExecutionContext): Callback[E, A] =
new AsyncFork(cb)
/** Given a [[Callback]] wraps it into an implementation that
* calls `onSuccess` and `onError` asynchronously, using the
* given [[scala.concurrent.ExecutionContext]].
*
* The async boundary created is "light", in the sense that a
* [[monix.execution.schedulers.TrampolinedRunnable TrampolinedRunnable]]
* is used and supporting schedulers can execute these using an internal
* trampoline, thus execution being faster and immediate, but still avoiding
* growing the call-stack and thus avoiding stack overflows.
*
* $isThreadSafe
*
* @see [[forked]]
*/
def trampolined[E, A](cb: Callback[E, A])(implicit ec: ExecutionContext): Callback[E, A] =
new TrampolinedCallback(cb)
/** Turns `Either[Throwable, A] => Unit` callbacks into Monix
* callbacks.
*
* These are common within Cats' implementation, used for
* example in `cats.effect.IO`.
*
* WARNING: the returned callback is NOT thread-safe!
*/
def fromAttempt[E, A](cb: Either[E, A] => Unit): Callback[E, A] =
cb match {
case ref: Callback[E, A] @unchecked => ref
case _ =>
new Callback[E, A] {
private[this] var isActive = true
override def onSuccess(value: A): Unit = apply(Right(value))
override def onError(e: E): Unit = apply(Left(e))
override def apply(result: Either[E, A]): Unit =
if (!tryApply(result)) {
throw CallbackCalledMultipleTimesException.forResult(result)
}
override def tryApply(result: Either[E, A]): Boolean =
if (isActive) {
isActive = false
cb(result)
true
} else {
false
}
}
}
/** Turns `Try[A] => Unit` callbacks into Monix callbacks.
*
* These are common within Scala's standard library implementation,
* due to usage with Scala's `Future`.
*
* WARNING: the returned callback is NOT thread-safe!
*/
def fromTry[A](cb: Try[A] => Unit): Callback[Throwable, A] =
new Callback[Throwable, A] {
private[this] var isActive = true
override def onSuccess(value: A): Unit = apply(Success(value))
override def onError(e: Throwable): Unit = apply(Failure(e))
override def apply(result: Try[A])(implicit ev: Throwable <:< Throwable): Unit =
if (!tryApply(result)) {
throw CallbackCalledMultipleTimesException.forResult(result)
}
override def tryApply(result: Try[A])(implicit ev: Throwable <:< Throwable): Boolean = {
if (isActive) {
isActive = false
cb(result)
true
} else {
false
}
}
}
private[monix] def callSuccess[E, A](cb: Either[E, A] => Unit, value: A): Unit =
cb match {
case ref: Callback[E, A] @unchecked => ref.onSuccess(value)
case _ => cb(Right(value))
}
private[monix] def callError[E, A](cb: Either[E, A] => Unit, value: E): Unit =
cb match {
case ref: Callback[E, A] @unchecked => ref.onError(value)
case _ => cb(Left(value))
}
private[monix] def signalErrorTrampolined[E, A](cb: Callback[E, A], e: E): Unit =
TrampolineExecutionContext.immediate.execute(new Runnable {
override def run(): Unit =
cb.onError(e)
})
/** Functions exposed via [[apply]]. */
final class Builders[E](val ev: Boolean = true) extends AnyVal {
/** See [[Callback.safe]]. */
def safe[A](cb: Callback[E, A])(implicit r: UncaughtExceptionReporter): Callback[E, A] =
Callback.safe(cb)
/** See [[Callback.empty]]. */
def empty[A](implicit r: UncaughtExceptionReporter): Callback[E, A] =
Callback.empty
/** See [[Callback.fromPromise]]. */
def fromPromise[A](p: Promise[A])(implicit ev: Throwable <:< E): Callback[Throwable, A] =
Callback.fromPromise(p)
/** See [[Callback.forked]]. */
def forked[A](cb: Callback[E, A])(implicit ec: ExecutionContext): Callback[E, A] =
Callback.forked(cb)
/** See [[Callback.trampolined]]. */
def trampolined[A](cb: Callback[E, A])(implicit ec: ExecutionContext): Callback[E, A] =
Callback.trampolined(cb)
/** See [[Callback.fromAttempt]]. */
def fromAttempt[A](cb: Either[E, A] => Unit): Callback[E, A] =
Callback.fromAttempt(cb)
/** See [[Callback.fromTry]]. */
def fromTry[A](cb: Try[A] => Unit)(implicit ev: Throwable <:< E): Callback[Throwable, A] =
Callback.fromTry(cb)
}
private final class AsyncFork[E, A](cb: Callback[E, A])(implicit ec: ExecutionContext) extends Base[E, A](cb)(ec)
private final class TrampolinedCallback[E, A](cb: Callback[E, A])(implicit ec: ExecutionContext)
extends Base[E, A](cb)(ec) with TrampolinedRunnable
/** Base implementation for `trampolined` and `forked`. */
private class Base[E, A](cb: Callback[E, A])(implicit ec: ExecutionContext) extends Callback[E, A] with Runnable {
private[this] val state = monix.execution.atomic.AtomicInt(0)
private[this] var value: A = _
private[this] var error: E = _
override final def onSuccess(value: A): Unit =
if (!tryOnSuccess(value)) {
throw new CallbackCalledMultipleTimesException("onSuccess")
}
override final def tryOnSuccess(value: A): Boolean = {
if (state.compareAndSet(0, 1)) {
this.value = value
ec.execute(this)
true
} else {
false
}
}
override final def onError(e: E): Unit =
if (!tryOnError(e)) {
throw new CallbackCalledMultipleTimesException(
"Callback.onError",
UncaughtErrorException.wrap(e)
)
}
override final def tryOnError(e: E): Boolean = {
if (state.compareAndSet(0, 2)) {
this.error = e
ec.execute(this)
true
} else {
false
}
}
override final def run(): Unit = {
state.get() match {
case 1 =>
val v = value
value = null.asInstanceOf[A]
cb.onSuccess(v)
case 2 =>
val e = error
error = null.asInstanceOf[E]
cb.onError(e)
}
}
}
/** An "empty" callback instance doesn't do anything `onSuccess` and
* only logs exceptions `onError`.
*/
private final class Empty(r: UncaughtExceptionReporter) extends Callback[Any, Any] {
def onSuccess(value: Any): Unit = ()
def onError(error: Any): Unit =
r.reportFailure(UncaughtErrorException.wrap(error))
}
/** A `SafeCallback` is a callback that ensures it can only be called
* once, with a simple check.
*/
private final class Safe[-E, -A](underlying: Callback[E, A])(implicit r: UncaughtExceptionReporter)
extends Callback[E, A] {
private[this] val isActive =
monix.execution.atomic.AtomicBoolean(true)
override def onSuccess(value: A): Unit = {
if (isActive.compareAndSet(true, false))
try {
underlying.onSuccess(value)
} catch {
case e: CallbackCalledMultipleTimesException =>
throw e
case e if NonFatal(e) =>
r.reportFailure(e)
}
else {
throw new CallbackCalledMultipleTimesException("onSuccess")
}
}
override def onError(e: E): Unit = {
if (isActive.compareAndSet(true, false)) {
try {
underlying.onError(e)
} catch {
case e: CallbackCalledMultipleTimesException =>
throw e
case e2 if NonFatal(e2) =>
r.reportFailure(UncaughtErrorException.wrap(e))
r.reportFailure(e2)
}
} else {
val ex = UncaughtErrorException.wrap(e)
throw new CallbackCalledMultipleTimesException("onError", ex)
}
}
}
private final class Contramap[-E, -A, -B](underlying: Callback[E, A], f: B => A) extends Callback[E, B] {
def onSuccess(value: B): Unit =
underlying.onSuccess(f(value))
def onError(error: E): Unit =
underlying.onError(error)
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy