Merge pull request #15281 from akka/wip-15218-mapFuture-patriknw
+str #15218 Add mapFuture operator
This commit is contained in:
commit
de22fa4b02
11 changed files with 365 additions and 7 deletions
|
|
@ -35,6 +35,9 @@ private[akka] object Ast {
|
||||||
case class Transform(transformer: Transformer[Any, Any]) extends AstNode {
|
case class Transform(transformer: Transformer[Any, Any]) extends AstNode {
|
||||||
override def name = transformer.name
|
override def name = transformer.name
|
||||||
}
|
}
|
||||||
|
case class MapFuture(f: Any ⇒ Future[Any]) extends AstNode {
|
||||||
|
override def name = "mapFuture"
|
||||||
|
}
|
||||||
case class GroupBy(f: Any ⇒ Any) extends AstNode {
|
case class GroupBy(f: Any ⇒ Any) extends AstNode {
|
||||||
override def name = "groupBy"
|
override def name = "groupBy"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ private[akka] object ActorProcessor {
|
||||||
case bf: Buffer ⇒ Props(new BufferImpl(settings, bf.size, bf.overflowStrategy))
|
case bf: Buffer ⇒ Props(new BufferImpl(settings, bf.size, bf.overflowStrategy))
|
||||||
case tt: PrefixAndTail ⇒ Props(new PrefixAndTailImpl(settings, tt.n))
|
case tt: PrefixAndTail ⇒ Props(new PrefixAndTailImpl(settings, tt.n))
|
||||||
case ConcatAll ⇒ Props(new ConcatAllImpl(settings))
|
case ConcatAll ⇒ Props(new ConcatAllImpl(settings))
|
||||||
|
case m: MapFuture ⇒ Props(new MapFutureProcessorImpl(settings, m.f))
|
||||||
}).withDispatcher(settings.dispatcher)
|
}).withDispatcher(settings.dispatcher)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -165,7 +166,8 @@ private[akka] abstract class FanoutOutputs(val maxBufferSize: Int, val initialBu
|
||||||
|
|
||||||
private var downstreamBufferSpace = 0
|
private var downstreamBufferSpace = 0
|
||||||
private var downstreamCompleted = false
|
private var downstreamCompleted = false
|
||||||
def demandAvailable = downstreamBufferSpace > 0
|
override def demandAvailable = downstreamBufferSpace > 0
|
||||||
|
def demandCount: Int = downstreamBufferSpace
|
||||||
|
|
||||||
override val subreceive = new SubReceive(waitingExposedPublisher)
|
override val subreceive = new SubReceive(waitingExposedPublisher)
|
||||||
|
|
||||||
|
|
@ -244,7 +246,7 @@ private[akka] abstract class ActorProcessorImpl(val settings: MaterializerSettin
|
||||||
override def inputOnError(e: Throwable): Unit = ActorProcessorImpl.this.onError(e)
|
override def inputOnError(e: Throwable): Unit = ActorProcessorImpl.this.onError(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected val primaryOutputs: Outputs =
|
protected val primaryOutputs: FanoutOutputs =
|
||||||
new FanoutOutputs(settings.maxFanOutBufferSize, settings.initialFanOutBufferSize, self, this) {
|
new FanoutOutputs(settings.maxFanOutBufferSize, settings.initialFanOutBufferSize, self, this) {
|
||||||
override def afterShutdown(): Unit = {
|
override def afterShutdown(): Unit = {
|
||||||
primaryOutputsShutdown = true
|
primaryOutputsShutdown = true
|
||||||
|
|
|
||||||
|
|
@ -152,6 +152,9 @@ private[akka] trait Builder[Out] {
|
||||||
override def name = "map"
|
override def name = "map"
|
||||||
})
|
})
|
||||||
|
|
||||||
|
def mapFuture[U](f: Out ⇒ Future[U]): Thing[U] =
|
||||||
|
andThen(MapFuture(f.asInstanceOf[Any ⇒ Future[Any]]))
|
||||||
|
|
||||||
def filter(p: Out ⇒ Boolean): Thing[Out] =
|
def filter(p: Out ⇒ Boolean): Thing[Out] =
|
||||||
transform(new Transformer[Out, Out] {
|
transform(new Transformer[Out, Out] {
|
||||||
override def onNext(in: Out) = if (p(in)) List(in) else Nil
|
override def onNext(in: Out) = if (p(in)) List(in) else Nil
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,156 @@
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2014 Typesafe Inc. <http://www.typesafe.com>
|
||||||
|
*/
|
||||||
|
package akka.stream.impl
|
||||||
|
|
||||||
|
import scala.collection.mutable
|
||||||
|
import scala.collection.immutable
|
||||||
|
import scala.collection.immutable.TreeSet
|
||||||
|
import scala.concurrent.Future
|
||||||
|
import scala.util.control.NonFatal
|
||||||
|
import akka.stream.MaterializerSettings
|
||||||
|
import akka.pattern.pipe
|
||||||
|
import scala.annotation.tailrec
|
||||||
|
|
||||||
|
/**
|
||||||
|
* INTERNAL API
|
||||||
|
*/
|
||||||
|
private[akka] object MapFutureProcessorImpl {
|
||||||
|
|
||||||
|
object FutureElement {
|
||||||
|
implicit val ordering: Ordering[FutureElement] = new Ordering[FutureElement] {
|
||||||
|
def compare(a: FutureElement, b: FutureElement): Int = {
|
||||||
|
a.seqNo compare b.seqNo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case class FutureElement(seqNo: Long, element: Any)
|
||||||
|
case class FutureFailure(cause: Throwable)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* INTERNAL API
|
||||||
|
*/
|
||||||
|
private[akka] class MapFutureProcessorImpl(_settings: MaterializerSettings, f: Any ⇒ Future[Any]) extends ActorProcessorImpl(_settings) {
|
||||||
|
import MapFutureProcessorImpl._
|
||||||
|
|
||||||
|
// Execution context for pipeTo and friends
|
||||||
|
import context.dispatcher
|
||||||
|
|
||||||
|
// TODO performance improvement: mutable buffer?
|
||||||
|
var emits = immutable.Seq.empty[Any]
|
||||||
|
|
||||||
|
var submittedSeqNo = 0L
|
||||||
|
var doneSeqNo = 0L
|
||||||
|
def gap: Long = submittedSeqNo - doneSeqNo
|
||||||
|
|
||||||
|
// TODO performance improvement: explore Endre's proposal of using an array based ring buffer addressed by
|
||||||
|
// seqNo & Mask and explicitly storing a Gap object to denote missing pieces instead of the sorted set
|
||||||
|
|
||||||
|
// keep future results arriving too early in a buffer sorted by seqNo
|
||||||
|
var orderedBuffer = TreeSet.empty[FutureElement]
|
||||||
|
|
||||||
|
override def receive = futureReceive orElse super.receive
|
||||||
|
|
||||||
|
def drainBuffer(): List[Any] = {
|
||||||
|
|
||||||
|
// this is mutable for speed
|
||||||
|
var n = 0
|
||||||
|
var elements = mutable.ListBuffer.empty[Any]
|
||||||
|
var error: Option[Throwable] = None
|
||||||
|
val iter = orderedBuffer.iterator
|
||||||
|
@tailrec def split(): Unit =
|
||||||
|
if (iter.hasNext) {
|
||||||
|
val next = iter.next()
|
||||||
|
val inOrder = next.seqNo == (doneSeqNo + 1)
|
||||||
|
// stop at first missing seqNo
|
||||||
|
if (inOrder) {
|
||||||
|
n += 1
|
||||||
|
doneSeqNo = next.seqNo
|
||||||
|
elements += next.element
|
||||||
|
split()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
split()
|
||||||
|
orderedBuffer = orderedBuffer.drop(n)
|
||||||
|
elements.toList
|
||||||
|
}
|
||||||
|
|
||||||
|
def futureReceive: Receive = {
|
||||||
|
case fe @ FutureElement(seqNo, element) ⇒
|
||||||
|
if (seqNo == (doneSeqNo + 1)) {
|
||||||
|
// successful element for the next sequence number
|
||||||
|
// emit that element and all elements from the buffer that are in order
|
||||||
|
// until next missing sequence number
|
||||||
|
doneSeqNo = seqNo
|
||||||
|
if (orderedBuffer.isEmpty) {
|
||||||
|
emits = List(element)
|
||||||
|
} else {
|
||||||
|
val fromBuffer = drainBuffer()
|
||||||
|
emits = element :: fromBuffer
|
||||||
|
}
|
||||||
|
emitAndThen(running)
|
||||||
|
pump()
|
||||||
|
} else {
|
||||||
|
assert(seqNo > doneSeqNo, s"Unexpected sequence number [$seqNo], expected seqNo > $doneSeqNo")
|
||||||
|
// out of order, buffer until missing elements arrive
|
||||||
|
orderedBuffer += fe
|
||||||
|
}
|
||||||
|
|
||||||
|
case FutureFailure(cause) ⇒
|
||||||
|
fail(cause)
|
||||||
|
}
|
||||||
|
|
||||||
|
override def onError(e: Throwable): Unit = {
|
||||||
|
// propagate upstream error immediately
|
||||||
|
fail(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
object RunningPhaseCondition extends TransferState {
|
||||||
|
def isReady = (primaryInputs.inputsAvailable && primaryOutputs.demandCount - gap > 0) ||
|
||||||
|
(primaryInputs.inputsDepleted && gap == 0)
|
||||||
|
def isCompleted = false
|
||||||
|
}
|
||||||
|
|
||||||
|
val running: TransferPhase = TransferPhase(RunningPhaseCondition) { () ⇒
|
||||||
|
if (primaryInputs.inputsDepleted) {
|
||||||
|
emitAndThen(completedPhase)
|
||||||
|
} else if (primaryInputs.inputsAvailable && primaryOutputs.demandCount - gap > 0) {
|
||||||
|
val elem = primaryInputs.dequeueInputElement()
|
||||||
|
submittedSeqNo += 1
|
||||||
|
val seqNo = submittedSeqNo
|
||||||
|
try {
|
||||||
|
f(elem).map(FutureElement(seqNo, _)).recover {
|
||||||
|
case err ⇒ FutureFailure(err)
|
||||||
|
}.pipeTo(self)
|
||||||
|
} catch {
|
||||||
|
case NonFatal(err) ⇒
|
||||||
|
// f threw, propagate error immediately
|
||||||
|
fail(err)
|
||||||
|
}
|
||||||
|
emitAndThen(running)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save previous phase we should return to in a var to avoid allocation
|
||||||
|
var phaseAfterFlush: TransferPhase = _
|
||||||
|
|
||||||
|
// Enters flushing phase if there are emits pending
|
||||||
|
def emitAndThen(andThen: TransferPhase): Unit =
|
||||||
|
if (emits.nonEmpty) {
|
||||||
|
phaseAfterFlush = andThen
|
||||||
|
nextPhase(emitting)
|
||||||
|
} else nextPhase(andThen)
|
||||||
|
|
||||||
|
// Emits all pending elements, then returns to savedPhase
|
||||||
|
val emitting = TransferPhase(primaryOutputs.NeedsDemand) { () ⇒
|
||||||
|
primaryOutputs.enqueueOutputElement(emits.head)
|
||||||
|
emits = emits.tail
|
||||||
|
if (emits.isEmpty) nextPhase(phaseAfterFlush)
|
||||||
|
}
|
||||||
|
|
||||||
|
nextPhase(running)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -19,6 +19,7 @@ import akka.stream.{ FlattenStrategy, OverflowStrategy, FlowMaterializer, Transf
|
||||||
import akka.stream.scaladsl.{ Duct ⇒ SDuct }
|
import akka.stream.scaladsl.{ Duct ⇒ SDuct }
|
||||||
import akka.stream.impl.Ast
|
import akka.stream.impl.Ast
|
||||||
import scala.concurrent.duration.FiniteDuration
|
import scala.concurrent.duration.FiniteDuration
|
||||||
|
import scala.concurrent.Future
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Java API
|
* Java API
|
||||||
|
|
@ -51,6 +52,15 @@ abstract class Duct[In, Out] {
|
||||||
*/
|
*/
|
||||||
def map[U](f: Function[Out, U]): Duct[In, U]
|
def map[U](f: Function[Out, U]): Duct[In, U]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform this stream by applying the given function to each of the elements
|
||||||
|
* as they pass through this processing step. The function returns a `Future` of the
|
||||||
|
* element that will be emitted downstream. As many futures as requested elements by
|
||||||
|
* downstream may run in parallel and may complete in any order, but the elements that
|
||||||
|
* are emitted downstream are in the same order as from upstream.
|
||||||
|
*/
|
||||||
|
def mapFuture[U](f: Function[Out, Future[U]]): Duct[In, U]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Only pass on those elements that satisfy the given predicate.
|
* Only pass on those elements that satisfy the given predicate.
|
||||||
*/
|
*/
|
||||||
|
|
@ -330,6 +340,8 @@ abstract class Duct[In, Out] {
|
||||||
private[akka] class DuctAdapter[In, T](delegate: SDuct[In, T]) extends Duct[In, T] {
|
private[akka] class DuctAdapter[In, T](delegate: SDuct[In, T]) extends Duct[In, T] {
|
||||||
override def map[U](f: Function[T, U]): Duct[In, U] = new DuctAdapter(delegate.map(f.apply))
|
override def map[U](f: Function[T, U]): Duct[In, U] = new DuctAdapter(delegate.map(f.apply))
|
||||||
|
|
||||||
|
override def mapFuture[U](f: Function[T, Future[U]]): Duct[In, U] = new DuctAdapter(delegate.mapFuture(f.apply))
|
||||||
|
|
||||||
override def filter(p: Predicate[T]): Duct[In, T] = new DuctAdapter(delegate.filter(p.test))
|
override def filter(p: Predicate[T]): Duct[In, T] = new DuctAdapter(delegate.filter(p.test))
|
||||||
|
|
||||||
override def collect[U](pf: PartialFunction[T, U]): Duct[In, U] = new DuctAdapter(delegate.collect(pf))
|
override def collect[U](pf: PartialFunction[T, U]): Duct[In, U] = new DuctAdapter(delegate.collect(pf))
|
||||||
|
|
|
||||||
|
|
@ -114,6 +114,15 @@ abstract class Flow[T] {
|
||||||
*/
|
*/
|
||||||
def map[U](f: Function[T, U]): Flow[U]
|
def map[U](f: Function[T, U]): Flow[U]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform this stream by applying the given function to each of the elements
|
||||||
|
* as they pass through this processing step. The function returns a `Future` of the
|
||||||
|
* element that will be emitted downstream. As many futures as requested elements by
|
||||||
|
* downstream may run in parallel and may complete in any order, but the elements that
|
||||||
|
* are emitted downstream are in the same order as from upstream.
|
||||||
|
*/
|
||||||
|
def mapFuture[U](f: Function[T, Future[U]]): Flow[U]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Only pass on those elements that satisfy the given predicate.
|
* Only pass on those elements that satisfy the given predicate.
|
||||||
*/
|
*/
|
||||||
|
|
@ -402,6 +411,8 @@ trait OnCompleteCallback {
|
||||||
private[akka] class FlowAdapter[T](delegate: SFlow[T]) extends Flow[T] {
|
private[akka] class FlowAdapter[T](delegate: SFlow[T]) extends Flow[T] {
|
||||||
override def map[U](f: Function[T, U]): Flow[U] = new FlowAdapter(delegate.map(f.apply))
|
override def map[U](f: Function[T, U]): Flow[U] = new FlowAdapter(delegate.map(f.apply))
|
||||||
|
|
||||||
|
override def mapFuture[U](f: Function[T, Future[U]]): Flow[U] = new FlowAdapter(delegate.mapFuture(f.apply))
|
||||||
|
|
||||||
override def filter(p: Predicate[T]): Flow[T] = new FlowAdapter(delegate.filter(p.test))
|
override def filter(p: Predicate[T]): Flow[T] = new FlowAdapter(delegate.filter(p.test))
|
||||||
|
|
||||||
override def collect[U](pf: PartialFunction[T, U]): Flow[U] = new FlowAdapter(delegate.collect(pf))
|
override def collect[U](pf: PartialFunction[T, U]): Flow[U] = new FlowAdapter(delegate.collect(pf))
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import akka.stream.{ FlattenStrategy, OverflowStrategy, FlowMaterializer, Transf
|
||||||
import akka.stream.impl.DuctImpl
|
import akka.stream.impl.DuctImpl
|
||||||
import akka.stream.impl.Ast
|
import akka.stream.impl.Ast
|
||||||
import scala.concurrent.duration.FiniteDuration
|
import scala.concurrent.duration.FiniteDuration
|
||||||
|
import scala.concurrent.Future
|
||||||
|
|
||||||
object Duct {
|
object Duct {
|
||||||
|
|
||||||
|
|
@ -42,6 +43,15 @@ trait Duct[In, +Out] {
|
||||||
*/
|
*/
|
||||||
def map[U](f: Out ⇒ U): Duct[In, U]
|
def map[U](f: Out ⇒ U): Duct[In, U]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform this stream by applying the given function to each of the elements
|
||||||
|
* as they pass through this processing step. The function returns a `Future` of the
|
||||||
|
* element that will be emitted downstream. As many futures as requested elements by
|
||||||
|
* downstream may run in parallel and may complete in any order, but the elements that
|
||||||
|
* are emitted downstream are in the same order as from upstream.
|
||||||
|
*/
|
||||||
|
def mapFuture[U](f: Out ⇒ Future[U]): Duct[In, U]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Only pass on those elements that satisfy the given predicate.
|
* Only pass on those elements that satisfy the given predicate.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -110,6 +110,15 @@ trait Flow[+T] {
|
||||||
*/
|
*/
|
||||||
def map[U](f: T ⇒ U): Flow[U]
|
def map[U](f: T ⇒ U): Flow[U]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform this stream by applying the given function to each of the elements
|
||||||
|
* as they pass through this processing step. The function returns a `Future` of the
|
||||||
|
* element that will be emitted downstream. As many futures as requested elements by
|
||||||
|
* downstream may run in parallel and may complete in any order, but the elements that
|
||||||
|
* are emitted downstream are in the same order as from upstream.
|
||||||
|
*/
|
||||||
|
def mapFuture[U](f: T ⇒ Future[U]): Flow[U]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Only pass on those elements that satisfy the given predicate.
|
* Only pass on those elements that satisfy the given predicate.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,10 @@ import org.junit.Test;
|
||||||
import org.reactivestreams.api.Consumer;
|
import org.reactivestreams.api.Consumer;
|
||||||
import org.reactivestreams.api.Producer;
|
import org.reactivestreams.api.Producer;
|
||||||
import scala.concurrent.duration.FiniteDuration;
|
import scala.concurrent.duration.FiniteDuration;
|
||||||
|
import scala.concurrent.Future;
|
||||||
import akka.actor.ActorRef;
|
import akka.actor.ActorRef;
|
||||||
import akka.actor.ActorSystem;
|
import akka.actor.ActorSystem;
|
||||||
|
import akka.dispatch.Futures;
|
||||||
import akka.japi.Function;
|
import akka.japi.Function;
|
||||||
import akka.japi.Function2;
|
import akka.japi.Function2;
|
||||||
import akka.japi.Pair;
|
import akka.japi.Pair;
|
||||||
|
|
@ -189,4 +190,24 @@ public class DuctTest {
|
||||||
probe.expectMsgEquals("done");
|
probe.expectMsgEquals("done");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void mustBeAbleToUseMapFuture() throws Exception {
|
||||||
|
final JavaTestKit probe = new JavaTestKit(system);
|
||||||
|
Consumer<String> c = Duct.create(String.class).mapFuture(new Function<String, Future<String>>() {
|
||||||
|
public Future<String> apply(String elem) {
|
||||||
|
return Futures.successful(elem.toUpperCase());
|
||||||
|
}
|
||||||
|
}).foreach(new Procedure<String>() {
|
||||||
|
public void apply(String elem) {
|
||||||
|
probe.getRef().tell(elem, ActorRef.noSender());
|
||||||
|
}
|
||||||
|
}).consume(materializer);
|
||||||
|
|
||||||
|
final java.lang.Iterable<String> input = Arrays.asList("a", "b", "c");
|
||||||
|
Flow.create(input).produceTo(materializer, c);
|
||||||
|
probe.expectMsgEquals("A");
|
||||||
|
probe.expectMsgEquals("B");
|
||||||
|
probe.expectMsgEquals("C");
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,22 +9,20 @@ import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.Callable;
|
import java.util.concurrent.Callable;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import akka.stream.FlattenStrategy;
|
import akka.stream.FlattenStrategy;
|
||||||
import akka.stream.OverflowStrategy;
|
import akka.stream.OverflowStrategy;
|
||||||
import org.junit.ClassRule;
|
import org.junit.ClassRule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
|
|
||||||
import org.reactivestreams.api.Producer;
|
import org.reactivestreams.api.Producer;
|
||||||
|
|
||||||
import scala.Option;
|
import scala.Option;
|
||||||
import scala.concurrent.Await;
|
import scala.concurrent.Await;
|
||||||
import scala.concurrent.Future;
|
import scala.concurrent.Future;
|
||||||
import scala.concurrent.duration.FiniteDuration;
|
import scala.concurrent.duration.FiniteDuration;
|
||||||
|
|
||||||
import akka.actor.ActorRef;
|
import akka.actor.ActorRef;
|
||||||
import akka.actor.ActorSystem;
|
import akka.actor.ActorSystem;
|
||||||
|
import akka.dispatch.Futures;
|
||||||
import akka.japi.Function;
|
import akka.japi.Function;
|
||||||
import akka.japi.Function2;
|
import akka.japi.Function2;
|
||||||
import akka.japi.Pair;
|
import akka.japi.Pair;
|
||||||
|
|
@ -523,4 +521,23 @@ public class FlowTest {
|
||||||
probe.expectNoMsg(FiniteDuration.create(200, TimeUnit.MILLISECONDS));
|
probe.expectNoMsg(FiniteDuration.create(200, TimeUnit.MILLISECONDS));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void mustBeAbleToUseMapFuture() throws Exception {
|
||||||
|
final JavaTestKit probe = new JavaTestKit(system);
|
||||||
|
final java.lang.Iterable<String> input = Arrays.asList("a", "b", "c");
|
||||||
|
Flow.create(input).mapFuture(new Function<String, Future<String>>() {
|
||||||
|
public Future<String> apply(String elem) {
|
||||||
|
return Futures.successful(elem.toUpperCase());
|
||||||
|
}
|
||||||
|
}).foreach(new Procedure<String>() {
|
||||||
|
public void apply(String elem) {
|
||||||
|
probe.getRef().tell(elem, ActorRef.noSender());
|
||||||
|
}
|
||||||
|
}).consume(materializer);
|
||||||
|
probe.expectMsgEquals("A");
|
||||||
|
probe.expectMsgEquals("B");
|
||||||
|
probe.expectMsgEquals("C");
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
114
akka-stream/src/test/scala/akka/stream/FlowMapFutureSpec.scala
Normal file
114
akka-stream/src/test/scala/akka/stream/FlowMapFutureSpec.scala
Normal file
|
|
@ -0,0 +1,114 @@
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2014 Typesafe Inc. <http://www.typesafe.com>
|
||||||
|
*/
|
||||||
|
package akka.stream
|
||||||
|
|
||||||
|
import scala.concurrent.Future
|
||||||
|
import scala.concurrent.duration._
|
||||||
|
import scala.concurrent.forkjoin.ThreadLocalRandom
|
||||||
|
import scala.util.control.NoStackTrace
|
||||||
|
import akka.stream.scaladsl.Flow
|
||||||
|
import akka.stream.testkit.AkkaSpec
|
||||||
|
import akka.stream.testkit.StreamTestKit
|
||||||
|
import akka.testkit.TestProbe
|
||||||
|
import akka.testkit.TestLatch
|
||||||
|
import scala.concurrent.Await
|
||||||
|
|
||||||
|
@org.junit.runner.RunWith(classOf[org.scalatest.junit.JUnitRunner])
|
||||||
|
class FlowMapFutureSpec extends AkkaSpec {
|
||||||
|
|
||||||
|
val materializer = FlowMaterializer(MaterializerSettings(
|
||||||
|
dispatcher = "akka.test.stream-dispatcher"))
|
||||||
|
|
||||||
|
"A Flow with mapFuture" must {
|
||||||
|
|
||||||
|
"produce future elements" in {
|
||||||
|
val c = StreamTestKit.consumerProbe[Int]
|
||||||
|
implicit val ec = system.dispatcher
|
||||||
|
val p = Flow(1 to 3).mapFuture(n ⇒ Future(n)).produceTo(materializer, c)
|
||||||
|
val sub = c.expectSubscription()
|
||||||
|
sub.requestMore(2)
|
||||||
|
c.expectNext(1)
|
||||||
|
c.expectNext(2)
|
||||||
|
c.expectNoMsg(200.millis)
|
||||||
|
sub.requestMore(2)
|
||||||
|
c.expectNext(3)
|
||||||
|
c.expectComplete()
|
||||||
|
}
|
||||||
|
|
||||||
|
"produce future elements in order" in {
|
||||||
|
val c = StreamTestKit.consumerProbe[Int]
|
||||||
|
implicit val ec = system.dispatcher
|
||||||
|
val p = Flow(1 to 50).mapFuture(n ⇒ Future {
|
||||||
|
Thread.sleep(ThreadLocalRandom.current().nextInt(1, 10))
|
||||||
|
n
|
||||||
|
}).produceTo(materializer, c)
|
||||||
|
val sub = c.expectSubscription()
|
||||||
|
sub.requestMore(1000)
|
||||||
|
for (n ← 1 to 50) c.expectNext(n)
|
||||||
|
c.expectComplete()
|
||||||
|
}
|
||||||
|
|
||||||
|
"not run more futures than requested elements" in {
|
||||||
|
val probe = TestProbe()
|
||||||
|
val c = StreamTestKit.consumerProbe[Int]
|
||||||
|
implicit val ec = system.dispatcher
|
||||||
|
val p = Flow(1 to 20).mapFuture(n ⇒ Future {
|
||||||
|
probe.ref ! n
|
||||||
|
n
|
||||||
|
}).produceTo(materializer, c)
|
||||||
|
val sub = c.expectSubscription()
|
||||||
|
// nothing before requested
|
||||||
|
probe.expectNoMsg(500.millis)
|
||||||
|
sub.requestMore(1)
|
||||||
|
probe.expectMsg(1)
|
||||||
|
probe.expectNoMsg(500.millis)
|
||||||
|
sub.requestMore(2)
|
||||||
|
probe.receiveN(2).toSet should be(Set(2, 3))
|
||||||
|
probe.expectNoMsg(500.millis)
|
||||||
|
sub.requestMore(10)
|
||||||
|
probe.receiveN(10).toSet should be((4 to 13).toSet)
|
||||||
|
probe.expectNoMsg(200.millis)
|
||||||
|
|
||||||
|
for (n ← 1 to 13) c.expectNext(n)
|
||||||
|
c.expectNoMsg(200.millis)
|
||||||
|
}
|
||||||
|
|
||||||
|
"signal future failure" in {
|
||||||
|
val latch = TestLatch(1)
|
||||||
|
val c = StreamTestKit.consumerProbe[Int]
|
||||||
|
implicit val ec = system.dispatcher
|
||||||
|
val p = Flow(1 to 5).mapFuture(n ⇒ Future {
|
||||||
|
if (n == 3) throw new RuntimeException("err1") with NoStackTrace
|
||||||
|
else {
|
||||||
|
Await.ready(latch, 10.seconds)
|
||||||
|
n
|
||||||
|
}
|
||||||
|
}).produceTo(materializer, c)
|
||||||
|
val sub = c.expectSubscription()
|
||||||
|
sub.requestMore(10)
|
||||||
|
c.expectError.getMessage should be("err1")
|
||||||
|
latch.countDown()
|
||||||
|
}
|
||||||
|
|
||||||
|
"signal error from mapFuture" in {
|
||||||
|
val latch = TestLatch(1)
|
||||||
|
val c = StreamTestKit.consumerProbe[Int]
|
||||||
|
implicit val ec = system.dispatcher
|
||||||
|
val p = Flow(1 to 5).mapFuture(n ⇒
|
||||||
|
if (n == 3) throw new RuntimeException("err2") with NoStackTrace
|
||||||
|
else {
|
||||||
|
Future {
|
||||||
|
Await.ready(latch, 10.seconds)
|
||||||
|
n
|
||||||
|
}
|
||||||
|
}).
|
||||||
|
produceTo(materializer, c)
|
||||||
|
val sub = c.expectSubscription()
|
||||||
|
sub.requestMore(10)
|
||||||
|
c.expectError.getMessage should be("err2")
|
||||||
|
latch.countDown()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue