diff --git a/akka-docs/rst/general/actors.rst b/akka-docs/rst/general/actors.rst index 793c299f4c..3e39f54c63 100644 --- a/akka-docs/rst/general/actors.rst +++ b/akka-docs/rst/general/actors.rst @@ -52,6 +52,10 @@ inconsistent state is fatal. Thus, when the actor fails and is restarted by its supervisor, the state will be created from scratch, like upon first creating the actor. This is to enable the ability of self-healing of the system. +Optionally, an actor's state can be automatically recovered to the state +before a restart by persisting received messages and replaying them after +restart (see :ref:`persistence`). + Behavior -------- diff --git a/akka-docs/rst/general/message-delivery-guarantees.rst b/akka-docs/rst/general/message-delivery-guarantees.rst index f0b14bcce8..48547703bc 100644 --- a/akka-docs/rst/general/message-delivery-guarantees.rst +++ b/akka-docs/rst/general/message-delivery-guarantees.rst @@ -257,11 +257,11 @@ throughput or lower latency by removing this guarantee again, which would mean that choosing between different implementations would allow trading guarantees versus performance. -Building On Top Of Akka -======================= +Higher-level abstractions +========================= -The philosophy of Akka is to provide a small and consistent tool set which is -well suited for building powerful abstractions on top. +Based on a small and consistent tool set in Akka's core, Akka also provides +powerful, higher-level abstractions on top it. Messaging Patterns ------------------ @@ -274,12 +274,15 @@ delivery is an explicit ACK–RETRY protocol. In its simplest form this requires - a retry mechanism which will resend messages if not acknowledged in time - a way for the receiver to detect and discard duplicates -The third becomes necessary by virtue of the acknowledgements not being -guaranteed to arrive either. An example of implementing all three requirements -is shown at :ref:`reliable-proxy`. Another way of implementing the third part -would be to make processing the messages idempotent at the receiving end on the -level of the business logic; this is convenient if it arises naturally and -otherwise implemented by keeping track of processed message IDs. +The third becomes necessary by virtue of the acknowledgements not being guaranteed +to arrive either. An ACK-RETRY protocol with business-level acknowledgements is +supported by :ref:`channels` of the Akka Persistence module. Duplicates can be +detected by tracking the sequence numbers of messages received via channels. +Another way of implementing the third part would be to make processing the messages +idempotent on the level of the business logic. + +Another example of implementing all three requirements is shown at +:ref:`reliable-proxy` (which is now superseded by :ref:`channels`). Event Sourcing -------------- @@ -296,7 +299,7 @@ state on a different continent or to react to changes). If the component’s state is lost—due to a machine failure or by being pushed out of a cache—it can easily be reconstructed by replaying the event stream (usually employing snapshots to speed up the process). :ref:`event-sourcing` is supported by -Akka (see :ref:`persistence`). +Akka Persistence. Mailbox with Explicit Acknowledgement ------------------------------------- diff --git a/akka-docs/rst/intro/what-is-akka.rst b/akka-docs/rst/intro/what-is-akka.rst index 9a31a987d0..3f89bcfa6e 100644 --- a/akka-docs/rst/intro/what-is-akka.rst +++ b/akka-docs/rst/intro/what-is-akka.rst @@ -62,6 +62,12 @@ It allows you to compose atomic message flows with automatic retry and rollback. See :ref:`Transactors (Scala) ` and :ref:`Transactors (Java) ` +Persistence +----------- + +Messages received by an actor can optionally be persisted and replayed when the actor is started or +restarted. This allows actors to recover their state, even after JVM crashes or when being migrated +to another node. Scala and Java APIs =================== diff --git a/akka-docs/rst/java/code/docs/persistence/PersistenceDocTest.java b/akka-docs/rst/java/code/docs/persistence/PersistenceDocTest.java index 5ea08d247c..079ff4378a 100644 --- a/akka-docs/rst/java/code/docs/persistence/PersistenceDocTest.java +++ b/akka-docs/rst/java/code/docs/persistence/PersistenceDocTest.java @@ -4,9 +4,13 @@ package docs.persistence; +import java.util.concurrent.TimeUnit; + import scala.Option; +import scala.concurrent.duration.Duration; import akka.actor.*; +import akka.japi.Procedure; import akka.persistence.*; import static java.util.Arrays.asList; @@ -144,7 +148,10 @@ public class PersistenceDocTest { public void onReceive(Object message) throws Exception { if (message instanceof ConfirmablePersistent) { ConfirmablePersistent p = (ConfirmablePersistent)message; - System.out.println("received " + p.payload()); + Object payload = p.payload(); + Long sequenceNr = p.sequenceNr(); + int redeliveries = p.redeliveries(); + // ... p.confirm(); } } @@ -160,6 +167,13 @@ public class PersistenceDocTest { //#channel-id-override this.channel = getContext().actorOf(Channel.props("my-stable-channel-id")); //#channel-id-override + + //#channel-custom-settings + getContext().actorOf(Channel.props( + ChannelSettings.create() + .withRedeliverInterval(Duration.create(30, TimeUnit.SECONDS)) + .withRedeliverMax(15))); + //#channel-custom-settings } public void onReceive(Object message) throws Exception { @@ -273,12 +287,56 @@ public class PersistenceDocTest { public void foo() { //#persistent-channel-example - final ActorRef channel = getContext().actorOf(PersistentChannel.props(), - "myPersistentChannel"); + final ActorRef channel = getContext().actorOf(PersistentChannel.props( + PersistentChannelSettings.create() + .withRedeliverInterval(Duration.create(30, TimeUnit.SECONDS)) + .withRedeliverMax(15)), "myPersistentChannel"); channel.tell(Deliver.create(Persistent.create("example"), destination), getSelf()); //#persistent-channel-example + + //#persistent-channel-reply + PersistentChannelSettings.create().withReplyPersistent(true); + //#persistent-channel-reply } } }; + + static Object o8 = new Object() { + //#reliable-event-delivery + class MyEventsourcedProcessor extends UntypedEventsourcedProcessor { + private ActorRef destination; + private ActorRef channel; + + public MyEventsourcedProcessor(ActorRef destination) { + this.destination = destination; + this.channel = getContext().actorOf(Channel.props(), "channel"); + } + + private void handleEvent(String event) { + // update state + // ... + // reliably deliver events + channel.tell(Deliver.create(Persistent.create( + event, getCurrentPersistentMessage()), destination), getSelf()); + } + + public void onReceiveReplay(Object msg) { + if (msg instanceof String) { + handleEvent((String)msg); + } + } + + public void onReceiveCommand(Object msg) { + if (msg.equals("cmd")) { + persist("evt", new Procedure() { + public void apply(String event) { + handleEvent(event); + } + }); + } + } + } + //#reliable-event-delivery + }; } diff --git a/akka-docs/rst/java/persistence.rst b/akka-docs/rst/java/persistence.rst index 31fbb8fb32..c9cb76a610 100644 --- a/akka-docs/rst/java/persistence.rst +++ b/akka-docs/rst/java/persistence.rst @@ -11,7 +11,8 @@ persisted but never its current state directly (except for optional snapshots). to storage, nothing is ever mutated, which allows for very high transaction rates and efficient replication. Stateful actors are recovered by replaying stored changes to these actors from which they can rebuild internal state. This can be either the full history of changes or starting from a snapshot of internal actor state which can dramatically -reduce recovery times. +reduce recovery times. Akka persistence also provides point-to-point communication channels with at-least-once +message delivery guarantees. Storage backends for state changes and snapshots are pluggable in Akka persistence. Currently, these are written to the local filesystem. Distributed and replicated storage, with the possibility of scaling writes, will be available @@ -48,7 +49,8 @@ Architecture to that processor, so that it can recover internal state from these messages. * *Channel*: Channels are used by processors to communicate with other actors. They prevent that replayed messages - are redundantly delivered to these actors. + are redundantly delivered to these actors and provide at-least-once message delivery guarantees, also in case of + sender and receiver JVM crashes. * *Journal*: A journal stores the sequence of messages sent to a processor. An application can control which messages are stored and which are received by the processor without being journaled. The storage backend of a journal is @@ -155,9 +157,17 @@ should override ``processorId``. Later versions of Akka persistence will likely offer a possibility to migrate processor ids. +.. _channels-java: + Channels ======== +.. warning:: + + There are further changes planned to the channel API that couldn't make it into the current milestone. + One example is to have only a single destination per channel to allow gap detection and more advanced + flow control. + Channels are special actors that are used by processors to communicate with other actors (channel destinations). Channels prevent redundant delivery of replayed messages to destinations during processor recovery. A replayed message is retained by a channel if its previous delivery has been confirmed by a destination. @@ -165,59 +175,106 @@ message is retained by a channel if its previous delivery has been confirmed by .. includecode:: code/docs/persistence/PersistenceDocTest.java#channel-example A channel is ready to use once it has been created, no recovery or further activation is needed. A ``Deliver`` -request instructs a channel to send a ``Persistent`` message to a destination where the sender of the ``Deliver`` -request is forwarded to the destination. A processor may also reply to a message sender directly by using -``getSender()`` as channel destination (not shown). +request instructs a channel to send a ``Persistent`` message to a destination. Sender references are preserved +by a channel, therefore, a destination can reply to the sender of a ``Deliver`` request. + +If a processor wants to reply to a ``Persistent`` message sender it should use the ``getSender()`` reference as +channel destination. .. includecode:: code/docs/persistence/PersistenceDocTest.java#channel-example-reply -Persistent messages delivered by a channel are of type ``ConfirmablePersistent``. It extends ``Persistent`` and -adds a ``confirm()`` method. Channel destinations confirm the delivery of a ``ConfirmablePersistent`` message by -calling ``confirm()``. This (asynchronously) writes a confirmation entry to the journal. Replayed messages -internally contain these confirmation entries which allows a channel to decide if a message should be retained or -not. ``ConfirmablePersistent`` messages can be used whereever ``Persistent`` messages are expected, which allows -processors to be used as channel destinations, for example. +Persistent messages delivered by a channel are of type ``ConfirmablePersistent``. ``ConfirmablePersistent`` extends +``Persistent`` by adding the methods ``confirm`` method and ``redeliveries`` (see also :ref:`redelivery-java`). Channel +destinations confirm the delivery of a ``ConfirmablePersistent`` message by calling ``confirm()`` an that message. +This asynchronously writes a confirmation entry to the journal. Replayed messages internally contain these confirmation +entries which allows a channel to decide if a message should be retained or not. + +A ``Processor`` can also be used as channel destination i.e. it can persist ``ConfirmablePersistent`` messages too. + +.. _redelivery-java: Message re-delivery ------------------- -If an application crashes after a destination called ``confirm()`` but before the confirmation entry could have -been written to the journal then the unconfirmed message will be re-delivered during next recovery of the sending -processor. It is the destination's responsibility to detect the duplicate or simply process the message again if -it's an idempotent receiver. Duplicates can be detected, for example, by tracking sequence numbers. +Channels re-deliver messages to destinations if they do not confirm their receipt within a configurable timeout. +This timeout can be specified as ``redeliverInterval`` when creating a channel, optionally together with the +maximum number of re-deliveries a channel should attempt for each unconfirmed message. -Although a channel prevents message loss in case of sender (JVM) crashes it doesn't attempt re-deliveries if a -destination is unavailable. To achieve reliable communication with a (remote) target, a channel destination may -want to use the :ref:`reliable-proxy` or add the message to a queue that is managed by a third party message -broker, for example. In latter case, the channel destination will first add the received message to the queue -and then call ``confirm()`` on the received ``ConfirmablePersistent`` message. +.. includecode:: code/docs/persistence/PersistenceDocTest.java#channel-custom-settings + +Message re-delivery is done out of order with regards to normal delivery i.e. redelivered messages may arrive +later than newer normally delivered messages. The number of re-delivery attempts can be obtained via the +``redeliveries`` method on ``ConfirmablePersistent``. + +A channel keeps messages in memory until their successful delivery has been confirmed by their destination(s) +or their maximum number of re-deliveries is reached. In the latter case, the application has to re-send the +correspnding ``Deliver`` request to the channel so that the channel can start a new series of delivery attempts +(starting again with a ``redeliveries`` count of ``0``). + +Re-sending ``Deliver`` requests is done automatically if the sending processor replays messages: only ``Deliver`` +requests of unconfirmed messages will be served again by the channel. A message replay can be enforced by an +application by restarting the sending processor, for example. A replay will also take place if the whole +application is restarted, either after normal termination or after a crash. + +This combination of + +* message persistence by sending processors +* message replays by sending processors +* message re-deliveries by channels and +* application-level confirmations (acknowledgements) by destinations + +enables channels to provide at-least-once message delivery guarantees. Possible duplicates can be detected by +destinations by tracking message sequence numbers. Message sequence numbers are generated per sending processor. +Depending on how a processor routes outbound messages to destinations, they may either see a contiguous message +sequence or a sequence with gaps. + +.. warning:: + + If a processor emits more than one outbound message per inbound ``Persistent`` message it **must** use a + separate channel for each outbound message to ensure that confirmations are uniquely identifiable, otherwise, + at-least-once message delivery is not guaranteed. This rule has been introduced to avoid writing additional + outbound message identifiers to the journal which would decrease the overall throughput. It is furthermore + recommended to collapse multiple outbound messages to the same destination into a single outbound message, + otherwise, if sent via multiple channels, their ordering is not defined. These restrictions are likely to be + removed in the final release. + +Whenever an application wants to have more control how sequence numbers are assigned to messages it should use +an application-specific sequence number generator and include the generated sequence numbers into the ``payload`` +of ``Persistent`` messages. Persistent channels ------------------- -Channels created with ``Channel.props`` do not persist messages. This is not necessary because these (transient) -channels shall only be used in combination with a sending processor that takes care of message persistence. +Channels created with ``Channel.props`` do not persist messages. These channels are usually used in combination +with a sending processor that takes care of persistence, hence, channel-specific persistence is not necessary in +this case. They are referred to as transient channels in the following. -However, if an application wants to use a channel standalone (without a sending processor), to prevent message -loss in case of a sender (JVM) crash, it should use a persistent channel which can be created with ``PersistentChannel.props``. -A persistent channel additionally persists messages before they are delivered. Persistence is achieved by an -internal processor that delegates delivery to a transient channel. A persistent channel, when used standalone, -can therefore provide the same message re-delivery semantics as a transient channel in combination with an -application-defined processor. +Applications may also use transient channels standalone (i.e. without a sending processor) if re-delivery attempts +to destinations are required but message loss in case of a sender JVM crash is not an issue. If applications want to +use standalone channels but message loss is not acceptable, they should use persistent channels. A persistent channel +can be created with ``PersistentChannel.props`` and configured with a ``PersistentChannelSettings`` object. - .. includecode:: code/docs/persistence/PersistenceDocTest.java#persistent-channel-example +.. includecode:: code/docs/persistence/PersistenceDocTest.java#persistent-channel-example + +A persistent channel is like a transient channel that additionally persists ``Deliver`` requests before serving it. +Hence, it can recover from sender JVM crashes and provide the same message re-delivery semantics as a transient +channel in combination with an application-defined processor. By default, a persistent channel doesn't reply whether a ``Persistent`` message, sent with ``Deliver``, has been -successfully persisted or not. This can be enabled by creating the channel with the ``persistentReply`` parameter -set to ``true``: ``PersistentChannel.props(true)``. With this setting, either the successfully persisted message -is replied to the sender or a ``PersistenceFailure``. In case of a persistence failure, the sender should re-send -the message. +successfully persisted or not. This can be enabled by creating the channel with the ``replyPersistent`` configuration +parameter set to ``true``: + +.. includecode:: code/docs/persistence/PersistenceDocTest.java#persistent-channel-reply + +With this setting, either the successfully persisted message is replied to the sender or a ``PersistenceFailure``. +In case of a persistence failure, the sender should re-send the message. Using a persistent channel in combination with an application-defined processor can make sense if destinations are unavailable for a long time and an application doesn't want to buffer all messages in memory (but write them to the -journal instead). In this case, delivery can be disabled with ``DisableDelivery`` (to stop delivery and persist-only) -and re-enabled with ``EnableDelivery``. A disabled channel that receives ``EnableDelivery`` will restart itself and -re-deliver all persisted, unconfirmed messages before serving new ``Deliver`` requests. +journal only). In this case, delivery can be disabled by sending the channel a ``DisableDelivery`` message (to +stop delivery and persist-only) and re-enabled again by sending it an ``EnableDelivery`` message. A disabled channel +that receives an ``EnableDelivery`` message, processes all persisted, unconfirmed ``Deliver`` requests again before +serving new ones. Sender resolution ----------------- @@ -272,7 +329,7 @@ Sequence number --------------- The sequence number of a ``Persistent`` message can be obtained via its ``sequenceNr`` method. Persistent -messages are assigned sequence numbers on a per-processor basis (or per persistent channel basis if used +messages are assigned sequence numbers on a per-processor basis (or per channel basis if used standalone). A sequence starts at ``1L`` and doesn't contain gaps unless a processor deletes a message. .. _snapshots-java: @@ -365,6 +422,19 @@ The example also demonstrates how to change the processor's default behavior, de another behavior, defined by ``otherCommandHandler``, and back using ``getContext().become()`` and ``getContext().unbecome()``. See also the API docs of ``persist`` for further details. +Reliable event delivery +----------------------- + +Sending events from an event handler to another actor directly doesn't guarantee delivery of these events. To +guarantee at-least-once delivery, :ref:`channels-java` must be used. In this case, also replayed events (received by +``receiveReplay``) must be sent to a channel, as shown in the following example: + +.. includecode:: code/docs/persistence/PersistenceDocTest.java#reliable-event-delivery + +In larger integration scenarios, channel destinations may be actors that submit received events to an external +message broker, for example. After having successfully submitted an event, they should call ``confirm()`` on the +received ``ConfirmablePersistent`` message. + Batch writes ============ @@ -458,6 +528,8 @@ directory. This location can be changed by configuration where the specified pat With this plugin, each actor system runs its own private LevelDB instance. +.. _shared-leveldb-journal-java: + Shared LevelDB journal ---------------------- @@ -518,3 +590,18 @@ it must add to the application configuration. If not specified, a default serializer is used, which is the ``JavaSerializer`` in this example. + +Testing +======= + +When running tests with LevelDB default settings in ``sbt``, make sure to set ``fork := true`` in your sbt project +otherwise, you'll see an ``UnsatisfiedLinkError``. Alternatively, you can switch to a LevelDB Java port by setting + +.. includecode:: ../scala/code/docs/persistence/PersistencePluginDocSpec.scala#native-config + +or + +.. includecode:: ../scala/code/docs/persistence/PersistencePluginDocSpec.scala#shared-store-native-config + +in your Akka configuration. The latter setting applies if you're using a :ref:`shared-leveldb-journal-java`. The LevelDB +Java port is for testing purposes only. diff --git a/akka-docs/rst/scala/code/docs/persistence/PersistenceDocSpec.scala b/akka-docs/rst/scala/code/docs/persistence/PersistenceDocSpec.scala index 5c151c0e13..d532061181 100644 --- a/akka-docs/rst/scala/code/docs/persistence/PersistenceDocSpec.scala +++ b/akka-docs/rst/scala/code/docs/persistence/PersistenceDocSpec.scala @@ -4,6 +4,9 @@ package docs.persistence +import scala.concurrent.duration._ +import scala.language.postfixOps + import akka.actor.ActorSystem import akka.persistence._ @@ -113,8 +116,8 @@ trait PersistenceDocSpec { class MyDestination extends Actor { def receive = { - case p @ ConfirmablePersistent(payload, _) ⇒ - println(s"received ${payload}") + case p @ ConfirmablePersistent(payload, sequenceNr, redeliveries) ⇒ + // ... p.confirm() } } @@ -129,6 +132,12 @@ trait PersistenceDocSpec { context.actorOf(Channel.props("my-stable-channel-id")) //#channel-id-override + //#channel-custom-settings + context.actorOf(Channel.props( + ChannelSettings(redeliverInterval = 30 seconds, redeliverMax = 15)), + name = "myChannel") + //#channel-custom-settings + def receive = { case p @ Persistent(payload, _) ⇒ //#channel-example-reply @@ -241,11 +250,44 @@ trait PersistenceDocSpec { trait MyActor extends Actor { val destination: ActorRef = null //#persistent-channel-example - val channel = context.actorOf(PersistentChannel.props(), + val channel = context.actorOf(PersistentChannel.props( + PersistentChannelSettings(redeliverInterval = 30 seconds, redeliverMax = 15)), name = "myPersistentChannel") channel ! Deliver(Persistent("example"), destination) //#persistent-channel-example + + //#persistent-channel-reply + PersistentChannelSettings(replyPersistent = true) + //#persistent-channel-reply } } + + new AnyRef { + import akka.actor.ActorRef + + //#reliable-event-delivery + class MyEventsourcedProcessor(destination: ActorRef) extends EventsourcedProcessor { + val channel = context.actorOf(Channel.props("channel")) + + def handleEvent(event: String) = { + // update state + // ... + // reliably deliver events + channel ! Deliver(Persistent(event), destination) + } + + def receiveReplay: Receive = { + case event: String ⇒ handleEvent(event) + } + + def receiveCommand: Receive = { + case "cmd" ⇒ { + // ... + persist("evt")(handleEvent) + } + } + } + //#reliable-event-delivery + } } diff --git a/akka-docs/rst/scala/code/docs/persistence/PersistencePluginDocSpec.scala b/akka-docs/rst/scala/code/docs/persistence/PersistencePluginDocSpec.scala index f25d7686cd..86fba92691 100644 --- a/akka-docs/rst/scala/code/docs/persistence/PersistencePluginDocSpec.scala +++ b/akka-docs/rst/scala/code/docs/persistence/PersistencePluginDocSpec.scala @@ -32,6 +32,9 @@ object PersistencePluginDocSpec { //#snapshot-config akka.persistence.snapshot-store.local.dir = "target/snapshots" //#snapshot-config + //#native-config + akka.persistence.journal.leveldb.native = off + //#native-config """ } @@ -80,6 +83,9 @@ object SharedLeveldbPluginDocSpec { //#shared-journal-config akka.persistence.journal.plugin = "akka.persistence.journal.leveldb-shared" //#shared-journal-config + //#shared-store-native-config + akka.persistence.journal.leveldb-shared.store.native = off + //#shared-store-native-config //#shared-store-config akka.persistence.journal.leveldb-shared.store.dir = "target/shared" //#shared-store-config diff --git a/akka-docs/rst/scala/persistence.rst b/akka-docs/rst/scala/persistence.rst index d98280b995..6a7b965a30 100644 --- a/akka-docs/rst/scala/persistence.rst +++ b/akka-docs/rst/scala/persistence.rst @@ -11,7 +11,8 @@ persisted but never its current state directly (except for optional snapshots). to storage, nothing is ever mutated, which allows for very high transaction rates and efficient replication. Stateful actors are recovered by replaying stored changes to these actors from which they can rebuild internal state. This can be either the full history of changes or starting from a snapshot of internal actor state which can dramatically -reduce recovery times. +reduce recovery times. Akka persistence also provides point-to-point communication channels with at-least-once +message delivery guarantees. Storage backends for state changes and snapshots are pluggable in Akka persistence. Currently, these are written to the local filesystem. Distributed and replicated storage, with the possibility of scaling writes, will be available @@ -44,7 +45,8 @@ Architecture to that processor, so that it can recover internal state from these messages. * *Channel*: Channels are used by processors to communicate with other actors. They prevent that replayed messages - are redundantly delivered to these actors. + are redundantly delivered to these actors and provide at-least-once message delivery guarantees, also in case of + sender and receiver JVM crashes. * *Journal*: A journal stores the sequence of messages sent to a processor. An application can control which messages are stored and which are received by the processor without being journaled. The storage backend of a journal is @@ -150,9 +152,17 @@ should override ``processorId``. Later versions of Akka persistence will likely offer a possibility to migrate processor ids. +.. _channels: + Channels ======== +.. warning:: + + There are further changes planned to the channel API that couldn't make it into the current milestone. + One example is to have only a single destination per channel to allow gap detection and more advanced + flow control. + Channels are special actors that are used by processors to communicate with other actors (channel destinations). Channels prevent redundant delivery of replayed messages to destinations during processor recovery. A replayed message is retained by a channel if its previous delivery has been confirmed by a destination. @@ -160,59 +170,106 @@ message is retained by a channel if its previous delivery has been confirmed by .. includecode:: code/docs/persistence/PersistenceDocSpec.scala#channel-example A channel is ready to use once it has been created, no recovery or further activation is needed. A ``Deliver`` -request instructs a channel to send a ``Persistent`` message to a destination where the sender of the ``Deliver`` -request is forwarded to the destination. A processor may also reply to a message sender directly by using ``sender`` -as channel destination (not shown). +request instructs a channel to send a ``Persistent`` message to a destination. Sender references are preserved +by a channel, therefore, a destination can reply to the sender of a ``Deliver`` request. + +If a processor wants to reply to a ``Persistent`` message sender it should use the ``sender`` reference as channel +destination. .. includecode:: code/docs/persistence/PersistenceDocSpec.scala#channel-example-reply -Persistent messages delivered by a channel are of type ``ConfirmablePersistent``. It extends ``Persistent`` and -adds a ``confirm()`` method. Channel destinations confirm the delivery of a ``ConfirmablePersistent`` message by -calling ``confirm()``. This (asynchronously) writes a confirmation entry to the journal. Replayed messages -internally contain these confirmation entries which allows a channel to decide if a message should be retained or -not. ``ConfirmablePersistent`` messages can be used whereever ``Persistent`` messages are expected, which allows -processors to be used as channel destinations, for example. +Persistent messages delivered by a channel are of type ``ConfirmablePersistent``. ``ConfirmablePersistent`` extends +``Persistent`` by adding the methods ``confirm`` method and ``redeliveries`` (see also :ref:`redelivery`). Channel +destinations confirm the delivery of a ``ConfirmablePersistent`` message by calling ``confirm()`` an that message. +This asynchronously writes a confirmation entry to the journal. Replayed messages internally contain these confirmation +entries which allows a channel to decide if a message should be retained or not. + +A ``Processor`` can also be used as channel destination i.e. it can persist ``ConfirmablePersistent`` messages too. + +.. _redelivery: Message re-delivery ------------------- -If an application crashes after a destination called ``confirm()`` but before the confirmation entry could have -been written to the journal then the unconfirmed message will be re-delivered during next recovery of the sending -processor. It is the destination's responsibility to detect the duplicate or simply process the message again if -it's an idempotent receiver. Duplicates can be detected, for example, by tracking sequence numbers. +Channels re-deliver messages to destinations if they do not confirm their receipt within a configurable timeout. +This timeout can be specified as ``redeliverInterval`` when creating a channel, optionally together with the +maximum number of re-deliveries a channel should attempt for each unconfirmed message. -Although a channel prevents message loss in case of sender (JVM) crashes it doesn't attempt re-deliveries if a -destination is unavailable. To achieve reliable communication with a (remote) target, a channel destination may -want to use the :ref:`reliable-proxy` or add the message to a queue that is managed by a third party message -broker, for example. In latter case, the channel destination will first add the received message to the queue -and then call ``confirm()`` on the received ``ConfirmablePersistent`` message. +.. includecode:: code/docs/persistence/PersistenceDocSpec.scala#channel-custom-settings + +Message re-delivery is done out of order with regards to normal delivery i.e. redelivered messages may arrive +later than newer normally delivered messages. The number of re-delivery attempts can be obtained via the +``redeliveries`` method on ``ConfirmablePersistent`` or by pattern matching. + +A channel keeps messages in memory until their successful delivery has been confirmed by their destination(s) +or their maximum number of re-deliveries is reached. In the latter case, the application has to re-send the +correspnding ``Deliver`` request to the channel so that the channel can start a new series of delivery attempts +(starting again with a ``redeliveries`` count of ``0``). + +Re-sending ``Deliver`` requests is done automatically if the sending processor replays messages: only ``Deliver`` +requests of unconfirmed messages will be served again by the channel. A message replay can be enforced by an +application by restarting the sending processor, for example. A replay will also take place if the whole +application is restarted, either after normal termination or after a crash. + +This combination of + +* message persistence by sending processors +* message replays by sending processors +* message re-deliveries by channels and +* application-level confirmations (acknowledgements) by destinations + +enables channels to provide at-least-once message delivery guarantees. Possible duplicates can be detected by +destinations by tracking message sequence numbers. Message sequence numbers are generated per sending processor. +Depending on how a processor routes outbound messages to destinations, they may either see a contiguous message +sequence or a sequence with gaps. + +.. warning:: + + If a processor emits more than one outbound message per inbound ``Persistent`` message it **must** use a + separate channel for each outbound message to ensure that confirmations are uniquely identifiable, otherwise, + at-least-once message delivery is not guaranteed. This rule has been introduced to avoid writing additional + outbound message identifiers to the journal which would decrease the overall throughput. It is furthermore + recommended to collapse multiple outbound messages to the same destination into a single outbound message, + otherwise, if sent via multiple channels, their ordering is not defined. These restrictions are likely to be + removed in the final release. + +Whenever an application wants to have more control how sequence numbers are assigned to messages it should use +an application-specific sequence number generator and include the generated sequence numbers into the ``payload`` +of ``Persistent`` messages. Persistent channels ------------------- -Channels created with ``Channel.props`` do not persist messages. This is not necessary because these (transient) -channels shall only be used in combination with a sending processor that takes care of message persistence. +Channels created with ``Channel.props`` do not persist messages. These channels are usually used in combination +with a sending processor that takes care of persistence, hence, channel-specific persistence is not necessary in +this case. They are referred to as transient channels in the following. -However, if an application wants to use a channel standalone (without a sending processor), to prevent message -loss in case of a sender (JVM) crash, it should use a persistent channel which can be created with ``PersistentChannel.props``. -A persistent channel additionally persists messages before they are delivered. Persistence is achieved by an -internal processor that delegates delivery to a transient channel. A persistent channel, when used standalone, -can therefore provide the same message re-delivery semantics as a transient channel in combination with an -application-defined processor. +Applications may also use transient channels standalone (i.e. without a sending processor) if re-delivery attempts +to destinations are required but message loss in case of a sender JVM crash is not an issue. If applications want to +use standalone channels but message loss is not acceptable, they should use persistent channels. A persistent channel +can be created with ``PersistentChannel.props`` and configured with a ``PersistentChannelSettings`` object. - .. includecode:: code/docs/persistence/PersistenceDocSpec.scala#persistent-channel-example +.. includecode:: code/docs/persistence/PersistenceDocSpec.scala#persistent-channel-example + +A persistent channel is like a transient channel that additionally persists ``Deliver`` requests before serving it. +Hence, it can recover from sender JVM crashes and provide the same message re-delivery semantics as a transient +channel in combination with an application-defined processor. By default, a persistent channel doesn't reply whether a ``Persistent`` message, sent with ``Deliver``, has been -successfully persisted or not. This can be enabled by creating the channel with -``PersistentChannel.props(persistentReply = true)``. With this setting, either the successfully persisted message -is replied to the sender or a ``PersistenceFailure``. In case of a persistence failure, the sender should re-send -the message. +successfully persisted or not. This can be enabled by creating the channel with the ``replyPersistent`` configuration +parameter set to ``true``: + +.. includecode:: code/docs/persistence/PersistenceDocSpec.scala#persistent-channel-reply + +With this setting, either the successfully persisted message is replied to the sender or a ``PersistenceFailure``. +In case of a persistence failure, the sender should re-send the message. Using a persistent channel in combination with an application-defined processor can make sense if destinations are unavailable for a long time and an application doesn't want to buffer all messages in memory (but write them to the -journal instead). In this case, delivery can be disabled with ``DisableDelivery`` (to stop delivery and persist-only) -and re-enabled with ``EnableDelivery``. A disabled channel that receives ``EnableDelivery`` will restart itself and -re-deliver all persisted, unconfirmed messages before serving new ``Deliver`` requests. +journal only). In this case, delivery can be disabled by sending the channel a ``DisableDelivery`` message (to +stop delivery and persist-only) and re-enabled again by sending it an ``EnableDelivery`` message. A disabled channel +that receives an ``EnableDelivery`` message, processes all persisted, unconfirmed ``Deliver`` requests again before +serving new ones. Sender resolution ----------------- @@ -279,7 +336,7 @@ method or by pattern matching .. includecode:: code/docs/persistence/PersistenceDocSpec.scala#sequence-nr-pattern-matching -Persistent messages are assigned sequence numbers on a per-processor basis (or per persistent channel basis if used +Persistent messages are assigned sequence numbers on a per-processor basis (or per channel basis if used standalone). A sequence starts at ``1L`` and doesn't contain gaps unless a processor deletes a message. .. _snapshots: @@ -376,6 +433,19 @@ The example also demonstrates how to change the processor's default behavior, de another behavior, defined by ``otherCommandHandler``, and back using ``context.become()`` and ``context.unbecome()``. See also the API docs of ``persist`` for further details. +Reliable event delivery +----------------------- + +Sending events from an event handler to another actor directly doesn't guarantee delivery of these events. To +guarantee at-least-once delivery, :ref:`channels` must be used. In this case, also replayed events (received by +``receiveReplay``) must be sent to a channel, as shown in the following example: + +.. includecode:: code/docs/persistence/PersistenceDocSpec.scala#reliable-event-delivery + +In larger integration scenarios, channel destinations may be actors that submit received events to an external +message broker, for example. After having successfully submitted an event, they should call ``confirm()`` on the +received ``ConfirmablePersistent`` message. + Batch writes ============ @@ -469,6 +539,9 @@ directory. This location can be changed by configuration where the specified pat With this plugin, each actor system runs its own private LevelDB instance. + +.. _shared-leveldb-journal: + Shared LevelDB journal ---------------------- @@ -537,6 +610,21 @@ it must add to the application configuration. If not specified, a default serializer is used, which is the ``JavaSerializer`` in this example. +Testing +======= + +When running tests with LevelDB default settings in ``sbt``, make sure to set ``fork := true`` in your sbt project +otherwise, you'll see an ``UnsatisfiedLinkError``. Alternatively, you can switch to a LevelDB Java port by setting + +.. includecode:: code/docs/persistence/PersistencePluginDocSpec.scala#native-config + +or + +.. includecode:: code/docs/persistence/PersistencePluginDocSpec.scala#shared-store-native-config + +in your Akka configuration. The latter setting applies if you're using a :ref:`shared-leveldb-journal`. The LevelDB +Java port is for testing purposes only. + Miscellaneous ============= diff --git a/akka-persistence/src/main/java/akka/persistence/serialization/MessageFormats.java b/akka-persistence/src/main/java/akka/persistence/serialization/MessageFormats.java index aaf8e81112..4ce1cda1c5 100644 --- a/akka-persistence/src/main/java/akka/persistence/serialization/MessageFormats.java +++ b/akka-persistence/src/main/java/akka/persistence/serialization/MessageFormats.java @@ -736,96 +736,106 @@ public final class MessageFormats { com.google.protobuf.ByteString getProcessorIdBytes(); - // optional bool deleted = 5; + // optional bool deleted = 4; /** - * optional bool deleted = 5; + * optional bool deleted = 4; */ boolean hasDeleted(); /** - * optional bool deleted = 5; + * optional bool deleted = 4; */ boolean getDeleted(); - // optional bool resolved = 6; + // optional bool resolved = 5; /** - * optional bool resolved = 6; + * optional bool resolved = 5; */ boolean hasResolved(); /** - * optional bool resolved = 6; + * optional bool resolved = 5; */ boolean getResolved(); - // repeated string confirms = 8; + // optional int32 redeliveries = 6; /** - * repeated string confirms = 8; + * optional int32 redeliveries = 6; + */ + boolean hasRedeliveries(); + /** + * optional int32 redeliveries = 6; + */ + int getRedeliveries(); + + // repeated string confirms = 7; + /** + * repeated string confirms = 7; */ java.util.List getConfirmsList(); /** - * repeated string confirms = 8; + * repeated string confirms = 7; */ int getConfirmsCount(); /** - * repeated string confirms = 8; + * repeated string confirms = 7; */ java.lang.String getConfirms(int index); /** - * repeated string confirms = 8; + * repeated string confirms = 7; */ com.google.protobuf.ByteString getConfirmsBytes(int index); - // optional bool confirmable = 11; + // optional bool confirmable = 8; /** - * optional bool confirmable = 11; + * optional bool confirmable = 8; */ boolean hasConfirmable(); /** - * optional bool confirmable = 11; + * optional bool confirmable = 8; */ boolean getConfirmable(); - // optional .ConfirmMessage confirmMessage = 10; + // optional .ConfirmMessage confirmMessage = 9; /** - * optional .ConfirmMessage confirmMessage = 10; + * optional .ConfirmMessage confirmMessage = 9; */ boolean hasConfirmMessage(); /** - * optional .ConfirmMessage confirmMessage = 10; + * optional .ConfirmMessage confirmMessage = 9; */ akka.persistence.serialization.MessageFormats.ConfirmMessage getConfirmMessage(); /** - * optional .ConfirmMessage confirmMessage = 10; + * optional .ConfirmMessage confirmMessage = 9; */ akka.persistence.serialization.MessageFormats.ConfirmMessageOrBuilder getConfirmMessageOrBuilder(); - // optional string confirmTarget = 9; + // optional string confirmTarget = 10; /** - * optional string confirmTarget = 9; + * optional string confirmTarget = 10; */ boolean hasConfirmTarget(); /** - * optional string confirmTarget = 9; + * optional string confirmTarget = 10; */ java.lang.String getConfirmTarget(); /** - * optional string confirmTarget = 9; + * optional string confirmTarget = 10; */ com.google.protobuf.ByteString getConfirmTargetBytes(); - // optional string sender = 7; + // optional string sender = 11; /** - * optional string sender = 7; + * optional string sender = 11; */ boolean hasSender(); /** - * optional string sender = 7; + * optional string sender = 11; */ java.lang.String getSender(); /** - * optional string sender = 7; + * optional string sender = 11; */ com.google.protobuf.ByteString getSenderBytes(); @@ -904,37 +914,37 @@ public final class MessageFormats { processorId_ = input.readBytes(); break; } - case 40: { + case 32: { bitField0_ |= 0x00000008; deleted_ = input.readBool(); break; } - case 48: { + case 40: { bitField0_ |= 0x00000010; resolved_ = input.readBool(); break; } - case 58: { - bitField0_ |= 0x00000100; - sender_ = input.readBytes(); + case 48: { + bitField0_ |= 0x00000020; + redeliveries_ = input.readInt32(); break; } - case 66: { - if (!((mutable_bitField0_ & 0x00000020) == 0x00000020)) { + case 58: { + if (!((mutable_bitField0_ & 0x00000040) == 0x00000040)) { confirms_ = new com.google.protobuf.LazyStringArrayList(); - mutable_bitField0_ |= 0x00000020; + mutable_bitField0_ |= 0x00000040; } confirms_.add(input.readBytes()); break; } - case 74: { - bitField0_ |= 0x00000080; - confirmTarget_ = input.readBytes(); + case 64: { + bitField0_ |= 0x00000040; + confirmable_ = input.readBool(); break; } - case 82: { + case 74: { akka.persistence.serialization.MessageFormats.ConfirmMessage.Builder subBuilder = null; - if (((bitField0_ & 0x00000040) == 0x00000040)) { + if (((bitField0_ & 0x00000080) == 0x00000080)) { subBuilder = confirmMessage_.toBuilder(); } confirmMessage_ = input.readMessage(akka.persistence.serialization.MessageFormats.ConfirmMessage.PARSER, extensionRegistry); @@ -942,12 +952,17 @@ public final class MessageFormats { subBuilder.mergeFrom(confirmMessage_); confirmMessage_ = subBuilder.buildPartial(); } - bitField0_ |= 0x00000040; + bitField0_ |= 0x00000080; break; } - case 88: { - bitField0_ |= 0x00000020; - confirmable_ = input.readBool(); + case 82: { + bitField0_ |= 0x00000100; + confirmTarget_ = input.readBytes(); + break; + } + case 90: { + bitField0_ |= 0x00000200; + sender_ = input.readBytes(); break; } } @@ -958,7 +973,7 @@ public final class MessageFormats { throw new com.google.protobuf.InvalidProtocolBufferException( e.getMessage()).setUnfinishedMessage(this); } finally { - if (((mutable_bitField0_ & 0x00000020) == 0x00000020)) { + if (((mutable_bitField0_ & 0x00000040) == 0x00000040)) { confirms_ = new com.google.protobuf.UnmodifiableLazyStringList(confirms_); } this.unknownFields = unknownFields.build(); @@ -1074,117 +1089,133 @@ public final class MessageFormats { } } - // optional bool deleted = 5; - public static final int DELETED_FIELD_NUMBER = 5; + // optional bool deleted = 4; + public static final int DELETED_FIELD_NUMBER = 4; private boolean deleted_; /** - * optional bool deleted = 5; + * optional bool deleted = 4; */ public boolean hasDeleted() { return ((bitField0_ & 0x00000008) == 0x00000008); } /** - * optional bool deleted = 5; + * optional bool deleted = 4; */ public boolean getDeleted() { return deleted_; } - // optional bool resolved = 6; - public static final int RESOLVED_FIELD_NUMBER = 6; + // optional bool resolved = 5; + public static final int RESOLVED_FIELD_NUMBER = 5; private boolean resolved_; /** - * optional bool resolved = 6; + * optional bool resolved = 5; */ public boolean hasResolved() { return ((bitField0_ & 0x00000010) == 0x00000010); } /** - * optional bool resolved = 6; + * optional bool resolved = 5; */ public boolean getResolved() { return resolved_; } - // repeated string confirms = 8; - public static final int CONFIRMS_FIELD_NUMBER = 8; + // optional int32 redeliveries = 6; + public static final int REDELIVERIES_FIELD_NUMBER = 6; + private int redeliveries_; + /** + * optional int32 redeliveries = 6; + */ + public boolean hasRedeliveries() { + return ((bitField0_ & 0x00000020) == 0x00000020); + } + /** + * optional int32 redeliveries = 6; + */ + public int getRedeliveries() { + return redeliveries_; + } + + // repeated string confirms = 7; + public static final int CONFIRMS_FIELD_NUMBER = 7; private com.google.protobuf.LazyStringList confirms_; /** - * repeated string confirms = 8; + * repeated string confirms = 7; */ public java.util.List getConfirmsList() { return confirms_; } /** - * repeated string confirms = 8; + * repeated string confirms = 7; */ public int getConfirmsCount() { return confirms_.size(); } /** - * repeated string confirms = 8; + * repeated string confirms = 7; */ public java.lang.String getConfirms(int index) { return confirms_.get(index); } /** - * repeated string confirms = 8; + * repeated string confirms = 7; */ public com.google.protobuf.ByteString getConfirmsBytes(int index) { return confirms_.getByteString(index); } - // optional bool confirmable = 11; - public static final int CONFIRMABLE_FIELD_NUMBER = 11; + // optional bool confirmable = 8; + public static final int CONFIRMABLE_FIELD_NUMBER = 8; private boolean confirmable_; /** - * optional bool confirmable = 11; + * optional bool confirmable = 8; */ public boolean hasConfirmable() { - return ((bitField0_ & 0x00000020) == 0x00000020); + return ((bitField0_ & 0x00000040) == 0x00000040); } /** - * optional bool confirmable = 11; + * optional bool confirmable = 8; */ public boolean getConfirmable() { return confirmable_; } - // optional .ConfirmMessage confirmMessage = 10; - public static final int CONFIRMMESSAGE_FIELD_NUMBER = 10; + // optional .ConfirmMessage confirmMessage = 9; + public static final int CONFIRMMESSAGE_FIELD_NUMBER = 9; private akka.persistence.serialization.MessageFormats.ConfirmMessage confirmMessage_; /** - * optional .ConfirmMessage confirmMessage = 10; + * optional .ConfirmMessage confirmMessage = 9; */ public boolean hasConfirmMessage() { - return ((bitField0_ & 0x00000040) == 0x00000040); + return ((bitField0_ & 0x00000080) == 0x00000080); } /** - * optional .ConfirmMessage confirmMessage = 10; + * optional .ConfirmMessage confirmMessage = 9; */ public akka.persistence.serialization.MessageFormats.ConfirmMessage getConfirmMessage() { return confirmMessage_; } /** - * optional .ConfirmMessage confirmMessage = 10; + * optional .ConfirmMessage confirmMessage = 9; */ public akka.persistence.serialization.MessageFormats.ConfirmMessageOrBuilder getConfirmMessageOrBuilder() { return confirmMessage_; } - // optional string confirmTarget = 9; - public static final int CONFIRMTARGET_FIELD_NUMBER = 9; + // optional string confirmTarget = 10; + public static final int CONFIRMTARGET_FIELD_NUMBER = 10; private java.lang.Object confirmTarget_; /** - * optional string confirmTarget = 9; + * optional string confirmTarget = 10; */ public boolean hasConfirmTarget() { - return ((bitField0_ & 0x00000080) == 0x00000080); + return ((bitField0_ & 0x00000100) == 0x00000100); } /** - * optional string confirmTarget = 9; + * optional string confirmTarget = 10; */ public java.lang.String getConfirmTarget() { java.lang.Object ref = confirmTarget_; @@ -1201,7 +1232,7 @@ public final class MessageFormats { } } /** - * optional string confirmTarget = 9; + * optional string confirmTarget = 10; */ public com.google.protobuf.ByteString getConfirmTargetBytes() { @@ -1217,17 +1248,17 @@ public final class MessageFormats { } } - // optional string sender = 7; - public static final int SENDER_FIELD_NUMBER = 7; + // optional string sender = 11; + public static final int SENDER_FIELD_NUMBER = 11; private java.lang.Object sender_; /** - * optional string sender = 7; + * optional string sender = 11; */ public boolean hasSender() { - return ((bitField0_ & 0x00000100) == 0x00000100); + return ((bitField0_ & 0x00000200) == 0x00000200); } /** - * optional string sender = 7; + * optional string sender = 11; */ public java.lang.String getSender() { java.lang.Object ref = sender_; @@ -1244,7 +1275,7 @@ public final class MessageFormats { } } /** - * optional string sender = 7; + * optional string sender = 11; */ public com.google.protobuf.ByteString getSenderBytes() { @@ -1266,6 +1297,7 @@ public final class MessageFormats { processorId_ = ""; deleted_ = false; resolved_ = false; + redeliveries_ = 0; confirms_ = com.google.protobuf.LazyStringArrayList.EMPTY; confirmable_ = false; confirmMessage_ = akka.persistence.serialization.MessageFormats.ConfirmMessage.getDefaultInstance(); @@ -1300,25 +1332,28 @@ public final class MessageFormats { output.writeBytes(3, getProcessorIdBytes()); } if (((bitField0_ & 0x00000008) == 0x00000008)) { - output.writeBool(5, deleted_); + output.writeBool(4, deleted_); } if (((bitField0_ & 0x00000010) == 0x00000010)) { - output.writeBool(6, resolved_); - } - if (((bitField0_ & 0x00000100) == 0x00000100)) { - output.writeBytes(7, getSenderBytes()); - } - for (int i = 0; i < confirms_.size(); i++) { - output.writeBytes(8, confirms_.getByteString(i)); - } - if (((bitField0_ & 0x00000080) == 0x00000080)) { - output.writeBytes(9, getConfirmTargetBytes()); - } - if (((bitField0_ & 0x00000040) == 0x00000040)) { - output.writeMessage(10, confirmMessage_); + output.writeBool(5, resolved_); } if (((bitField0_ & 0x00000020) == 0x00000020)) { - output.writeBool(11, confirmable_); + output.writeInt32(6, redeliveries_); + } + for (int i = 0; i < confirms_.size(); i++) { + output.writeBytes(7, confirms_.getByteString(i)); + } + if (((bitField0_ & 0x00000040) == 0x00000040)) { + output.writeBool(8, confirmable_); + } + if (((bitField0_ & 0x00000080) == 0x00000080)) { + output.writeMessage(9, confirmMessage_); + } + if (((bitField0_ & 0x00000100) == 0x00000100)) { + output.writeBytes(10, getConfirmTargetBytes()); + } + if (((bitField0_ & 0x00000200) == 0x00000200)) { + output.writeBytes(11, getSenderBytes()); } getUnknownFields().writeTo(output); } @@ -1343,15 +1378,15 @@ public final class MessageFormats { } if (((bitField0_ & 0x00000008) == 0x00000008)) { size += com.google.protobuf.CodedOutputStream - .computeBoolSize(5, deleted_); + .computeBoolSize(4, deleted_); } if (((bitField0_ & 0x00000010) == 0x00000010)) { size += com.google.protobuf.CodedOutputStream - .computeBoolSize(6, resolved_); + .computeBoolSize(5, resolved_); } - if (((bitField0_ & 0x00000100) == 0x00000100)) { + if (((bitField0_ & 0x00000020) == 0x00000020)) { size += com.google.protobuf.CodedOutputStream - .computeBytesSize(7, getSenderBytes()); + .computeInt32Size(6, redeliveries_); } { int dataSize = 0; @@ -1362,17 +1397,21 @@ public final class MessageFormats { size += dataSize; size += 1 * getConfirmsList().size(); } - if (((bitField0_ & 0x00000080) == 0x00000080)) { - size += com.google.protobuf.CodedOutputStream - .computeBytesSize(9, getConfirmTargetBytes()); - } if (((bitField0_ & 0x00000040) == 0x00000040)) { size += com.google.protobuf.CodedOutputStream - .computeMessageSize(10, confirmMessage_); + .computeBoolSize(8, confirmable_); } - if (((bitField0_ & 0x00000020) == 0x00000020)) { + if (((bitField0_ & 0x00000080) == 0x00000080)) { size += com.google.protobuf.CodedOutputStream - .computeBoolSize(11, confirmable_); + .computeMessageSize(9, confirmMessage_); + } + if (((bitField0_ & 0x00000100) == 0x00000100)) { + size += com.google.protobuf.CodedOutputStream + .computeBytesSize(10, getConfirmTargetBytes()); + } + if (((bitField0_ & 0x00000200) == 0x00000200)) { + size += com.google.protobuf.CodedOutputStream + .computeBytesSize(11, getSenderBytes()); } size += getUnknownFields().getSerializedSize(); memoizedSerializedSize = size; @@ -1506,20 +1545,22 @@ public final class MessageFormats { bitField0_ = (bitField0_ & ~0x00000008); resolved_ = false; bitField0_ = (bitField0_ & ~0x00000010); - confirms_ = com.google.protobuf.LazyStringArrayList.EMPTY; + redeliveries_ = 0; bitField0_ = (bitField0_ & ~0x00000020); - confirmable_ = false; + confirms_ = com.google.protobuf.LazyStringArrayList.EMPTY; bitField0_ = (bitField0_ & ~0x00000040); + confirmable_ = false; + bitField0_ = (bitField0_ & ~0x00000080); if (confirmMessageBuilder_ == null) { confirmMessage_ = akka.persistence.serialization.MessageFormats.ConfirmMessage.getDefaultInstance(); } else { confirmMessageBuilder_.clear(); } - bitField0_ = (bitField0_ & ~0x00000080); - confirmTarget_ = ""; bitField0_ = (bitField0_ & ~0x00000100); - sender_ = ""; + confirmTarget_ = ""; bitField0_ = (bitField0_ & ~0x00000200); + sender_ = ""; + bitField0_ = (bitField0_ & ~0x00000400); return this; } @@ -1572,31 +1613,35 @@ public final class MessageFormats { to_bitField0_ |= 0x00000010; } result.resolved_ = resolved_; - if (((bitField0_ & 0x00000020) == 0x00000020)) { - confirms_ = new com.google.protobuf.UnmodifiableLazyStringList( - confirms_); - bitField0_ = (bitField0_ & ~0x00000020); - } - result.confirms_ = confirms_; - if (((from_bitField0_ & 0x00000040) == 0x00000040)) { + if (((from_bitField0_ & 0x00000020) == 0x00000020)) { to_bitField0_ |= 0x00000020; } - result.confirmable_ = confirmable_; + result.redeliveries_ = redeliveries_; + if (((bitField0_ & 0x00000040) == 0x00000040)) { + confirms_ = new com.google.protobuf.UnmodifiableLazyStringList( + confirms_); + bitField0_ = (bitField0_ & ~0x00000040); + } + result.confirms_ = confirms_; if (((from_bitField0_ & 0x00000080) == 0x00000080)) { to_bitField0_ |= 0x00000040; } + result.confirmable_ = confirmable_; + if (((from_bitField0_ & 0x00000100) == 0x00000100)) { + to_bitField0_ |= 0x00000080; + } if (confirmMessageBuilder_ == null) { result.confirmMessage_ = confirmMessage_; } else { result.confirmMessage_ = confirmMessageBuilder_.build(); } - if (((from_bitField0_ & 0x00000100) == 0x00000100)) { - to_bitField0_ |= 0x00000080; - } - result.confirmTarget_ = confirmTarget_; if (((from_bitField0_ & 0x00000200) == 0x00000200)) { to_bitField0_ |= 0x00000100; } + result.confirmTarget_ = confirmTarget_; + if (((from_bitField0_ & 0x00000400) == 0x00000400)) { + to_bitField0_ |= 0x00000200; + } result.sender_ = sender_; result.bitField0_ = to_bitField0_; onBuilt(); @@ -1631,10 +1676,13 @@ public final class MessageFormats { if (other.hasResolved()) { setResolved(other.getResolved()); } + if (other.hasRedeliveries()) { + setRedeliveries(other.getRedeliveries()); + } if (!other.confirms_.isEmpty()) { if (confirms_.isEmpty()) { confirms_ = other.confirms_; - bitField0_ = (bitField0_ & ~0x00000020); + bitField0_ = (bitField0_ & ~0x00000040); } else { ensureConfirmsIsMutable(); confirms_.addAll(other.confirms_); @@ -1648,12 +1696,12 @@ public final class MessageFormats { mergeConfirmMessage(other.getConfirmMessage()); } if (other.hasConfirmTarget()) { - bitField0_ |= 0x00000100; + bitField0_ |= 0x00000200; confirmTarget_ = other.confirmTarget_; onChanged(); } if (other.hasSender()) { - bitField0_ |= 0x00000200; + bitField0_ |= 0x00000400; sender_ = other.sender_; onChanged(); } @@ -1914,22 +1962,22 @@ public final class MessageFormats { return this; } - // optional bool deleted = 5; + // optional bool deleted = 4; private boolean deleted_ ; /** - * optional bool deleted = 5; + * optional bool deleted = 4; */ public boolean hasDeleted() { return ((bitField0_ & 0x00000008) == 0x00000008); } /** - * optional bool deleted = 5; + * optional bool deleted = 4; */ public boolean getDeleted() { return deleted_; } /** - * optional bool deleted = 5; + * optional bool deleted = 4; */ public Builder setDeleted(boolean value) { bitField0_ |= 0x00000008; @@ -1938,7 +1986,7 @@ public final class MessageFormats { return this; } /** - * optional bool deleted = 5; + * optional bool deleted = 4; */ public Builder clearDeleted() { bitField0_ = (bitField0_ & ~0x00000008); @@ -1947,22 +1995,22 @@ public final class MessageFormats { return this; } - // optional bool resolved = 6; + // optional bool resolved = 5; private boolean resolved_ ; /** - * optional bool resolved = 6; + * optional bool resolved = 5; */ public boolean hasResolved() { return ((bitField0_ & 0x00000010) == 0x00000010); } /** - * optional bool resolved = 6; + * optional bool resolved = 5; */ public boolean getResolved() { return resolved_; } /** - * optional bool resolved = 6; + * optional bool resolved = 5; */ public Builder setResolved(boolean value) { bitField0_ |= 0x00000010; @@ -1971,7 +2019,7 @@ public final class MessageFormats { return this; } /** - * optional bool resolved = 6; + * optional bool resolved = 5; */ public Builder clearResolved() { bitField0_ = (bitField0_ & ~0x00000010); @@ -1980,42 +2028,75 @@ public final class MessageFormats { return this; } - // repeated string confirms = 8; + // optional int32 redeliveries = 6; + private int redeliveries_ ; + /** + * optional int32 redeliveries = 6; + */ + public boolean hasRedeliveries() { + return ((bitField0_ & 0x00000020) == 0x00000020); + } + /** + * optional int32 redeliveries = 6; + */ + public int getRedeliveries() { + return redeliveries_; + } + /** + * optional int32 redeliveries = 6; + */ + public Builder setRedeliveries(int value) { + bitField0_ |= 0x00000020; + redeliveries_ = value; + onChanged(); + return this; + } + /** + * optional int32 redeliveries = 6; + */ + public Builder clearRedeliveries() { + bitField0_ = (bitField0_ & ~0x00000020); + redeliveries_ = 0; + onChanged(); + return this; + } + + // repeated string confirms = 7; private com.google.protobuf.LazyStringList confirms_ = com.google.protobuf.LazyStringArrayList.EMPTY; private void ensureConfirmsIsMutable() { - if (!((bitField0_ & 0x00000020) == 0x00000020)) { + if (!((bitField0_ & 0x00000040) == 0x00000040)) { confirms_ = new com.google.protobuf.LazyStringArrayList(confirms_); - bitField0_ |= 0x00000020; + bitField0_ |= 0x00000040; } } /** - * repeated string confirms = 8; + * repeated string confirms = 7; */ public java.util.List getConfirmsList() { return java.util.Collections.unmodifiableList(confirms_); } /** - * repeated string confirms = 8; + * repeated string confirms = 7; */ public int getConfirmsCount() { return confirms_.size(); } /** - * repeated string confirms = 8; + * repeated string confirms = 7; */ public java.lang.String getConfirms(int index) { return confirms_.get(index); } /** - * repeated string confirms = 8; + * repeated string confirms = 7; */ public com.google.protobuf.ByteString getConfirmsBytes(int index) { return confirms_.getByteString(index); } /** - * repeated string confirms = 8; + * repeated string confirms = 7; */ public Builder setConfirms( int index, java.lang.String value) { @@ -2028,7 +2109,7 @@ public final class MessageFormats { return this; } /** - * repeated string confirms = 8; + * repeated string confirms = 7; */ public Builder addConfirms( java.lang.String value) { @@ -2041,7 +2122,7 @@ public final class MessageFormats { return this; } /** - * repeated string confirms = 8; + * repeated string confirms = 7; */ public Builder addAllConfirms( java.lang.Iterable values) { @@ -2051,16 +2132,16 @@ public final class MessageFormats { return this; } /** - * repeated string confirms = 8; + * repeated string confirms = 7; */ public Builder clearConfirms() { confirms_ = com.google.protobuf.LazyStringArrayList.EMPTY; - bitField0_ = (bitField0_ & ~0x00000020); + bitField0_ = (bitField0_ & ~0x00000040); onChanged(); return this; } /** - * repeated string confirms = 8; + * repeated string confirms = 7; */ public Builder addConfirmsBytes( com.google.protobuf.ByteString value) { @@ -2073,51 +2154,51 @@ public final class MessageFormats { return this; } - // optional bool confirmable = 11; + // optional bool confirmable = 8; private boolean confirmable_ ; /** - * optional bool confirmable = 11; + * optional bool confirmable = 8; */ public boolean hasConfirmable() { - return ((bitField0_ & 0x00000040) == 0x00000040); + return ((bitField0_ & 0x00000080) == 0x00000080); } /** - * optional bool confirmable = 11; + * optional bool confirmable = 8; */ public boolean getConfirmable() { return confirmable_; } /** - * optional bool confirmable = 11; + * optional bool confirmable = 8; */ public Builder setConfirmable(boolean value) { - bitField0_ |= 0x00000040; + bitField0_ |= 0x00000080; confirmable_ = value; onChanged(); return this; } /** - * optional bool confirmable = 11; + * optional bool confirmable = 8; */ public Builder clearConfirmable() { - bitField0_ = (bitField0_ & ~0x00000040); + bitField0_ = (bitField0_ & ~0x00000080); confirmable_ = false; onChanged(); return this; } - // optional .ConfirmMessage confirmMessage = 10; + // optional .ConfirmMessage confirmMessage = 9; private akka.persistence.serialization.MessageFormats.ConfirmMessage confirmMessage_ = akka.persistence.serialization.MessageFormats.ConfirmMessage.getDefaultInstance(); private com.google.protobuf.SingleFieldBuilder< akka.persistence.serialization.MessageFormats.ConfirmMessage, akka.persistence.serialization.MessageFormats.ConfirmMessage.Builder, akka.persistence.serialization.MessageFormats.ConfirmMessageOrBuilder> confirmMessageBuilder_; /** - * optional .ConfirmMessage confirmMessage = 10; + * optional .ConfirmMessage confirmMessage = 9; */ public boolean hasConfirmMessage() { - return ((bitField0_ & 0x00000080) == 0x00000080); + return ((bitField0_ & 0x00000100) == 0x00000100); } /** - * optional .ConfirmMessage confirmMessage = 10; + * optional .ConfirmMessage confirmMessage = 9; */ public akka.persistence.serialization.MessageFormats.ConfirmMessage getConfirmMessage() { if (confirmMessageBuilder_ == null) { @@ -2127,7 +2208,7 @@ public final class MessageFormats { } } /** - * optional .ConfirmMessage confirmMessage = 10; + * optional .ConfirmMessage confirmMessage = 9; */ public Builder setConfirmMessage(akka.persistence.serialization.MessageFormats.ConfirmMessage value) { if (confirmMessageBuilder_ == null) { @@ -2139,11 +2220,11 @@ public final class MessageFormats { } else { confirmMessageBuilder_.setMessage(value); } - bitField0_ |= 0x00000080; + bitField0_ |= 0x00000100; return this; } /** - * optional .ConfirmMessage confirmMessage = 10; + * optional .ConfirmMessage confirmMessage = 9; */ public Builder setConfirmMessage( akka.persistence.serialization.MessageFormats.ConfirmMessage.Builder builderForValue) { @@ -2153,15 +2234,15 @@ public final class MessageFormats { } else { confirmMessageBuilder_.setMessage(builderForValue.build()); } - bitField0_ |= 0x00000080; + bitField0_ |= 0x00000100; return this; } /** - * optional .ConfirmMessage confirmMessage = 10; + * optional .ConfirmMessage confirmMessage = 9; */ public Builder mergeConfirmMessage(akka.persistence.serialization.MessageFormats.ConfirmMessage value) { if (confirmMessageBuilder_ == null) { - if (((bitField0_ & 0x00000080) == 0x00000080) && + if (((bitField0_ & 0x00000100) == 0x00000100) && confirmMessage_ != akka.persistence.serialization.MessageFormats.ConfirmMessage.getDefaultInstance()) { confirmMessage_ = akka.persistence.serialization.MessageFormats.ConfirmMessage.newBuilder(confirmMessage_).mergeFrom(value).buildPartial(); @@ -2172,11 +2253,11 @@ public final class MessageFormats { } else { confirmMessageBuilder_.mergeFrom(value); } - bitField0_ |= 0x00000080; + bitField0_ |= 0x00000100; return this; } /** - * optional .ConfirmMessage confirmMessage = 10; + * optional .ConfirmMessage confirmMessage = 9; */ public Builder clearConfirmMessage() { if (confirmMessageBuilder_ == null) { @@ -2185,19 +2266,19 @@ public final class MessageFormats { } else { confirmMessageBuilder_.clear(); } - bitField0_ = (bitField0_ & ~0x00000080); + bitField0_ = (bitField0_ & ~0x00000100); return this; } /** - * optional .ConfirmMessage confirmMessage = 10; + * optional .ConfirmMessage confirmMessage = 9; */ public akka.persistence.serialization.MessageFormats.ConfirmMessage.Builder getConfirmMessageBuilder() { - bitField0_ |= 0x00000080; + bitField0_ |= 0x00000100; onChanged(); return getConfirmMessageFieldBuilder().getBuilder(); } /** - * optional .ConfirmMessage confirmMessage = 10; + * optional .ConfirmMessage confirmMessage = 9; */ public akka.persistence.serialization.MessageFormats.ConfirmMessageOrBuilder getConfirmMessageOrBuilder() { if (confirmMessageBuilder_ != null) { @@ -2207,7 +2288,7 @@ public final class MessageFormats { } } /** - * optional .ConfirmMessage confirmMessage = 10; + * optional .ConfirmMessage confirmMessage = 9; */ private com.google.protobuf.SingleFieldBuilder< akka.persistence.serialization.MessageFormats.ConfirmMessage, akka.persistence.serialization.MessageFormats.ConfirmMessage.Builder, akka.persistence.serialization.MessageFormats.ConfirmMessageOrBuilder> @@ -2223,16 +2304,16 @@ public final class MessageFormats { return confirmMessageBuilder_; } - // optional string confirmTarget = 9; + // optional string confirmTarget = 10; private java.lang.Object confirmTarget_ = ""; /** - * optional string confirmTarget = 9; + * optional string confirmTarget = 10; */ public boolean hasConfirmTarget() { - return ((bitField0_ & 0x00000100) == 0x00000100); + return ((bitField0_ & 0x00000200) == 0x00000200); } /** - * optional string confirmTarget = 9; + * optional string confirmTarget = 10; */ public java.lang.String getConfirmTarget() { java.lang.Object ref = confirmTarget_; @@ -2246,7 +2327,7 @@ public final class MessageFormats { } } /** - * optional string confirmTarget = 9; + * optional string confirmTarget = 10; */ public com.google.protobuf.ByteString getConfirmTargetBytes() { @@ -2262,51 +2343,51 @@ public final class MessageFormats { } } /** - * optional string confirmTarget = 9; + * optional string confirmTarget = 10; */ public Builder setConfirmTarget( java.lang.String value) { if (value == null) { throw new NullPointerException(); } - bitField0_ |= 0x00000100; + bitField0_ |= 0x00000200; confirmTarget_ = value; onChanged(); return this; } /** - * optional string confirmTarget = 9; + * optional string confirmTarget = 10; */ public Builder clearConfirmTarget() { - bitField0_ = (bitField0_ & ~0x00000100); + bitField0_ = (bitField0_ & ~0x00000200); confirmTarget_ = getDefaultInstance().getConfirmTarget(); onChanged(); return this; } /** - * optional string confirmTarget = 9; + * optional string confirmTarget = 10; */ public Builder setConfirmTargetBytes( com.google.protobuf.ByteString value) { if (value == null) { throw new NullPointerException(); } - bitField0_ |= 0x00000100; + bitField0_ |= 0x00000200; confirmTarget_ = value; onChanged(); return this; } - // optional string sender = 7; + // optional string sender = 11; private java.lang.Object sender_ = ""; /** - * optional string sender = 7; + * optional string sender = 11; */ public boolean hasSender() { - return ((bitField0_ & 0x00000200) == 0x00000200); + return ((bitField0_ & 0x00000400) == 0x00000400); } /** - * optional string sender = 7; + * optional string sender = 11; */ public java.lang.String getSender() { java.lang.Object ref = sender_; @@ -2320,7 +2401,7 @@ public final class MessageFormats { } } /** - * optional string sender = 7; + * optional string sender = 11; */ public com.google.protobuf.ByteString getSenderBytes() { @@ -2336,36 +2417,36 @@ public final class MessageFormats { } } /** - * optional string sender = 7; + * optional string sender = 11; */ public Builder setSender( java.lang.String value) { if (value == null) { throw new NullPointerException(); } - bitField0_ |= 0x00000200; + bitField0_ |= 0x00000400; sender_ = value; onChanged(); return this; } /** - * optional string sender = 7; + * optional string sender = 11; */ public Builder clearSender() { - bitField0_ = (bitField0_ & ~0x00000200); + bitField0_ = (bitField0_ & ~0x00000400); sender_ = getDefaultInstance().getSender(); onChanged(); return this; } /** - * optional string sender = 7; + * optional string sender = 11; */ public Builder setSenderBytes( com.google.protobuf.ByteString value) { if (value == null) { throw new NullPointerException(); } - bitField0_ |= 0x00000200; + bitField0_ |= 0x00000400; sender_ = value; onChanged(); return this; @@ -2980,15 +3061,15 @@ public final class MessageFormats { com.google.protobuf.ByteString getProcessorIdBytes(); - // optional int64 sequenceNr = 2; + // optional int64 messageSequenceNr = 2; /** - * optional int64 sequenceNr = 2; + * optional int64 messageSequenceNr = 2; */ - boolean hasSequenceNr(); + boolean hasMessageSequenceNr(); /** - * optional int64 sequenceNr = 2; + * optional int64 messageSequenceNr = 2; */ - long getSequenceNr(); + long getMessageSequenceNr(); // optional string channelId = 3; /** @@ -3004,6 +3085,31 @@ public final class MessageFormats { */ com.google.protobuf.ByteString getChannelIdBytes(); + + // optional int64 wrapperSequenceNr = 4; + /** + * optional int64 wrapperSequenceNr = 4; + */ + boolean hasWrapperSequenceNr(); + /** + * optional int64 wrapperSequenceNr = 4; + */ + long getWrapperSequenceNr(); + + // optional string channelEndpoint = 5; + /** + * optional string channelEndpoint = 5; + */ + boolean hasChannelEndpoint(); + /** + * optional string channelEndpoint = 5; + */ + java.lang.String getChannelEndpoint(); + /** + * optional string channelEndpoint = 5; + */ + com.google.protobuf.ByteString + getChannelEndpointBytes(); } /** * Protobuf type {@code ConfirmMessage} @@ -3063,7 +3169,7 @@ public final class MessageFormats { } case 16: { bitField0_ |= 0x00000002; - sequenceNr_ = input.readInt64(); + messageSequenceNr_ = input.readInt64(); break; } case 26: { @@ -3071,6 +3177,16 @@ public final class MessageFormats { channelId_ = input.readBytes(); break; } + case 32: { + bitField0_ |= 0x00000008; + wrapperSequenceNr_ = input.readInt64(); + break; + } + case 42: { + bitField0_ |= 0x00000010; + channelEndpoint_ = input.readBytes(); + break; + } } } } catch (com.google.protobuf.InvalidProtocolBufferException e) { @@ -3154,20 +3270,20 @@ public final class MessageFormats { } } - // optional int64 sequenceNr = 2; - public static final int SEQUENCENR_FIELD_NUMBER = 2; - private long sequenceNr_; + // optional int64 messageSequenceNr = 2; + public static final int MESSAGESEQUENCENR_FIELD_NUMBER = 2; + private long messageSequenceNr_; /** - * optional int64 sequenceNr = 2; + * optional int64 messageSequenceNr = 2; */ - public boolean hasSequenceNr() { + public boolean hasMessageSequenceNr() { return ((bitField0_ & 0x00000002) == 0x00000002); } /** - * optional int64 sequenceNr = 2; + * optional int64 messageSequenceNr = 2; */ - public long getSequenceNr() { - return sequenceNr_; + public long getMessageSequenceNr() { + return messageSequenceNr_; } // optional string channelId = 3; @@ -3213,10 +3329,71 @@ public final class MessageFormats { } } + // optional int64 wrapperSequenceNr = 4; + public static final int WRAPPERSEQUENCENR_FIELD_NUMBER = 4; + private long wrapperSequenceNr_; + /** + * optional int64 wrapperSequenceNr = 4; + */ + public boolean hasWrapperSequenceNr() { + return ((bitField0_ & 0x00000008) == 0x00000008); + } + /** + * optional int64 wrapperSequenceNr = 4; + */ + public long getWrapperSequenceNr() { + return wrapperSequenceNr_; + } + + // optional string channelEndpoint = 5; + public static final int CHANNELENDPOINT_FIELD_NUMBER = 5; + private java.lang.Object channelEndpoint_; + /** + * optional string channelEndpoint = 5; + */ + public boolean hasChannelEndpoint() { + return ((bitField0_ & 0x00000010) == 0x00000010); + } + /** + * optional string channelEndpoint = 5; + */ + public java.lang.String getChannelEndpoint() { + java.lang.Object ref = channelEndpoint_; + if (ref instanceof java.lang.String) { + return (java.lang.String) ref; + } else { + com.google.protobuf.ByteString bs = + (com.google.protobuf.ByteString) ref; + java.lang.String s = bs.toStringUtf8(); + if (bs.isValidUtf8()) { + channelEndpoint_ = s; + } + return s; + } + } + /** + * optional string channelEndpoint = 5; + */ + public com.google.protobuf.ByteString + getChannelEndpointBytes() { + java.lang.Object ref = channelEndpoint_; + if (ref instanceof java.lang.String) { + com.google.protobuf.ByteString b = + com.google.protobuf.ByteString.copyFromUtf8( + (java.lang.String) ref); + channelEndpoint_ = b; + return b; + } else { + return (com.google.protobuf.ByteString) ref; + } + } + private void initFields() { processorId_ = ""; - sequenceNr_ = 0L; + messageSequenceNr_ = 0L; channelId_ = ""; + wrapperSequenceNr_ = 0L; + channelEndpoint_ = ""; } private byte memoizedIsInitialized = -1; public final boolean isInitialized() { @@ -3234,11 +3411,17 @@ public final class MessageFormats { output.writeBytes(1, getProcessorIdBytes()); } if (((bitField0_ & 0x00000002) == 0x00000002)) { - output.writeInt64(2, sequenceNr_); + output.writeInt64(2, messageSequenceNr_); } if (((bitField0_ & 0x00000004) == 0x00000004)) { output.writeBytes(3, getChannelIdBytes()); } + if (((bitField0_ & 0x00000008) == 0x00000008)) { + output.writeInt64(4, wrapperSequenceNr_); + } + if (((bitField0_ & 0x00000010) == 0x00000010)) { + output.writeBytes(5, getChannelEndpointBytes()); + } getUnknownFields().writeTo(output); } @@ -3254,12 +3437,20 @@ public final class MessageFormats { } if (((bitField0_ & 0x00000002) == 0x00000002)) { size += com.google.protobuf.CodedOutputStream - .computeInt64Size(2, sequenceNr_); + .computeInt64Size(2, messageSequenceNr_); } if (((bitField0_ & 0x00000004) == 0x00000004)) { size += com.google.protobuf.CodedOutputStream .computeBytesSize(3, getChannelIdBytes()); } + if (((bitField0_ & 0x00000008) == 0x00000008)) { + size += com.google.protobuf.CodedOutputStream + .computeInt64Size(4, wrapperSequenceNr_); + } + if (((bitField0_ & 0x00000010) == 0x00000010)) { + size += com.google.protobuf.CodedOutputStream + .computeBytesSize(5, getChannelEndpointBytes()); + } size += getUnknownFields().getSerializedSize(); memoizedSerializedSize = size; return size; @@ -3378,10 +3569,14 @@ public final class MessageFormats { super.clear(); processorId_ = ""; bitField0_ = (bitField0_ & ~0x00000001); - sequenceNr_ = 0L; + messageSequenceNr_ = 0L; bitField0_ = (bitField0_ & ~0x00000002); channelId_ = ""; bitField0_ = (bitField0_ & ~0x00000004); + wrapperSequenceNr_ = 0L; + bitField0_ = (bitField0_ & ~0x00000008); + channelEndpoint_ = ""; + bitField0_ = (bitField0_ & ~0x00000010); return this; } @@ -3417,11 +3612,19 @@ public final class MessageFormats { if (((from_bitField0_ & 0x00000002) == 0x00000002)) { to_bitField0_ |= 0x00000002; } - result.sequenceNr_ = sequenceNr_; + result.messageSequenceNr_ = messageSequenceNr_; if (((from_bitField0_ & 0x00000004) == 0x00000004)) { to_bitField0_ |= 0x00000004; } result.channelId_ = channelId_; + if (((from_bitField0_ & 0x00000008) == 0x00000008)) { + to_bitField0_ |= 0x00000008; + } + result.wrapperSequenceNr_ = wrapperSequenceNr_; + if (((from_bitField0_ & 0x00000010) == 0x00000010)) { + to_bitField0_ |= 0x00000010; + } + result.channelEndpoint_ = channelEndpoint_; result.bitField0_ = to_bitField0_; onBuilt(); return result; @@ -3443,14 +3646,22 @@ public final class MessageFormats { processorId_ = other.processorId_; onChanged(); } - if (other.hasSequenceNr()) { - setSequenceNr(other.getSequenceNr()); + if (other.hasMessageSequenceNr()) { + setMessageSequenceNr(other.getMessageSequenceNr()); } if (other.hasChannelId()) { bitField0_ |= 0x00000004; channelId_ = other.channelId_; onChanged(); } + if (other.hasWrapperSequenceNr()) { + setWrapperSequenceNr(other.getWrapperSequenceNr()); + } + if (other.hasChannelEndpoint()) { + bitField0_ |= 0x00000010; + channelEndpoint_ = other.channelEndpoint_; + onChanged(); + } this.mergeUnknownFields(other.getUnknownFields()); return this; } @@ -3552,35 +3763,35 @@ public final class MessageFormats { return this; } - // optional int64 sequenceNr = 2; - private long sequenceNr_ ; + // optional int64 messageSequenceNr = 2; + private long messageSequenceNr_ ; /** - * optional int64 sequenceNr = 2; + * optional int64 messageSequenceNr = 2; */ - public boolean hasSequenceNr() { + public boolean hasMessageSequenceNr() { return ((bitField0_ & 0x00000002) == 0x00000002); } /** - * optional int64 sequenceNr = 2; + * optional int64 messageSequenceNr = 2; */ - public long getSequenceNr() { - return sequenceNr_; + public long getMessageSequenceNr() { + return messageSequenceNr_; } /** - * optional int64 sequenceNr = 2; + * optional int64 messageSequenceNr = 2; */ - public Builder setSequenceNr(long value) { + public Builder setMessageSequenceNr(long value) { bitField0_ |= 0x00000002; - sequenceNr_ = value; + messageSequenceNr_ = value; onChanged(); return this; } /** - * optional int64 sequenceNr = 2; + * optional int64 messageSequenceNr = 2; */ - public Builder clearSequenceNr() { + public Builder clearMessageSequenceNr() { bitField0_ = (bitField0_ & ~0x00000002); - sequenceNr_ = 0L; + messageSequenceNr_ = 0L; onChanged(); return this; } @@ -3659,6 +3870,113 @@ public final class MessageFormats { return this; } + // optional int64 wrapperSequenceNr = 4; + private long wrapperSequenceNr_ ; + /** + * optional int64 wrapperSequenceNr = 4; + */ + public boolean hasWrapperSequenceNr() { + return ((bitField0_ & 0x00000008) == 0x00000008); + } + /** + * optional int64 wrapperSequenceNr = 4; + */ + public long getWrapperSequenceNr() { + return wrapperSequenceNr_; + } + /** + * optional int64 wrapperSequenceNr = 4; + */ + public Builder setWrapperSequenceNr(long value) { + bitField0_ |= 0x00000008; + wrapperSequenceNr_ = value; + onChanged(); + return this; + } + /** + * optional int64 wrapperSequenceNr = 4; + */ + public Builder clearWrapperSequenceNr() { + bitField0_ = (bitField0_ & ~0x00000008); + wrapperSequenceNr_ = 0L; + onChanged(); + return this; + } + + // optional string channelEndpoint = 5; + private java.lang.Object channelEndpoint_ = ""; + /** + * optional string channelEndpoint = 5; + */ + public boolean hasChannelEndpoint() { + return ((bitField0_ & 0x00000010) == 0x00000010); + } + /** + * optional string channelEndpoint = 5; + */ + public java.lang.String getChannelEndpoint() { + java.lang.Object ref = channelEndpoint_; + if (!(ref instanceof java.lang.String)) { + java.lang.String s = ((com.google.protobuf.ByteString) ref) + .toStringUtf8(); + channelEndpoint_ = s; + return s; + } else { + return (java.lang.String) ref; + } + } + /** + * optional string channelEndpoint = 5; + */ + public com.google.protobuf.ByteString + getChannelEndpointBytes() { + java.lang.Object ref = channelEndpoint_; + if (ref instanceof String) { + com.google.protobuf.ByteString b = + com.google.protobuf.ByteString.copyFromUtf8( + (java.lang.String) ref); + channelEndpoint_ = b; + return b; + } else { + return (com.google.protobuf.ByteString) ref; + } + } + /** + * optional string channelEndpoint = 5; + */ + public Builder setChannelEndpoint( + java.lang.String value) { + if (value == null) { + throw new NullPointerException(); + } + bitField0_ |= 0x00000010; + channelEndpoint_ = value; + onChanged(); + return this; + } + /** + * optional string channelEndpoint = 5; + */ + public Builder clearChannelEndpoint() { + bitField0_ = (bitField0_ & ~0x00000010); + channelEndpoint_ = getDefaultInstance().getChannelEndpoint(); + onChanged(); + return this; + } + /** + * optional string channelEndpoint = 5; + */ + public Builder setChannelEndpointBytes( + com.google.protobuf.ByteString value) { + if (value == null) { + throw new NullPointerException(); + } + bitField0_ |= 0x00000010; + channelEndpoint_ = value; + onChanged(); + return this; + } + // @@protoc_insertion_point(builder_scope:ConfirmMessage) } @@ -4562,23 +4880,25 @@ public final class MessageFormats { java.lang.String[] descriptorData = { "\n\024MessageFormats.proto\";\n\026PersistentMess" + "ageBatch\022!\n\005batch\030\001 \003(\0132\022.PersistentMess" + - "age\"\373\001\n\021PersistentMessage\022#\n\007payload\030\001 \001" + + "age\"\221\002\n\021PersistentMessage\022#\n\007payload\030\001 \001" + "(\0132\022.PersistentPayload\022\022\n\nsequenceNr\030\002 \001" + - "(\003\022\023\n\013processorId\030\003 \001(\t\022\017\n\007deleted\030\005 \001(\010" + - "\022\020\n\010resolved\030\006 \001(\010\022\020\n\010confirms\030\010 \003(\t\022\023\n\013" + - "confirmable\030\013 \001(\010\022\'\n\016confirmMessage\030\n \001(" + - "\0132\017.ConfirmMessage\022\025\n\rconfirmTarget\030\t \001(" + - "\t\022\016\n\006sender\030\007 \001(\t\"S\n\021PersistentPayload\022\024" + - "\n\014serializerId\030\001 \002(\005\022\017\n\007payload\030\002 \002(\014\022\027\n", - "\017payloadManifest\030\003 \001(\014\"L\n\016ConfirmMessage" + - "\022\023\n\013processorId\030\001 \001(\t\022\022\n\nsequenceNr\030\002 \001(" + - "\003\022\021\n\tchannelId\030\003 \001(\t\"\270\001\n\016DeliverMessage\022" + - "&\n\npersistent\030\001 \001(\0132\022.PersistentMessage\022" + - "\023\n\013destination\030\002 \001(\t\0220\n\007resolve\030\003 \001(\0162\037." + - "DeliverMessage.ResolveStrategy\"7\n\017Resolv" + - "eStrategy\022\007\n\003Off\020\001\022\n\n\006Sender\020\002\022\017\n\013Destin" + - "ation\020\003B\"\n\036akka.persistence.serializatio" + - "nH\001" + "(\003\022\023\n\013processorId\030\003 \001(\t\022\017\n\007deleted\030\004 \001(\010" + + "\022\020\n\010resolved\030\005 \001(\010\022\024\n\014redeliveries\030\006 \001(\005" + + "\022\020\n\010confirms\030\007 \003(\t\022\023\n\013confirmable\030\010 \001(\010\022" + + "\'\n\016confirmMessage\030\t \001(\0132\017.ConfirmMessage" + + "\022\025\n\rconfirmTarget\030\n \001(\t\022\016\n\006sender\030\013 \001(\t\"" + + "S\n\021PersistentPayload\022\024\n\014serializerId\030\001 \002", + "(\005\022\017\n\007payload\030\002 \002(\014\022\027\n\017payloadManifest\030\003" + + " \001(\014\"\207\001\n\016ConfirmMessage\022\023\n\013processorId\030\001" + + " \001(\t\022\031\n\021messageSequenceNr\030\002 \001(\003\022\021\n\tchann" + + "elId\030\003 \001(\t\022\031\n\021wrapperSequenceNr\030\004 \001(\003\022\027\n" + + "\017channelEndpoint\030\005 \001(\t\"\270\001\n\016DeliverMessag" + + "e\022&\n\npersistent\030\001 \001(\0132\022.PersistentMessag" + + "e\022\023\n\013destination\030\002 \001(\t\0220\n\007resolve\030\003 \001(\0162" + + "\037.DeliverMessage.ResolveStrategy\"7\n\017Reso" + + "lveStrategy\022\007\n\003Off\020\001\022\n\n\006Sender\020\002\022\017\n\013Dest" + + "ination\020\003B\"\n\036akka.persistence.serializat", + "ionH\001" }; com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner assigner = new com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner() { @@ -4596,7 +4916,7 @@ public final class MessageFormats { internal_static_PersistentMessage_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_PersistentMessage_descriptor, - new java.lang.String[] { "Payload", "SequenceNr", "ProcessorId", "Deleted", "Resolved", "Confirms", "Confirmable", "ConfirmMessage", "ConfirmTarget", "Sender", }); + new java.lang.String[] { "Payload", "SequenceNr", "ProcessorId", "Deleted", "Resolved", "Redeliveries", "Confirms", "Confirmable", "ConfirmMessage", "ConfirmTarget", "Sender", }); internal_static_PersistentPayload_descriptor = getDescriptor().getMessageTypes().get(2); internal_static_PersistentPayload_fieldAccessorTable = new @@ -4608,7 +4928,7 @@ public final class MessageFormats { internal_static_ConfirmMessage_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_ConfirmMessage_descriptor, - new java.lang.String[] { "ProcessorId", "SequenceNr", "ChannelId", }); + new java.lang.String[] { "ProcessorId", "MessageSequenceNr", "ChannelId", "WrapperSequenceNr", "ChannelEndpoint", }); internal_static_DeliverMessage_descriptor = getDescriptor().getMessageTypes().get(4); internal_static_DeliverMessage_fieldAccessorTable = new diff --git a/akka-persistence/src/main/protobuf/MessageFormats.proto b/akka-persistence/src/main/protobuf/MessageFormats.proto index 986a65a80b..668a92796a 100644 --- a/akka-persistence/src/main/protobuf/MessageFormats.proto +++ b/akka-persistence/src/main/protobuf/MessageFormats.proto @@ -13,13 +13,14 @@ message PersistentMessage { optional PersistentPayload payload = 1; optional int64 sequenceNr = 2; optional string processorId = 3; - optional bool deleted = 5; - optional bool resolved = 6; - repeated string confirms = 8; - optional bool confirmable = 11; - optional ConfirmMessage confirmMessage = 10; - optional string confirmTarget = 9; - optional string sender = 7; + optional bool deleted = 4; + optional bool resolved = 5; + optional int32 redeliveries = 6; + repeated string confirms = 7; + optional bool confirmable = 8; + optional ConfirmMessage confirmMessage = 9; + optional string confirmTarget = 10; + optional string sender = 11; } message PersistentPayload { @@ -30,8 +31,10 @@ message PersistentPayload { message ConfirmMessage { optional string processorId = 1; - optional int64 sequenceNr = 2; + optional int64 messageSequenceNr = 2; optional string channelId = 3; + optional int64 wrapperSequenceNr = 4; + optional string channelEndpoint = 5; } message DeliverMessage { diff --git a/akka-persistence/src/main/resources/reference.conf b/akka-persistence/src/main/resources/reference.conf index 5ff3c47843..ce9e83eeca 100644 --- a/akka-persistence/src/main/resources/reference.conf +++ b/akka-persistence/src/main/resources/reference.conf @@ -26,10 +26,10 @@ akka { journal { - # Maximum size of a persistent message batch written to the journal. Only applies to - # internally created batches by processors that receive persistent messages individually. - # Application-defined batches, even if larger than this setting, are always written as - # a single isolated batch. + # Maximum size of a persistent message batch written to the journal. + # Only applies to internally created batches by processors that receive + # persistent messages individually. Application-defined batches, even if + # larger than this setting, are always written as a single isolated batch. max-batch-size = 200 # Path to the journal plugin to be used @@ -131,18 +131,18 @@ akka { } default-replay-dispatcher { type = Dispatcher - executor = "thread-pool-executor" - thread-pool-executor { - core-pool-size-min = 2 - core-pool-size-max = 8 + executor = "fork-join-executor" + fork-join-executor { + parallelism-min = 2 + parallelism-max = 8 } } default-stream-dispatcher { type = Dispatcher - executor = "thread-pool-executor" - thread-pool-executor { - core-pool-size-min = 2 - core-pool-size-max = 8 + executor = "fork-join-executor" + fork-join-executor { + parallelism-min = 2 + parallelism-max = 8 } } } diff --git a/akka-persistence/src/main/scala/akka/persistence/Channel.scala b/akka-persistence/src/main/scala/akka/persistence/Channel.scala index 4e9bad76d7..c4546f0532 100644 --- a/akka-persistence/src/main/scala/akka/persistence/Channel.scala +++ b/akka-persistence/src/main/scala/akka/persistence/Channel.scala @@ -4,17 +4,63 @@ package akka.persistence -import akka.AkkaException -import akka.actor._ +import scala.collection.immutable +import scala.concurrent.duration._ +import scala.language.postfixOps +import akka.actor._ +import akka.dispatch.Envelope + +import akka.persistence.JournalProtocol.Confirm import akka.persistence.serialization.Message +/** + * A [[Channel]] configuration object. + * + * @param redeliverMax maximum number of redeliveries (default is 5). + * @param redeliverInterval interval between redeliveries (default is 5 seconds). + */ +@SerialVersionUID(1L) +class ChannelSettings( + val redeliverMax: Int, + val redeliverInterval: FiniteDuration) extends Serializable { + + /** + * Java API. + */ + def withRedeliverMax(redeliverMax: Int): ChannelSettings = + update(redeliverMax = redeliverMax) + + /** + * Java API. + */ + def withRedeliverInterval(redeliverInterval: FiniteDuration): ChannelSettings = + update(redeliverInterval = redeliverInterval) + + private def update( + redeliverMax: Int = redeliverMax, + redeliverInterval: FiniteDuration = redeliverInterval): ChannelSettings = + new ChannelSettings(redeliverMax, redeliverInterval) +} + +object ChannelSettings { + def apply( + redeliverMax: Int = 5, + redeliverInterval: FiniteDuration = 5 seconds): ChannelSettings = + new ChannelSettings(redeliverMax, redeliverInterval) + + /** + * Java API. + */ + def create() = apply() +} + /** * A channel is used by [[Processor]]s for sending [[Persistent]] messages to destinations. The main * responsibility of a channel is to prevent redundant delivery of replayed messages to destinations * when a processor is recovered. * - * A channel can be instructed to deliver a persistent message to a `destination` via the [[Deliver]] + * A channel is instructed to deliver a persistent message to a `destination` with the [[Deliver]] * command. * * {{{ @@ -53,224 +99,84 @@ import akka.persistence.serialization.Message * {{{ * class MyDestination extends Actor { * def receive = { - * case cp @ ConfirmablePersistent(payload, sequenceNr) => cp.confirm() + * case cp @ ConfirmablePersistent(payload, sequenceNr, redeliveries) => cp.confirm() * } * } * }}} * - * A channel will only re-deliver messages if the sending processor is recovered and delivery of these - * messages has not been confirmed yet. Hence, a channel can be used to avoid message loss in case of - * sender JVM crashes, for example. A channel, however, does not attempt any re-deliveries should a - * destination be unavailable. Re-delivery to destinations (in case of network failures or destination - * JVM crashes) is an application-level concern and can be done by using a reliable proxy, for example. + * If a destination does not confirm the receipt of a `ConfirmablePersistent` message, it will be redelivered + * by the channel according to the parameters in [[ChannelSettings]]. Message redelivery is done out of order + * with regards to normal delivery i.e. redelivered messages may arrive later than newer normally delivered + * messages. Redelivered messages have a `redeliveries` value greater than zero. + * + * If the maximum number of redeliveries for a certain message is reached and there is still no confirmation + * from the destination, then this message is removed from the channel. In order to deliver that message to + * the destination again, the processor must replay its stored messages to the channel (during start or restart). + * Replayed, unconfirmed messages are then processed and delivered by the channel again. These messages are now + * duplicates (with a `redeliveries` counter starting from zero). Duplicates can be detected by destinations + * by tracking message sequence numbers. * * @see [[Deliver]] */ -sealed class Channel private[akka] (_channelId: Option[String]) extends Actor with Stash { - private val extension = Persistence(context.system) +final class Channel private[akka] (_channelId: Option[String], channelSettings: ChannelSettings) extends Actor { private val id = _channelId match { case Some(cid) ⇒ cid - case None ⇒ extension.channelId(self) + case None ⇒ Persistence(context.system).channelId(self) } - import ResolvedDelivery._ + private val journal = Persistence(context.system).journalFor(id) - private val delivering: Actor.Receive = { - case Deliver(persistent: PersistentRepr, destination, resolve) ⇒ - if (!persistent.confirms.contains(id)) { - val prepared = prepareDelivery(persistent) - resolve match { - case Resolve.Sender if !prepared.resolved ⇒ - context.actorOf(Props(classOf[ResolvedSenderDelivery], prepared, destination, sender)) ! DeliverResolved - context.become(buffering, false) - case Resolve.Destination if !prepared.resolved ⇒ - context.actorOf(Props(classOf[ResolvedDestinationDelivery], prepared, destination, sender)) ! DeliverResolved - context.become(buffering, false) - case _ ⇒ destination tell (prepared, sender) - } - } - unstash() + private val reliableDelivery = context.actorOf(Props(classOf[ReliableDelivery], channelSettings)) + private val resolvedDelivery = context.actorOf(Props(classOf[ResolvedDelivery], reliableDelivery)) + + def receive = { + case d @ Deliver(persistent: PersistentRepr, _, _) ⇒ + if (!persistent.confirms.contains(id)) resolvedDelivery forward d.copy(prepareDelivery(persistent)) } - private val buffering: Actor.Receive = { - case DeliveredResolved | DeliveredUnresolved ⇒ - context.unbecome() - unstash() - case _: Deliver ⇒ stash() - } - - def receive = delivering - - private[akka] def prepareDelivery(persistent: PersistentRepr): PersistentRepr = { - ConfirmablePersistentImpl( - persistent = persistent, - confirmTarget = extension.journalFor(persistent.processorId), + private def prepareDelivery(persistent: PersistentRepr): PersistentRepr = + ConfirmablePersistentImpl(persistent, + confirmTarget = journal, confirmMessage = Confirm(persistent.processorId, persistent.sequenceNr, id)) - } } object Channel { /** - * Returns a channel configuration object for creating a [[Channel]] with a - * generated id. + * Returns a channel actor configuration object for creating a [[Channel]] with a + * generated id and default [[ChannelSettings]]. */ - def props(): Props = Props(classOf[Channel], None) + def props(): Props = + props(ChannelSettings()) /** - * Returns a channel configuration object for creating a [[Channel]] with the - * specified id. + * Returns a channel actor configuration object for creating a [[Channel]] with a + * generated id and specified `channelSettings`. + * + * @param channelSettings channel configuration object. + */ + def props(channelSettings: ChannelSettings): Props = + Props(classOf[Channel], None, channelSettings) + + /** + * Returns a channel actor configuration object for creating a [[Channel]] with the + * specified id and default [[ChannelSettings]]. * * @param channelId channel id. */ - def props(channelId: String): Props = Props(classOf[Channel], Some(channelId)) -} - -/** - * A [[PersistentChannel]] implements the same functionality as a [[Channel]] but additionally - * persists messages before they are delivered. Therefore, the main use case of a persistent - * channel is standalone usage i.e. independent of a sending [[Processor]]. Messages that have - * been persisted by a persistent channel are deleted again when destinations confirm the receipt - * of these messages. - * - * Using a persistent channel in combination with a [[Processor]] can make sense if destinations - * are unavailable for a long time and an application doesn't want to buffer all messages in - * memory (but write them to a journal instead). In this case, delivery can be disabled with - * [[DisableDelivery]] (to stop delivery and persist-only) and re-enabled with [[EnableDelivery]]. - * - * A persistent channel can also be configured to reply whether persisting a message was successful - * or not (see `PersistentChannel.props` methods). If enabled, the sender will receive the persisted - * message as reply (i.e. a [[Persistent]] message), otherwise a [[PersistenceFailure]] message. - * - * A persistent channel will only re-deliver un-confirmed, stored messages if it is started or re- - * enabled with [[EnableDelivery]]. Hence, a persistent channel can be used to avoid message loss - * in case of sender JVM crashes, for example. A channel, however, does not attempt any re-deliveries - * should a destination be unavailable. Re-delivery to destinations (in case of network failures or - * destination JVM crashes) is an application-level concern and can be done by using a reliable proxy, - * for example. - */ -final class PersistentChannel private[akka] (_channelId: Option[String], persistentReply: Boolean) extends EventsourcedProcessor { - override val processorId = _channelId.getOrElse(super.processorId) - - private val journal = Persistence(context.system).journalFor(processorId) - private val channel = context.actorOf(Props(classOf[NoPrepChannel], processorId)) - - private var deliveryEnabled = true - - def receiveReplay: Receive = { - case Deliver(persistent: PersistentRepr, destination, resolve) ⇒ deliver(prepareDelivery(persistent), destination, resolve) - } - - def receiveCommand: Receive = { - case d @ Deliver(persistent: PersistentRepr, destination, resolve) ⇒ - if (!persistent.confirms.contains(processorId)) { - persist(d) { _ ⇒ - val prepared = prepareDelivery(persistent) - - if (persistent.processorId != PersistentRepr.Undefined) - journal ! Confirm(persistent.processorId, persistent.sequenceNr, processorId) - - if (persistentReply) - sender ! prepared - - if (deliveryEnabled) - deliver(prepared, destination, resolve) - } - } - case c: Confirm ⇒ deleteMessage(c.sequenceNr, true) - case DisableDelivery ⇒ deliveryEnabled = false - case EnableDelivery if (!deliveryEnabled) ⇒ throw new ChannelRestartRequiredException - case p: PersistenceFailure if (persistentReply) ⇒ sender ! p - } - - private def prepareDelivery(persistent: PersistentRepr): PersistentRepr = currentPersistentMessage.map { current ⇒ - val sequenceNr = if (persistent.sequenceNr == 0L) current.sequenceNr else persistent.sequenceNr - val resolved = persistent.resolved && current.asInstanceOf[PersistentRepr].resolved - persistent.update(sequenceNr = sequenceNr, resolved = resolved) - } getOrElse (persistent) - - private def deliver(persistent: PersistentRepr, destination: ActorRef, resolve: Resolve.ResolveStrategy) = currentPersistentMessage.foreach { current ⇒ - channel forward Deliver(persistent = ConfirmablePersistentImpl(persistent, - confirmTarget = self, - confirmMessage = Confirm(processorId, current.sequenceNr, PersistentRepr.Undefined)), destination, resolve) - } -} - -object PersistentChannel { - /** - * Returns a channel configuration object for creating a [[PersistentChannel]] with a - * generated id. The sender will not receive persistence completion replies. - */ - def props(): Props = props(persistentReply = false) + def props(channelId: String): Props = + props(channelId, ChannelSettings()) /** - * Returns a channel configuration object for creating a [[PersistentChannel]] with a - * generated id. - * - * @param persistentReply if `true` the sender will receive the successfully stored - * [[Persistent]] message that has been submitted with a - * [[Deliver]] request, or a [[PersistenceFailure]] message - * in case of a persistence failure. - */ - def props(persistentReply: Boolean): Props = Props(classOf[PersistentChannel], None, persistentReply) - - /** - * Returns a channel configuration object for creating a [[PersistentChannel]] with the - * specified id. The sender will not receive persistence completion replies. + * Returns a channel actor configuration object for creating a [[Channel]] with the + * specified id and specified `channelSettings`. * * @param channelId channel id. + * @param channelSettings channel configuration object. */ - def props(channelId: String): Props = props(channelId, persistentReply = false) - - /** - * Returns a channel configuration object for creating a [[PersistentChannel]] with the - * specified id. - * - * @param channelId channel id. - * @param persistentReply if `true` the sender will receive the successfully stored - * [[Persistent]] message that has been submitted with a - * [[Deliver]] request, or a [[PersistenceFailure]] message - * in case of a persistence failure. - */ - def props(channelId: String, persistentReply: Boolean): Props = Props(classOf[PersistentChannel], Some(channelId), persistentReply) + def props(channelId: String, channelSettings: ChannelSettings): Props = + Props(classOf[Channel], Some(channelId), channelSettings) } -/** - * Instructs a [[PersistentChannel]] to disable the delivery of [[Persistent]] messages to their destination. - * The persistent channel, however, continues to persist messages (for later delivery). - * - * @see [[EnableDelivery]] - */ -@SerialVersionUID(1L) -case object DisableDelivery { - /** - * Java API. - */ - def getInstance = this -} - -/** - * Instructs a [[PersistentChannel]] to re-enable the delivery of [[Persistent]] messages to their destination. - * This will first deliver all messages that have been stored by a persistent channel for which no confirmation - * is available yet. New [[Deliver]] requests are processed after all stored messages have been delivered. This - * request only has an effect if a persistent channel has previously been disabled with [[DisableDelivery]]. - * - * @see [[DisableDelivery]] - */ -@SerialVersionUID(1L) -case object EnableDelivery { - /** - * Java API. - */ - def getInstance = this -} - -/** - * Thrown by a persistent channel when [[EnableDelivery]] has been requested and delivery has been previously - * disabled for that channel. - */ -@SerialVersionUID(1L) -class ChannelRestartRequiredException extends AkkaException("channel restart required for enabling delivery") - /** * Instructs a [[Channel]] or [[PersistentChannel]] to deliver `persistent` message to * destination `destination`. The `resolve` parameter can be: @@ -375,68 +281,130 @@ object Resolve { } /** - * Resolved delivery support. + * Resolves actor references as specified by [[Deliver]] requests and then delegates delivery + * to `next`. */ -private trait ResolvedDelivery extends Actor { - import scala.concurrent.duration._ - import scala.language.postfixOps - import ResolvedDelivery._ +private class ResolvedDelivery(next: ActorRef) extends Actor with Stash { + private var currentResolution: Envelope = _ - context.setReceiveTimeout(5 seconds) // TODO: make configurable + private val delivering: Receive = { + case d @ Deliver(persistent: PersistentRepr, destination, resolve) ⇒ + resolve match { + case Resolve.Sender if !persistent.resolved ⇒ + context.actorSelection(sender.path) ! Identify(1) + context.become(resolving, discardOld = false) + currentResolution = Envelope(d, sender, context.system) + case Resolve.Destination if !persistent.resolved ⇒ + context.actorSelection(destination.path) ! Identify(1) + context.become(resolving, discardOld = false) + currentResolution = Envelope(d, sender, context.system) + case _ ⇒ next forward d + } + unstash() + } - def path: ActorPath - def onResolveSuccess(ref: ActorRef): Unit - def onResolveFailure(): Unit + private val resolving: Receive = { + case ActorIdentity(1, resolvedOption) ⇒ + val Envelope(d: Deliver, sender) = currentResolution + if (d.resolve == Resolve.Sender) { + next tell (d, resolvedOption.getOrElse(sender)) + } else if (d.resolve == Resolve.Destination) { + next tell (d.copy(destination = resolvedOption.getOrElse(d.destination)), sender) + } + context.unbecome() + unstash() + case _: Deliver ⇒ stash() + } + + def receive = delivering +} + +/** + * Reliably deliver messages contained in [[Deliver]] requests to their destinations. Unconfirmed + * messages are redelivered according to the parameters in [[ChannelSettings]]. + */ +private class ReliableDelivery(channelSettings: ChannelSettings) extends Actor { + import channelSettings._ + import ReliableDelivery._ + + private val redelivery = context.actorOf(Props(classOf[Redelivery], channelSettings)) + private var attempts: DeliveryAttempts = Map.empty + private var sequenceNr: Long = 0L def receive = { - case DeliverResolved ⇒ - context.actorSelection(path) ! Identify(1) - case ActorIdentity(1, Some(ref)) ⇒ - onResolveSuccess(ref) - shutdown(DeliveredResolved) - case ActorIdentity(1, None) ⇒ - onResolveFailure() - shutdown(DeliveredUnresolved) - case ReceiveTimeout ⇒ - onResolveFailure() - shutdown(DeliveredUnresolved) + case d @ Deliver(persistent: PersistentRepr, destination, _) ⇒ + val dsnr = nextSequenceNr() + val psnr = persistent.sequenceNr + val confirm = persistent.confirmMessage.copy(channelEndpoint = self) + val updated = persistent.update(confirmMessage = confirm, sequenceNr = if (psnr == 0) dsnr else psnr) + destination forward updated + attempts += ((updated.processorId, updated.sequenceNr) -> DeliveryAttempt(updated, destination, sender, dsnr)) + case c @ Confirm(processorId, messageSequenceNr, _, _, _) ⇒ + attempts -= ((processorId, messageSequenceNr)) + case Redeliver ⇒ + val limit = System.nanoTime - redeliverInterval.toNanos + val (older, younger) = attempts.partition { case (_, a) ⇒ a.timestamp < limit } + redelivery ! Redeliver(older, redeliverMax) + attempts = younger } - def shutdown(message: Any) { - context.parent ! message - context.stop(self) + private def nextSequenceNr(): Long = { + sequenceNr += 1 + sequenceNr } } -private object ResolvedDelivery { - case object DeliverResolved - case object DeliveredResolved - case object DeliveredUnresolved +private object ReliableDelivery { + type DeliveryAttempts = immutable.Map[(String, Long), DeliveryAttempt] + + case class DeliveryAttempt(persistent: PersistentRepr, destination: ActorRef, sender: ActorRef, deliverySequenceNr: Long, timestamp: Long = System.nanoTime) { + def withChannelEndpoint(channelEndpoint: ActorRef) = + copy(persistent.update(confirmMessage = persistent.confirmMessage.copy(channelEndpoint = channelEndpoint))) + + def incrementRedeliveryCount = + copy(persistent.update(redeliveries = persistent.redeliveries + 1)) + } + + case class Redeliver(attempts: DeliveryAttempts, redeliveryMax: Int) } /** - * Resolves `destination` before sending `persistent` message to the resolved destination using - * the specified sender (`sdr`) as message sender. + * Redelivery process used by [[ReliableDelivery]]. */ -private class ResolvedDestinationDelivery(persistent: PersistentRepr, destination: ActorRef, sdr: ActorRef) extends ResolvedDelivery { - val path = destination.path - def onResolveSuccess(ref: ActorRef) = ref tell (persistent.update(resolved = true), sdr) - def onResolveFailure() = destination tell (persistent, sdr) +private class Redelivery(channelSettings: ChannelSettings) extends Actor { + import context.dispatcher + import channelSettings._ + import ReliableDelivery._ + + private var attempts: DeliveryAttempts = Map.empty + private var schedule: Cancellable = _ + + def receive = { + case Redeliver(as, max) ⇒ + attempts ++= as.map { case (k, a) ⇒ (k, a.withChannelEndpoint(self)) } + attempts = attempts.foldLeft[DeliveryAttempts](Map.empty) { + case (acc, (k, attempt)) ⇒ + // drop redelivery attempts that exceed redeliveryMax + if (attempt.persistent.redeliveries >= redeliverMax) acc + // increase redelivery count of attempt + else acc + (k -> attempt.incrementRedeliveryCount) + } + redeliver(attempts) + scheduleRedelivery() + case c @ Confirm(processorId, messageSequenceNr, _, _, _) ⇒ + attempts -= ((processorId, messageSequenceNr)) + } + + override def preStart(): Unit = + scheduleRedelivery() + + override def postStop(): Unit = + schedule.cancel() + + private def scheduleRedelivery(): Unit = + schedule = context.system.scheduler.scheduleOnce(redeliverInterval, context.parent, Redeliver) + + private def redeliver(attempts: DeliveryAttempts): Unit = + attempts.values.toSeq.sortBy(_.deliverySequenceNr).foreach(ad ⇒ ad.destination tell (ad.persistent, ad.sender)) } -/** - * Resolves `sdr` before sending `persistent` message to specified `destination` using - * the resolved sender as message sender. - */ -private class ResolvedSenderDelivery(persistent: PersistentRepr, destination: ActorRef, sdr: ActorRef) extends ResolvedDelivery { - val path = sdr.path - def onResolveSuccess(ref: ActorRef) = destination tell (persistent.update(resolved = true), ref) - def onResolveFailure() = destination tell (persistent, sdr) -} - -/** - * [[Channel]] specialization used by [[PersistentChannel]] to deliver stored messages. - */ -private class NoPrepChannel(channelId: String) extends Channel(Some(channelId)) { - override private[akka] def prepareDelivery(persistent: PersistentRepr) = persistent -} diff --git a/akka-persistence/src/main/scala/akka/persistence/JournalProtocol.scala b/akka-persistence/src/main/scala/akka/persistence/JournalProtocol.scala index 52064ac5b1..d0626ff221 100644 --- a/akka-persistence/src/main/scala/akka/persistence/JournalProtocol.scala +++ b/akka-persistence/src/main/scala/akka/persistence/JournalProtocol.scala @@ -8,6 +8,8 @@ import scala.collection.immutable import akka.actor._ +import akka.persistence.serialization.Message + /** * INTERNAL API. * @@ -22,6 +24,24 @@ private[persistence] object JournalProtocol { */ case class Delete(processorId: String, fromSequenceNr: Long, toSequenceNr: Long, permanent: Boolean) + /** + * Message sent after confirming the receipt of a [[ConfirmablePersistent]] message. + * + * @param processorId id of the processor that sent the message corresponding to + * this confirmation to a channel. + * @param messageSequenceNr sequence number of the sent message. + * @param channelId id of the channel that delivered the message corresponding to + * this confirmation. + * @param wrapperSequenceNr sequence number of the message stored by a persistent + * channel. This message contains the [[Deliver]] request + * with the message identified by `processorId` and + * `messageSequenceNumber`. + * @param channelEndpoint actor reference that sent the the message corresponding to + * this confirmation. This is a child actor of the sending + * [[Channel]] or [[PersistentChannel]]. + */ + case class Confirm(processorId: String, messageSequenceNr: Long, channelId: String, wrapperSequenceNr: Long = 0L, channelEndpoint: ActorRef = null) extends Message + /** * Instructs a journal to persist a sequence of messages. * diff --git a/akka-persistence/src/main/scala/akka/persistence/Persistent.scala b/akka-persistence/src/main/scala/akka/persistence/Persistent.scala index c071d3b98b..9dfe757730 100644 --- a/akka-persistence/src/main/scala/akka/persistence/Persistent.scala +++ b/akka-persistence/src/main/scala/akka/persistence/Persistent.scala @@ -12,6 +12,7 @@ import scala.collection.immutable import akka.actor.{ ActorContext, ActorRef } import akka.japi.Util.immutableSeq import akka.pattern.PromiseActorRef +import akka.persistence.JournalProtocol.Confirm import akka.persistence.serialization.Message /** @@ -85,14 +86,20 @@ sealed abstract class ConfirmablePersistent extends Persistent { * persistent message. */ def confirm(): Unit + + /** + * Number of redeliveries. Only greater than zero if message has been redelivered by a [[Channel]] + * or [[PersistentChannel]]. + */ + def redeliveries: Int } object ConfirmablePersistent { /** * [[ConfirmablePersistent]] extractor. */ - def unapply(persistent: ConfirmablePersistent): Option[(Any, Long)] = - Some((persistent.payload, persistent.sequenceNr)) + def unapply(persistent: ConfirmablePersistent): Option[(Any, Long, Int)] = + Some((persistent.payload, persistent.sequenceNr, persistent.redeliveries)) } /** @@ -145,6 +152,12 @@ trait PersistentRepr extends Persistent with Message { */ def resolved: Boolean + /** + * Number of redeliveries. Only greater than zero if message has been redelivered by a [[Channel]] + * or [[PersistentChannel]]. + */ + def redeliveries: Int + /** * Channel ids of delivery confirmations that are available for this message. Only non-empty * for replayed messages. @@ -196,6 +209,7 @@ trait PersistentRepr extends Persistent with Message { processorId: String = processorId, deleted: Boolean = deleted, resolved: Boolean = resolved, + redeliveries: Int = redeliveries, confirms: immutable.Seq[String] = confirms, confirmMessage: Confirm = confirmMessage, confirmTarget: ActorRef = confirmTarget, @@ -217,12 +231,13 @@ object PersistentRepr { processorId: String = PersistentRepr.Undefined, deleted: Boolean = false, resolved: Boolean = true, + redeliveries: Int = 0, confirms: immutable.Seq[String] = Nil, confirmable: Boolean = false, confirmMessage: Confirm = null, confirmTarget: ActorRef = null, sender: ActorRef = null) = - if (confirmable) ConfirmablePersistentImpl(payload, sequenceNr, processorId, deleted, resolved, confirms, confirmMessage, confirmTarget, sender) + if (confirmable) ConfirmablePersistentImpl(payload, sequenceNr, processorId, deleted, resolved, redeliveries, confirms, confirmMessage, confirmTarget, sender) else PersistentImpl(payload, sequenceNr, processorId, deleted, confirms, sender) /** @@ -261,6 +276,7 @@ private[persistence] case class PersistentImpl( processorId: String, deleted: Boolean, resolved: Boolean, + redeliveries: Int, confirms: immutable.Seq[String], confirmMessage: Confirm, confirmTarget: ActorRef, @@ -268,6 +284,7 @@ private[persistence] case class PersistentImpl( copy(sequenceNr = sequenceNr, processorId = processorId, deleted = deleted, confirms = confirms, sender = sender) val resolved: Boolean = false + val redeliveries: Int = 0 val confirmable: Boolean = false val confirmMessage: Confirm = null val confirmTarget: ActorRef = null @@ -282,12 +299,13 @@ private[persistence] case class ConfirmablePersistentImpl( processorId: String, deleted: Boolean, resolved: Boolean, + redeliveries: Int, confirms: immutable.Seq[String], confirmMessage: Confirm, confirmTarget: ActorRef, sender: ActorRef) extends ConfirmablePersistent with PersistentRepr { - def withPayload(payload: Any): Persistent = + def withPayload(payload: Any): ConfirmablePersistent = copy(payload = payload) def confirm(): Unit = @@ -298,21 +316,14 @@ private[persistence] case class ConfirmablePersistentImpl( def prepareWrite(sender: ActorRef) = copy(sender = sender, resolved = false, confirmMessage = null, confirmTarget = null) - def update(sequenceNr: Long, processorId: String, deleted: Boolean, resolved: Boolean, confirms: immutable.Seq[String], confirmMessage: Confirm, confirmTarget: ActorRef, sender: ActorRef) = - copy(sequenceNr = sequenceNr, processorId = processorId, deleted = deleted, resolved = resolved, confirms = confirms, confirmMessage = confirmMessage, confirmTarget = confirmTarget, sender = sender) + def update(sequenceNr: Long, processorId: String, deleted: Boolean, resolved: Boolean, redeliveries: Int, confirms: immutable.Seq[String], confirmMessage: Confirm, confirmTarget: ActorRef, sender: ActorRef) = + copy(sequenceNr = sequenceNr, processorId = processorId, deleted = deleted, resolved = resolved, redeliveries = redeliveries, confirms = confirms, confirmMessage = confirmMessage, confirmTarget = confirmTarget, sender = sender) } /** * INTERNAL API. */ private[persistence] object ConfirmablePersistentImpl { - def apply(persistent: PersistentRepr, confirmMessage: Confirm, confirmTarget: ActorRef): ConfirmablePersistentImpl = - ConfirmablePersistentImpl(persistent.payload, persistent.sequenceNr, persistent.processorId, persistent.deleted, persistent.resolved, persistent.confirms, confirmMessage, confirmTarget, persistent.sender) + def apply(persistent: PersistentRepr, confirmMessage: Confirm, confirmTarget: ActorRef = null): ConfirmablePersistentImpl = + ConfirmablePersistentImpl(persistent.payload, persistent.sequenceNr, persistent.processorId, persistent.deleted, persistent.resolved, persistent.redeliveries, persistent.confirms, confirmMessage, confirmTarget, persistent.sender) } - -/** - * INTERNAL API. - * - * Message to confirm the receipt of a [[ConfirmablePersistent]] message. - */ -private[persistence] case class Confirm(processorId: String, sequenceNr: Long, channelId: String) extends Message diff --git a/akka-persistence/src/main/scala/akka/persistence/PersistentChannel.scala b/akka-persistence/src/main/scala/akka/persistence/PersistentChannel.scala new file mode 100644 index 0000000000..1742908df6 --- /dev/null +++ b/akka-persistence/src/main/scala/akka/persistence/PersistentChannel.scala @@ -0,0 +1,231 @@ +/** + * Copyright (C) 2009-2013 Typesafe Inc. + */ + +package akka.persistence + +import scala.concurrent.duration._ +import scala.language.postfixOps + +import akka.AkkaException +import akka.actor._ + +import akka.persistence.JournalProtocol.Confirm + +/** + * A [[PersistentChannel]] configuration object. + * + * @param redeliverMax maximum number of redeliveries (default is 5). + * @param redeliverInterval interval between redeliveries (default is 5 seconds). + * @param replyPersistent if `true` the sender will receive the successfully stored [[Persistent]] + * message that has been submitted with a [[Deliver]] request, or a + * [[PersistenceFailure]] message in case of a persistence failure. + */ +class PersistentChannelSettings( + redeliverMax: Int, + redeliverInterval: FiniteDuration, + val replyPersistent: Boolean) extends ChannelSettings(redeliverMax, redeliverInterval) { + + /** + * Java API. + */ + override def withRedeliverMax(redeliverMax: Int): PersistentChannelSettings = + updatePersistent(redeliverMax = redeliverMax) + + /** + * Java API. + */ + override def withRedeliverInterval(redeliverInterval: FiniteDuration): PersistentChannelSettings = + updatePersistent(redeliverInterval = redeliverInterval) + + /** + * Java API. + */ + def withReplyPersistent(replayPersistent: Boolean) = + updatePersistent(replyPersistent = replyPersistent) + + private def updatePersistent( // compile error if method name is 'update' + redeliverMax: Int = redeliverMax, + redeliverInterval: FiniteDuration = redeliverInterval, + replyPersistent: Boolean = replyPersistent): PersistentChannelSettings = + new PersistentChannelSettings(redeliverMax, redeliverInterval, replyPersistent) +} + +object PersistentChannelSettings { + def apply( + redeliverMax: Int = 5, + redeliverInterval: FiniteDuration = 5 seconds, + replyPersistent: Boolean = false): PersistentChannelSettings = + new PersistentChannelSettings(redeliverMax, redeliverInterval, replyPersistent) + + /** + * Java API. + */ + def create() = apply() +} + +/** + * A [[PersistentChannel]] implements the same functionality as a [[Channel]] but additionally + * persists messages before they are delivered. This is done by using internally a special-purpose + * [[Processor]]. Therefore, the main use case of a persistent channel is standalone usage i.e. + * independent of an application-specific [[Processor]] sending messages to a channel. Messages + * that have been persisted by a persistent channel are deleted when destinations confirm the + * receipt of these messages. + * + * Using a persistent channel in combination with a [[Processor]] can make sense if destinations + * are unavailable for a long time and an application doesn't want to buffer all messages in + * memory (but write them to the journal instead). In this case, delivery can be disabled with + * [[DisableDelivery]] (to stop delivery and persist-only) and re-enabled with [[EnableDelivery]]. + * `EnableDelivery` replays persistent messages to this channel and the channel delivers all + * unconfirmed messages again (which may then show up as duplicates at destinations as described + * in the API docs of [[Channel]]. Duplicates can be detected by tracking message sequence numbers + * and redelivery counters). + * + * A persistent channel can also reply to [[Deliver]] senders whether persisting a message was + * successful or not (see `replyPersistent` of [[PersistentChannelSettings]]). If enabled, the + * sender will receive the persisted message as reply (i.e. a [[Persistent]] message), otherwise + * a [[PersistenceFailure]] message. + */ +final class PersistentChannel private[akka] (_channelId: Option[String], channelSettings: PersistentChannelSettings) extends Actor { + private val id = _channelId match { + case Some(cid) ⇒ cid + case None ⇒ Persistence(context.system).channelId(self) + } + + private val reliableDelivery = context.actorOf(Props(classOf[ReliableDelivery], channelSettings)) + private val resolvedDelivery = context.actorOf(Props(classOf[ResolvedDelivery], reliableDelivery)) + private val reliableStorage = context.actorOf(Props(classOf[ReliableStorage], id, channelSettings, resolvedDelivery)) + + def receive = { + case d @ Deliver(persistent: PersistentRepr, destination, resolve) ⇒ + // Persist the Deliver request by sending reliableStorage a Persistent message + // with the Deliver request as payload. This persistent message is referred to + // as the wrapper message, whereas the persistent message contained in the Deliver + // request is referred to as wrapped message (see also class ReliableStorage). + if (!persistent.confirms.contains(id)) reliableStorage forward Persistent(d) + case DisableDelivery ⇒ reliableStorage ! DisableDelivery + case EnableDelivery ⇒ reliableStorage ! EnableDelivery + } +} + +object PersistentChannel { + /** + * Returns a channel actor configuration object for creating a [[PersistentChannel]] with a + * generated id and default [[PersistentChannelSettings]]. + */ + def props(): Props = props(PersistentChannelSettings()) + + /** + * Returns a channel actor configuration object for creating a [[PersistentChannel]] with a + * generated id and specified `channelSettings`. + * + * @param channelSettings channel configuration object. + */ + def props(channelSettings: PersistentChannelSettings): Props = + Props(classOf[PersistentChannel], None, channelSettings) + + /** + * Returns a channel actor configuration object for creating a [[PersistentChannel]] with the + * specified id and default [[PersistentChannelSettings]]. + * + * @param channelId channel id. + */ + def props(channelId: String): Props = + props(channelId, PersistentChannelSettings()) + + /** + * Returns a channel actor configuration object for creating a [[PersistentChannel]] with the + * specified id and specified `channelSettings`. + * + * @param channelId channel id. + * @param channelSettings channel configuration object. + */ + def props(channelId: String, channelSettings: PersistentChannelSettings): Props = + Props(classOf[PersistentChannel], Some(channelId), channelSettings) +} + +/** + * Instructs a [[PersistentChannel]] to disable the delivery of [[Persistent]] messages to their destination. + * The persistent channel, however, continues to persist messages (for later delivery). + * + * @see [[EnableDelivery]] + */ +@SerialVersionUID(1L) +case object DisableDelivery { + /** + * Java API. + */ + def getInstance = this +} + +/** + * Instructs a [[PersistentChannel]] to re-enable the delivery of [[Persistent]] messages to their destination. + * This will first deliver all messages that have been stored by a persistent channel for which no confirmation + * is available yet. New [[Deliver]] requests are processed after all stored messages have been delivered. This + * request only has an effect if a persistent channel has previously been disabled with [[DisableDelivery]]. + * + * @see [[DisableDelivery]] + */ +@SerialVersionUID(1L) +case object EnableDelivery { + /** + * Java API. + */ + def getInstance = this +} + +/** + * Thrown by a persistent channel when [[EnableDelivery]] has been requested and delivery has been previously + * disabled for that channel. + */ +@SerialVersionUID(1L) +class ChannelRestartRequiredException extends AkkaException("channel restart required for enabling delivery") + +private class ReliableStorage(channelId: String, channelSettings: PersistentChannelSettings, next: ActorRef) extends Processor { + import channelSettings._ + + override val processorId = channelId + + private val journal = Persistence(context.system).journalFor(channelId) + private var deliveryEnabled = true + + def receive = { + case p @ Persistent(d @ Deliver(wrapped: PersistentRepr, destination, resolve), snr) ⇒ + val wrapper = p.asInstanceOf[PersistentRepr] + val prepared = prepareDelivery(wrapped, wrapper) + + if (!recoveryRunning && wrapped.processorId != PersistentRepr.Undefined) + // Write a delivery confirmation to the journal so that replayed Deliver + // requests from a sending processor are not persisted again. Replaying + // Deliver requests is now the responsibility of this processor. + journal ! Confirm(prepared.processorId, prepared.sequenceNr, channelId) + + if (!recoveryRunning && replyPersistent) + sender ! prepared + + if (deliveryEnabled) + next forward d.copy(prepared) + + case p: PersistenceFailure if (replyPersistent) ⇒ sender ! p + case EnableDelivery if (!deliveryEnabled) ⇒ throw new ChannelRestartRequiredException + case DisableDelivery ⇒ deliveryEnabled = false + } + + /** + * @param wrapped persistent message contained in a deliver request + * @param wrapper persistent message that contains a deliver request + */ + private def prepareDelivery(wrapped: PersistentRepr, wrapper: PersistentRepr): PersistentRepr = { + // use the sequence number of the wrapper message if the channel is used standalone, + // otherwise, use sequence number of the wrapped message (that has been generated by + // the sending processor). + val sequenceNr = if (wrapped.sequenceNr == 0L) wrapper.sequenceNr else wrapped.sequenceNr + val resolved = wrapped.resolved && wrapper.asInstanceOf[PersistentRepr].resolved + val updated = wrapped.update(sequenceNr = sequenceNr, resolved = resolved) + // include the wrapper sequence number in the Confirm message so that the wrapper can + // be deleted later when the confirmation arrives. + ConfirmablePersistentImpl(updated, + confirmTarget = journal, + confirmMessage = Confirm(updated.processorId, sequenceNr, channelId, wrapper.sequenceNr)) + } +} diff --git a/akka-persistence/src/main/scala/akka/persistence/journal/AsyncWriteJournal.scala b/akka-persistence/src/main/scala/akka/persistence/journal/AsyncWriteJournal.scala index c20909842d..13e03bbd93 100644 --- a/akka-persistence/src/main/scala/akka/persistence/journal/AsyncWriteJournal.scala +++ b/akka-persistence/src/main/scala/akka/persistence/journal/AsyncWriteJournal.scala @@ -10,7 +10,7 @@ import scala.concurrent.Future import scala.util._ import akka.actor._ -import akka.pattern.{ pipe, PromiseActorRef } +import akka.pattern.pipe import akka.persistence._ import akka.persistence.JournalProtocol._ @@ -51,11 +51,22 @@ trait AsyncWriteJournal extends Actor with AsyncReplay { } recover { case e ⇒ ReplayFailure(e) } pipeTo (processor) - case c @ Confirm(processorId, sequenceNr, channelId) ⇒ - confirmAsync(processorId, sequenceNr, channelId) onComplete { - case Success(_) ⇒ if (extension.publishPluginCommands) context.system.eventStream.publish(c) + case c @ Confirm(processorId, messageSequenceNr, channelId, wrapperSequenceNr, channelEndpoint) ⇒ + val op = if (wrapperSequenceNr == 0L) { + // A wrapperSequenceNr == 0L means that the corresponding message was delivered by a + // transient channel. We can now write a delivery confirmation for this message. + confirmAsync(processorId, messageSequenceNr, channelId) + } else { + // A wrapperSequenceNr != 0L means that the corresponding message was delivered by a + // persistent channel. We can now safely delete the wrapper message (that contains the + // delivered message). + deleteAsync(channelId, wrapperSequenceNr, wrapperSequenceNr, true) + } + op onComplete { + case Success(_) ⇒ + if (extension.publishPluginCommands) context.system.eventStream.publish(c) + if (channelEndpoint != null) channelEndpoint ! c case Failure(e) ⇒ // TODO: publish failure to event stream - context.system.eventStream.publish(c) } case d @ Delete(processorId, fromSequenceNr, toSequenceNr, permanent) ⇒ deleteAsync(processorId, fromSequenceNr, toSequenceNr, permanent) onComplete { diff --git a/akka-persistence/src/main/scala/akka/persistence/journal/SyncWriteJournal.scala b/akka-persistence/src/main/scala/akka/persistence/journal/SyncWriteJournal.scala index f9329b2dc6..1145957918 100644 --- a/akka-persistence/src/main/scala/akka/persistence/journal/SyncWriteJournal.scala +++ b/akka-persistence/src/main/scala/akka/persistence/journal/SyncWriteJournal.scala @@ -9,7 +9,7 @@ import scala.collection.immutable import scala.util._ import akka.actor.Actor -import akka.pattern.{ pipe, PromiseActorRef } +import akka.pattern.pipe import akka.persistence._ /** @@ -40,8 +40,18 @@ trait SyncWriteJournal extends Actor with AsyncReplay { } recover { case e ⇒ ReplayFailure(e) } pipeTo (processor) - case c @ Confirm(processorId, sequenceNr, channelId) ⇒ - confirm(processorId, sequenceNr, channelId) + case c @ Confirm(processorId, messageSequenceNr, channelId, wrapperSequenceNr, channelEndpoint) ⇒ + if (wrapperSequenceNr == 0L) { + // A wrapperSequenceNr == 0L means that the corresponding message was delivered by a + // transient channel. We can now write a delivery confirmation for this message. + confirm(processorId, messageSequenceNr, channelId) + } else { + // A wrapperSequenceNr != 0L means that the corresponding message was delivered by a + // persistent channel. We can now safely delete the wrapper message (that contains the + // delivered message). + delete(channelId, wrapperSequenceNr, wrapperSequenceNr, true) + } + if (channelEndpoint != null) channelEndpoint ! c if (extension.publishPluginCommands) context.system.eventStream.publish(c) case d @ Delete(processorId, fromSequenceNr, toSequenceNr, permanent) ⇒ delete(processorId, fromSequenceNr, toSequenceNr, permanent) diff --git a/akka-persistence/src/main/scala/akka/persistence/serialization/MessageSerializer.scala b/akka-persistence/src/main/scala/akka/persistence/serialization/MessageSerializer.scala index e841e9b1fb..a80e57f55c 100644 --- a/akka-persistence/src/main/scala/akka/persistence/serialization/MessageSerializer.scala +++ b/akka-persistence/src/main/scala/akka/persistence/serialization/MessageSerializer.scala @@ -11,6 +11,7 @@ import com.google.protobuf._ import akka.actor.ExtendedActorSystem import akka.japi.Util.immutableSeq import akka.persistence._ +import akka.persistence.JournalProtocol.Confirm import akka.persistence.serialization.MessageFormats._ import akka.persistence.serialization.MessageFormats.DeliverMessage.ResolveStrategy import akka.serialization._ @@ -100,6 +101,7 @@ class MessageSerializer(val system: ExtendedActorSystem) extends Serializer { builder.setSequenceNr(persistent.sequenceNr) builder.setDeleted(persistent.deleted) builder.setResolved(persistent.resolved) + builder.setRedeliveries(persistent.redeliveries) builder.setConfirmable(persistent.confirmable) builder } @@ -116,10 +118,15 @@ class MessageSerializer(val system: ExtendedActorSystem) extends Serializer { } private def confirmMessageBuilder(confirm: Confirm) = { - ConfirmMessage.newBuilder - .setProcessorId(confirm.processorId) - .setSequenceNr(confirm.sequenceNr) - .setChannelId(confirm.channelId) + val builder = ConfirmMessage.newBuilder + + if (confirm.channelEndpoint != null) builder.setChannelEndpoint(Serialization.serializedActorPath(confirm.channelEndpoint)) + + builder.setProcessorId(confirm.processorId) + builder.setMessageSequenceNr(confirm.messageSequenceNr) + builder.setChannelId(confirm.channelId) + builder.setWrapperSequenceNr(confirm.wrapperSequenceNr) + builder } // @@ -147,6 +154,7 @@ class MessageSerializer(val system: ExtendedActorSystem) extends Serializer { if (persistentMessage.hasProcessorId) persistentMessage.getProcessorId else Undefined, persistentMessage.getDeleted, persistentMessage.getResolved, + persistentMessage.getRedeliveries, immutableSeq(persistentMessage.getConfirmsList), persistentMessage.getConfirmable, if (persistentMessage.hasConfirmMessage) confirm(persistentMessage.getConfirmMessage) else null, @@ -167,7 +175,9 @@ class MessageSerializer(val system: ExtendedActorSystem) extends Serializer { private def confirm(confirmMessage: ConfirmMessage): Confirm = { Confirm( confirmMessage.getProcessorId, - confirmMessage.getSequenceNr, - confirmMessage.getChannelId) + confirmMessage.getMessageSequenceNr, + confirmMessage.getChannelId, + confirmMessage.getWrapperSequenceNr, + if (confirmMessage.hasChannelEndpoint) system.provider.resolveActorRef(confirmMessage.getChannelEndpoint) else null) } } diff --git a/akka-persistence/src/test/scala/akka/persistence/ChannelSpec.scala b/akka-persistence/src/test/scala/akka/persistence/ChannelSpec.scala index 9ede279ea7..1de0863789 100644 --- a/akka-persistence/src/test/scala/akka/persistence/ChannelSpec.scala +++ b/akka-persistence/src/test/scala/akka/persistence/ChannelSpec.scala @@ -4,44 +4,36 @@ package akka.persistence +import scala.concurrent.duration._ +import scala.language.postfixOps + import com.typesafe.config._ import akka.actor._ import akka.testkit._ +import akka.persistence.JournalProtocol.Confirm + object ChannelSpec { - class TestProcessor(name: String, channelProps: Props) extends NamedProcessor(name) { - val destination = context.actorOf(Props[TestDestination]) - val channel = context.actorOf(channelProps) - - def receive = { - case m @ Persistent(s: String, _) if s.startsWith("a") ⇒ - // forward to destination via channel, - // destination replies to initial sender - channel forward Deliver(m.withPayload(s"fw: ${s}"), destination) - case m @ Persistent(s: String, _) if s.startsWith("b") ⇒ - // reply to sender via channel - channel ! Deliver(m.withPayload(s"re: ${s}"), sender) - } - } - class TestDestination extends Actor { def receive = { - case m: Persistent ⇒ sender ! m - } - } - - class TestReceiver(testActor: ActorRef) extends Actor { - def receive = { - case Persistent(payload, _) ⇒ testActor ! payload + case m: ConfirmablePersistent ⇒ sender ! m } } class TestDestinationProcessor(name: String) extends NamedProcessor(name) { def receive = { - case cp @ ConfirmablePersistent("a", _) ⇒ cp.confirm() - case cp @ ConfirmablePersistent("b", _) ⇒ cp.confirm() - case cp @ ConfirmablePersistent("boom", _) if (recoveryFinished) ⇒ throw new TestException("boom") + case cp @ ConfirmablePersistent("a", _, _) ⇒ cp.confirm() + case cp @ ConfirmablePersistent("b", _, _) ⇒ cp.confirm() + case cp @ ConfirmablePersistent("boom", _, _) if (recoveryFinished) ⇒ throw new TestException("boom") + } + } + + class TestReceiver(testActor: ActorRef) extends Actor { + def receive = { + case cp @ ConfirmablePersistent(payload, _, _) ⇒ + testActor ! payload + cp.confirm() } } } @@ -49,32 +41,29 @@ object ChannelSpec { abstract class ChannelSpec(config: Config) extends AkkaSpec(config) with PersistenceSpec with ImplicitSender { import ChannelSpec._ - override protected def beforeEach() { + protected var defaultTestChannel: ActorRef = _ + protected var redeliverTestChannel: ActorRef = _ + + override protected def beforeEach: Unit = { super.beforeEach() - - val confirmProbe = TestProbe() - val forwardProbe = TestProbe() - val replyProbe = TestProbe() - - val processor = system.actorOf(Props(classOf[TestProcessor], name, channelProps(s"${name}-channel"))) - - subscribeToConfirmation(confirmProbe) - - processor tell (Persistent("a1"), forwardProbe.ref) - processor tell (Persistent("b1"), replyProbe.ref) - - forwardProbe.expectMsgPF() { case m @ ConfirmablePersistent("fw: a1", _) ⇒ m.confirm() } - replyProbe.expectMsgPF() { case m @ ConfirmablePersistent("re: b1", _) ⇒ m.confirm() } - - awaitConfirmation(confirmProbe) - awaitConfirmation(confirmProbe) + defaultTestChannel = createDefaultTestChannel() + redeliverTestChannel = createRedeliverTestChannel() } - def actorRefFor(topLevelName: String) = - extension.system.provider.resolveActorRef(RootActorPath(Address("akka", system.name)) / "user" / topLevelName) + override protected def afterEach(): Unit = { + system.stop(defaultTestChannel) + system.stop(redeliverTestChannel) + super.afterEach() + } - def channelProps(channelId: String): Props = - Channel.props(channelId) + def redeliverChannelSettings: ChannelSettings = + ChannelSettings(redeliverMax = 2, redeliverInterval = 100 milliseconds) + + def createDefaultTestChannel(): ActorRef = + system.actorOf(Channel.props(name, ChannelSettings())) + + def createRedeliverTestChannel(): ActorRef = + system.actorOf(Channel.props(name, redeliverChannelSettings)) def subscribeToConfirmation(probe: TestProbe): Unit = system.eventStream.subscribe(probe.ref, classOf[Confirm]) @@ -82,196 +71,117 @@ abstract class ChannelSpec(config: Config) extends AkkaSpec(config) with Persist def awaitConfirmation(probe: TestProbe): Unit = probe.expectMsgType[Confirm] + def actorRefFor(topLevelName: String) = + extension.system.provider.resolveActorRef(RootActorPath(Address("akka", system.name)) / "user" / topLevelName) + "A channel" must { - "forward new messages to destination" in { - val processor = system.actorOf(Props(classOf[TestProcessor], name, channelProps(s"${name}-channel"))) - processor ! Persistent("a2") - expectMsgPF() { case m @ ConfirmablePersistent("fw: a2", _) ⇒ m.confirm() } - } - "reply new messages to senders" in { - val processor = system.actorOf(Props(classOf[TestProcessor], name, channelProps(s"${name}-channel"))) - processor ! Persistent("b2") - expectMsgPF() { case m @ ConfirmablePersistent("re: b2", _) ⇒ m.confirm() } - } - "forward un-confirmed stored messages to destination during recovery" in { - val confirmProbe = TestProbe() - val forwardProbe = TestProbe() - - subscribeToConfirmation(confirmProbe) - - val processor1 = system.actorOf(Props(classOf[TestProcessor], name, channelProps(s"${name}-channel"))) - - processor1 tell (Persistent("a1"), forwardProbe.ref) - processor1 tell (Persistent("a2"), forwardProbe.ref) - - forwardProbe.expectMsgPF() { case m @ ConfirmablePersistent("fw: a1", _) ⇒ /* no confirmation */ } - forwardProbe.expectMsgPF() { case m @ ConfirmablePersistent("fw: a2", _) ⇒ m.confirm() } - - awaitConfirmation(confirmProbe) - - val processor2 = system.actorOf(Props(classOf[TestProcessor], name, channelProps(s"${name}-channel"))) - - processor2 tell (Persistent("a3"), forwardProbe.ref) - - forwardProbe.expectMsgPF() { case m @ ConfirmablePersistent("fw: a1", _) ⇒ m.confirm() } // sender still valid, no need to resolve - forwardProbe.expectMsgPF() { case m @ ConfirmablePersistent("fw: a3", _) ⇒ m.confirm() } - - awaitConfirmation(confirmProbe) - awaitConfirmation(confirmProbe) - } "must resolve sender references and preserve message order" in { - val channel = system.actorOf(channelProps("channel-1")) val destination = system.actorOf(Props[TestDestination]) val empty = actorRefFor("testSender") // will be an EmptyLocalActorRef val sender = system.actorOf(Props(classOf[TestReceiver], testActor), "testSender") // replayed message (resolved = false) and invalid sender reference - channel tell (Deliver(PersistentRepr("a", resolved = false), destination, Resolve.Sender), empty) + defaultTestChannel tell (Deliver(PersistentRepr("a", resolved = false), destination, Resolve.Sender), empty) // new messages (resolved = true) and valid sender references - channel tell (Deliver(Persistent("b"), destination), sender) - channel tell (Deliver(Persistent("c"), destination), sender) + defaultTestChannel tell (Deliver(Persistent("b"), destination), sender) + defaultTestChannel tell (Deliver(Persistent("c"), destination), sender) expectMsg("a") expectMsg("b") expectMsg("c") } "must resolve destination references and preserve message order" in { - val channel = system.actorOf(channelProps("channel-2")) - val empty = actorRefFor("testDestination") // will be an EmptyLocalActorRef val destination = system.actorOf(Props(classOf[TestReceiver], testActor), "testDestination") // replayed message (resolved = false) and invalid destination reference - channel ! Deliver(PersistentRepr("a", resolved = false), empty, Resolve.Destination) + defaultTestChannel ! Deliver(PersistentRepr("a", resolved = false), empty, Resolve.Destination) // new messages (resolved = true) and valid destination references - channel ! Deliver(Persistent("b"), destination) - channel ! Deliver(Persistent("c"), destination) + defaultTestChannel ! Deliver(Persistent("b"), destination) + defaultTestChannel ! Deliver(Persistent("c"), destination) expectMsg("a") expectMsg("b") expectMsg("c") } "support processors as destination" in { - val channel = system.actorOf(channelProps(s"${name}-channel-new")) - val destination = system.actorOf(Props(classOf[TestDestinationProcessor], s"${name}-new")) + val destination = system.actorOf(Props(classOf[TestDestinationProcessor], name)) val confirmProbe = TestProbe() subscribeToConfirmation(confirmProbe) - channel ! Deliver(Persistent("a"), destination) + defaultTestChannel ! Deliver(Persistent("a"), destination) awaitConfirmation(confirmProbe) } "support processors as destination that may fail" in { - val channel = system.actorOf(channelProps(s"${name}-channel-new")) - val destination = system.actorOf(Props(classOf[TestDestinationProcessor], s"${name}-new")) + val destination = system.actorOf(Props(classOf[TestDestinationProcessor], name)) val confirmProbe = TestProbe() subscribeToConfirmation(confirmProbe) - channel ! Deliver(Persistent("a"), destination) - channel ! Deliver(Persistent("boom"), destination) - channel ! Deliver(Persistent("b"), destination) + defaultTestChannel ! Deliver(Persistent("a"), destination) + defaultTestChannel ! Deliver(Persistent("boom"), destination) + defaultTestChannel ! Deliver(Persistent("b"), destination) awaitConfirmation(confirmProbe) awaitConfirmation(confirmProbe) } "accept confirmable persistent messages for delivery" in { - val channel = system.actorOf(channelProps(s"${name}-channel-new")) val destination = system.actorOf(Props[TestDestination]) val confirmProbe = TestProbe() subscribeToConfirmation(confirmProbe) - channel ! Deliver(PersistentRepr("a", confirmable = true), destination) + defaultTestChannel ! Deliver(PersistentRepr("a", confirmable = true), destination) - expectMsgPF() { case m @ ConfirmablePersistent("a", _) ⇒ m.confirm() } + expectMsgPF() { case m @ ConfirmablePersistent("a", _, _) ⇒ m.confirm() } awaitConfirmation(confirmProbe) } - } -} + "redeliver on missing confirmation" in { + val probe = TestProbe() -abstract class PersistentChannelSpec(config: Config) extends ChannelSpec(config) { - override def channelProps(channelId: String): Props = - PersistentChannel.props(channelId) + redeliverTestChannel ! Deliver(Persistent("b"), probe.ref) - override def subscribeToConfirmation(probe: TestProbe): Unit = - system.eventStream.subscribe(probe.ref, classOf[JournalProtocol.Delete]) - - override def awaitConfirmation(probe: TestProbe): Unit = - probe.expectMsgType[JournalProtocol.Delete] - - "A persistent channel" must { - "support disabling and re-enabling delivery" in { - val channel = system.actorOf(channelProps(s"${name}-channel")) - val confirmProbe = TestProbe() - - subscribeToConfirmation(confirmProbe) - - channel ! Deliver(Persistent("a"), testActor) - - expectMsgPF() { case m @ ConfirmablePersistent("a", _) ⇒ m.confirm() } - awaitConfirmation(confirmProbe) - - channel ! DisableDelivery - channel ! Deliver(Persistent("b"), testActor) - channel ! EnableDelivery - channel ! Deliver(Persistent("c"), testActor) - - expectMsgPF() { case m @ ConfirmablePersistent("b", _) ⇒ m.confirm() } - expectMsgPF() { case m @ ConfirmablePersistent("c", _) ⇒ m.confirm() } + probe.expectMsgPF() { case m @ ConfirmablePersistent("b", _, redeliveries) ⇒ redeliveries must be(0) } + probe.expectMsgPF() { case m @ ConfirmablePersistent("b", _, redeliveries) ⇒ redeliveries must be(1) } + probe.expectMsgPF() { case m @ ConfirmablePersistent("b", _, redeliveries) ⇒ redeliveries must be(2); m.confirm() } } - "support Persistent replies to Deliver senders" in { - val channel = system.actorOf(PersistentChannel.props(s"${name}-channel-new", true)) + "redeliver in correct relative order" in { + val deliveries = redeliverChannelSettings.redeliverMax + 1 + val interval = redeliverChannelSettings.redeliverInterval.toMillis / 5 * 4 - channel ! Deliver(Persistent("a"), system.deadLetters) - expectMsgPF() { case Persistent("a", 1) ⇒ } + val probe = TestProbe() + val cycles = 9 - channel ! Deliver(PersistentRepr("b", sequenceNr = 13), system.deadLetters) - expectMsgPF() { case Persistent("b", 13) ⇒ } - } - "must not modify certain persistent message field" in { - val channel = system.actorOf(channelProps(s"${name}-channel-new")) - val persistent1 = PersistentRepr(payload = "a", processorId = "p1", confirms = List("c1", "c2"), sender = channel, sequenceNr = 13) - val persistent2 = PersistentRepr(payload = "b", processorId = "p1", confirms = List("c1", "c2"), sender = channel) - - channel ! Deliver(persistent1, testActor) - channel ! Deliver(persistent2, testActor) - - expectMsgPF() { case ConfirmablePersistentImpl("a", 13, "p1", _, _, Seq("c1", "c2"), _, _, channel) ⇒ } - expectMsgPF() { case ConfirmablePersistentImpl("b", 2, "p1", _, _, Seq("c1", "c2"), _, _, channel) ⇒ } - } - } - - "A persistent channel" when { - "used standalone" must { - "redeliver un-confirmed stored messages during recovery" in { - val confirmProbe = TestProbe() - val forwardProbe = TestProbe() - - subscribeToConfirmation(confirmProbe) - - val channel1 = system.actorOf(channelProps(s"${name}-channel")) - channel1 tell (Deliver(Persistent("a1"), forwardProbe.ref), null) - channel1 tell (Deliver(Persistent("a2"), forwardProbe.ref), null) - - forwardProbe.expectMsgPF() { case m @ ConfirmablePersistent("a1", _) ⇒ /* no confirmation */ } - forwardProbe.expectMsgPF() { case m @ ConfirmablePersistent("a2", _) ⇒ m.confirm() } - - awaitConfirmation(confirmProbe) - - val channel2 = system.actorOf(channelProps(s"${name}-channel")) - channel2 tell (Deliver(Persistent("a3"), forwardProbe.ref), null) - - forwardProbe.expectMsgPF() { case m @ ConfirmablePersistent("a1", _) ⇒ m.confirm() } // sender still valid, no need to resolve - forwardProbe.expectMsgPF() { case m @ ConfirmablePersistent("a3", _) ⇒ m.confirm() } - - awaitConfirmation(confirmProbe) - awaitConfirmation(confirmProbe) + 1 to cycles foreach { i ⇒ + redeliverTestChannel ! Deliver(Persistent(i), probe.ref) + Thread.sleep(interval) } + + val received = (1 to (cycles * deliveries)).foldLeft(Vector.empty[ConfirmablePersistent]) { + case (acc, _) ⇒ acc :+ probe.expectMsgType[ConfirmablePersistent] + } + + val grouped = received.groupBy(_.redeliveries) + val expected = 1 to 9 toVector + + grouped(0).map(_.payload) must be(expected) + grouped(1).map(_.payload) must be(expected) + grouped(2).map(_.payload) must be(expected) + } + "redeliver not more than redeliverMax on missing confirmation" in { + val probe = TestProbe() + + redeliverTestChannel ! Deliver(PersistentRepr("a"), probe.ref) + + probe.expectMsgPF() { case m @ ConfirmablePersistent("a", _, redeliveries) ⇒ redeliveries must be(0) } + probe.expectMsgPF() { case m @ ConfirmablePersistent("a", _, redeliveries) ⇒ redeliveries must be(1) } + probe.expectMsgPF() { case m @ ConfirmablePersistent("a", _, redeliveries) ⇒ redeliveries must be(2) } + probe.expectNoMsg(300 milliseconds) } } } @@ -279,5 +189,3 @@ abstract class PersistentChannelSpec(config: Config) extends ChannelSpec(config) class LeveldbChannelSpec extends ChannelSpec(PersistenceSpec.config("leveldb", "channel")) class InmemChannelSpec extends ChannelSpec(PersistenceSpec.config("inmem", "channel")) -class LeveldbPersistentChannelSpec extends PersistentChannelSpec(PersistenceSpec.config("leveldb", "persistent-channel")) -class InmemPersistentChannelSpec extends PersistentChannelSpec(PersistenceSpec.config("inmem", "persistent-channel")) diff --git a/akka-persistence/src/test/scala/akka/persistence/FailureSpec.scala b/akka-persistence/src/test/scala/akka/persistence/FailureSpec.scala index fd4c8bbcd2..87b37fee3e 100644 --- a/akka-persistence/src/test/scala/akka/persistence/FailureSpec.scala +++ b/akka-persistence/src/test/scala/akka/persistence/FailureSpec.scala @@ -18,6 +18,7 @@ object FailureSpec { s""" akka.persistence.processor.chaos.live-processing-failure-rate = 0.3 akka.persistence.processor.chaos.replay-processing-failure-rate = 0.1 + akka.persistence.destination.chaos.confirm-failure-rate = 0.3 akka.persistence.journal.plugin = "akka.persistence.journal.chaos" akka.persistence.journal.chaos.write-failure-rate = 0.3 akka.persistence.journal.chaos.delete-failure-rate = 0.3 @@ -34,28 +35,42 @@ object FailureSpec { case class ProcessingFailure(i: Int) case class JournalingFailure(i: Int) - class ChaosProcessor extends Processor with ActorLogging { + trait ChaosSupport { this: Actor ⇒ + def random = ThreadLocalRandom.current + + var state = Vector.empty[Int] + + def contains(i: Int): Boolean = + state.contains(i) + + def add(i: Int): Unit = { + state :+= i + if (state.length == numMessages) sender ! Done(state) + } + + def shouldFail(rate: Double) = + random.nextDouble() < rate + } + + class ChaosProcessor(destination: ActorRef) extends Processor with ChaosSupport with ActorLogging { val config = context.system.settings.config.getConfig("akka.persistence.processor.chaos") val liveProcessingFailureRate = config.getDouble("live-processing-failure-rate") val replayProcessingFailureRate = config.getDouble("replay-processing-failure-rate") - // processor state - var ints = Vector.empty[Int] + val channel = context.actorOf(Channel.props("channel", ChannelSettings(redeliverMax = 10, redeliverInterval = 500 milliseconds)), "channel") override def processorId = "chaos" - def random = ThreadLocalRandom.current - def receive = { - case Persistent(i: Int, _) ⇒ + case p @ Persistent(i: Int, _) ⇒ val failureRate = if (recoveryRunning) replayProcessingFailureRate else liveProcessingFailureRate - if (ints.contains(i)) { + if (contains(i)) { log.debug(debugMessage(s"ignored duplicate ${i}")) } else if (shouldFail(failureRate)) { throw new TestException(debugMessage(s"rejected payload ${i}")) } else { - ints :+= i - if (ints.length == numMessages) sender ! Done(ints) + add(i) + channel forward Deliver(p, destination) log.debug(debugMessage(s"processed payload ${i}")) } case PersistenceFailure(i: Int, _, _) ⇒ @@ -78,15 +93,34 @@ object FailureSpec { super.preRestart(reason, message) } - private def shouldFail(rate: Double) = - random.nextDouble() < rate - private def debugMessage(msg: String): String = - s"${msg} (mode = ${if (recoveryRunning) "replay" else "live"} snr = ${lastSequenceNr} state = ${ints.sorted})" + s"[processor] ${msg} (mode = ${if (recoveryRunning) "replay" else "live"} snr = ${lastSequenceNr} state = ${state.sorted})" + } + + class ChaosDestination extends Actor with ChaosSupport with ActorLogging { + val config = context.system.settings.config.getConfig("akka.persistence.destination.chaos") + val confirmFailureRate = config.getDouble("confirm-failure-rate") + + def receive = { + case cp @ ConfirmablePersistent(i: Int, _, _) ⇒ + if (shouldFail(confirmFailureRate)) { + log.error(debugMessage("confirm message failed", cp)) + } else if (contains(i)) { + log.debug(debugMessage("ignored duplicate", cp)) + } else { + add(i) + cp.confirm() + log.debug(debugMessage("received and confirmed message", cp)) + } + } + + private def debugMessage(msg: String, cp: ConfirmablePersistent): String = + s"[destination] ${msg} (message = ConfirmablePersistent(${cp.payload}, ${cp.sequenceNr}, ${cp.redeliveries}), state = ${state.sorted})" } class ChaosProcessorApp(probe: ActorRef) extends Actor with ActorLogging { - val processor = context.actorOf(Props[ChaosProcessor]) + val destination = context.actorOf(Props[ChaosDestination], "destination") + val processor = context.actorOf(Props(classOf[ChaosProcessor], destination), "processor") def receive = { case Start ⇒ 1 to numMessages foreach (processor ! Persistent(_)) @@ -107,10 +141,15 @@ class FailureSpec extends AkkaSpec(FailureSpec.config) with Cleanup with Implici "The journaling protocol (= conversation between a processor and a journal)" must { "tolerate and recover from random failures" in { system.actorOf(Props(classOf[ChaosProcessorApp], testActor)) ! Start - expectMsgPF(numMessages seconds) { case Done(ints) ⇒ ints.sorted must be(1 to numMessages toVector) } + expectDone() // by processor + expectDone() // by destination system.actorOf(Props(classOf[ChaosProcessorApp], testActor)) // recovery of new instance must have same outcome - expectMsgPF(numMessages seconds) { case Done(ints) ⇒ ints.sorted must be(1 to numMessages toVector) } + expectDone() // by processor + // destination doesn't receive messages again because all have been confirmed already } } + + def expectDone() = + expectMsgPF(numMessages seconds) { case Done(ints) ⇒ ints.sorted must be(1 to numMessages toVector) } } diff --git a/akka-persistence/src/test/scala/akka/persistence/PerformanceSpec.scala b/akka-persistence/src/test/scala/akka/persistence/PerformanceSpec.scala index a6606d7022..87abc6a903 100644 --- a/akka-persistence/src/test/scala/akka/persistence/PerformanceSpec.scala +++ b/akka-persistence/src/test/scala/akka/persistence/PerformanceSpec.scala @@ -21,7 +21,7 @@ object PerformanceSpec { case object StopMeasure case class FailAt(sequenceNr: Long) - abstract class PerformanceTestProcessor(name: String) extends NamedProcessor(name) { + trait Measure extends { this: Actor ⇒ val NanoToSecond = 1000.0 * 1000 * 1000 var startTime: Long = 0L @@ -30,16 +30,43 @@ object PerformanceSpec { var startSequenceNr = 0L; var stopSequenceNr = 0L; + def startMeasure(): Unit = { + startSequenceNr = lastSequenceNr + startTime = System.nanoTime + } + + def stopMeasure(): Unit = { + stopSequenceNr = lastSequenceNr + stopTime = System.nanoTime + sender ! (NanoToSecond * (stopSequenceNr - startSequenceNr) / (stopTime - startTime)) + } + + def lastSequenceNr: Long + } + + class PerformanceTestDestination extends Actor with Measure { + var lastSequenceNr = 0L + + val confirm: PartialFunction[Any, Any] = { + case cp @ ConfirmablePersistent(payload, sequenceNr, _) ⇒ + lastSequenceNr = sequenceNr + cp.confirm() + payload + } + + def receive = confirm andThen { + case StartMeasure ⇒ startMeasure() + case StopMeasure ⇒ stopMeasure() + case m ⇒ if (lastSequenceNr % 1000 == 0) print(".") + } + } + + abstract class PerformanceTestProcessor(name: String) extends NamedProcessor(name) with Measure { var failAt: Long = -1 val controlBehavior: Receive = { - case StartMeasure ⇒ - startSequenceNr = lastSequenceNr - startTime = System.nanoTime - case StopMeasure ⇒ - stopSequenceNr = lastSequenceNr - stopTime = System.nanoTime - sender ! (NanoToSecond * (stopSequenceNr - startSequenceNr) / (stopTime - startTime)) + case StartMeasure ⇒ startMeasure() + case StopMeasure ⇒ stopMeasure() case FailAt(sequenceNr) ⇒ failAt = sequenceNr } @@ -136,6 +163,18 @@ class PerformanceSpec extends AkkaSpec(PersistenceSpec.config("leveldb", "perfor } } + def stressPersistentChannel(): Unit = { + val channel = system.actorOf(PersistentChannel.props()) + val destination = system.actorOf(Props[PerformanceTestDestination]) + 1 to warmupCycles foreach { i ⇒ channel ! Deliver(Persistent(s"msg${i}"), destination) } + channel ! Deliver(Persistent(StartMeasure), destination) + 1 to loadCycles foreach { i ⇒ channel ! Deliver(Persistent(s"msg${i}"), destination) } + channel ! Deliver(Persistent(StopMeasure), destination) + expectMsgPF(100 seconds) { + case throughput: Double ⇒ println(f"\nthroughput = $throughput%.2f persistent commands per second") + } + } + "A command sourced processor" should { "have some reasonable throughput" in { stressCommandsourcedProcessor(None) @@ -156,4 +195,10 @@ class PerformanceSpec extends AkkaSpec(PersistenceSpec.config("leveldb", "perfor stressStashingEventsourcedProcessor() } } + + "A persistent channel" should { + "have some reasonable throughput" in { + stressPersistentChannel() + } + } } diff --git a/akka-persistence/src/test/scala/akka/persistence/PersistenceSpec.scala b/akka-persistence/src/test/scala/akka/persistence/PersistenceSpec.scala index c1192ced3e..6becf0bc8d 100644 --- a/akka-persistence/src/test/scala/akka/persistence/PersistenceSpec.scala +++ b/akka-persistence/src/test/scala/akka/persistence/PersistenceSpec.scala @@ -32,7 +32,7 @@ trait PersistenceSpec extends BeforeAndAfterEach with Cleanup { this: AkkaSpec /** * Prefix for generating a unique name per test. */ - def namePrefix: String = "processor" + def namePrefix: String = "test" /** * Creates a processor with current name as constructor argument. @@ -41,7 +41,7 @@ trait PersistenceSpec extends BeforeAndAfterEach with Cleanup { this: AkkaSpec system.actorOf(Props(implicitly[ClassTag[T]].runtimeClass, name)) override protected def beforeEach() { - _name = namePrefix + counter.incrementAndGet() + _name = s"${namePrefix}-${counter.incrementAndGet()}" } } diff --git a/akka-persistence/src/test/scala/akka/persistence/PersistentChannelSpec.scala b/akka-persistence/src/test/scala/akka/persistence/PersistentChannelSpec.scala new file mode 100644 index 0000000000..9892f82f5f --- /dev/null +++ b/akka-persistence/src/test/scala/akka/persistence/PersistentChannelSpec.scala @@ -0,0 +1,103 @@ +/** + * Copyright (C) 2009-2013 Typesafe Inc. + */ + +package akka.persistence + +import scala.concurrent.duration._ +import scala.language.postfixOps + +import com.typesafe.config._ + +import akka.actor._ +import akka.testkit._ + +abstract class PersistentChannelSpec(config: Config) extends ChannelSpec(config) { + override def redeliverChannelSettings: PersistentChannelSettings = + PersistentChannelSettings(redeliverMax = 2, redeliverInterval = 100 milliseconds) + + override def createDefaultTestChannel(): ActorRef = + system.actorOf(PersistentChannel.props(name, PersistentChannelSettings())) + + override def createRedeliverTestChannel(): ActorRef = + system.actorOf(PersistentChannel.props(name, redeliverChannelSettings)) + + "A persistent channel" must { + "support disabling and re-enabling delivery" in { + val confirmProbe = TestProbe() + + subscribeToConfirmation(confirmProbe) + + defaultTestChannel ! Deliver(Persistent("a"), testActor) + + expectMsgPF() { case m @ ConfirmablePersistent("a", _, _) ⇒ m.confirm() } + awaitConfirmation(confirmProbe) + + defaultTestChannel ! DisableDelivery + defaultTestChannel ! Deliver(Persistent("b"), testActor) + defaultTestChannel ! EnableDelivery + defaultTestChannel ! Deliver(Persistent("c"), testActor) + + expectMsgPF() { case m @ ConfirmablePersistent("b", _, _) ⇒ m.confirm() } + expectMsgPF() { case m @ ConfirmablePersistent("c", _, _) ⇒ m.confirm() } + } + "support Persistent replies to Deliver senders" in { + val channel1 = system.actorOf(PersistentChannel.props(s"${name}-with-reply", PersistentChannelSettings(replyPersistent = true))) + + channel1 ! Deliver(Persistent("a"), system.deadLetters) + expectMsgPF() { case Persistent("a", 1) ⇒ } + + channel1 ! Deliver(PersistentRepr("b", sequenceNr = 13), system.deadLetters) + expectMsgPF() { case Persistent("b", 13) ⇒ } + + system.stop(channel1) + } + "must not modify certain persistent message field" in { + val persistent1 = PersistentRepr(payload = "a", processorId = "p1", confirms = List("c1", "c2"), sender = defaultTestChannel, sequenceNr = 13) + val persistent2 = PersistentRepr(payload = "b", processorId = "p1", confirms = List("c1", "c2"), sender = defaultTestChannel) + + defaultTestChannel ! Deliver(persistent1, testActor) + defaultTestChannel ! Deliver(persistent2, testActor) + + expectMsgPF() { case cp @ ConfirmablePersistentImpl("a", 13, "p1", _, _, _, Seq("c1", "c2"), _, _, channel) ⇒ cp.confirm() } + expectMsgPF() { case cp @ ConfirmablePersistentImpl("b", 2, "p1", _, _, _, Seq("c1", "c2"), _, _, channel) ⇒ cp.confirm() } + } + } + + "A persistent channel" when { + "used standalone" must { + "redeliver un-confirmed stored messages during recovery" in { + val confirmProbe = TestProbe() + val forwardProbe = TestProbe() + + subscribeToConfirmation(confirmProbe) + + val channel1 = createDefaultTestChannel() + channel1 tell (Deliver(Persistent("a1"), forwardProbe.ref), null) + channel1 tell (Deliver(Persistent("a2"), forwardProbe.ref), null) + + forwardProbe.expectMsgPF() { case m @ ConfirmablePersistent("a1", _, _) ⇒ /* no confirmation */ } + forwardProbe.expectMsgPF() { case m @ ConfirmablePersistent("a2", _, _) ⇒ m.confirm() } + + awaitConfirmation(confirmProbe) + + system.stop(channel1) + + val channel2 = createDefaultTestChannel() + channel2 tell (Deliver(Persistent("a3"), forwardProbe.ref), null) + + forwardProbe.expectMsgPF() { case m @ ConfirmablePersistent("a1", _, _) ⇒ m.confirm() } // sender still valid, no need to resolve + forwardProbe.expectMsgPF() { case m @ ConfirmablePersistent("a3", _, _) ⇒ m.confirm() } + + awaitConfirmation(confirmProbe) + awaitConfirmation(confirmProbe) + + system.stop(channel2) + } + } + } +} + +class LeveldbPersistentChannelSpec extends PersistentChannelSpec(PersistenceSpec.config("leveldb", "persistent-channel")) +class InmemPersistentChannelSpec extends PersistentChannelSpec(PersistenceSpec.config("inmem", "persistent-channel")) + diff --git a/akka-persistence/src/test/scala/akka/persistence/ProcessorChannelSpec.scala b/akka-persistence/src/test/scala/akka/persistence/ProcessorChannelSpec.scala new file mode 100644 index 0000000000..852967a2ff --- /dev/null +++ b/akka-persistence/src/test/scala/akka/persistence/ProcessorChannelSpec.scala @@ -0,0 +1,162 @@ +/** + * Copyright (C) 2009-2013 Typesafe Inc. + */ + +package akka.persistence + +import scala.concurrent.duration._ +import scala.language.postfixOps + +import com.typesafe.config._ + +import akka.actor._ +import akka.testkit._ + +import akka.persistence.JournalProtocol.Confirm + +object ProcessorChannelSpec { + class TestProcessor(name: String) extends NamedProcessor(name) { + val destination = context.actorOf(Props[TestDestination]) + val channel = context.actorOf(Channel.props(s"${name}-channel")) + + def receive = { + case m @ Persistent(s: String, _) if s.startsWith("a") ⇒ + // forward to destination via channel, + // destination replies to initial sender + channel forward Deliver(m.withPayload(s"fw: ${s}"), destination) + case m @ Persistent(s: String, _) if s.startsWith("b") ⇒ + // reply to sender via channel + channel ! Deliver(m.withPayload(s"re: ${s}"), sender) + } + } + + class TestDestination extends Actor { + def receive = { + case m: Persistent ⇒ sender ! m + } + } + + class ResendingProcessor(name: String, destination: ActorRef) extends NamedProcessor(name) { + val channel = context.actorOf(Channel.props("channel", ChannelSettings(redeliverMax = 1, redeliverInterval = 100 milliseconds))) + + def receive = { + case p: Persistent ⇒ channel ! Deliver(p, destination) + case "replay" ⇒ throw new TestException("replay requested") + } + } + + class ResendingEventsourcedProcessor(name: String, destination: ActorRef) extends NamedProcessor(name) with EventsourcedProcessor { + val channel = context.actorOf(Channel.props("channel", ChannelSettings(redeliverMax = 1, redeliverInterval = 100 milliseconds))) + + var events: List[String] = Nil + + def handleEvent(event: String) = { + events = event :: events + channel ! Deliver(Persistent(event), destination) + } + + def receiveReplay: Receive = { + case event: String ⇒ handleEvent(event) + } + + def receiveCommand: Receive = { + case "cmd" ⇒ persist("evt")(handleEvent) + case "replay" ⇒ throw new TestException("replay requested") + } + } +} + +abstract class ProcessorChannelSpec(config: Config) extends AkkaSpec(config) with PersistenceSpec with ImplicitSender { + import ProcessorChannelSpec._ + + private var processor: ActorRef = _ + + override protected def beforeEach: Unit = { + super.beforeEach() + setupTestProcessorData() + processor = createTestProcessor() + } + + override protected def afterEach(): Unit = { + system.stop(processor) + super.afterEach() + } + + def subscribeToConfirmation(probe: TestProbe): Unit = + system.eventStream.subscribe(probe.ref, classOf[Confirm]) + + def awaitConfirmation(probe: TestProbe): Unit = + probe.expectMsgType[Confirm] + + def createTestProcessor(): ActorRef = + system.actorOf(Props(classOf[TestProcessor], name)) + + def setupTestProcessorData(): Unit = { + val confirmProbe = TestProbe() + val forwardProbe = TestProbe() + val replyProbe = TestProbe() + + val processor = createTestProcessor() + + subscribeToConfirmation(confirmProbe) + + processor tell (Persistent("a1"), forwardProbe.ref) + processor tell (Persistent("b1"), replyProbe.ref) + + forwardProbe.expectMsgPF() { case m @ ConfirmablePersistent("fw: a1", _, _) ⇒ m.confirm() } + replyProbe.expectMsgPF() { case m @ ConfirmablePersistent("re: b1", _, _) ⇒ m.confirm() } + + awaitConfirmation(confirmProbe) + awaitConfirmation(confirmProbe) + + system.stop(processor) + } + + "A processor that uses a channel" can { + "forward new messages to destination" in { + processor ! Persistent("a2") + expectMsgPF() { case m @ ConfirmablePersistent("fw: a2", _, _) ⇒ m.confirm() } + } + "reply new messages to senders" in { + processor ! Persistent("b2") + expectMsgPF() { case m @ ConfirmablePersistent("re: b2", _, _) ⇒ m.confirm() } + } + "resend unconfirmed messages on restart" in { + val probe = TestProbe() + val p = system.actorOf(Props(classOf[ResendingProcessor], "rp", probe.ref)) + + p ! Persistent("a") + + probe.expectMsgPF() { case cp @ ConfirmablePersistent("a", 1L, 0) ⇒ } + probe.expectMsgPF() { case cp @ ConfirmablePersistent("a", 1L, 1) ⇒ } + probe.expectNoMsg(200 milliseconds) + + p ! "replay" + + probe.expectMsgPF() { case cp @ ConfirmablePersistent("a", 1L, 0) ⇒ } + probe.expectMsgPF() { case cp @ ConfirmablePersistent("a", 1L, 1) ⇒ cp.confirm() } + } + } + + "An eventsourced processor that uses a channel" can { + "reliably deliver events" in { + val probe = TestProbe() + val ep = system.actorOf(Props(classOf[ResendingEventsourcedProcessor], "rep", probe.ref)) + + ep ! "cmd" + + probe.expectMsgPF() { case cp @ ConfirmablePersistent("evt", 1L, 0) ⇒ } + probe.expectMsgPF() { case cp @ ConfirmablePersistent("evt", 1L, 1) ⇒ } + probe.expectNoMsg(200 milliseconds) + + ep ! "replay" + + probe.expectMsgPF() { case cp @ ConfirmablePersistent("evt", 1L, 0) ⇒ } + probe.expectMsgPF() { case cp @ ConfirmablePersistent("evt", 1L, 1) ⇒ cp.confirm() } + } + } +} + +class LeveldbProcessorChannelSpec extends ProcessorChannelSpec(PersistenceSpec.config("leveldb", "channel")) +class InmemProcessorChannelSpec extends ProcessorChannelSpec(PersistenceSpec.config("inmem", "channel")) + diff --git a/akka-persistence/src/test/scala/akka/persistence/serialization/SerializerSpec.scala b/akka-persistence/src/test/scala/akka/persistence/serialization/SerializerSpec.scala index 3a92e9c20f..48a7845122 100644 --- a/akka-persistence/src/test/scala/akka/persistence/serialization/SerializerSpec.scala +++ b/akka-persistence/src/test/scala/akka/persistence/serialization/SerializerSpec.scala @@ -10,6 +10,7 @@ import com.typesafe.config._ import akka.actor._ import akka.persistence._ +import akka.persistence.JournalProtocol.Confirm import akka.serialization._ import akka.testkit._ @@ -75,7 +76,7 @@ class MessageSerializerPersistenceSpec extends AkkaSpec(config(customSerializers "A message serializer" when { "not given a manifest" must { "handle custom ConfirmablePersistent message serialization" in { - val persistent = PersistentRepr(MyPayload("a"), 13, "p1", true, true, List("c1", "c2"), confirmable = true, Confirm("p2", 14, "c2"), testActor, testActor) + val persistent = PersistentRepr(MyPayload("a"), 13, "p1", true, true, 3, List("c1", "c2"), confirmable = true, Confirm("p2", 14, "c2"), testActor, testActor) val serializer = serialization.findSerializerFor(persistent) val bytes = serializer.toBinary(persistent) @@ -84,7 +85,7 @@ class MessageSerializerPersistenceSpec extends AkkaSpec(config(customSerializers deserialized must be(persistent.withPayload(MyPayload(".a."))) } "handle custom Persistent message serialization" in { - val persistent = PersistentRepr(MyPayload("a"), 13, "p1", true, true, List("c1", "c2"), confirmable = false, Confirm("p2", 14, "c2"), testActor, testActor) + val persistent = PersistentRepr(MyPayload("a"), 13, "p1", true, true, 0, List("c1", "c2"), confirmable = false, Confirm("p2", 14, "c2"), testActor, testActor) val serializer = serialization.findSerializerFor(persistent) val bytes = serializer.toBinary(persistent) @@ -95,7 +96,7 @@ class MessageSerializerPersistenceSpec extends AkkaSpec(config(customSerializers } "given a PersistentRepr manifest" must { "handle custom ConfirmablePersistent message serialization" in { - val persistent = PersistentRepr(MyPayload("b"), 13, "p1", true, true, List("c1", "c2"), confirmable = true, Confirm("p2", 14, "c2"), testActor, testActor) + val persistent = PersistentRepr(MyPayload("b"), 13, "p1", true, true, 3, List("c1", "c2"), confirmable = true, Confirm("p2", 14, "c2"), testActor, testActor) val serializer = serialization.findSerializerFor(persistent) val bytes = serializer.toBinary(persistent) @@ -104,7 +105,7 @@ class MessageSerializerPersistenceSpec extends AkkaSpec(config(customSerializers deserialized must be(persistent.withPayload(MyPayload(".b."))) } "handle custom Persistent message serialization" in { - val persistent = PersistentRepr(MyPayload("b"), 13, "p1", true, true, List("c1", "c2"), confirmable = true, Confirm("p2", 14, "c2"), testActor, testActor) + val persistent = PersistentRepr(MyPayload("b"), 13, "p1", true, true, 3, List("c1", "c2"), confirmable = true, Confirm("p2", 14, "c2"), testActor, testActor) val serializer = serialization.findSerializerFor(persistent) val bytes = serializer.toBinary(persistent) @@ -137,9 +138,9 @@ object MessageSerializerRemotingSpec { class RemoteActor extends Actor { def receive = { case PersistentBatch(Persistent(MyPayload(data), _) +: tail) ⇒ sender ! s"b${data}" - case ConfirmablePersistent(MyPayload(data), _) ⇒ sender ! s"c${data}" + case ConfirmablePersistent(MyPayload(data), _, _) ⇒ sender ! s"c${data}" case Persistent(MyPayload(data), _) ⇒ sender ! s"p${data}" - case Confirm(pid, snr, cid) ⇒ sender ! s"${pid},${snr},${cid}" + case p @ Confirm(pid, msnr, cid, wsnr, ep) ⇒ sender ! s"${pid},${msnr},${cid},${wsnr},${ep.path.name.startsWith("testActor")}" } } @@ -176,8 +177,8 @@ class MessageSerializerRemotingSpec extends AkkaSpec(config(systemA).withFallbac expectMsg("b.a.") } "serialize Confirm messages during remoting" in { - localActor ! Confirm("a", 2, "b") - expectMsg("a,2,b") + localActor ! Confirm("a", 2, "b", 3, testActor) + expectMsg("a,2,b,3,true") } } } diff --git a/akka-samples/akka-sample-persistence/src/main/scala/sample/persistence/ConversationRecoveryExample.scala b/akka-samples/akka-sample-persistence/src/main/scala/sample/persistence/ConversationRecoveryExample.scala index 04a830922e..2dc6eabde6 100644 --- a/akka-samples/akka-sample-persistence/src/main/scala/sample/persistence/ConversationRecoveryExample.scala +++ b/akka-samples/akka-sample-persistence/src/main/scala/sample/persistence/ConversationRecoveryExample.scala @@ -16,7 +16,7 @@ object ConversationRecoveryExample extends App { var counter = 0 def receive = { - case m @ ConfirmablePersistent(Ping, _) ⇒ + case m @ ConfirmablePersistent(Ping, _, _) ⇒ counter += 1 println(s"received ping ${counter} times ...") m.confirm() @@ -33,7 +33,7 @@ object ConversationRecoveryExample extends App { var counter = 0 def receive = { - case m @ ConfirmablePersistent(Pong, _) ⇒ + case m @ ConfirmablePersistent(Pong, _, _) ⇒ counter += 1 println(s"received pong ${counter} times ...") m.confirm() diff --git a/akka-samples/akka-sample-persistence/src/main/scala/sample/persistence/ProcessorChannelExample.scala b/akka-samples/akka-sample-persistence/src/main/scala/sample/persistence/ProcessorChannelExample.scala index 0735334ed8..e4b6045bf6 100644 --- a/akka-samples/akka-sample-persistence/src/main/scala/sample/persistence/ProcessorChannelExample.scala +++ b/akka-samples/akka-sample-persistence/src/main/scala/sample/persistence/ProcessorChannelExample.scala @@ -24,7 +24,7 @@ object ProcessorChannelExample extends App { class ExampleDestination extends Actor { def receive = { - case p @ ConfirmablePersistent(payload, snr) ⇒ + case p @ ConfirmablePersistent(payload, snr, _) ⇒ println(s"received ${payload}") sender ! s"re: ${payload} (${snr})" p.confirm()