This is an automated email from the ASF dual-hosted git repository.

He-Pin pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/pekko.git


The following commit(s) were added to refs/heads/main by this push:
     new b9bd7d0f5a feat: add FilteredPayload placeholder for filtered journal 
events (#3275)
b9bd7d0f5a is described below

commit b9bd7d0f5a9f6845573b69307e869835936c43b1
Author: He-Pin(kerr) <[email protected]>
AuthorDate: Wed Jul 1 17:56:17 2026 +0800

    feat: add FilteredPayload placeholder for filtered journal events (#3275)
    
    Motivation:
    Projections and events-by-slice queries need to keep per-persistence-id
    sequence numbers gap free, which means filtered events must still be stored
    in the journal. There was no shared placeholder payload for this in Pekko
    Persistence, so query/projection plugins had nothing to align on.
    
    Modification:
    - Add the public `FilteredPayload` case object, serialized to a 0-byte
      representation by the new internal `FilteredPayloadSerializer` (manifest
      "F", serializer id 34 - matching Pekko's convention of keeping upstream
      serializer ids, verified free in Pekko).
    - Register the serializer, binding and identifier in persistence
      reference.conf.
    - Make typed `EventEnvelope.event`/`getEvent` raise a clearer error when the
      payload is absent because the event was filtered (vs. simply not loaded).
    
    Result:
    Pekko Persistence has a standard placeholder for filtered journal events,
    flagged as `filtered` in the typed `EventEnvelope`, enabling query and
    projection plugins to represent gap-free sequence numbers.
    
    Tests:
    - sbt "persistence/testOnly 
org.apache.pekko.persistence.serialization.FilteredPayloadSerializerSpec" (1/1 
passed)
    - sbt "persistence-query/testOnly 
org.apache.pekko.persistence.query.typed.EventEnvelopeSpec" (2/2 passed; 
directional test for the filtered vs not-loaded error, not present upstream)
    - sbt "persistence/mimaReportBinaryIssues" 
"persistence-query/mimaReportBinaryIssues" (clean)
    
    References:
    Upstream: akka/akka-core@b179bb15279962d657b9192f374a98123fabd7ee 
