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())