From 20ef747a355cc4dc8cee560089582102d30db843 Mon Sep 17 00:00:00 2001 From: strailov Date: Thu, 13 Aug 2026 11:18:04 +0300 Subject: [PATCH 1/3] Fix nondeterministic ordering with postgres on artifacts Signed-off-by: strailov --- .../eclipse/hawkbit/repository/jpa/model/JpaSoftwareModule.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaSoftwareModule.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaSoftwareModule.java index a4e59d1694..7803a54963 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaSoftwareModule.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaSoftwareModule.java @@ -29,6 +29,7 @@ import jakarta.persistence.NamedAttributeNode; import jakarta.persistence.NamedEntityGraph; import jakarta.persistence.OneToMany; +import jakarta.persistence.OrderBy; import jakarta.persistence.Table; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; @@ -73,6 +74,7 @@ public class JpaSoftwareModule @OneToMany(fetch = FetchType.LAZY, mappedBy = "softwareModule", cascade = { CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REMOVE }, targetEntity = JpaArtifact.class, orphanRemoval = true) + @OrderBy("filename ASC") private List artifacts; @Column(name = "vendor", length = SoftwareModule.VENDOR_MAX_SIZE) From 76465dacd4da198962d951bcb3987a9145f6495d Mon Sep 17 00:00:00 2001 From: strailov Date: Thu, 13 Aug 2026 14:06:09 +0300 Subject: [PATCH 2/3] change sort to id asc Signed-off-by: strailov --- .../eclipse/hawkbit/repository/jpa/model/JpaSoftwareModule.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaSoftwareModule.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaSoftwareModule.java index 7803a54963..378aeef286 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaSoftwareModule.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaSoftwareModule.java @@ -74,7 +74,7 @@ public class JpaSoftwareModule @OneToMany(fetch = FetchType.LAZY, mappedBy = "softwareModule", cascade = { CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REMOVE }, targetEntity = JpaArtifact.class, orphanRemoval = true) - @OrderBy("filename ASC") + @OrderBy("id ASC") private List artifacts; @Column(name = "vendor", length = SoftwareModule.VENDOR_MAX_SIZE) From 510d4cea8a14c9d4bc51dea0d33c67a4eaf0d001 Mon Sep 17 00:00:00 2001 From: strailov Date: Fri, 14 Aug 2026 09:23:17 +0300 Subject: [PATCH 3/3] fix tests not to expect artifacts in specific order --- .../AbstractDDiApiIntegrationTest.java | 42 +++++++++---------- .../jpa/model/JpaSoftwareModule.java | 2 - 2 files changed, 20 insertions(+), 24 deletions(-) diff --git a/hawkbit-ddi/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/AbstractDDiApiIntegrationTest.java b/hawkbit-ddi/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/AbstractDDiApiIntegrationTest.java index 17109b1e7b..2181eb24b8 100644 --- a/hawkbit-ddi/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/AbstractDDiApiIntegrationTest.java +++ b/hawkbit-ddi/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/AbstractDDiApiIntegrationTest.java @@ -328,6 +328,12 @@ private ResultActions verifyBasePayload( final String prefix, final ResultActions resultActions, final String controllerId, final DistributionSet ds, final Artifact artifact, final Artifact artifactSignature, final Long actionId, final Long osModuleId, final String downloadType, final String updateType) throws Exception { + // artifacts are matched by filename rather than by index, since the DDI payload does not + // guarantee a specific artifact order (order is DB-dependent - see #3251) + final String osArtifact = prefix + ".chunks[?(@.part=='os')].artifacts[?(@.filename=='" + + artifact.getFilename() + "')]"; + final String osSignature = prefix + ".chunks[?(@.part=='os')].artifacts[?(@.filename=='" + + artifactSignature.getFilename() + "')]"; return resultActions.andExpect(jsonPath("$.id", equalTo(String.valueOf(actionId)))) .andExpect(jsonPath(prefix + ".download", equalTo(downloadType))) .andExpect(jsonPath(prefix + ".update", equalTo(updateType))) @@ -339,34 +345,26 @@ private ResultActions verifyBasePayload( contains(findFirstModuleByType(ds, osType).orElseThrow().getName()))) .andExpect(jsonPath(prefix + ".chunks[?(@.part=='os')].version", contains(findFirstModuleByType(ds, osType).orElseThrow().getVersion()))) - .andExpect(jsonPath(prefix + ".chunks[?(@.part=='os')].artifacts[0].size", contains(ARTIFACT_SIZE))) - .andExpect(jsonPath(prefix + ".chunks[?(@.part=='os')].artifacts[0].filename", - contains(artifact.getFilename()))) - .andExpect(jsonPath(prefix + ".chunks[?(@.part=='os')].artifacts[0].hashes.md5", - contains(artifact.getMd5Hash()))) - .andExpect(jsonPath(prefix + ".chunks[?(@.part=='os')].artifacts[0].hashes.sha1", - contains(artifact.getSha1Hash()))) - .andExpect(jsonPath(prefix + ".chunks[?(@.part=='os')].artifacts[0].hashes.sha256", - contains(artifact.getSha256Hash()))) - .andExpect(jsonPath(prefix + ".chunks[?(@.part=='os')].artifacts[0]._links.download-http.href", + .andExpect(jsonPath(osArtifact + ".size", contains(ARTIFACT_SIZE))) + .andExpect(jsonPath(osArtifact + ".filename", contains(artifact.getFilename()))) + .andExpect(jsonPath(osArtifact + ".hashes.md5", contains(artifact.getMd5Hash()))) + .andExpect(jsonPath(osArtifact + ".hashes.sha1", contains(artifact.getSha1Hash()))) + .andExpect(jsonPath(osArtifact + ".hashes.sha256", contains(artifact.getSha256Hash()))) + .andExpect(jsonPath(osArtifact + "._links.download-http.href", contains(HTTP_LOCALHOST + AccessContext.tenant() + "/controller/v1/" + controllerId + "/softwaremodules/" + osModuleId + "/artifacts/" + artifact.getFilename()))) - .andExpect(jsonPath(prefix + ".chunks[?(@.part=='os')].artifacts[0]._links.md5sum-http.href", + .andExpect(jsonPath(osArtifact + "._links.md5sum-http.href", contains(HTTP_LOCALHOST + AccessContext.tenant() + "/controller/v1/" + controllerId + "/softwaremodules/" + osModuleId + "/artifacts/" + artifact.getFilename() + ".MD5SUM"))) - .andExpect(jsonPath(prefix + ".chunks[?(@.part=='os')].artifacts[1].size", contains(ARTIFACT_SIZE))) - .andExpect(jsonPath(prefix + ".chunks[?(@.part=='os')].artifacts[1].filename", - contains(artifactSignature.getFilename()))) - .andExpect(jsonPath(prefix + ".chunks[?(@.part=='os')].artifacts[1].hashes.md5", - contains(artifactSignature.getMd5Hash()))) - .andExpect(jsonPath(prefix + ".chunks[?(@.part=='os')].artifacts[1].hashes.sha1", - contains(artifactSignature.getSha1Hash()))) - .andExpect(jsonPath(prefix + ".chunks[?(@.part=='os')].artifacts[1].hashes.sha256", - contains(artifactSignature.getSha256Hash()))) - .andExpect(jsonPath(prefix + ".chunks[?(@.part=='os')].artifacts[1]._links.download-http.href", + .andExpect(jsonPath(osSignature + ".size", contains(ARTIFACT_SIZE))) + .andExpect(jsonPath(osSignature + ".filename", contains(artifactSignature.getFilename()))) + .andExpect(jsonPath(osSignature + ".hashes.md5", contains(artifactSignature.getMd5Hash()))) + .andExpect(jsonPath(osSignature + ".hashes.sha1", contains(artifactSignature.getSha1Hash()))) + .andExpect(jsonPath(osSignature + ".hashes.sha256", contains(artifactSignature.getSha256Hash()))) + .andExpect(jsonPath(osSignature + "._links.download-http.href", contains(HTTP_LOCALHOST + AccessContext.tenant() + "/controller/v1/" + controllerId + "/softwaremodules/" + osModuleId + "/artifacts/" + artifactSignature.getFilename()))) - .andExpect(jsonPath(prefix + ".chunks[?(@.part=='os')].artifacts[1]._links.md5sum-http.href", + .andExpect(jsonPath(osSignature + "._links.md5sum-http.href", contains(HTTP_LOCALHOST + AccessContext.tenant() + "/controller/v1/" + controllerId + "/softwaremodules/" + osModuleId + "/artifacts/" + artifactSignature.getFilename() + ".MD5SUM"))) .andExpect(jsonPath(prefix + ".chunks[?(@.part=='bApp')].version", diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaSoftwareModule.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaSoftwareModule.java index 378aeef286..a4e59d1694 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaSoftwareModule.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaSoftwareModule.java @@ -29,7 +29,6 @@ import jakarta.persistence.NamedAttributeNode; import jakarta.persistence.NamedEntityGraph; import jakarta.persistence.OneToMany; -import jakarta.persistence.OrderBy; import jakarta.persistence.Table; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; @@ -74,7 +73,6 @@ public class JpaSoftwareModule @OneToMany(fetch = FetchType.LAZY, mappedBy = "softwareModule", cascade = { CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REMOVE }, targetEntity = JpaArtifact.class, orphanRemoval = true) - @OrderBy("id ASC") private List artifacts; @Column(name = "vendor", length = SoftwareModule.VENDOR_MAX_SIZE)