(akka#31985), which is now Apache licensed.
---
 .../persistence/query/typed/EventEnvelope.scala    | 18 ++++++--
 .../query/typed/EventEnvelopeSpec.scala            | 53 ++++++++++++++++++++++
 persistence/src/main/resources/reference.conf      |  3 ++
 .../apache/pekko/persistence/FilteredPayload.scala | 26 +++++++++++
 .../serialization/FilteredPayloadSerializer.scala  | 45 ++++++++++++++++++
 .../FilteredPayloadSerializerSpec.scala            | 35 ++++++++++++++
 6 files changed, 176 insertions(+), 4 deletions(-)

diff --git 
a/persistence-query/src/main/scala/org/apache/pekko/persistence/query/typed/EventEnvelope.scala
 
b/persistence-query/src/main/scala/org/apache/pekko/persistence/query/typed/EventEnvelope.scala
index 6cbb7e5335..5f8018365a 100644
--- 
a/persistence-query/src/main/scala/org/apache/pekko/persistence/query/typed/EventEnvelope.scala
+++ 
b/persistence-query/src/main/scala/org/apache/pekko/persistence/query/typed/EventEnvelope.scala
@@ -187,8 +187,13 @@ final class EventEnvelope[Event](
     eventOption match {
       case Some(evt) => evt
       case None      =>
-        throw new IllegalStateException(
-          "Event was not loaded. Use eventOption and load the event on demand 
with LoadEventQuery.")
+        if (filtered) {
+          throw new IllegalStateException(
+            "Event was filtered so payload is not present. Use eventOption to 
handle more gracefully.")
+        } else {
+          throw new IllegalStateException(
+            "Event was not loaded. Use eventOption and load the event on 
demand with LoadEventQuery.")
+        }
     }
 
   /**
@@ -198,8 +203,13 @@ final class EventEnvelope[Event](
     eventOption match {
       case Some(evt) => evt
       case None      =>
-        throw new IllegalStateException(
-          "Event was not loaded. Use getOptionalEvent and load the event on 
demand with LoadEventQuery.")
+        if (filtered) {
+          throw new IllegalStateException(
+            "Event was filtered so payload is not present. Use 
getOptionalEvent to handle more gracefully.")
+        } else {
+          throw new IllegalStateException(
+            "Event was not loaded. Use getOptionalEvent and load the event on 
demand with LoadEventQuery.")
+        }
     }
 
   /**
diff --git 
a/persistence-query/src/test/scala/org/apache/pekko/persistence/query/typed/EventEnvelopeSpec.scala
 
b/persistence-query/src/test/scala/org/apache/pekko/persistence/query/typed/EventEnvelopeSpec.scala
new file mode 100644
index 0000000000..ff482463d6
--- /dev/null
+++ 
b/persistence-query/src/test/scala/org/apache/pekko/persistence/query/typed/EventEnvelopeSpec.scala
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.pekko.persistence.query.typed
+
+import org.apache.pekko
+import pekko.persistence.query.NoOffset
+import pekko.testkit.PekkoSpec
+
+class EventEnvelopeSpec extends PekkoSpec {
+
+  private def envelope(filtered: Boolean): EventEnvelope[String] =
+    new EventEnvelope[String](
+      NoOffset,
+      persistenceId = "pid-1",
+      sequenceNr = 1L,
+      eventOption = None,
+      timestamp = 0L,
+      eventMetadata = None,
+      entityType = "entity",
+      slice = 0,
+      filtered = filtered,
+      source = "")
+
+  "EventEnvelope" should {
+
+    "throw a filtered-specific error from event/getEvent when the payload was 
filtered out" in {
+      val env = envelope(filtered = true)
+      intercept[IllegalStateException](env.event).getMessage should 
include("filtered")
+      intercept[IllegalStateException](env.getEvent()).getMessage should 
include("filtered")
+    }
+
+    "throw a not-loaded error from event/getEvent when the payload was simply 
not loaded" in {
+      val env = envelope(filtered = false)
+      intercept[IllegalStateException](env.event).getMessage should 
include("not loaded")
+      intercept[IllegalStateException](env.getEvent()).getMessage should 
include("not loaded")
+    }
+  }
+}
diff --git a/persistence/src/main/resources/reference.conf 
b/persistence/src/main/resources/reference.conf
index 6634dd6233..ff34a035bb 100644
--- a/persistence/src/main/resources/reference.conf
+++ b/persistence/src/main/resources/reference.conf
@@ -233,14 +233,17 @@ pekko.actor {
     serializers {
         pekko-persistence-message = 
"org.apache.pekko.persistence.serialization.MessageSerializer"
         pekko-persistence-snapshot = 
"org.apache.pekko.persistence.serialization.SnapshotSerializer"
+        pekko-persistence-filtered = 
"org.apache.pekko.persistence.serialization.FilteredPayloadSerializer"
     }
     serialization-bindings {
         "org.apache.pekko.persistence.serialization.Message" = 
pekko-persistence-message
         "org.apache.pekko.persistence.serialization.Snapshot" = 
pekko-persistence-snapshot
+        "org.apache.pekko.persistence.FilteredPayload$" = 
pekko-persistence-filtered
     }
     serialization-identifiers {
         "org.apache.pekko.persistence.serialization.MessageSerializer" = 7
         "org.apache.pekko.persistence.serialization.SnapshotSerializer" = 8
+        "org.apache.pekko.persistence.serialization.FilteredPayloadSerializer" 
= 34
     }
 }
 
diff --git 
a/persistence/src/main/scala/org/apache/pekko/persistence/FilteredPayload.scala 
b/persistence/src/main/scala/org/apache/pekko/persistence/FilteredPayload.scala
new file mode 100644
index 0000000000..d624f1b0b0
--- /dev/null
+++ 
b/persistence/src/main/scala/org/apache/pekko/persistence/FilteredPayload.scala
@@ -0,0 +1,26 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * license agreements; and to You under the Apache License, version 2.0:
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * This file is part of the Apache Pekko project, which was derived from Akka.
+ */
+
+/*
+ * Copyright (C) 2023 Lightbend Inc. <https://www.lightbend.com>
+ */
+
+package org.apache.pekko.persistence
+
+/**
+ * In some use cases with projections and events by slice filtered events 
needs to be stored in the journal
+ * to keep the sequence numbers for a given persistence id gap free. This 
placeholder payload is used for those
+ * cases and serialized down to a 0-byte representation when stored in the 
database.
+ *
+ * This payload is not in general expected to show up for users but in some 
scenarios/queries it may.
+ *
+ * In the typed queries `EventEnvelope` this should be flagged as `filtered` 
and turned into a non-present payload
+ * by the query plugin implementations.
+ */
+case object FilteredPayload
diff --git 
a/persistence/src/main/scala/org/apache/pekko/persistence/serialization/FilteredPayloadSerializer.scala
 
b/persistence/src/main/scala/org/apache/pekko/persistence/serialization/FilteredPayloadSerializer.scala
new file mode 100644
index 0000000000..c95dbbe39c
--- /dev/null
+++ 
b/persistence/src/main/scala/org/apache/pekko/persistence/serialization/FilteredPayloadSerializer.scala
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * license agreements; and to You under the Apache License, version 2.0:
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * This file is part of the Apache Pekko project, which was derived from Akka.
+ */
+
+/*
+ * Copyright (C) 2023 Lightbend Inc. <https://www.lightbend.com>
+ */
+
+package org.apache.pekko.persistence.serialization
+
+import org.apache.pekko
+import pekko.actor.ExtendedActorSystem
+import pekko.annotation.InternalApi
+import pekko.persistence.FilteredPayload
+import pekko.serialization.BaseSerializer
+import pekko.serialization.SerializerWithStringManifest
+
+/**
+ * INTERNAL API
+ */
+@InternalApi
+final class FilteredPayloadSerializer(val system: ExtendedActorSystem)
+    extends SerializerWithStringManifest
+    with BaseSerializer {
+
+  private val FilteredPayloadManifest = "F"
+
+  override def manifest(o: AnyRef): String = o match {
+    case FilteredPayload => FilteredPayloadManifest
+    case _               => throw new IllegalArgumentException(s"Can't 
serialize object of type ${o.getClass}")
+  }
+
+  override def toBinary(o: AnyRef): Array[Byte] = o match {
+    case FilteredPayload => Array.empty
+    case _               => throw new IllegalArgumentException(s"Can't 
serialize object of type ${o.getClass}")
+  }
+
+  override def fromBinary(bytes: Array[Byte], manifest: String): AnyRef = 
FilteredPayload
+
+}
diff --git 
a/persistence/src/test/scala/org/apache/pekko/persistence/serialization/FilteredPayloadSerializerSpec.scala
 
b/persistence/src/test/scala/org/apache/pekko/persistence/serialization/FilteredPayloadSerializerSpec.scala
new file mode 100644
index 0000000000..1858a5486c
--- /dev/null
+++ 
b/persistence/src/test/scala/org/apache/pekko/persistence/serialization/FilteredPayloadSerializerSpec.scala
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * license agreements; and to You under the Apache License, version 2.0:
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * This file is part of the Apache Pekko project, which was derived from Akka.
+ */
+
+/*
+ * Copyright (C) 2020-2023 Lightbend Inc. <https://www.lightbend.com>
+ */
+
+package org.apache.pekko.persistence.serialization
+
+import org.apache.pekko
+import pekko.persistence.FilteredPayload
+import pekko.serialization.SerializationExtension
+import pekko.serialization.SerializerWithStringManifest
+import pekko.testkit.PekkoSpec
+
+class FilteredPayloadSerializerSpec extends PekkoSpec {
+
+  "FilteredPayload serializer" should {
+    "serialize FilteredPayload to zero-byte array" in {
+      val serialization = SerializationExtension(system)
+      val serializer = 
serialization.findSerializerFor(FilteredPayload).asInstanceOf[SerializerWithStringManifest]
+      val manifest = serializer.manifest(FilteredPayload)
+      val serialized = serializer.toBinary(FilteredPayload)
+      serialized should have(size(0))
+      serializer.fromBinary(serialized, manifest) should be(FilteredPayload)
+    }
+  }
+
+}


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

Reply via email to