diff --git a/application/src/main/java/org/thingsboard/server/service/install/lts/LtsMigrationService.java b/application/src/main/java/org/thingsboard/server/service/install/lts/LtsMigrationService.java index d4181cdade..9c5907a3fe 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/lts/LtsMigrationService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/lts/LtsMigrationService.java @@ -114,28 +114,23 @@ public class LtsMigrationService { private List select(String fromVersion, String toVersion) { LtsVersion from = LtsVersion.parse(fromVersion); LtsVersion to = LtsVersion.parse(toVersion); + // Select every migration in the (from, to] range, regardless of family. On a cross-family offline + // upgrade (e.g. 4.3.x -> 4.4) this is what makes the real in-range older-family beans run: it picks the + // 4.3.1.x schema/data changes the source has not yet passed AND the new target-family beans, each exactly + // once -- the half-open (from, to] range skips anything the source already applied. One logical migration + // is thus authored once (one bean + one lts//schema_update.sql) and reused by both the offline + // and no-downtime paths; nothing is reproduced into a newer family. + // + // Load-bearing invariant: no two beans may reproduce the same change within a single supported upgrade + // range. A reproduction-duplicate bean (one that re-does an older bean's work on a newer-family branch) + // must sit STRICTLY BELOW the minimum supported upgrade source, so it can never be selected together with + // the bean it duplicates. The only such pair today is 4.2.2.3 <-> 4.3.1.3, and the supported-source floor + // is 4.3.0.0 (SUPPORTED_VERSIONS_FOR_UPGRADE), well above 4.2.2.3. LtsMigrationServiceTest guards this. return migrations.stream() - .filter(vm -> isInRange(vm.version(), from, to)) + .filter(vm -> vm.version().isInRange(from, to)) .toList(); } - // Select every migration in the (from, to] range, regardless of family. On a cross-family offline - // upgrade (e.g. 4.3.x -> 4.4) this is what makes the real in-range older-family beans run: it picks the - // 4.3.1.x schema/data changes the source has not yet passed AND the new target-family beans, each exactly - // once -- the half-open (from, to] range skips anything the source already applied. One logical migration - // is thus authored once (one bean + one lts//schema_update.sql) and reused by both the offline - // and no-downtime paths; nothing is reproduced into a newer family. - // - // Load-bearing invariant: no two beans may reproduce the same change within a single supported upgrade - // range. A reproduction-duplicate bean (one that re-does an older bean's work on a newer-family branch) - // must sit STRICTLY BELOW the minimum supported upgrade source, so it can never be selected together with - // the bean it duplicates. The only such pair today is 4.2.2.3 <-> 4.3.1.3, and the supported-source floor - // is 4.3.0.0 (SUPPORTED_VERSIONS_FOR_UPGRADE), well above 4.2.2.3. LtsMigrationServiceTest guards this. - static boolean isInRange(LtsVersion version, LtsVersion from, LtsVersion to) { - return version.compareTo(from) > 0 - && version.compareTo(to) <= 0; - } - private void runSchemaUpdate(String version) { Path sqlFile = Paths.get(installScripts.getDataDir(), "upgrade", "lts", version, SCHEMA_UPDATE_SQL); if (!Files.exists(sqlFile)) { diff --git a/application/src/main/java/org/thingsboard/server/service/install/lts/LtsVersion.java b/application/src/main/java/org/thingsboard/server/service/install/lts/LtsVersion.java index cb55e61b3e..9a0fa5be34 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/lts/LtsVersion.java +++ b/application/src/main/java/org/thingsboard/server/service/install/lts/LtsVersion.java @@ -37,6 +37,14 @@ public record LtsVersion(int major, int minor, int maintenance, int patch) imple return major == other.major && minor == other.minor; } + /** + * Half-open range check: {@code from < this <= to}. The lower bound is exclusive (a source already at + * {@code from} has applied that version) and the upper bound inclusive (the target version's own migration runs). + */ + public boolean isInRange(LtsVersion from, LtsVersion to) { + return compareTo(from) > 0 && compareTo(to) <= 0; + } + @Override public int compareTo(LtsVersion o) { int c = Integer.compare(major, o.major); diff --git a/application/src/test/java/org/thingsboard/server/service/install/lts/LtsMigrationServiceTest.java b/application/src/test/java/org/thingsboard/server/service/install/lts/LtsMigrationServiceTest.java index af044cf35f..8fc02c5629 100644 --- a/application/src/test/java/org/thingsboard/server/service/install/lts/LtsMigrationServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/install/lts/LtsMigrationServiceTest.java @@ -179,28 +179,28 @@ class LtsMigrationServiceTest { LtsVersion from = LtsVersion.parse("4.3.1.1"); LtsVersion to = LtsVersion.parse("4.3.1.3"); // within-family in range - assertTrue(LtsMigrationService.isInRange(LtsVersion.parse("4.3.1.2"), from, to)); + assertTrue(LtsVersion.parse("4.3.1.2").isInRange(from, to)); // the target itself (upper boundary, inclusive) - assertTrue(LtsMigrationService.isInRange(to, from, to)); + assertTrue(to.isInRange(from, to)); // at from (lower boundary, exclusive) - assertFalse(LtsMigrationService.isInRange(from, from, to)); + assertFalse(from.isInRange(from, to)); // below from - assertFalse(LtsMigrationService.isInRange(LtsVersion.parse("4.3.1.0"), from, to)); + assertFalse(LtsVersion.parse("4.3.1.0").isInRange(from, to)); // above to - assertFalse(LtsMigrationService.isInRange(LtsVersion.parse("4.3.1.4"), from, to)); + assertFalse(LtsVersion.parse("4.3.1.4").isInRange(from, to)); // Cross-family: an in-range older-family bean IS now selected (the whole point of the loosened filter), // as is the target-family baseline; only a bean below `from` stays out of range. LtsVersion crossFrom = LtsVersion.parse("4.3.0.0"); LtsVersion crossTo = LtsVersion.parse("4.4.0.0"); - assertTrue(LtsMigrationService.isInRange(LtsVersion.parse("4.3.1.3"), crossFrom, crossTo)); - assertTrue(LtsMigrationService.isInRange(crossTo, crossFrom, crossTo)); - assertFalse(LtsMigrationService.isInRange(LtsVersion.parse("4.2.2.3"), crossFrom, crossTo)); + assertTrue(LtsVersion.parse("4.3.1.3").isInRange(crossFrom, crossTo)); + assertTrue(crossTo.isInRange(crossFrom, crossTo)); + assertFalse(LtsVersion.parse("4.2.2.3").isInRange(crossFrom, crossTo)); } @Test void reproductionDuplicateBeanSitsBelowSupportedSourceFloor() { - // Load-bearing invariant (see LtsMigrationService.isInRange): the only reproduction-duplicate pair is + // Load-bearing invariant (see LtsMigrationService.select): the only reproduction-duplicate pair is // 4.2.2.3 <-> 4.3.1.3 (both do the solution-template / widget-bundle work). For the loosened (from, to] // filter to never select both in a single supported upgrade, the duplicate 4.2.2.3 must sit strictly // below the minimum supported upgrade source. That floor is 4.3.0.0 (SUPPORTED_VERSIONS_FOR_UPGRADE). @@ -208,7 +208,7 @@ class LtsMigrationServiceTest { LtsVersion supportedSourceFloor = LtsVersion.parse("4.3.0.0"); assertTrue(duplicate.compareTo(supportedSourceFloor) < 0); // So on any supported cross-family upgrade (from >= floor), the duplicate is out of range. - assertFalse(LtsMigrationService.isInRange(duplicate, supportedSourceFloor, LtsVersion.parse("4.4.0.0"))); + assertFalse(duplicate.isInRange(supportedSourceFloor, LtsVersion.parse("4.4.0.0"))); } @Test