!per #15377 Mandate atomic writes for persistAll, and support rejections
* changing Plugin API for asyncWriteMessages and writeMessages * passing explicit AtomicWrite that represents the events of persistAll, or a single event from persist * journal may reject events before storing them, and that will result in onPersistRejected (logging) and continue in the persistent actor * clarified the semantics with regards to batches and atomic writes, and failures and rejections in the api docs of asyncWriteMessages and writeMessages * adjust the Java plugin API, asyncReplayMessages, doLoadAsync
This commit is contained in:
parent
33ee447ec9
commit
8c47e01e9d
38 changed files with 1500 additions and 216 deletions
|
|
@ -8,10 +8,12 @@ package akka.persistence.journal
|
|||
import akka.actor._
|
||||
import akka.pattern.pipe
|
||||
import akka.persistence._
|
||||
|
||||
import scala.collection.immutable
|
||||
import scala.concurrent.Future
|
||||
import scala.util._
|
||||
import scala.util.control.NonFatal
|
||||
import scala.util.Try
|
||||
import scala.util.Success
|
||||
import scala.util.Failure
|
||||
|
||||
/**
|
||||
* Abstract journal, optimized for asynchronous, non-blocking writes.
|
||||
|
|
@ -27,24 +29,68 @@ trait AsyncWriteJournal extends Actor with WriteJournalBase with AsyncRecovery {
|
|||
private val resequencer = context.actorOf(Props[Resequencer]())
|
||||
private var resequencerCounter = 1L
|
||||
|
||||
final def receive = receiveWriteJournal orElse receivePluginInternal
|
||||
final def receive = receiveWriteJournal.orElse[Any, Unit](receivePluginInternal)
|
||||
|
||||
final val receiveWriteJournal: Actor.Receive = {
|
||||
case WriteMessages(messages, persistentActor, actorInstanceId) ⇒
|
||||
val cctr = resequencerCounter
|
||||
def resequence(f: PersistentRepr ⇒ Any) = messages.zipWithIndex.foreach {
|
||||
case (p: PersistentRepr, i) ⇒ resequencer ! Desequenced(f(p), cctr + i + 1, persistentActor, p.sender)
|
||||
case (r, i) ⇒ resequencer ! Desequenced(LoopMessageSuccess(r.payload, actorInstanceId), cctr + i + 1, persistentActor, r.sender)
|
||||
resequencerCounter += messages.foldLeft(0)((acc, m) ⇒ acc + m.size) + 1
|
||||
|
||||
val prepared = Try(preparePersistentBatch(messages))
|
||||
val writeResult = (prepared match {
|
||||
case Success(prep) ⇒
|
||||
// in case the asyncWriteMessages throws
|
||||
try asyncWriteMessages(prep) catch { case NonFatal(e) ⇒ Future.failed(e) }
|
||||
case f @ Failure(_) ⇒
|
||||
// exception from preparePersistentBatch => rejected
|
||||
Future.successful(messages.collect { case a: AtomicWrite ⇒ f })
|
||||
}).map { results ⇒
|
||||
if (results.size != prepared.get.size)
|
||||
throw new IllegalStateException("asyncWriteMessages returned invalid number of results. " +
|
||||
s"Expected [${prepared.get.size}], but got [${results.size}]")
|
||||
results
|
||||
}
|
||||
asyncWriteMessages(preparePersistentBatch(messages)) onComplete {
|
||||
case Success(_) ⇒
|
||||
|
||||
writeResult.onComplete {
|
||||
case Success(results) ⇒
|
||||
resequencer ! Desequenced(WriteMessagesSuccessful, cctr, persistentActor, self)
|
||||
resequence(WriteMessageSuccess(_, actorInstanceId))
|
||||
|
||||
val resultsIter = results.iterator
|
||||
var n = cctr + 1
|
||||
messages.foreach {
|
||||
case a: AtomicWrite ⇒
|
||||
resultsIter.next() match {
|
||||
case Success(_) ⇒
|
||||
a.payload.foreach { p ⇒
|
||||
resequencer ! Desequenced(WriteMessageSuccess(p, actorInstanceId), n, persistentActor, p.sender)
|
||||
n += 1
|
||||
}
|
||||
case Failure(e) ⇒
|
||||
a.payload.foreach { p ⇒
|
||||
resequencer ! Desequenced(WriteMessageRejected(p, e, actorInstanceId), n, persistentActor, p.sender)
|
||||
n += 1
|
||||
}
|
||||
}
|
||||
|
||||
case r: NonPersistentRepr ⇒
|
||||
resequencer ! Desequenced(LoopMessageSuccess(r.payload, actorInstanceId), n, persistentActor, r.sender)
|
||||
n += 1
|
||||
}
|
||||
|
||||
case Failure(e) ⇒
|
||||
resequencer ! Desequenced(WriteMessagesFailed(e), cctr, persistentActor, self)
|
||||
resequence(WriteMessageFailure(_, e, actorInstanceId))
|
||||
var n = cctr + 1
|
||||
messages.foreach {
|
||||
case a: AtomicWrite ⇒
|
||||
a.payload.foreach { p ⇒
|
||||
resequencer ! Desequenced(WriteMessageFailure(p, e, actorInstanceId), n, persistentActor, p.sender)
|
||||
n += 1
|
||||
}
|
||||
case r: NonPersistentRepr ⇒
|
||||
resequencer ! Desequenced(LoopMessageSuccess(r.payload, actorInstanceId), n, persistentActor, r.sender)
|
||||
n += 1
|
||||
}
|
||||
}
|
||||
resequencerCounter += messages.length + 1
|
||||
|
||||
case r @ ReplayMessages(fromSequenceNr, toSequenceNr, max, persistenceId, persistentActor, replayDeleted) ⇒
|
||||
// Send replayed messages and replay result to persistentActor directly. No need
|
||||
|
|
@ -80,11 +126,42 @@ trait AsyncWriteJournal extends Actor with WriteJournalBase with AsyncRecovery {
|
|||
|
||||
//#journal-plugin-api
|
||||
/**
|
||||
* Plugin API: asynchronously writes a batch of persistent messages to the journal.
|
||||
* The batch write must be atomic i.e. either all persistent messages in the batch
|
||||
* are written or none.
|
||||
* Plugin API: asynchronously writes a batch (`Seq`) of persistent messages to the journal.
|
||||
*
|
||||
* The batch is only for performance reasons, i.e. all messages don't have to be written
|
||||
* atomically. Higher throughput can typically be achieved by using batch inserts of many
|
||||
* records compared inserting records one-by-one, but this aspect depends on the underlying
|
||||
* data store and a journal implementation can implement it as efficient as possible with
|
||||
* the assumption that the messages of the batch are unrelated.
|
||||
*
|
||||
* Each `AtomicWrite` message contains the single `PersistentRepr` that corresponds to the
|
||||
* event that was passed to the `persist` method of the `PersistentActor`, or it contains
|
||||
* several `PersistentRepr` that corresponds to the events that were passed to the `persistAll`
|
||||
* method of the `PersistentActor`. All `PersistentRepr` of the `AtomicWrite` must be
|
||||
* written to the data store atomically, i.e. all or none must be stored.
|
||||
* If the journal (data store) cannot support atomic writes of multiple events it should
|
||||
* reject such writes with a `Try` `Failure` with an `UnsupportedOperationException`
|
||||
* describing the issue. This limitation should also be documented by the journal plugin.
|
||||
*
|
||||
* If there are failures when storing any of the messages in the batch the returned
|
||||
* `Future` must be completed with failure. The `Future` must only be completed with
|
||||
* success when all messages in the batch have been confirmed to be stored successfully,
|
||||
* i.e. they will be readable, and visible, in a subsequent replay. If there are uncertainty
|
||||
* about if the messages were stored or not the `Future` must be completed with failure.
|
||||
*
|
||||
* Data store connection problems must be signaled by completing the `Future` with
|
||||
* failure.
|
||||
*
|
||||
* The journal can also signal that it rejects individual messages (`AtomicWrite`) by
|
||||
* the returned `immutable.Seq[Try[Unit]]`. The returned `Seq` must have as many elements
|
||||
* as the input `messages` `Seq`. Each `Try` element signals if the corresponding `AtomicWrite`
|
||||
* is rejected or not, with an exception describing the problem. Rejecting a message means it
|
||||
* was not stored, i.e. it must not be included in a later replay. Rejecting a message is
|
||||
* typically done before attempting to store it, e.g. because of serialization error.
|
||||
*
|
||||
* Data store connection problems must not be signaled as rejections.
|
||||
*/
|
||||
def asyncWriteMessages(messages: immutable.Seq[PersistentRepr]): Future[Unit]
|
||||
def asyncWriteMessages(messages: immutable.Seq[AtomicWrite]): Future[immutable.Seq[Try[Unit]]]
|
||||
|
||||
/**
|
||||
* Plugin API: asynchronously deletes all persistent messages up to `toSequenceNr`
|
||||
|
|
@ -108,6 +185,8 @@ trait AsyncWriteJournal extends Actor with WriteJournalBase with AsyncRecovery {
|
|||
* INTERNAL API.
|
||||
*/
|
||||
private[persistence] object AsyncWriteJournal {
|
||||
val successUnit: Success[Unit] = Success(())
|
||||
|
||||
final case class Desequenced(msg: Any, snr: Long, target: ActorRef, sender: ActorRef)
|
||||
|
||||
class Resequencer extends Actor {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue