This is an automated email from the ASF dual-hosted git repository.
davidzollo pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/seatunnel.git
The following commit(s) were added to refs/heads/dev by this push:
new 2586f37e96 [Fix][Shade] Relocate Jackson in seatunnel-hadoop-aws to
match the uber jar (#11673)
2586f37e96 is described below
commit 2586f37e96dff5a80be0065d020c8830dde96b00
Author: Dongyeon Lee <[email protected]>
AuthorDate: Thu Aug 13 13:33:39 2026 +0900
[Fix][Shade] Relocate Jackson in seatunnel-hadoop-aws to match the uber jar
(#11673)
Co-authored-by: Dongyeon <[email protected]>
Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
---
.github/workflows/backend.yml | 13 +-
seatunnel-shade/seatunnel-hadoop-aws/pom.xml | 21 ++++
tools/dependencies/check_shaded_jackson_refs.py | 161 ++++++++++++++++++++++++
3 files changed, 194 insertions(+), 1 deletion(-)
diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml
index 91e713b95c..15a0ae5161 100644
--- a/.github/workflows/backend.yml
+++ b/.github/workflows/backend.yml
@@ -227,7 +227,7 @@ jobs:
echo "dist=$true_or_false" >> $GITHUB_OUTPUT
echo "dist_files=$file_list" >> $GITHUB_OUTPUT
- api_files=`python tools/update_modules_check/check_file_updates.py
ua $workspace apache/dev origin/$current_branch "seatunnel-api/**"
"seatunnel-common/**" "seatunnel-config/**" "seatunnel-core/**"
"seatunnel-e2e/seatunnel-e2e-common/**" "seatunnel-formats/**"
"seatunnel-plugin-discovery/**" "seatunnel-transforms-v2/**"
"seatunnel-translation/**" "seatunnel-e2e/seatunnel-transforms-v2-e2e/**"
"pom.xml" "**/workflows/**" "tools/**" "seatunnel-dist/**"`
+ api_files=`python tools/update_modules_check/check_file_updates.py
ua $workspace apache/dev origin/$current_branch "seatunnel-api/**"
"seatunnel-common/**" "seatunnel-config/**" "seatunnel-core/**"
"seatunnel-e2e/seatunnel-e2e-common/**" "seatunnel-formats/**"
"seatunnel-plugin-discovery/**" "seatunnel-transforms-v2/**"
"seatunnel-translation/**" "seatunnel-e2e/seatunnel-transforms-v2-e2e/**"
"seatunnel-shade/**" "pom.xml" "**/workflows/**" "tools/**" "seatunnel-dist/**"`
true_or_false=${api_files%%$'\n'*}
file_list=${api_files#*$'\n'}
# Apache dev, main, master, and numeric *-release pushes force full
API coverage.
@@ -401,6 +401,17 @@ jobs:
retry_on: error
command: |
./mvnw -B install -DskipTests -D"maven.test.skip"=true
-D"maven.javadoc.skip"=true -D"license.skipAddThirdParty" -D"skip.ui"=true
+ # seatunnel-hadoop-aws bundles no Jackson of its own - it resolves those
+ # references against the seatunnel-hadoop3-*-uber jar, which relocates
Jackson.
+ # When the two disagree, S3A dies at runtime with NoSuchMethodError on
the
+ # assumed-role path and nothing at build time notices. The jar is
already built
+ # by the Install step above, so this costs seconds.
+ #
+ # Must run BEFORE Check Dependencies Licenses: checkLicense.sh starts
with
+ # `./mvnw clean -pl '!seatunnel-dist' ... dependency:copy-dependencies`,
which
+ # wipes every module's target/ directory, this jar included.
+ - name: Check shaded Jackson references
+ run: python3 tools/dependencies/check_shaded_jackson_refs.py
seatunnel-shade/seatunnel-hadoop-aws/target/seatunnel-hadoop-aws.jar
- name: Check Dependencies Licenses
run: tools/dependencies/checkLicense.sh
diff --git a/seatunnel-shade/seatunnel-hadoop-aws/pom.xml
b/seatunnel-shade/seatunnel-hadoop-aws/pom.xml
index e02b02e140..c1cf423899 100644
--- a/seatunnel-shade/seatunnel-hadoop-aws/pom.xml
+++ b/seatunnel-shade/seatunnel-hadoop-aws/pom.xml
@@ -74,6 +74,27 @@
<include>com.google.common.collect.*</include>
</includes>
</relocation>
+ <!--
+ hadoop-aws carries no Jackson classes of
its own; it only
+ *references* them, and it reaches them
through hadoop-common,
+ which this jar does not bundle either -
the uber jar supplies it.
+ The uber jar relocates Jackson, so its
JsonSerialization#getMapper
+ is declared to return the relocated
ObjectMapper. Left unrelocated,
+ RoleModel's call site keeps the original
descriptor
+
()Lcom/fasterxml/jackson/databind/ObjectMapper; and fails at
+ runtime with NoSuchMethodError on any
fs.s3a.assumed.role.* path.
+
+ This must therefore use the UBER jar's
shaded pattern, not this
+ module's own prefix: the point is to agree
with the class that
+ supplies the method, not to be
self-consistent. Bundling a second
+ Jackson under a different name would leave
the descriptor just as
+ mismatched. Nothing is added to this jar -
only references are
+ rewritten. Keep in lockstep with
seatunnel-hadoop3-*-uber.
+ -->
+ <relocation>
+ <pattern>com.fasterxml.jackson</pattern>
+
<shadedPattern>${seatunnel.shade.package}.hadoop.com.fasterxml.jackson</shadedPattern>
+ </relocation>
</relocations>
</configuration>
</execution>
diff --git a/tools/dependencies/check_shaded_jackson_refs.py
b/tools/dependencies/check_shaded_jackson_refs.py
new file mode 100755
index 0000000000..59e6b76405
--- /dev/null
+++ b/tools/dependencies/check_shaded_jackson_refs.py
@@ -0,0 +1,161 @@
+#!/usr/bin/env python3
+
+# 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.
+
+"""
+Assert that a shaded jar carries no references to the ORIGINAL
com.fasterxml.jackson
+package, i.e. that its Jackson relocation actually reached every class.
+
+Usage: check_shaded_jackson_refs.py <shaded.jar> [<shaded.jar> ...]
+
+Why this exists
+---------------
+seatunnel-hadoop-aws bundles no Jackson classes of its own. It only
*references* them,
+and it reaches the implementation through hadoop-common, which the
seatunnel-hadoop3-*-uber
+jar supplies - with Jackson relocated. So the uber's
+
+ org.apache.hadoop.util.JsonSerialization#getMapper
+
+is declared to return the RELOCATED ObjectMapper. If hadoop-aws is shaded
without the
+matching relocation, RoleModel's call site keeps the original descriptor
+
+ invokevirtual
JsonSerialization.getMapper:()Lcom/fasterxml/jackson/databind/ObjectMapper;
+
+which resolves against nothing at runtime: every fs.s3a.assumed.role.* path
dies with
+NoSuchMethodError. That is a link-time descriptor mismatch, so it is invisible
at build
+time and invisible to any test that does not exercise assumed-role - which is
why it
+survived undetected across Hadoop versions.
+
+A jar-level check catches it deterministically, in seconds, with no container
and no AWS
+credentials, and it keeps catching it after future Hadoop upgrades drag new
Jackson
+references into the jar.
+
+Two distinct failure modes, both fixed by the same relocation and only one of
them loud:
+
+ * RoleModel keeps the original descriptor on its getMapper() call -
NoSuchMethodError.
+ This is what hadoop-aws 3.1.4 exhibits (RoleModel plus two nested types).
+ * On newer hadoop-aws (checked against 3.4.3), SuccessData / PendingSet /
+ SinglePendingCommit additionally carry unrelocated @JsonProperty
annotations. The
+ relocated Jackson looks for relocated annotations, does not find them, and
silently
+ falls back to default property naming and inclusion when writing the S3A
committer's
+ _SUCCESS and .pending files - no exception, just wrong output. That second
mode is
+ the reason to keep this check after the next Hadoop upgrade, not only for
the
+ version in tree today.
+
+What it checks
+--------------
+Every CONSTANT_Utf8 entry of every class under org/apache/hadoop/, for the
byte pattern
+"com/fasterxml/jackson" (and its dotted form). That covers class references,
method and
+field descriptors, generic signatures, annotations, and string constants alike
- a
+targeted javap of one known class would not.
+
+Scope note: only org/apache/hadoop/ classes are checked, deliberately. The
invariant
+being defended is that the Hadoop classes in this jar agree with the Hadoop
classes in
+the uber jar. The AWS SDK ships its own vendored Jackson under
com.amazonaws.thirdparty
+/ software.amazon.awssdk.thirdparty and resolves those references among its
own classes;
+whether that vendoring has leaks of its own is not this jar's contract, and
flagging it
+here would make the check permanently red for a reason nobody can act on.
+
+Exit 0 if clean, 1 if any original-package reference remains.
+"""
+import os
+import re
+import sys
+import zipfile
+
+ORIGINAL = (b"com/fasterxml/jackson", b"com.fasterxml.jackson")
+SCOPE = "org/apache/hadoop/"
+# Mirrors ${seatunnel.shade.package} in the root pom. Kept as the full prefix
rather
+# than the bare word "shade": matching one common word would stop recognising
correct
+# relocations the moment that property is renamed, and the check would then
fail every
+# build for a reason with no obvious hint in the code.
+SHADED_PREFIX = rb"org[/.]apache[/.]seatunnel[/.]shade"
+# The relocated form legitimately contains the original substring as a suffix
+# (org/apache/seatunnel/shade/hadoop/com/fasterxml/jackson/...), so a naive
search
+# would report every correctly-relocated reference as a violation.
+RELOCATED = re.compile(SHADED_PREFIX +
rb"[A-Za-z0-9_$/.]*?(?:com[/.]fasterxml[/.]jackson)")
+
+
+def offending_refs(class_bytes):
+ """Return original-package references, ignoring ones that are part of a
relocated name."""
+ masked = RELOCATED.sub(b"#", class_bytes)
+ return [m for m in ORIGINAL if m in masked]
+
+
+def check(path):
+ bad = []
+ scanned = 0
+ with zipfile.ZipFile(path) as z:
+ for name in z.namelist():
+ if not name.endswith(".class") or not name.startswith(SCOPE):
+ continue
+ scanned += 1
+ data = z.read(name)
+ if offending_refs(data):
+ bad.append(name)
+ return scanned, bad
+
+
+def main(paths):
+ failed = False
+ for path in paths:
+ print(f"{path}")
+ if not os.path.isfile(path):
+ # Most likely the jar was never built, or something cleaned it
away between
+ # the build and this check - note that
tools/dependencies/checkLicense.sh
+ # runs `mvnw clean`, so this check has to come before it in CI.
+ failed = True
+ print(" FAIL: no such file. The shaded jar must exist before this
runs;")
+ print(" build it with `./mvnw -pl
seatunnel-shade/seatunnel-hadoop-aws -am")
+ print(" -DskipTests package`, and make sure nothing has run `mvn
clean` in")
+ print(" between (tools/dependencies/checkLicense.sh does).")
+ continue
+ try:
+ scanned, bad = check(path)
+ except zipfile.BadZipFile as e:
+ failed = True
+ print(f" FAIL: not a readable jar/zip ({e}).")
+ continue
+ if scanned == 0:
+ # Without this the check would pass vacuously on a jar whose
layout changed,
+ # which is exactly when it most needs to speak up.
+ failed = True
+ print(f" FAIL: no classes under {SCOPE} were found in this jar -
nothing was")
+ print(" actually checked. Either the wrong artifact was passed,
or the jar's")
+ print(" layout changed and SCOPE needs updating.")
+ continue
+ if bad:
+ failed = True
+ print(f" FAIL: {len(bad)} of {scanned} class(es) under {SCOPE}
still reference "
+ f"the original com.fasterxml.jackson package:")
+ for name in sorted(bad)[:20]:
+ print(f" - {name}")
+ if len(bad) > 20:
+ print(f" ... and {len(bad) - 20} more")
+ print(" The Jackson relocation in this module's
maven-shade-plugin config must")
+ print(" use the SAME shadedPattern as seatunnel-hadoop3-*-uber,
since that jar")
+ print(" supplies the classes these references resolve against.")
+ else:
+ print(f" PASS: {scanned} class(es) under {SCOPE}, none
referencing the original "
+ f"com.fasterxml.jackson package")
+ return 1 if failed else 0
+
+
+if __name__ == "__main__":
+ if len(sys.argv) < 2:
+ print(__doc__)
+ sys.exit(2)
+ sys.exit(main(sys.argv[1:]))