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

rmetzger pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/flink.git


The following commit(s) were added to refs/heads/master by this push:
     new 2b978526a39 [FLINK-32212][runtime] Match library BLOBs by content in 
BlobLibraryCacheManager (#27579)
2b978526a39 is described below

commit 2b978526a3921c7a16f804d81b7a452ccb5e2a57
Author: Natea Eshetu Beshada <[email protected]>
AuthorDate: Fri Aug 14 00:11:43 2026 -0700

    [FLINK-32212][runtime] Match library BLOBs by content in 
BlobLibraryCacheManager (#27579)
    
    A PermanentBlobKey carries a random component on top of the content hash
    (FLINK-7140), so re-uploading an unchanged JAR yields a key that is unequal
    to the one a cached class loader was resolved with. When a JobManager fails
    over and resubmits the job, TaskExecutors that survived the failover still
    hold a class loader resolved with the old keys, so verifyClassLoader() 
throws
    
      The library registration references a different set of library BLOBs than
      previous registrations for this job:
      old:[p-<hash>-<random1>] new:[p-<hash>-<random2>]
    
    even though the JAR content is identical. Every subsequent deployment 
attempt
    hits the same mismatch, so the job restarts indefinitely.
    
    verifyClassLoader() now compares the content hashes of the required BLOBs
    instead of whole keys. Identical content therefore resolves to the cached
    class loader regardless of the random component, while a genuinely different
    set of JARs or class paths still fails as before.
    
    The comparison is over the set of distinct contents, so adding or dropping a
    duplicate of a JAR already in the required library set is also accepted - 
the
    resolved class loader can serve it either way. This makes the check 
symmetric.
    The previous size-then-containsAll comparison was not: it accepted a
    registration of [A, A] followed by [A], but rejected [A, A] followed by
    [A, A], even though both describe the same libraries.
    
    Comparing by content rather than re-creating the class loader keeps the
    existing contract check intact. A single ResolvedClassLoader is shared by 
all
    leases of a job, so replacing it on a mismatch would invalidate the class
    loader other leases already resolved, let old and new class loaders for the
    same job coexist, and orphan any release hooks registered against the
    replaced instance.
---
 .../e5126cae-f3fe-48aa-b6fb-60ae6cc3fcd5           |  5 --
 .../org/apache/flink/runtime/blob/BlobKey.java     |  6 +-
 .../librarycache/BlobLibraryCacheManager.java      | 55 +++++++++++----
 .../librarycache/LibraryCacheManager.java          |  3 +
 .../librarycache/BlobLibraryCacheManagerTest.java  | 81 ++++++++++++++++++++++
 5 files changed, 130 insertions(+), 20 deletions(-)

diff --git 
a/flink-architecture-tests/flink-architecture-tests-production/archunit-violations/e5126cae-f3fe-48aa-b6fb-60ae6cc3fcd5
 
b/flink-architecture-tests/flink-architecture-tests-production/archunit-violations/e5126cae-f3fe-48aa-b6fb-60ae6cc3fcd5
index f5af66381c0..3a6ae92e8f7 100644
--- 
a/flink-architecture-tests/flink-architecture-tests-production/archunit-violations/e5126cae-f3fe-48aa-b6fb-60ae6cc3fcd5
+++ 
b/flink-architecture-tests/flink-architecture-tests-production/archunit-violations/e5126cae-f3fe-48aa-b6fb-60ae6cc3fcd5
@@ -1,11 +1,6 @@
 Constructor 
<org.apache.flink.runtime.state.heap.CopyOnWriteStateMapSnapshot.<init>(org.apache.flink.runtime.state.heap.CopyOnWriteStateMap)>
 calls method 
<org.apache.flink.runtime.state.heap.CopyOnWriteStateMap.snapshotMapArrays()> 
in (CopyOnWriteStateMapSnapshot.java:86)
 Constructor 
<org.apache.flink.streaming.runtime.io.StreamTaskExternallyInducedSourceInput.<init>(org.apache.flink.streaming.api.operators.SourceOperator,
 java.util.function.Consumer, int, int)> calls method 
<org.apache.flink.streaming.api.operators.SourceOperator.getSourceReader()> in 
(StreamTaskExternallyInducedSourceInput.java:41)
 Method 
<org.apache.flink.cep.nfa.sharedbuffer.LockableTypeSerializerSnapshot.getNestedSerializers(org.apache.flink.cep.nfa.sharedbuffer.Lockable$LockableTypeSerializer)>
 calls method 
<org.apache.flink.cep.nfa.sharedbuffer.Lockable$LockableTypeSerializer.getElementSerializer()>
 in (LockableTypeSerializerSnapshot.java:58)
-Method <org.apache.flink.runtime.blob.BlobInputStream.read()> calls method 
<org.apache.flink.runtime.blob.BlobKey.getHash()> in (BlobInputStream.java:127)
-Method <org.apache.flink.runtime.blob.BlobInputStream.read([B, int, int)> 
calls method <org.apache.flink.runtime.blob.BlobKey.getHash()> in 
(BlobInputStream.java:163)
-Method 
<org.apache.flink.runtime.blob.BlobOutputStream.receiveAndCheckPutResponse(java.io.InputStream,
 java.security.MessageDigest, org.apache.flink.runtime.blob.BlobKey$BlobType)> 
calls method <org.apache.flink.runtime.blob.BlobKey.getHash()> in 
(BlobOutputStream.java:186)
-Method 
<org.apache.flink.runtime.blob.BlobUtils.checkAndDeleteCorruptedBlobs(java.nio.file.Path,
 org.slf4j.Logger)> calls method 
<org.apache.flink.runtime.blob.BlobKey.getHash()> in (BlobUtils.java:665)
-Method 
<org.apache.flink.runtime.blob.FileSystemBlobStore.get(java.lang.String, 
java.io.File, org.apache.flink.runtime.blob.BlobKey)> calls method 
<org.apache.flink.runtime.blob.BlobKey.getHash()> in 
(FileSystemBlobStore.java:148)
 Method 
<org.apache.flink.runtime.executiongraph.Execution.finishPartitionsAndUpdateConsumers()>
 calls method 
<org.apache.flink.runtime.executiongraph.ExecutionVertex.finishPartitionsIfNeeded()>
 in (Execution.java:1202)
 Method 
<org.apache.flink.runtime.highavailability.nonha.embedded.EmbeddedHaServicesWithLeadershipControl.grantDispatcherLeadership()>
 calls method 
<org.apache.flink.runtime.highavailability.nonha.embedded.EmbeddedLeaderService.grantLeadership()>
 in (EmbeddedHaServicesWithLeadershipControl.java:83)
 Method 
<org.apache.flink.runtime.highavailability.nonha.embedded.EmbeddedHaServicesWithLeadershipControl.grantJobMasterLeadership(org.apache.flink.api.common.JobID)>
 calls method 
<org.apache.flink.runtime.highavailability.nonha.embedded.EmbeddedLeaderService.grantLeadership()>
 in (EmbeddedHaServicesWithLeadershipControl.java:95)
diff --git 
a/flink-runtime/src/main/java/org/apache/flink/runtime/blob/BlobKey.java 
b/flink-runtime/src/main/java/org/apache/flink/runtime/blob/BlobKey.java
index bf89636df1c..1f802bf2a7b 100644
--- a/flink-runtime/src/main/java/org/apache/flink/runtime/blob/BlobKey.java
+++ b/flink-runtime/src/main/java/org/apache/flink/runtime/blob/BlobKey.java
@@ -159,9 +159,11 @@ public abstract class BlobKey implements Serializable, 
Comparable<BlobKey> {
     /**
      * Returns the hash component of this key.
      *
-     * @return a 20 bit hash of the contents the key refers to
+     * <p>Unlike {@link #equals(Object)}, this excludes the random component 
of the key, so two keys
+     * referring to the same content share the same hash.
+     *
+     * @return a 20 byte hash of the contents the key refers to
      */
-    @VisibleForTesting
     public byte[] getHash() {
         return key;
     }
diff --git 
a/flink-runtime/src/main/java/org/apache/flink/runtime/execution/librarycache/BlobLibraryCacheManager.java
 
b/flink-runtime/src/main/java/org/apache/flink/runtime/execution/librarycache/BlobLibraryCacheManager.java
index c031e049aff..395815074e8 100644
--- 
a/flink-runtime/src/main/java/org/apache/flink/runtime/execution/librarycache/BlobLibraryCacheManager.java
+++ 
b/flink-runtime/src/main/java/org/apache/flink/runtime/execution/librarycache/BlobLibraryCacheManager.java
@@ -28,6 +28,7 @@ import org.apache.flink.util.ExceptionUtils;
 import org.apache.flink.util.FlinkUserCodeClassLoader;
 import org.apache.flink.util.FlinkUserCodeClassLoaders;
 import org.apache.flink.util.Preconditions;
+import org.apache.flink.util.StringUtils;
 import org.apache.flink.util.UserCodeClassLoader;
 
 import 
org.apache.flink.shaded.jackson2.com.fasterxml.jackson.databind.type.TypeFactory;
@@ -391,12 +392,27 @@ public class BlobLibraryCacheManager implements 
LibraryCacheManager {
         private final ClassLoader classLoader;
 
         /**
-         * Set of BLOB keys used for a previous job/task registration.
+         * Set of BLOB keys used for a previous job/task registration, kept 
for error messages and
+         * logging only. Later registrations are checked against {@link 
#libraryHashes}.
+         */
+        private final Set<PermanentBlobKey> libraries;
+
+        /**
+         * Content hashes of {@link #libraries}.
          *
          * <p>The purpose of this is to make sure, future registrations do not 
differ in content as
          * this is a contract of the {@link BlobLibraryCacheManager}.
+         *
+         * <p>A {@link PermanentBlobKey} contains a random component on top of 
the content hash (see
+         * FLINK-7140), so the same JAR uploaded twice yields two unequal 
keys. Comparing content
+         * hashes instead of whole keys keeps the contract check meaningful 
while tolerating the
+         * re-upload of unchanged JARs that happens when a JobManager fails 
over (FLINK-32212).
+         *
+         * <p>Note that this compares the set of distinct contents, so a 
registration that only adds
+         * or drops a duplicate of a JAR already in the required library set 
is accepted: the
+         * resolved class loader can serve it either way.
          */
-        private final Set<PermanentBlobKey> libraries;
+        private final Set<String> libraryHashes;
 
         /**
          * Set of class path URLs used for a previous job/task registration.
@@ -425,6 +441,7 @@ public class BlobLibraryCacheManager implements 
LibraryCacheManager {
                 classPaths.add(url.toString());
             }
             this.libraries = new HashSet<>(requiredLibraries);
+            this.libraryHashes = hashesOf(requiredLibraries);
             this.wrapsSystemClassLoader = wrapsSystemClassLoader;
 
             this.releaseHooks = new HashMap<>();
@@ -444,21 +461,25 @@ public class BlobLibraryCacheManager implements 
LibraryCacheManager {
                 Collection<PermanentBlobKey> requiredLibraries,
                 Collection<URL> requiredClassPaths) {
             // Make sure the previous registration referred to the same 
libraries and class paths.
-            // NOTE: the original collections may contain duplicates and may 
not already be Set
-            //       collections with fast checks whether an item is contained 
in it.
-
-            // lazy construction of a new set for faster comparisons
-            if (libraries.size() != requiredLibraries.size()
-                    || !new 
HashSet<>(requiredLibraries).containsAll(libraries)) {
 
+            // Libraries are compared by BLOB content rather than by key, see 
#libraryHashes.
+            final Set<String> requiredLibraryHashes = 
hashesOf(requiredLibraries);
+            if (!libraryHashes.equals(requiredLibraryHashes)) {
                 throw new IllegalStateException(
-                        "The library registration references a different set 
of library BLOBs than"
-                                + " previous registrations for this job:\nold:"
+                        "The library registration references a different set 
of library BLOB"
+                                + " contents than previous registrations for 
this job:\nold:"
                                 + libraries
-                                + "\nnew:"
-                                + requiredLibraries);
+                                + " (contents: "
+                                + libraryHashes
+                                + ")\nnew:"
+                                + requiredLibraries
+                                + " (contents: "
+                                + requiredLibraryHashes
+                                + ")");
             }
 
+            // NOTE: the original collection may contain duplicates and may 
not already be a Set
+            //       collection with fast checks whether an item is contained 
in it.
             // lazy construction of a new set with String representations of 
the URLs
             if (classPaths.size() != requiredClassPaths.size()
                     || !requiredClassPaths.stream()
@@ -467,7 +488,7 @@ public class BlobLibraryCacheManager implements 
LibraryCacheManager {
                             .containsAll(classPaths)) {
 
                 throw new IllegalStateException(
-                        "The library registration references a different set 
of library BLOBs than"
+                        "The library registration references a different set 
of class paths than"
                                 + " previous registrations for this job:\nold:"
                                 + classPaths
                                 + "\nnew:"
@@ -475,6 +496,14 @@ public class BlobLibraryCacheManager implements 
LibraryCacheManager {
             }
         }
 
+        private static Set<String> hashesOf(Collection<PermanentBlobKey> 
libraries) {
+            final Set<String> hashes = 
CollectionUtil.newHashSetWithExpectedSize(libraries.size());
+            for (PermanentBlobKey library : libraries) {
+                hashes.add(StringUtils.byteToHexString(library.getHash()));
+            }
+            return hashes;
+        }
+
         /**
          * Release the class loader to ensure any file descriptors are closed 
and the cached
          * libraries are deleted immediately.
diff --git 
a/flink-runtime/src/main/java/org/apache/flink/runtime/execution/librarycache/LibraryCacheManager.java
 
b/flink-runtime/src/main/java/org/apache/flink/runtime/execution/librarycache/LibraryCacheManager.java
index e7f9e82f1a6..a0e7be8830d 100644
--- 
a/flink-runtime/src/main/java/org/apache/flink/runtime/execution/librarycache/LibraryCacheManager.java
+++ 
b/flink-runtime/src/main/java/org/apache/flink/runtime/execution/librarycache/LibraryCacheManager.java
@@ -67,6 +67,9 @@ public interface LibraryCacheManager {
          * Every subsequent call to this method, will ensure that created user 
code class loader can
          * fulfill the required jar files and class paths.
          *
+         * <p>Required jar files are matched by content, not by {@link 
PermanentBlobKey} identity,
+         * so a jar that is re-uploaded without changing still resolves to the 
cached class loader.
+         *
          * @param requiredJarFiles requiredJarFiles the user code class loader 
needs to load
          * @param requiredClasspaths requiredClasspaths the user code class 
loader needs to be
          *     started with
diff --git 
a/flink-runtime/src/test/java/org/apache/flink/runtime/execution/librarycache/BlobLibraryCacheManagerTest.java
 
b/flink-runtime/src/test/java/org/apache/flink/runtime/execution/librarycache/BlobLibraryCacheManagerTest.java
index 1047ce577a0..cf11611647a 100644
--- 
a/flink-runtime/src/test/java/org/apache/flink/runtime/execution/librarycache/BlobLibraryCacheManagerTest.java
+++ 
b/flink-runtime/src/test/java/org/apache/flink/runtime/execution/librarycache/BlobLibraryCacheManagerTest.java
@@ -56,8 +56,10 @@ import static 
org.apache.flink.runtime.blob.TestingBlobHelpers.checkFileCountFor
 import static org.apache.flink.runtime.blob.TestingBlobHelpers.checkFilesExist;
 import static org.hamcrest.Matchers.not;
 import static org.hamcrest.Matchers.sameInstance;
+import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertThat;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
@@ -615,6 +617,85 @@ public class BlobLibraryCacheManagerTest extends 
TestLogger {
         releaseHookLatch.await();
     }
 
+    /**
+     * A {@link PermanentBlobKey} carries a random component on top of the 
content hash, so
+     * re-uploading an unchanged JAR - as happens when a JobManager fails over 
and resubmits the job
+     * - yields a key that is unequal to the one the cached class loader was 
resolved with. Such a
+     * key must resolve to the cached class loader, while a key referring to 
different content must
+     * still be rejected (FLINK-32212).
+     */
+    @Test
+    public void libraryBlobsAreComparedByContentNotByBlobKey() throws 
Exception {
+        final JobID jobId = new JobID();
+        final byte[] content = new byte[] {1, 2, 3, 4, 5, 6, 7, 8};
+        final byte[] otherContent = new byte[] {8, 7, 6, 5, 4, 3, 2, 1};
+
+        BlobServer server = null;
+        PermanentBlobCache cache = null;
+        BlobLibraryCacheManager libCache = null;
+
+        try {
+            Configuration config = new Configuration();
+            config.set(BlobServerOptions.CLEANUP_INTERVAL, 1_000_000L);
+
+            server = new BlobServer(config, temporaryFolder.newFolder(), new 
VoidBlobStore());
+            server.start();
+            InetSocketAddress serverAddress = new 
InetSocketAddress("localhost", server.getPort());
+            cache =
+                    new PermanentBlobCache(
+                            config,
+                            temporaryFolder.newFolder(),
+                            new VoidBlobStore(),
+                            serverAddress);
+
+            final PermanentBlobKey key = server.putPermanent(jobId, content);
+            final PermanentBlobKey reuploadedKey = server.putPermanent(jobId, 
content);
+            final PermanentBlobKey otherKey = server.putPermanent(jobId, 
otherContent);
+
+            assertNotEquals(key, reuploadedKey);
+            assertArrayEquals(key.getHash(), reuploadedKey.getHash());
+
+            libCache = createBlobLibraryCacheManager(cache);
+            cache.registerJob(jobId, applicationId);
+
+            final LibraryCacheManager.ClassLoaderLease lease =
+                    libCache.registerClassLoaderLease(jobId, applicationId);
+            final UserCodeClassLoader classLoader =
+                    lease.getOrResolveClassLoader(
+                            Collections.singletonList(key), 
Collections.emptyList());
+
+            assertThat(
+                    lease.getOrResolveClassLoader(
+                            Collections.singletonList(reuploadedKey), 
Collections.emptyList()),
+                    sameInstance(classLoader));
+
+            // a duplicate does not add content, so the required set is still 
the same
+            assertThat(
+                    lease.getOrResolveClassLoader(Arrays.asList(key, key), 
Collections.emptyList()),
+                    sameInstance(classLoader));
+
+            try {
+                lease.getOrResolveClassLoader(
+                        Collections.singletonList(otherKey), 
Collections.emptyList());
+                fail("Should fail with an IllegalStateException");
+            } catch (IllegalStateException e) {
+                // that's what we want
+            }
+
+            lease.release();
+        } finally {
+            if (libCache != null) {
+                libCache.shutdown();
+            }
+            if (cache != null) {
+                cache.close();
+            }
+            if (server != null) {
+                server.close();
+            }
+        }
+    }
+
     private BlobLibraryCacheManager createSimpleBlobLibraryCacheManager() 
throws IOException {
         return new TestingBlobLibraryCacheManagerBuilder().build();
     }

Reply via email to