This is an automated email from the ASF dual-hosted git repository.
pjfanning 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 9e28139fb7 fix: fail fast when both Jackson serializer modules are on
the classpath (#3673)
9e28139fb7 is described below
commit 9e28139fb76e0c3e79aae907526109b136f3ec53
Author: PJ Fanning <[email protected]>
AuthorDate: Fri Oct 23 13:51:43 2026 +0100
fix: fail fast when both Jackson serializer modules are on the classpath
(#3673)
* fix: fail fast when both Jackson serializer modules are on the classpath
pekko-serialization-jackson and pekko-serialization-jackson3 register
serializers with the same names and identifiers. With both on the
classpath, whichever reference.conf is merged last silently takes over
the bindings of both modules. Each module's serializer now throws an
IllegalStateException at construction if the other module's serializer
class is on the classpath.
Refs #3671
* docs: note in the migration guide that only one Jackson serializer module
can be used
Refs #3671
* docs: warn against mixing the Jackson serializer modules in a cluster
Refs #3671
---
.../paradox/migration/migration-guide-1.x-2.x.md | 7 +++
docs/src/main/paradox/serialization-jackson.md | 23 +++++++++-
.../serialization/jackson/JacksonSerializer.scala | 18 +++++++-
.../jackson/JacksonModuleClasspathCheckSpec.scala | 50 ++++++++++++++++++++++
.../serialization/jackson3/JacksonSerializer.scala | 18 +++++++-
.../jackson3/JacksonModuleClasspathCheckSpec.scala | 50 ++++++++++++++++++++++
6 files changed, 162 insertions(+), 4 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 59fb37a56f..fa6332d521 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
@@ -260,3 +260,10 @@ Pekko 1.x nodes during a rolling update.
([PR3650](https://github.com/apache/pek
`jackson-cbor`. Your own marker traits and bindings keep working. If you
switch messages to the
predefined traits, Pekko 1.x nodes cannot load those classes, so only do that
once no Pekko 1.x nodes
remain in the cluster. ([PR3649](https://github.com/apache/pekko/pull/3649))
+* Only one of `pekko-serialization-jackson` and `pekko-serialization-jackson3`
can be on the
+classpath. Both register serializers with the same names and identifiers, and
the serializers now fail
+at ActorSystem startup if both modules are present. The two modules have
different default setups
+(Jackson 2 and Jackson 3 differ in their default features and registered
modules), so the same message
+can be serialized differently, and data written by one may not be read
correctly by the other. In a
+Cluster or Remoting setup, make sure all Pekko nodes use the same Jackson
serializer module.
+([PR3673](https://github.com/apache/pekko/pull/3673))
diff --git a/docs/src/main/paradox/serialization-jackson.md
b/docs/src/main/paradox/serialization-jackson.md
index 16f5e9f785..5626358994 100644
--- a/docs/src/main/paradox/serialization-jackson.md
+++ b/docs/src/main/paradox/serialization-jackson.md
@@ -43,8 +43,23 @@ The rest of this page applies to both modules, with these
differences for `pekko
the Pekko modules, only the Jackson Scala module is registered by default.
The date/time features such as
`WRITE_DATES_AS_TIMESTAMPS` are configured in a `datetime-features` section
instead of `serialization-features`.
-Both modules register their serializers with the same names (`jackson-json`
and `jackson-cbor`) and the
-same serializer identifiers, so only add one of them to your application.
+@@@ warning
+
+Do not use `pekko-serialization-jackson` and `pekko-serialization-jackson3`
together. Both modules register
+their serializers with the same names (`jackson-json` and `jackson-cbor`) and
the same serializer identifiers,
+so only add one of them to your application. The serializers fail at
ActorSystem startup with an
+`IllegalStateException` if both modules are on the classpath.
+
+Do not mix the two modules in a Cluster or Remoting setup either. Jackson 2
and Jackson 3 have different
+default setups, so the same message can be serialized differently, and data
written by one module may not be
+read correctly by the other. All Pekko nodes should use the same Jackson
serializer module. This matters most
+during a @ref:[rolling update](#rolling-updates), when old and new nodes run
side by side, and when reading
+data that was persisted with the other module.
+
+Moving from `pekko-serialization-jackson` to `pekko-serialization-jackson3`
needs serious testing of your
+messages and persisted data before you put it into production.
+
+@@@
## Introduction
@@ -419,6 +434,10 @@ To complete a no-degradation rolling update, you need to
make two deployments. F
the new schema but still uses the old schema. Then, deploy a second binary
which serializes data using the new schema
and drops the downcasting code from the migration.
+Switching between `pekko-serialization-jackson` and
`pekko-serialization-jackson3` is also a serialization
+change: during a rolling update, nodes using different modules may not be able
to read each other's messages.
+See the warning in [Jackson 3](#jackson-3).
+
Let's take, for example, the case above where we [renamed a
field](#rename-field).
The starting schema is:
diff --git
a/serialization-jackson/src/main/scala/org/apache/pekko/serialization/jackson/JacksonSerializer.scala
b/serialization-jackson/src/main/scala/org/apache/pekko/serialization/jackson/JacksonSerializer.scala
index 0349acdb9b..e3819d51b8 100644
---
a/serialization-jackson/src/main/scala/org/apache/pekko/serialization/jackson/JacksonSerializer.scala
+++
b/serialization-jackson/src/main/scala/org/apache/pekko/serialization/jackson/JacksonSerializer.scala
@@ -27,7 +27,7 @@ import com.fasterxml.jackson.dataformat.cbor.CBORFactory
import net.jpountz.lz4.LZ4Factory
import org.apache.pekko
-import pekko.actor.ExtendedActorSystem
+import pekko.actor.{ DynamicAccess, ExtendedActorSystem }
import pekko.annotation.InternalApi
import pekko.event.{ LogMarker, Logging }
import pekko.io.UnsynchronizedByteArrayInputStream
@@ -40,6 +40,20 @@ import pekko.util.OptionVal
*/
@InternalApi private[pekko] object JacksonSerializer {
+ private val OtherJacksonModuleSerializerClassName =
"org.apache.pekko.serialization.jackson3.JacksonSerializer"
+
+ /**
+ * pekko-serialization-jackson and pekko-serialization-jackson3 register
serializers with the same
+ * names and identifiers, so whichever `reference.conf` is merged last
silently takes over all bindings
+ * of both modules. Fail fast instead.
+ */
+ def checkOtherJacksonModuleNotOnClasspath(dynamicAccess: DynamicAccess):
Unit =
+ if
(dynamicAccess.classIsOnClasspath(OtherJacksonModuleSerializerClassName))
+ throw new IllegalStateException(
+ "pekko-serialization-jackson and pekko-serialization-jackson3 must not
both be on the classpath. " +
+ "They register serializers with the same names and identifiers, so
only one of them can be used. " +
+ s"Found [$OtherJacksonModuleSerializerClassName]. Remove one of them
from your dependencies.")
+
/**
* Using the deny list from Jackson databind of class names that shouldn't
be allowed.
* Not nice to depend on implementation details of Jackson, but good to use
the same
@@ -220,6 +234,8 @@ import pekko.util.OptionVal
extends SerializerWithStringManifest {
import JacksonSerializer._
+ checkOtherJacksonModuleNotOnClasspath(system.dynamicAccess)
+
// TODO issue #27107: it should be possible to implement
ByteBufferSerializer as well, using Jackson's
// ByteBufferBackedOutputStream/ByteBufferBackedInputStream
diff --git
a/serialization-jackson/src/test/scala/org/apache/pekko/serialization/jackson/JacksonModuleClasspathCheckSpec.scala
b/serialization-jackson/src/test/scala/org/apache/pekko/serialization/jackson/JacksonModuleClasspathCheckSpec.scala
new file mode 100644
index 0000000000..eb59790062
--- /dev/null
+++
b/serialization-jackson/src/test/scala/org/apache/pekko/serialization/jackson/JacksonModuleClasspathCheckSpec.scala
@@ -0,0 +1,50 @@
+/*
+ * 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.serialization.jackson
+
+import org.apache.pekko
+import pekko.actor.ReflectiveDynamicAccess
+
+import org.scalatest.matchers.should.Matchers
+import org.scalatest.wordspec.AnyWordSpec
+
+class JacksonModuleClasspathCheckSpec extends AnyWordSpec with Matchers {
+
+ private val otherModuleSerializer =
"org.apache.pekko.serialization.jackson3.JacksonSerializer"
+
+ private def dynamicAccess(otherModuleOnClasspath: Boolean) =
+ new ReflectiveDynamicAccess(getClass.getClassLoader) {
+ override def classIsOnClasspath(fqcn: String): Boolean =
+ if (fqcn == otherModuleSerializer) otherModuleOnClasspath else
super.classIsOnClasspath(fqcn)
+ }
+
+ "The Jackson serializer classpath check" should {
+ "pass when the other Jackson serializer module is not on the classpath" in
{
+ new
ReflectiveDynamicAccess(getClass.getClassLoader).classIsOnClasspath(otherModuleSerializer)
should ===(false)
+
JacksonSerializer.checkOtherJacksonModuleNotOnClasspath(dynamicAccess(otherModuleOnClasspath
= false))
+ }
+
+ "fail when the other Jackson serializer module is on the classpath" in {
+ val e = intercept[IllegalStateException] {
+
JacksonSerializer.checkOtherJacksonModuleNotOnClasspath(dynamicAccess(otherModuleOnClasspath
= true))
+ }
+ e.getMessage should include("must not both be on the classpath")
+ e.getMessage should include(otherModuleSerializer)
+ }
+ }
+}
diff --git
a/serialization-jackson3/src/main/scala/org/apache/pekko/serialization/jackson3/JacksonSerializer.scala
b/serialization-jackson3/src/main/scala/org/apache/pekko/serialization/jackson3/JacksonSerializer.scala
index 4aed2417e6..307561d762 100644
---
a/serialization-jackson3/src/main/scala/org/apache/pekko/serialization/jackson3/JacksonSerializer.scala
+++
b/serialization-jackson3/src/main/scala/org/apache/pekko/serialization/jackson3/JacksonSerializer.scala
@@ -27,7 +27,7 @@ import tools.jackson.databind.jsontype.impl.SubTypeValidator
import tools.jackson.dataformat.cbor.CBORFactory
import org.apache.pekko
-import pekko.actor.ExtendedActorSystem
+import pekko.actor.{ DynamicAccess, ExtendedActorSystem }
import pekko.annotation.InternalApi
import pekko.event.{ LogMarker, Logging }
import pekko.io.UnsynchronizedByteArrayInputStream
@@ -40,6 +40,20 @@ import pekko.util.OptionVal
*/
@InternalApi private[pekko] object JacksonSerializer {
+ private val OtherJacksonModuleSerializerClassName =
"org.apache.pekko.serialization.jackson.JacksonSerializer"
+
+ /**
+ * pekko-serialization-jackson and pekko-serialization-jackson3 register
serializers with the same
+ * names and identifiers, so whichever `reference.conf` is merged last
silently takes over all bindings
+ * of both modules. Fail fast instead.
+ */
+ def checkOtherJacksonModuleNotOnClasspath(dynamicAccess: DynamicAccess):
Unit =
+ if
(dynamicAccess.classIsOnClasspath(OtherJacksonModuleSerializerClassName))
+ throw new IllegalStateException(
+ "pekko-serialization-jackson and pekko-serialization-jackson3 must not
both be on the classpath. " +
+ "They register serializers with the same names and identifiers, so
only one of them can be used. " +
+ s"Found [$OtherJacksonModuleSerializerClassName]. Remove one of them
from your dependencies.")
+
/**
* Using the deny list from Jackson databind of class names that shouldn't
be allowed.
* Not nice to depend on implementation details of Jackson, but good to use
the same
@@ -221,6 +235,8 @@ import pekko.util.OptionVal
extends SerializerWithStringManifest {
import JacksonSerializer._
+ checkOtherJacksonModuleNotOnClasspath(system.dynamicAccess)
+
// TODO issue #27107: it should be possible to implement
ByteBufferSerializer as well, using Jackson's
// ByteBufferBackedOutputStream/ByteBufferBackedInputStream
diff --git
a/serialization-jackson3/src/test/scala/org/apache/pekko/serialization/jackson3/JacksonModuleClasspathCheckSpec.scala
b/serialization-jackson3/src/test/scala/org/apache/pekko/serialization/jackson3/JacksonModuleClasspathCheckSpec.scala
new file mode 100644
index 0000000000..647442530e
--- /dev/null
+++
b/serialization-jackson3/src/test/scala/org/apache/pekko/serialization/jackson3/JacksonModuleClasspathCheckSpec.scala
@@ -0,0 +1,50 @@
+/*
+ * 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.serialization.jackson3
+
+import org.apache.pekko
+import pekko.actor.ReflectiveDynamicAccess
+
+import org.scalatest.matchers.should.Matchers
+import org.scalatest.wordspec.AnyWordSpec
+
+class JacksonModuleClasspathCheckSpec extends AnyWordSpec with Matchers {
+
+ private val otherModuleSerializer =
"org.apache.pekko.serialization.jackson.JacksonSerializer"
+
+ private def dynamicAccess(otherModuleOnClasspath: Boolean) =
+ new ReflectiveDynamicAccess(getClass.getClassLoader) {
+ override def classIsOnClasspath(fqcn: String): Boolean =
+ if (fqcn == otherModuleSerializer) otherModuleOnClasspath else
super.classIsOnClasspath(fqcn)
+ }
+
+ "The Jackson serializer classpath check" should {
+ "pass when the other Jackson serializer module is not on the classpath" in
{
+ new
ReflectiveDynamicAccess(getClass.getClassLoader).classIsOnClasspath(otherModuleSerializer)
should ===(false)
+
JacksonSerializer.checkOtherJacksonModuleNotOnClasspath(dynamicAccess(otherModuleOnClasspath
= false))
+ }
+
+ "fail when the other Jackson serializer module is on the classpath" in {
+ val e = intercept[IllegalStateException] {
+
JacksonSerializer.checkOtherJacksonModuleNotOnClasspath(dynamicAccess(otherModuleOnClasspath
= true))
+ }
+ e.getMessage should include("must not both be on the classpath")
+ e.getMessage should include(otherModuleSerializer)
+ }
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]