!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:
Patrik Nordwall 2015-06-23 21:01:36 +02:00
parent 33ee447ec9
commit 8c47e01e9d
38 changed files with 1500 additions and 216 deletions

View file

@ -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 {