unstashAll as terminal Effect, #26489
* Effect.persist(event).thenUnstashAll().thenRun(..) can be misinterpreted as the callback of thenRun is invoked when all unstashing has been completed, while it is actually running the callback first and the the unstashing process follows. * Unstashing is a process where stashed commands are processed one-by-one and waiting for persist effects to complete before processing next. * Even if we would come up with a way to keep pending callbacks around during the unstashing it would probably be complicated for user to reason about it. Suddenly a callback is executed from an old command although several other commands (that were stashed/unstashed) have been processed inbetween. * This change makes the unstashAll Effect terminal, meaning that additional effects like thenRun can't be added after unstashAll. * ReplyEffect is also terminal, which makes sense since it's supposed to be returned effect. It must still be possible to combine with thenUnstashAll, thenReply.thenUnstashAll.
This commit is contained in:
parent
08abca7956
commit
11a18370c4
5 changed files with 124 additions and 85 deletions
|
|
@ -5,17 +5,19 @@
|
|||
package akka.persistence.typed.internal
|
||||
|
||||
import scala.collection.immutable
|
||||
|
||||
import akka.annotation.InternalApi
|
||||
import akka.persistence.typed.ExpectingReply
|
||||
import akka.persistence.typed.javadsl
|
||||
import akka.persistence.typed.scaladsl
|
||||
import akka.persistence.typed.scaladsl.ReplyEffect
|
||||
|
||||
/** INTERNAL API */
|
||||
@InternalApi
|
||||
private[akka] abstract class EffectImpl[+Event, State]
|
||||
extends javadsl.ReplyEffect[Event, State]
|
||||
with scaladsl.ReplyEffect[Event, State] {
|
||||
extends javadsl.EffectBuilder[Event, State]
|
||||
with javadsl.ReplyEffect[Event, State]
|
||||
with scaladsl.ReplyEffect[Event, State]
|
||||
with scaladsl.EffectBuilder[Event, State] {
|
||||
/* All events that will be persisted in this effect */
|
||||
override def events: immutable.Seq[Event] = Nil
|
||||
|
||||
|
|
@ -23,7 +25,7 @@ private[akka] abstract class EffectImpl[+Event, State]
|
|||
CompositeEffect(this, new Callback[State](chainedEffect))
|
||||
|
||||
override def thenReply[ReplyMessage](cmd: ExpectingReply[ReplyMessage])(
|
||||
replyWithMessage: State => ReplyMessage): ReplyEffect[Event, State] =
|
||||
replyWithMessage: State => ReplyMessage): EffectImpl[Event, State] =
|
||||
CompositeEffect(this, new ReplyEffectImpl[ReplyMessage, State](cmd.replyTo, replyWithMessage))
|
||||
|
||||
override def thenUnstashAll(): EffectImpl[Event, State] =
|
||||
|
|
@ -41,7 +43,7 @@ private[akka] abstract class EffectImpl[+Event, State]
|
|||
@InternalApi
|
||||
private[akka] object CompositeEffect {
|
||||
def apply[Event, State](
|
||||
effect: scaladsl.Effect[Event, State],
|
||||
effect: scaladsl.EffectBuilder[Event, State],
|
||||
sideEffects: SideEffect[State]): CompositeEffect[Event, State] =
|
||||
CompositeEffect[Event, State](effect, sideEffects :: Nil)
|
||||
}
|
||||
|
|
@ -49,7 +51,7 @@ private[akka] object CompositeEffect {
|
|||
/** INTERNAL API */
|
||||
@InternalApi
|
||||
private[akka] final case class CompositeEffect[Event, State](
|
||||
persistingEffect: scaladsl.Effect[Event, State],
|
||||
persistingEffect: scaladsl.EffectBuilder[Event, State],
|
||||
_sideEffects: immutable.Seq[SideEffect[State]])
|
||||
extends EffectImpl[Event, State] {
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import akka.persistence.typed.internal._
|
|||
@InternalApi private[akka] object EffectFactories extends EffectFactories[Nothing, Nothing]
|
||||
|
||||
/**
|
||||
* Factory methods for creating [[Effect]] directives - how a persistent actor reacts on a command.
|
||||
* Factory methods for creating [[Effect]] directives - how an event sourced actor reacts on a command.
|
||||
* Created via [[EventSourcedBehavior.Effect]].
|
||||
*
|
||||
* Not for user extension
|
||||
|
|
@ -28,29 +28,29 @@ import akka.persistence.typed.internal._
|
|||
/**
|
||||
* Persist a single event
|
||||
*/
|
||||
final def persist(event: Event): Effect[Event, State] = Persist(event)
|
||||
final def persist(event: Event): EffectBuilder[Event, State] = Persist(event)
|
||||
|
||||
/**
|
||||
* Persist all of a the given events. Each event will be applied through `applyEffect` separately but not until
|
||||
* all events has been persisted. If an `afterCallBack` is added through [[Effect#andThen]] that will invoked
|
||||
* all events has been persisted. If `callback` is added through [[Effect#thenRun]] that will invoked
|
||||
* after all the events has been persisted.
|
||||
*/
|
||||
final def persist(events: java.util.List[Event]): Effect[Event, State] = PersistAll(events.asScala.toVector)
|
||||
final def persist(events: java.util.List[Event]): EffectBuilder[Event, State] = PersistAll(events.asScala.toVector)
|
||||
|
||||
/**
|
||||
* Do not persist anything
|
||||
*/
|
||||
def none(): Effect[Event, State] = PersistNothing.asInstanceOf[Effect[Event, State]]
|
||||
def none(): EffectBuilder[Event, State] = PersistNothing.asInstanceOf[EffectBuilder[Event, State]]
|
||||
|
||||
/**
|
||||
* Stop this persistent actor
|
||||
*/
|
||||
def stop(): Effect[Event, State] = none().thenStop()
|
||||
def stop(): EffectBuilder[Event, State] = none().thenStop()
|
||||
|
||||
/**
|
||||
* This command is not handled, but it is not an error that it isn't.
|
||||
*/
|
||||
def unhandled(): Effect[Event, State] = Unhandled.asInstanceOf[Effect[Event, State]]
|
||||
def unhandled(): EffectBuilder[Event, State] = Unhandled.asInstanceOf[EffectBuilder[Event, State]]
|
||||
|
||||
/**
|
||||
* Stash the current command. Can be unstashed later with `Effect.thenUnstashAll`
|
||||
|
|
@ -61,10 +61,10 @@ import akka.persistence.typed.internal._
|
|||
* thrown from processing a command or side effect after persisting. The stash buffer is preserved for persist
|
||||
* failures if an `onPersistFailure` backoff supervisor strategy is defined.
|
||||
*
|
||||
* Side effects can be chained with `andThen`.
|
||||
* Side effects can be chained with `thenRun`.
|
||||
*/
|
||||
def stash(): ReplyEffect[Event, State] =
|
||||
Stash.asInstanceOf[Effect[Event, State]].thenNoReply()
|
||||
Stash.asInstanceOf[EffectBuilder[Event, State]].thenNoReply()
|
||||
|
||||
/**
|
||||
* Unstash the commands that were stashed with `EffectFactories.stash`.
|
||||
|
|
@ -73,9 +73,6 @@ import akka.persistence.typed.internal._
|
|||
* commands will not be processed by this `unstashAll` effect and have to be unstashed
|
||||
* by another `unstashAll`.
|
||||
*
|
||||
* Side effects can be chained with `andThen`, but note that the side effect is run immediately and not after
|
||||
* processing all unstashed commands.
|
||||
*
|
||||
* @see [[Effect.thenUnstashAll]]
|
||||
*/
|
||||
def unstashAll(): Effect[Event, State] =
|
||||
|
|
@ -112,13 +109,24 @@ import akka.persistence.typed.internal._
|
|||
/**
|
||||
* A command handler returns an `Effect` directive that defines what event or events to persist.
|
||||
*
|
||||
* Additional side effects can be performed in the callback `andThen`
|
||||
* Instances of `Effect` are available through factories [[EventSourcedBehavior.Effect]].
|
||||
*
|
||||
* Not intended for user extension.
|
||||
*/
|
||||
@DoNotInherit trait Effect[+Event, State] {
|
||||
self: EffectImpl[Event, State] =>
|
||||
}
|
||||
|
||||
/**
|
||||
* A command handler returns an `Effect` directive that defines what event or events to persist.
|
||||
*
|
||||
* Additional side effects can be performed in the callback `thenRun`
|
||||
*
|
||||
* Instances of `Effect` are available through factories [[EventSourcedBehavior.Effect]].
|
||||
*
|
||||
* Not intended for user extension.
|
||||
*/
|
||||
@DoNotInherit abstract class Effect[+Event, State] {
|
||||
@DoNotInherit abstract class EffectBuilder[+Event, State] extends Effect[Event, State] {
|
||||
self: EffectImpl[Event, State] =>
|
||||
|
||||
/**
|
||||
|
|
@ -130,17 +138,17 @@ import akka.persistence.typed.internal._
|
|||
* If the state is not of the expected type an [[java.lang.ClassCastException]] is thrown.
|
||||
*
|
||||
*/
|
||||
final def thenRun[NewState <: State](callback: function.Procedure[NewState]): Effect[Event, State] =
|
||||
final def thenRun[NewState <: State](callback: function.Procedure[NewState]): EffectBuilder[Event, State] =
|
||||
CompositeEffect(this, SideEffect[State](s => callback.apply(s.asInstanceOf[NewState])))
|
||||
|
||||
/**
|
||||
* Run the given callback. Callbacks are run sequentially.
|
||||
*/
|
||||
final def thenRun(callback: function.Effect): Effect[Event, State] =
|
||||
final def thenRun(callback: function.Effect): EffectBuilder[Event, State] =
|
||||
CompositeEffect(this, SideEffect[State]((_: State) => callback.apply()))
|
||||
|
||||
/** The side effect is to stop the actor */
|
||||
def thenStop(): Effect[Event, State]
|
||||
def thenStop(): EffectBuilder[Event, State]
|
||||
|
||||
/**
|
||||
* Unstash the commands that were stashed with `EffectFactories.stash`.
|
||||
|
|
@ -182,6 +190,15 @@ import akka.persistence.typed.internal._
|
|||
* Then there will be compilation errors if the returned effect isn't a [[ReplyEffect]], which can be
|
||||
* created with `Effects().reply`, `Effects().noReply`, [[Effect.thenReply]], or [[Effect.thenNoReply]].
|
||||
*/
|
||||
@DoNotInherit abstract class ReplyEffect[+Event, State] extends Effect[Event, State] {
|
||||
@DoNotInherit trait ReplyEffect[+Event, State] extends Effect[Event, State] {
|
||||
self: EffectImpl[Event, State] =>
|
||||
|
||||
/**
|
||||
* Unstash the commands that were stashed with `EffectFactories.stash`.
|
||||
*
|
||||
* It's allowed to stash messages while unstashing. Those newly added
|
||||
* commands will not be processed by this `unstashAll` effect and have to be unstashed
|
||||
* by another `unstashAll`.
|
||||
*/
|
||||
def thenUnstashAll(): ReplyEffect[Event, State]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,52 +11,53 @@ import akka.persistence.typed.internal.SideEffect
|
|||
import akka.persistence.typed.internal._
|
||||
|
||||
/**
|
||||
* Factory methods for creating [[Effect]] directives - how a persistent actor reacts on a command.
|
||||
* Factory methods for creating [[Effect]] directives - how an event sourced actor reacts on a command.
|
||||
*/
|
||||
object Effect {
|
||||
|
||||
/**
|
||||
* Persist a single event
|
||||
*
|
||||
* Side effects can be chained with `andThen`
|
||||
* Side effects can be chained with `thenRun`
|
||||
*/
|
||||
def persist[Event, State](event: Event): Effect[Event, State] = Persist(event)
|
||||
def persist[Event, State](event: Event): EffectBuilder[Event, State] = Persist(event)
|
||||
|
||||
/**
|
||||
* Persist multiple events
|
||||
*
|
||||
* Side effects can be chained with `andThen`
|
||||
* Side effects can be chained with `thenRun`
|
||||
*/
|
||||
def persist[Event, A <: Event, B <: Event, State](evt1: A, evt2: B, events: Event*): Effect[Event, State] =
|
||||
def persist[Event, A <: Event, B <: Event, State](evt1: A, evt2: B, events: Event*): EffectBuilder[Event, State] =
|
||||
persist(evt1 :: evt2 :: events.toList)
|
||||
|
||||
/**
|
||||
* Persist multiple events
|
||||
*
|
||||
* Side effects can be chained with `andThen`
|
||||
* Side effects can be chained with `thenRun`
|
||||
*/
|
||||
def persist[Event, State](events: im.Seq[Event]): Effect[Event, State] =
|
||||
def persist[Event, State](events: im.Seq[Event]): EffectBuilder[Event, State] =
|
||||
PersistAll(events)
|
||||
|
||||
/**
|
||||
* Do not persist anything
|
||||
*
|
||||
* Side effects can be chained with `andThen`
|
||||
* Side effects can be chained with `thenRun`
|
||||
*/
|
||||
def none[Event, State]: Effect[Event, State] = PersistNothing.asInstanceOf[Effect[Event, State]]
|
||||
def none[Event, State]: EffectBuilder[Event, State] = PersistNothing.asInstanceOf[EffectBuilder[Event, State]]
|
||||
|
||||
/**
|
||||
* This command is not handled, but it is not an error that it isn't.
|
||||
*
|
||||
* Side effects can be chained with `andThen`
|
||||
* Side effects can be chained with `thenRun`
|
||||
*/
|
||||
def unhandled[Event, State]: Effect[Event, State] = Unhandled.asInstanceOf[Effect[Event, State]]
|
||||
def unhandled[Event, State]: EffectBuilder[Event, State] = Unhandled.asInstanceOf[EffectBuilder[Event, State]]
|
||||
|
||||
/**
|
||||
* Stop this persistent actor
|
||||
* Side effects can be chained with `andThen`
|
||||
* Side effects can be chained with `thenRun`
|
||||
*/
|
||||
def stop[Event, State](): Effect[Event, State] = none.thenStop()
|
||||
def stop[Event, State](): EffectBuilder[Event, State] =
|
||||
none.thenStop()
|
||||
|
||||
/**
|
||||
* Stash the current command. Can be unstashed later with [[Effect.unstashAll]].
|
||||
|
|
@ -66,10 +67,10 @@ object Effect {
|
|||
* thrown from processing a command or side effect after persisting. The stash buffer is preserved for persist
|
||||
* failures if a backoff supervisor strategy is defined with [[EventSourcedBehavior.onPersistFailure]].
|
||||
*
|
||||
* Side effects can be chained with `andThen`
|
||||
* Side effects can be chained with `thenRun`
|
||||
*/
|
||||
def stash[Event, State](): ReplyEffect[Event, State] =
|
||||
Stash.asInstanceOf[Effect[Event, State]].thenNoReply()
|
||||
Stash.asInstanceOf[EffectBuilder[Event, State]].thenNoReply()
|
||||
|
||||
/**
|
||||
* Unstash the commands that were stashed with [[Effect.stash]].
|
||||
|
|
@ -78,13 +79,10 @@ object Effect {
|
|||
* commands will not be processed by this `unstashAll` effect and have to be unstashed
|
||||
* by another `unstashAll`.
|
||||
*
|
||||
* Side effects can be chained with `andThen`, but note that the side effect is run immediately and not after
|
||||
* processing all unstashed commands.
|
||||
*
|
||||
* @see [[Effect.thenUnstashAll]]
|
||||
*/
|
||||
def unstashAll[Event, State](): Effect[Event, State] =
|
||||
CompositeEffect(none.asInstanceOf[Effect[Event, State]], SideEffect.unstashAll[State]())
|
||||
CompositeEffect(none.asInstanceOf[EffectBuilder[Event, State]], SideEffect.unstashAll[State]())
|
||||
|
||||
/**
|
||||
* Send a reply message to the command, which implements [[ExpectingReply]]. The type of the
|
||||
|
|
@ -113,22 +111,36 @@ object Effect {
|
|||
}
|
||||
|
||||
/**
|
||||
* A command handler returns an `Effect` directive that defines what event or events to persist.
|
||||
*
|
||||
* Instances are created through the factories in the [[Effect]] companion object.
|
||||
*
|
||||
* Not for user extension.
|
||||
*/
|
||||
@DoNotInherit
|
||||
trait Effect[+Event, State] {
|
||||
trait Effect[+Event, State]
|
||||
|
||||
/**
|
||||
* A command handler returns an `Effect` directive that defines what event or events to persist.
|
||||
*
|
||||
* Instances are created through the factories in the [[Effect]] companion object.
|
||||
*
|
||||
* Additional side effects can be performed in the callback `thenRun`
|
||||
*
|
||||
* Not for user extension.
|
||||
*/
|
||||
@DoNotInherit
|
||||
trait EffectBuilder[+Event, State] extends Effect[Event, State] {
|
||||
/* All events that will be persisted in this effect */
|
||||
def events: im.Seq[Event]
|
||||
|
||||
/**
|
||||
* Run the given callback. Callbacks are run sequentially.
|
||||
*/
|
||||
def thenRun(callback: State => Unit): Effect[Event, State]
|
||||
def thenRun(callback: State => Unit): EffectBuilder[Event, State]
|
||||
|
||||
/** The side effect is to stop the actor */
|
||||
def thenStop(): Effect[Event, State]
|
||||
def thenStop(): EffectBuilder[Event, State]
|
||||
|
||||
/**
|
||||
* Unstash the commands that were stashed with [[Effect.stash]].
|
||||
|
|
@ -170,4 +182,14 @@ trait Effect[+Event, State] {
|
|||
*
|
||||
* Not intended for user extension.
|
||||
*/
|
||||
@DoNotInherit trait ReplyEffect[+Event, State] extends Effect[Event, State]
|
||||
@DoNotInherit trait ReplyEffect[+Event, State] extends Effect[Event, State] {
|
||||
|
||||
/**
|
||||
* Unstash the commands that were stashed with [[Effect.stash]].
|
||||
*
|
||||
* It's allowed to stash messages while unstashing. Those newly added
|
||||
* commands will not be processed by this `unstashAll` effect and have to be unstashed
|
||||
* by another `unstashAll`.
|
||||
*/
|
||||
def thenUnstashAll(): ReplyEffect[Event, State]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -152,7 +152,7 @@ object EventSourcedBehaviorStashSpec {
|
|||
// already inactive
|
||||
Effect.reply(cmd)(Ack(cmd.id))
|
||||
case cmd: Activate =>
|
||||
Effect.persist(Activated).thenUnstashAll().thenReply(cmd)(_ => Ack(cmd.id))
|
||||
Effect.persist(Activated).thenReply(cmd)((_: State) => Ack(cmd.id)).thenUnstashAll()
|
||||
case _: Unhandled =>
|
||||
Effect.unhandled.thenNoReply()
|
||||
case Throw(id, t, replyTo) =>
|
||||
|
|
@ -505,40 +505,38 @@ class EventSourcedBehaviorStashSpec
|
|||
"discard when stash has reached limit with default dropped setting" in {
|
||||
val probe = TestProbe[AnyRef]()
|
||||
system.toUntyped.eventStream.subscribe(probe.ref.toUntyped, classOf[Dropped])
|
||||
val behavior = EventSourcedBehavior[String, String, Boolean](
|
||||
persistenceId = PersistenceId("stash-is-full-drop"),
|
||||
emptyState = false,
|
||||
commandHandler = { (state, command) =>
|
||||
state match {
|
||||
case false =>
|
||||
command match {
|
||||
case "ping" =>
|
||||
probe.ref ! "pong"
|
||||
Effect.none
|
||||
case "start-stashing" =>
|
||||
Effect.persist("start-stashing")
|
||||
case msg =>
|
||||
probe.ref ! msg
|
||||
Effect.none
|
||||
}
|
||||
val behavior = Behaviors.setup[String] { context =>
|
||||
EventSourcedBehavior[String, String, Boolean](
|
||||
persistenceId = PersistenceId("stash-is-full-drop"),
|
||||
emptyState = false,
|
||||
commandHandler = { (state, command) =>
|
||||
state match {
|
||||
case false =>
|
||||
command match {
|
||||
case "ping" =>
|
||||
probe.ref ! "pong"
|
||||
Effect.none
|
||||
case "start-stashing" =>
|
||||
Effect.persist("start-stashing")
|
||||
case msg =>
|
||||
probe.ref ! msg
|
||||
Effect.none
|
||||
}
|
||||
|
||||
case true =>
|
||||
command match {
|
||||
case "unstash" =>
|
||||
Effect
|
||||
.persist("unstash")
|
||||
.thenUnstashAll()
|
||||
// FIXME #26489: this is run before unstash, so not sequentially as the docs say
|
||||
.thenRun(_ => probe.ref ! "done-unstashing")
|
||||
case _ =>
|
||||
Effect.stash()
|
||||
}
|
||||
}
|
||||
}, {
|
||||
case (_, "start-stashing") => true
|
||||
case (_, "unstash") => false
|
||||
case (_, _) => throw new IllegalArgumentException()
|
||||
})
|
||||
case true =>
|
||||
command match {
|
||||
case "unstash" =>
|
||||
Effect.persist("unstash").thenRun((_: Boolean) => context.self ! "done-unstashing").thenUnstashAll()
|
||||
case _ =>
|
||||
Effect.stash()
|
||||
}
|
||||
}
|
||||
}, {
|
||||
case (_, "start-stashing") => true
|
||||
case (_, "unstash") => false
|
||||
case (_, _) => throw new IllegalArgumentException()
|
||||
})
|
||||
}
|
||||
|
||||
val c = spawn(behavior)
|
||||
|
||||
|
|
@ -558,10 +556,10 @@ class EventSourcedBehaviorStashSpec
|
|||
|
||||
// we can still unstash and continue interacting
|
||||
c ! "unstash"
|
||||
probe.expectMessage("done-unstashing") // before actually unstashing, see above
|
||||
(0 to (limit - 1)).foreach { n =>
|
||||
(0 until limit).foreach { n =>
|
||||
probe.expectMessage(s"cmd-$n")
|
||||
}
|
||||
probe.expectMessage("done-unstashing") // before actually unstashing, see above
|
||||
|
||||
c ! "ping"
|
||||
probe.expectMessage("pong")
|
||||
|
|
|
|||
|
|
@ -310,7 +310,7 @@ object PersistentActorCompileOnlyTest {
|
|||
case class MoodChanged(to: Mood) extends Event
|
||||
case class Remembered(memory: String) extends Event
|
||||
|
||||
def changeMoodIfNeeded(currentState: Mood, newMood: Mood): Effect[Event, Mood] =
|
||||
def changeMoodIfNeeded(currentState: Mood, newMood: Mood): EffectBuilder[Event, Mood] =
|
||||
if (currentState == newMood) Effect.none
|
||||
else Effect.persist(MoodChanged(newMood))
|
||||
|
||||
|
|
@ -337,7 +337,7 @@ object PersistentActorCompileOnlyTest {
|
|||
case Remember(memory) =>
|
||||
// A more elaborate example to show we still have full control over the effects
|
||||
// if needed (e.g. when some logic is factored out but you want to add more effects)
|
||||
val commonEffects: Effect[Event, Mood] = changeMoodIfNeeded(state, Happy)
|
||||
val commonEffects: EffectBuilder[Event, Mood] = changeMoodIfNeeded(state, Happy)
|
||||
Effect.persist(commonEffects.events :+ Remembered(memory)).thenRun(commonChainedEffects)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue