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 5c21c24988 feat: add SerializedEvent for writing pre-serialized events
(#3650)
5c21c24988 is described below
commit 5c21c24988dcb2d91c6216171129c8987e585244
Author: PJ Fanning <[email protected]>
AuthorDate: Fri Oct 23 04:33:58 2026 +0100
feat: add SerializedEvent for writing pre-serialized events (#3650)
Motivation:
Some journal implementations can write an event payload together with its
serialization
information without serializing it again. Callers that already hold the
serialized bytes of
an event need a way to pass them through to the journal without a
deserialize/serialize round
trip. Such a wrapper may also be sent across the cluster, so it needs a
serializer.
Modification:
- New public `org.apache.pekko.persistence.SerializedEvent(bytes,
serializerId, serializerManifest)`.
- The internal `FilteredPayloadSerializer` (id 34) now also serializes
`SerializedEvent`
with manifest `"S"`, using the existing `PersistentPayload` protobuf
message.
- `SerializedEvent` is bound to the `pekko-persistence-filtered` serializer
in `reference.conf`.
- Unknown manifests now fail with `NotSerializableException` instead of
decoding to `FilteredPayload`.
- `FilteredPayloadSerializerSpec` extended with `SerializedEvent` round
trip tests.
Result:
`SerializedEvent` can be handed to journals that support it and can be sent
over remoting.
Tests:
Not run locally - left to CI
References:
Ported from (Apache-2.0 since 2026-10-23):
- https://github.com/akka/akka-core/pull/32068
Notes for reviewers:
- `FilteredPayloadSerializer` keeps its class name, serializer id 34 and
`"F"` manifest, so
the `FilteredPayload` wire format is unchanged and no MiMa excludes are
needed.
- Rolling upgrade: nodes on older versions decode any manifest of
serializer 34 as
`FilteredPayload`, so a `SerializedEvent` sent to an old node would be
silently read as
`FilteredPayload`. Avoid sending `SerializedEvent` across the cluster
until all nodes run
the new version.
- No journal in this repository writes `SerializedEvent` yet; support is up
to journal plugins.
- `SerializedEvent` has no structural `equals`/`hashCode` (it wraps an
`Array[Byte]`).
- No new config settings; only a new serialization binding.
---
.../paradox/migration/migration-guide-1.x-2.x.md | 3 ++
persistence/src/main/resources/reference.conf | 1 +
.../apache/pekko/persistence/SerializedEvent.scala | 24 +++++++++++
.../serialization/FilteredPayloadSerializer.scala | 42 ++++++++++++++++---
.../FilteredPayloadSerializerSpec.scala | 49 +++++++++++++++++++++-
5 files changed, 113 insertions(+), 6 deletions(-)
diff --git a/docs/src/main/paradox/migration/migration-guide-1.x-2.x.md
b/docs/src/main/paradox/migration/migration-guide-1.x-2.x.md
index 0b4d1f7a78..c5b6cd97b6 100644
--- a/docs/src/main/paradox/migration/migration-guide-1.x-2.x.md
+++ b/docs/src/main/paradox/migration/migration-guide-1.x-2.x.md
@@ -225,3 +225,6 @@ number of processes) that Pekko 1.x nodes cannot
deserialize. ([PR2755](https://
(for example with events-by-slice queries and projections), has a new
serializer with identifier `34`.
Pekko 1.x cannot deserialize it, so do not run Pekko 1.x against a journal
once such events have been
written. ([PR3275](https://github.com/apache/pekko/pull/3275))
+The same serializer also handles the new `SerializedEvent`, used to write
events that are already
+serialized; Pekko 1.x cannot deserialize that either, so do not send
`SerializedEvent` messages to
+Pekko 1.x nodes during a rolling update.
([PR3650](https://github.com/apache/pekko/pull/3650))
diff --git a/persistence/src/main/resources/reference.conf
b/persistence/src/main/resources/reference.conf
index 6e13d82a8c..6213e01e2f 100644
--- a/persistence/src/main/resources/reference.conf
+++ b/persistence/src/main/resources/reference.conf
@@ -244,6 +244,7 @@ pekko.actor {
"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
+ "org.apache.pekko.persistence.SerializedEvent" =
pekko-persistence-filtered
}
serialization-identifiers {
"org.apache.pekko.persistence.serialization.MessageSerializer" = 7
diff --git
a/persistence/src/main/scala/org/apache/pekko/persistence/SerializedEvent.scala
b/persistence/src/main/scala/org/apache/pekko/persistence/SerializedEvent.scala
new file mode 100644
index 0000000000..7a126194ff
--- /dev/null
+++
b/persistence/src/main/scala/org/apache/pekko/persistence/SerializedEvent.scala
@@ -0,0 +1,24 @@
+/*
+ * 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
+
+/**
+ * Some journal implementations may support events of this type by writing the
event payload and
+ * serialization information without having to serialize it.
+ *
+ * @param bytes the already serialized event payload
+ * @param serializerId the identifier of the serializer that was used to
serialize the event
+ * @param serializerManifest the manifest of the serializer that was used,
empty string if none
+ */
+final class SerializedEvent(val bytes: Array[Byte], val serializerId: Int, val
serializerManifest: String)
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
index c95dbbe39c..6cd5c4a6a9 100644
---
a/persistence/src/main/scala/org/apache/pekko/persistence/serialization/FilteredPayloadSerializer.scala
+++
b/persistence/src/main/scala/org/apache/pekko/persistence/serialization/FilteredPayloadSerializer.scala
@@ -13,15 +13,23 @@
package org.apache.pekko.persistence.serialization
+import java.io.NotSerializableException
+
import org.apache.pekko
import pekko.actor.ExtendedActorSystem
import pekko.annotation.InternalApi
import pekko.persistence.FilteredPayload
+import pekko.persistence.SerializedEvent
+import pekko.persistence.serialization.{ MessageFormats => mf }
+import pekko.protobufv3.internal.ByteString
+import pekko.protobufv3.internal.UnsafeByteOperations
import pekko.serialization.BaseSerializer
import pekko.serialization.SerializerWithStringManifest
/**
* INTERNAL API
+ *
+ * Serializer for persistence payload wrappers: `FilteredPayload` and
`SerializedEvent`.
*/
@InternalApi
final class FilteredPayloadSerializer(val system: ExtendedActorSystem)
@@ -29,17 +37,41 @@ final class FilteredPayloadSerializer(val system:
ExtendedActorSystem)
with BaseSerializer {
private val FilteredPayloadManifest = "F"
+ private val SerializedEventManifest = "S"
override def manifest(o: AnyRef): String = o match {
- case FilteredPayload => FilteredPayloadManifest
- case _ => throw new IllegalArgumentException(s"Can't
serialize object of type ${o.getClass}")
+ case FilteredPayload => FilteredPayloadManifest
+ case _: SerializedEvent => SerializedEventManifest
+ 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}")
+ case FilteredPayload => Array.empty
+ case se: SerializedEvent => payloadBuilder(se).build.toByteArray
+ case _ => throw new IllegalArgumentException(s"Can't
serialize object of type ${o.getClass}")
+ }
+
+ private def payloadBuilder(se: SerializedEvent):
mf.PersistentPayload.Builder = {
+ val builder = mf.PersistentPayload.newBuilder()
+ builder.setPayload(UnsafeByteOperations.unsafeWrap(se.bytes))
+ builder.setSerializerId(se.serializerId)
+ if (se.serializerManifest.nonEmpty)
builder.setPayloadManifest(ByteString.copyFromUtf8(se.serializerManifest))
+ builder
}
- override def fromBinary(bytes: Array[Byte], manifest: String): AnyRef =
FilteredPayload
+ override def fromBinary(bytes: Array[Byte], manifest: String): AnyRef =
manifest match {
+ case FilteredPayloadManifest => FilteredPayload
+ case SerializedEventManifest =>
serializedEvent(mf.PersistentPayload.parseFrom(bytes))
+ case _ => throw new
NotSerializableException(s"Unknown manifest [$manifest]")
+ }
+
+ private def serializedEvent(persistentPayload: mf.PersistentPayload):
SerializedEvent = {
+ val manifest =
+ if (persistentPayload.hasPayloadManifest)
+ persistentPayload.getPayloadManifest.toStringUtf8
+ else ""
+
+ new SerializedEvent(persistentPayload.getPayload.toByteArray,
persistentPayload.getSerializerId, manifest)
+ }
}
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
index 1858a5486c..0c464ed130 100644
---
a/persistence/src/test/scala/org/apache/pekko/persistence/serialization/FilteredPayloadSerializerSpec.scala
+++
b/persistence/src/test/scala/org/apache/pekko/persistence/serialization/FilteredPayloadSerializerSpec.scala
@@ -15,21 +15,68 @@ package org.apache.pekko.persistence.serialization
import org.apache.pekko
import pekko.persistence.FilteredPayload
+import pekko.persistence.SerializedEvent
import pekko.serialization.SerializationExtension
import pekko.serialization.SerializerWithStringManifest
+import pekko.serialization.Serializers
import pekko.testkit.PekkoSpec
class FilteredPayloadSerializerSpec extends PekkoSpec {
+ private val serialization = SerializationExtension(system)
+
"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)
}
+
+ "keep the existing serializer identifier and manifest for FilteredPayload"
in {
+ val serializer = serialization.findSerializerFor(FilteredPayload)
+ serializer.identifier shouldBe 34
+ Serializers.manifestFor(serializer, FilteredPayload) shouldBe "F"
+ }
+
+ "serialize SerializedEvent" in {
+ val event = "event1"
+ val eventSerializer = serialization.findSerializerFor(event)
+ val serializedEvent = new SerializedEvent(
+ bytes = eventSerializer.toBinary(event),
+ serializerId = eventSerializer.identifier,
+ serializerManifest = Serializers.manifestFor(eventSerializer, event))
+
+ val serializedEventSerializer =
serialization.findSerializerFor(serializedEvent)
+ serializedEventSerializer.identifier shouldBe 34
+ val serializedEventBytes = serialization.serialize(serializedEvent).get
+
+ val deserialized = serialization
+ .deserialize(
+ serializedEventBytes,
+ serializedEventSerializer.identifier,
+ Serializers.manifestFor(serializedEventSerializer, serializedEvent))
+ .get
+ .asInstanceOf[SerializedEvent]
+
+ deserialized.serializerId shouldBe eventSerializer.identifier
+ deserialized.serializerManifest shouldBe
serializedEvent.serializerManifest
+ val deserializedEvent =
+ serialization.deserialize(deserialized.bytes,
deserialized.serializerId, deserialized.serializerManifest).get
+ deserializedEvent shouldBe event
+ }
+
+ "serialize SerializedEvent with a non-empty serializer manifest" in {
+ val serializedEvent = new SerializedEvent(Array[Byte](1, 2, 3), 4711,
"my-manifest")
+ val serializer =
serialization.findSerializerFor(serializedEvent).asInstanceOf[SerializerWithStringManifest]
+ val deserialized = serializer
+ .fromBinary(serializer.toBinary(serializedEvent),
serializer.manifest(serializedEvent))
+ .asInstanceOf[SerializedEvent]
+ deserialized.bytes.toSeq shouldBe Seq[Byte](1, 2, 3)
+ deserialized.serializerId shouldBe 4711
+ deserialized.serializerManifest shouldBe "my-manifest"
+ }
}
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]