He-Pin commented on code in PR #3653:
URL: https://github.com/apache/pekko/pull/3653#discussion_r4359711171


##########
persistence-typed/src/main/scala/org/apache/pekko/persistence/typed/internal/EventWriter.scala:
##########
@@ -378,6 +687,62 @@ private[pekko] object EventWriter {
             }
             Behaviors.same
 
+          case MaxSeqNrForPid(pid, maxSeqNr, AskMaxSeqNrReason.FillGaps | 
AskMaxSeqNrReason.SnapshotEvent) =>
+            bypassCircuitBreaker = true
+            perPidWriteState.get(pid) match {
+              case None =>
+                context.log.debug("Got max seq nr with no waiting previous 
state for pid, ignoring (pid [{}])", pid)
+              case Some(state) =>
+                context.log.debug("Retrieved max seq nr [{}] for pid [{}]", 
maxSeqNr, pid)
+                val waiting = state.waitingForSeqNrLookup
+                perPidWriteState = perPidWriteState.updated(
+                  pid,
+                  state.copy(latestSeqNr = maxSeqNr, waitingForSeqNrLookup = 
Vector.empty))
+                // each queued write keeps its own snapshot event flag, the 
lookup reason only
+                // belongs to the write that triggered it
+                waiting.foreach { pending =>
+                  handleWrite(pending.repr, pending.replyTo, 
pending.isSnapshotEvent, calledAfterMaxSeqNr = true)
+                }

Review Comment:
   Please run the idle-state cleanup after replaying this lookup queue. With 
gap filling enabled and cache capacity 2, send sequence 5 for six distinct 
persistence ids and answer each initial lookup with highest=5. Every caller is 
acknowledged as a duplicate, but all six states remain in perPidWriteState: 
this path never calls handleUpdatedStateForPid, where eviction happens. Sending 
sequence 6 for all six ids before acknowledging any writes produces six direct 
WriteMessages instead of at most two. I reproduced this against fd68ca81ad. A 
duplicate-only stream can therefore retain unbounded pid state despite 
latest-sequence-number-cache-capacity. Please route the final idle state 
through cleanup/eviction and add this lookup-only regression.



##########
persistence-typed/src/main/scala/org/apache/pekko/persistence/typed/internal/EventWriter.scala:
##########
@@ -264,13 +449,168 @@ private[pekko] object EventWriter {
               }
               Behaviors.same
 
+            case JournalProtocol.WriteMessageRejected(message, cause, 
transactionId) =>
+              val pid = message.persistenceId
+              val sequenceNr = message.sequenceNr
+              perPidWriteState.get(pid) match {
+                case Some(state)
+                    if transactionId == state.currentTransactionId && 
state.waitingForReply.contains(sequenceNr) =>
+                  // the journal rejected the write before storing it, e.g. it 
could not serialize the event
+                  // or a FilteredPayload gap filler, so all events of the 
atomic write are rejected
+                  context.log.warnN(
+                    "Journal rejected write of event persistence id [{}], 
sequence nr [{}]: {}",
+                    pid,
+                    sequenceNr,
+                    cause.getMessage)
+                  val newState = state.replyTo(sequenceNr, 
StatusReply.error(journalWriteRejected(cause)))
+                  handleUpdatedStateForPid(pid, newState.copy(waitingForReply 
= newState.waitingForReply - sequenceNr))
+                case _ =>
+                  context.log.debugN(
+                    "Got write rejection for event with no waiting request, 
ignoring (pid [{}], seq nr [{}], tx id [{}])",
+                    pid,
+                    sequenceNr,
+                    transactionId)
+              }
+              Behaviors.same
+
             case _ =>
               // ignore all other journal protocol messages
               Behaviors.same
           }
 
+        def handleWrite(
+            repr: PersistentRepr,
+            replyTo: ActorRef[StatusReply[WriteAck]],
+            isSnapshotEvent: Boolean,
+            calledAfterMaxSeqNr: Boolean): Unit = {
+          val persistenceId = repr.persistenceId
+          val sequenceNumber = repr.sequenceNr
+          val newStateForPid =
+            perPidWriteState.get(persistenceId) match {
+              case None =>
+                if ((fillSequenceNumberGaps || isSnapshotEvent) && 
sequenceNumber != 1L) {
+                  val reason = if (isSnapshotEvent) 
AskMaxSeqNrReason.SnapshotEvent else AskMaxSeqNrReason.FillGaps
+                  askMaxSeqNr(persistenceId, reason)
+                  StateForPid(
+                    waitingForReply = Map.empty,
+                    waitingForSeqNrLookup = Vector(PendingLookup(repr, 
replyTo, isSnapshotEvent)))
+                } else {
+                  sendToJournal(1, Vector(repr))
+                  StateForPid(Map((repr.sequenceNr, (repr, replyTo))), 
currentTransactionId = 1)
+                }
+
+              case Some(state) =>
+                val expectedSeqNr =
+                  if (fillSequenceNumberGaps || isSnapshotEvent) 
state.nextExpectedSeqNr
+                  else repr.sequenceNr
+
+                if (state.seqNrlookupInProgress) {
+                  context.log.trace2(
+                    "Seq nr lookup in progress for persistence id [{}], adding 
sequence nr [{}] to pending",
+                    persistenceId,
+                    sequenceNumber)
+                  state.copy(waitingForSeqNrLookup =
+                    state.waitingForSeqNrLookup :+ PendingLookup(repr, 
replyTo, isSnapshotEvent))
+                } else if (sequenceNumber == expectedSeqNr) {
+                  if (state.idle) {
+                    sendToJournal(state.currentTransactionId + 1, Vector(repr))
+                    state.copy(
+                      waitingForReply = Map((repr.sequenceNr, (repr, 
replyTo))),
+                      currentTransactionId = state.currentTransactionId + 1)
+                  } else {
+                    // write in progress for pid, add write to batch and 
perform once current write completes
+                    if 
(state.waitingForWriteExceedingMaxBatchSize(settings.maxBatchSize)) {
+                      replyTo ! StatusReply.error(
+                        s"Max batch reached for pid $persistenceId, at most 
${settings.maxBatchSize} writes for " +
+                        "the same pid may be in flight at the same time")
+                      state
+                    } else {
+                      context.log.trace2(
+                        "Writing event in progress for persistence id [{}], 
adding sequence nr [{}] to batch",
+                        persistenceId,
+                        sequenceNumber)
+                      state.copy(waitingForWrite = state.waitingForWrite :+ 
((repr, replyTo)))
+                    }
+                  }
+                } else if (sequenceNumber < expectedSeqNr) {
+                  if (state.isPending(sequenceNumber)) {
+                    // not durable yet, reply when the outcome of the pending 
write is known
+                    context.log.trace2(
+                      "Duplicate of pending seq nr [{}] for persistence id 
[{}]",
+                      sequenceNumber,
+                      persistenceId)
+                    state.copy(duplicateWaiters = 
state.duplicateWaiters.updated(
+                      sequenceNumber,
+                      state.duplicateWaiters.getOrElse(sequenceNumber, 
Vector.empty) :+ replyTo))
+                  } else {
+                    context.log.trace2("Duplicate seq nr [{}] for persistence 
id [{}]", sequenceNumber, persistenceId)
+                    replyTo ! StatusReply.success(WriteAck(persistenceId, 
sequenceNumber)) // duplicate
+                    state
+                  }
+                } else { // sequenceNumber > expectedSeqNr
+                  require(
+                    fillSequenceNumberGaps || isSnapshotEvent,
+                    s"Unexpected sequence number gap, expected 
[$expectedSeqNr], received [$sequenceNumber]. " +
+                    "Enable 
pekko.persistence.typed.event-writer.fill-sequence-number-gaps config if gaps 
are " +
+                    "expected and should be filled with FilteredPayload.")
+
+                  if (calledAfterMaxSeqNr || !state.idle) {
+                    // here we can trust that the expectedSeqNr is correct and 
don't have to lookup max
+                    val fillRepr = (expectedSeqNr until sequenceNumber).map { 
n =>
+                      PersistentRepr(
+                        FilteredPayload,
+                        persistenceId = persistenceId,
+                        sequenceNr = n,
+                        manifest = "", // adapters would be on the producing 
side, already applied
+                        writerUuid = writerUuid,
+                        sender = pekko.actor.ActorRef.noSender)
+                    }.toVector
+                    context.log.debugN(
+                      "Detected sequence nr gap{} [{}-{}] for persistence id 
[{}]. Filling with FilteredPayload.",
+                      if (isSnapshotEvent) " for snapshot event" else "",
+                      fillRepr.head.sequenceNr,
+                      fillRepr.last.sequenceNr,
+                      persistenceId)
+                    val ignoreRef = context.system.ignoreRef
+                    if (state.idle) {
+                      sendToJournal(state.currentTransactionId + 1, fillRepr 
:+ repr)
+                      val newWaitingForReply =
+                        (fillRepr.map(r => r.sequenceNr -> (r -> ignoreRef)) :+
+                        (repr.sequenceNr -> (repr -> replyTo))).toMap
+                      state.copy(
+                        waitingForReply = newWaitingForReply,
+                        currentTransactionId = state.currentTransactionId + 1)
+                    } else {
+                      if (context.log.isTraceEnabled)
+                        context.log.traceN(
+                          "Writing event in progress for persistence id [{}], 
adding sequence nrs [{}-{}] to batch",
+                          persistenceId,
+                          fillRepr.head.sequenceNr,
+                          sequenceNumber)
+                      val newWaitingForWrite = fillRepr.map(_ -> ignoreRef) :+ 
(repr -> replyTo)
+                      state.copy(waitingForWrite = state.waitingForWrite ++ 
newWaitingForWrite)

Review Comment:
   This gap branch appends another real event without checking maxBatchSize. 
With gap filling enabled and max-batch-size=2, hold sequence 1 in flight and 
enqueue 3, 5, then 7. After acknowledging 1, the next AtomicWrite contains real 
events [3,5,7] plus fillers; I reproduced this against fd68ca81ad. With gap 
filling disabled, the same sequence correctly rejects 7 and batches [3,5]. 
Repeating gapped inputs can grow the pending batch without the configured 
backpressure. Please apply the real-event batch-limit check before 
creating/appending the fillers, and test gapped writes at the limit.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to