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

yuqi1129 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git


The following commit(s) were added to refs/heads/main by this push:
     new fe44f02ecf [#11976] fix(core): refresh built-in job template when 
content drifts even if version is unchanged (#11978)
fe44f02ecf is described below

commit fe44f02ecf63fd7f36c3b53b4c180d659ac75605
Author: Jerry Shao <[email protected]>
AuthorDate: Mon Jul 13 16:46:51 2026 +0800

    [#11976] fix(core): refresh built-in job template when content drifts even 
if version is unchanged (#11978)
    
    ### What changes were proposed in this pull request?
    
    `BuiltInJobTemplateEventListener.reconcileBuiltInJobTemplates()` decided
    whether to refresh a persisted built-in job template by comparing only
    the `version` custom field. This PR makes it also compare the full
    template content (via `JobTemplate.equals()`) and refresh the persisted
    record whenever it drifts, regardless of the version field. Also added a
    regression test,
    `testReconcileBuiltInJobTemplatesUpdateWhenExecutableChangedButVersionSame`.
    
    ### Why are the changes needed?
    
    The `executable` path of a built-in job template is resolved from the
    on-disk jar location at server startup
    (`BuiltInJob.resolveExecutable()`), so it changes across a Gravitino
    release upgrade even though the job logic version (`"v1"`) stays the
    same. Since reconciliation only checked `version`, the stale jar path
    from the old release was never refreshed in the database, causing
    built-in job runs to fail after upgrade with `Source file does not
    exist: /opt/gravitino/auxlib/gravitino-jobs-<old-version>.jar`.
    
    Fix: #11976
    
    ### Does this PR introduce _any_ user-facing change?
    
    No user-facing API changes. Built-in job templates will now self-heal
    their `executable` path (and other fields) on server startup after an
    upgrade, instead of requiring manual intervention.
    
    ### How was this patch tested?
    
    Added a unit test verifying that when `executable` differs but `version`
    is unchanged, the persisted template is updated. Ran
    `TestBuiltInJobTemplateEventListener` (14 tests, all passing).
---
 .../job/BuiltInJobTemplateEventListener.java       |  7 ++-
 .../job/TestBuiltInJobTemplateEventListener.java   | 52 +++++++++++++++++++++-
 2 files changed, 57 insertions(+), 2 deletions(-)

diff --git 
a/core/src/main/java/org/apache/gravitino/job/BuiltInJobTemplateEventListener.java
 
b/core/src/main/java/org/apache/gravitino/job/BuiltInJobTemplateEventListener.java
index 11070fe2e2..48568e3687 100644
--- 
a/core/src/main/java/org/apache/gravitino/job/BuiltInJobTemplateEventListener.java
+++ 
b/core/src/main/java/org/apache/gravitino/job/BuiltInJobTemplateEventListener.java
@@ -269,7 +269,12 @@ public class BuiltInJobTemplateEventListener implements 
EventListenerPlugin {
           } else {
             int existingVersion = 
version(existing.templateContent().customFields());
             int newVersion = version(newTemplate.customFields());
-            if (newVersion > existingVersion) {
+            // The version field only tracks changes to job logic, but fields 
like `executable`
+            // can drift independently (e.g. the jar path changes with the 
Gravitino release
+            // version even though the job logic version stays the same), so 
also refresh the
+            // persisted template whenever its content no longer matches, 
regardless of version.
+            boolean contentChanged = 
!existing.toJobTemplate().equals(newTemplate);
+            if (newVersion > existingVersion || contentChanged) {
               updateBuiltInJobTemplate(metalake, existing, newTemplate);
             } else {
               LOG.info("Built-in job template {} under metalake {} is up to 
date", name, metalake);
diff --git 
a/core/src/test/java/org/apache/gravitino/job/TestBuiltInJobTemplateEventListener.java
 
b/core/src/test/java/org/apache/gravitino/job/TestBuiltInJobTemplateEventListener.java
index 7b6a004c4c..c20cfcb8ea 100644
--- 
a/core/src/test/java/org/apache/gravitino/job/TestBuiltInJobTemplateEventListener.java
+++ 
b/core/src/test/java/org/apache/gravitino/job/TestBuiltInJobTemplateEventListener.java
@@ -416,10 +416,11 @@ public class TestBuiltInJobTemplateEventListener {
     String metalakeName = "test_metalake";
     Map<String, JobTemplate> builtInTemplates = new HashMap<>();
 
+    // Same version and identical content as the entity created by 
createJobTemplateEntity(...)
     ShellJobTemplate template =
         ShellJobTemplate.builder()
             .withName("builtin-existing")
-            .withComment("same version")
+            .withComment("test")
             .withExecutable("/bin/echo")
             .withCustomFields(Collections.singletonMap("version", "v1"))
             .build();
@@ -444,6 +445,50 @@ public class TestBuiltInJobTemplateEventListener {
             Entity.EntityType.JOB_TEMPLATE,
             JobTemplateEntity.class);
     Assertions.assertEquals("v1", 
result.templateContent().customFields().get("version"));
+
+    // Verify no update actually happened: an update would stamp 
lastModifier/lastModifiedTime,
+    // which createJobTemplateEntity(...) leaves null, so these staying null 
proves the entity
+    // was left untouched rather than merely ending up with the same version 
by coincidence.
+    Assertions.assertNull(result.auditInfo().lastModifier());
+    Assertions.assertNull(result.auditInfo().lastModifiedTime());
+  }
+
+  @Test
+  public void 
testReconcileBuiltInJobTemplatesUpdateWhenExecutableChangedButVersionSame()
+      throws IOException {
+    String metalakeName = "test_metalake";
+    Map<String, JobTemplate> builtInTemplates = new HashMap<>();
+
+    // Same version as the persisted entity, but a different executable path, 
e.g. because the
+    // Gravitino release version (and thus the jar filename) changed while the 
job logic did not.
+    ShellJobTemplate template =
+        ShellJobTemplate.builder()
+            .withName("builtin-existing")
+            .withComment("test")
+            
.withExecutable("/opt/gravitino/auxlib/gravitino-jobs-2.0.0-SNAPSHOT.jar")
+            .withCustomFields(Collections.singletonMap("version", "v1"))
+            .build();
+    builtInTemplates.put("builtin-existing", template);
+
+    // Existing entity was persisted with the old jar path.
+    JobTemplateEntity existingEntity = 
createJobTemplateEntity("builtin-existing", "v1");
+    entityStore.put(existingEntity, false);
+
+    when(jobManager.listJobTemplates(metalakeName))
+        .thenReturn(Collections.singletonList(existingEntity));
+
+    listener.reconcileBuiltInJobTemplates(metalakeName, builtInTemplates);
+
+    // Verify the entity's executable path was refreshed despite the version 
staying at v1
+    JobTemplateEntity result =
+        entityStore.get(
+            existingEntity.nameIdentifier(),
+            Entity.EntityType.JOB_TEMPLATE,
+            JobTemplateEntity.class);
+    Assertions.assertEquals("v1", 
result.templateContent().customFields().get("version"));
+    Assertions.assertEquals(
+        "/opt/gravitino/auxlib/gravitino-jobs-2.0.0-SNAPSHOT.jar",
+        result.templateContent().executable());
   }
 
   @Test
@@ -552,6 +597,11 @@ public class TestBuiltInJobTemplateEventListener {
         .withId(1L)
         .withName(name)
         .withNamespace(NamespaceUtil.ofJobTemplate("test_metalake"))
+        // TemplateContent does not carry comment (see 
JobTemplateEntity#toJobTemplate(), which
+        // reads comment from the entity itself), so it must be set explicitly 
here to match what
+        // registerNewBuiltInJobTemplate(...) does in production and keep 
entity.toJobTemplate()
+        // consistent with `template` above.
+        .withComment(template.comment())
         
.withTemplateContent(JobTemplateEntity.TemplateContent.fromJobTemplate(template))
         .withAuditInfo(
             
AuditInfo.builder().withCreator("test").withCreateTime(Instant.now()).build())

Reply via email to