Browse Source

Add LtsMigration interface and LtsMigrationService runner

pull/15808/head
Viacheslav Klimov 1 month ago
parent
commit
04e5c910cd
Failed to extract signature
  1. 2
      application/src/main/java/org/thingsboard/server/service/install/DatabaseSchemaSettingsService.java
  2. 30
      application/src/main/java/org/thingsboard/server/service/install/lts/LtsMigration.java
  3. 131
      application/src/main/java/org/thingsboard/server/service/install/lts/LtsMigrationService.java
  4. 179
      application/src/test/java/org/thingsboard/server/service/install/lts/LtsMigrationServiceTest.java

2
application/src/main/java/org/thingsboard/server/service/install/DatabaseSchemaSettingsService.java

@ -23,6 +23,8 @@ public interface DatabaseSchemaSettingsService {
void updateSchemaVersion();
void updateSchemaVersion(String version);
String getPackageSchemaVersion();
String getDbSchemaVersion();

30
application/src/main/java/org/thingsboard/server/service/install/lts/LtsMigration.java

@ -0,0 +1,30 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.install.lts;
/**
* One LTS migration, keyed by the version it ships in (e.g. "4.2.2.3").
* The runner runs the version's data/upgrade/lts/<version>/schema_update.sql by convention
* (if present), then calls {@link #apply()} for any programmatic work beyond SQL.
* Most migrations are SQL-only and only override {@link #getVersion()}.
*/
public interface LtsMigration {
String getVersion();
default void apply() {
}
}

131
application/src/main/java/org/thingsboard/server/service/install/lts/LtsMigrationService.java

@ -0,0 +1,131 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.install.lts;
import lombok.extern.slf4j.Slf4j;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.support.TransactionTemplate;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.install.DatabaseSchemaSettingsService;
import org.thingsboard.server.service.install.InstallScripts;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@Slf4j
@Component
@TbCoreComponent
public class LtsMigrationService {
private static final String SCHEMA_UPDATE_SQL = "schema_update.sql";
private final JdbcTemplate jdbcTemplate;
private final InstallScripts installScripts;
private final DatabaseSchemaSettingsService schemaSettingsService;
private final TransactionTemplate transactionTemplate;
private final List<LtsMigration> migrations;
public LtsMigrationService(JdbcTemplate jdbcTemplate,
InstallScripts installScripts,
DatabaseSchemaSettingsService schemaSettingsService,
PlatformTransactionManager transactionManager,
List<LtsMigration> migrations) {
this.jdbcTemplate = jdbcTemplate;
this.installScripts = installScripts;
this.schemaSettingsService = schemaSettingsService;
this.transactionTemplate = new TransactionTemplate(transactionManager);
this.migrations = validateAndSort(migrations);
log.info("Discovered {} LTS migration(s): {}", this.migrations.size(),
this.migrations.stream().map(LtsMigration::getVersion).toList());
}
private static List<LtsMigration> validateAndSort(List<LtsMigration> migrations) {
Set<String> seen = new HashSet<>();
for (LtsMigration m : migrations) {
LtsVersion.parse(m.getVersion()); // fail loud on unparseable version
if (!seen.add(m.getVersion())) {
throw new IllegalStateException("Duplicate LTS migration version: " + m.getVersion());
}
}
return migrations.stream()
.sorted(Comparator.comparing(m -> LtsVersion.parse(m.getVersion())))
.toList();
}
/** No-downtime path: per migration in (from, to] run SQL, then apply(), then record the version. */
public void applyMigrations(String fromVersion, String toVersion) {
for (LtsMigration migration : select(fromVersion, toVersion)) {
String version = migration.getVersion();
transactionTemplate.executeWithoutResult(status -> {
runSchemaUpdate(version);
migration.apply();
schemaSettingsService.updateSchemaVersion(version);
});
log.info("Applied LTS migration {}", version);
}
}
/** Offline major-upgrade schema phase: per migration in (from, to] run SQL only. No version record. */
public void runSchemaMigrations(String fromVersion, String toVersion) {
for (LtsMigration migration : select(fromVersion, toVersion)) {
String version = migration.getVersion();
transactionTemplate.executeWithoutResult(status -> runSchemaUpdate(version));
log.info("Applied LTS schema migration {}", version);
}
}
/** Offline major-upgrade data phase: per migration in (from, to] run apply() only. No SQL, no version record. */
public void runDataMigrations(String fromVersion, String toVersion) {
for (LtsMigration migration : select(fromVersion, toVersion)) {
migration.apply();
log.info("Applied LTS data migration {}", migration.getVersion());
}
}
private List<LtsMigration> select(String fromVersion, String toVersion) {
LtsVersion from = LtsVersion.parse(fromVersion);
LtsVersion to = LtsVersion.parse(toVersion);
return migrations.stream()
.filter(migration -> {
LtsVersion version = LtsVersion.parse(migration.getVersion());
return version.compareTo(from) > 0 && version.compareTo(to) <= 0;
})
.toList();
}
private void runSchemaUpdate(String version) {
Path sqlFile = Paths.get(installScripts.getDataDir(), "upgrade", "lts", version, SCHEMA_UPDATE_SQL);
if (!Files.exists(sqlFile)) {
log.trace("No LTS schema update file for version {} at {}", version, sqlFile);
return;
}
try {
jdbcTemplate.execute(Files.readString(sqlFile));
log.info("Applied LTS SQL schema update from {}", sqlFile);
} catch (IOException e) {
throw new UncheckedIOException("Failed to read LTS schema update file: " + sqlFile, e);
}
}
}

179
application/src/test/java/org/thingsboard/server/service/install/lts/LtsMigrationServiceTest.java

@ -0,0 +1,179 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.install.lts;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.Mockito;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.support.SimpleTransactionStatus;
import org.thingsboard.server.service.install.DatabaseSchemaSettingsService;
import org.thingsboard.server.service.install.InstallScripts;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
class LtsMigrationServiceTest {
private JdbcTemplate jdbcTemplate;
private InstallScripts installScripts;
private DatabaseSchemaSettingsService schemaSettingsService;
private PlatformTransactionManager txManager;
@TempDir
Path dataDir;
@BeforeEach
void setUp() {
jdbcTemplate = Mockito.mock(JdbcTemplate.class);
installScripts = Mockito.mock(InstallScripts.class);
schemaSettingsService = Mockito.mock(DatabaseSchemaSettingsService.class);
txManager = Mockito.mock(PlatformTransactionManager.class);
when(txManager.getTransaction(any())).thenReturn(new SimpleTransactionStatus());
when(installScripts.getDataDir()).thenReturn(dataDir.toString());
}
private void writeSql(String version, String sql) throws Exception {
Path dir = dataDir.resolve("upgrade").resolve("lts").resolve(version);
Files.createDirectories(dir);
Files.writeString(dir.resolve("schema_update.sql"), sql);
}
/** Records which apply() hooks fired, in order. */
private LtsMigration migration(String version, List<String> applied) {
return new LtsMigration() {
@Override public String getVersion() { return version; }
@Override public void apply() { applied.add(version); }
};
}
private LtsMigrationService service(List<LtsMigration> migrations) {
return new LtsMigrationService(jdbcTemplate, installScripts, schemaSettingsService, txManager, migrations);
}
@Test
void selectsOnlyInRangeMigrationsInAscendingOrder() throws Exception {
List<String> applied = new ArrayList<>();
writeSql("4.2.2.3", "SELECT 1;");
writeSql("4.2.2.4", "SELECT 2;");
// intentionally unsorted input; service must sort ascending
LtsMigrationService service = service(List.of(
migration("4.2.2.4", applied),
migration("4.2.2.2", applied),
migration("4.2.2.3", applied)));
service.applyMigrations("4.2.2.2", "4.2.2.3");
// only 4.2.2.3 is in (4.2.2.2, 4.2.2.3]
assertEquals(List.of("4.2.2.3"), applied);
verify(jdbcTemplate).execute("SELECT 1;");
verify(jdbcTemplate, never()).execute("SELECT 2;");
verify(schemaSettingsService).updateSchemaVersion("4.2.2.3");
}
@Test
void appliesAllInRangeAndRecordsEachVersion() throws Exception {
List<String> applied = new ArrayList<>();
writeSql("4.2.2.3", "SELECT 1;");
writeSql("4.2.2.4", "SELECT 2;");
LtsMigrationService service = service(List.of(
migration("4.2.2.3", applied), migration("4.2.2.4", applied)));
service.applyMigrations("4.2.2.2", "4.2.2.4");
assertEquals(List.of("4.2.2.3", "4.2.2.4"), applied);
verify(jdbcTemplate).execute("SELECT 1;");
verify(jdbcTemplate).execute("SELECT 2;");
verify(schemaSettingsService).updateSchemaVersion("4.2.2.3");
verify(schemaSettingsService).updateSchemaVersion("4.2.2.4");
}
@Test
void reRunAtCurrentVersionIsNoOp() throws Exception {
List<String> applied = new ArrayList<>();
writeSql("4.2.2.3", "SELECT 1;");
LtsMigrationService service = service(List.of(migration("4.2.2.3", applied)));
service.applyMigrations("4.2.2.3", "4.2.2.3");
assertEquals(List.of(), applied);
verify(jdbcTemplate, never()).execute(anyString());
verify(schemaSettingsService, never()).updateSchemaVersion(anyString());
}
@Test
void runSchemaMigrationsRunsSqlButNeverAppliesOrRecords() throws Exception {
List<String> applied = new ArrayList<>();
writeSql("4.2.2.3", "SELECT 1;");
LtsMigrationService service = service(List.of(migration("4.2.2.3", applied)));
service.runSchemaMigrations("4.2.2.2", "4.2.2.3");
verify(jdbcTemplate).execute("SELECT 1;");
assertEquals(List.of(), applied);
verify(schemaSettingsService, never()).updateSchemaVersion(anyString());
}
@Test
void runDataMigrationsAppliesButNeverRunsSqlOrRecords() throws Exception {
List<String> applied = new ArrayList<>();
writeSql("4.2.2.3", "SELECT 1;");
LtsMigrationService service = service(List.of(migration("4.2.2.3", applied)));
service.runDataMigrations("4.2.2.2", "4.2.2.3");
assertEquals(List.of("4.2.2.3"), applied);
verify(jdbcTemplate, never()).execute(anyString());
verify(schemaSettingsService, never()).updateSchemaVersion(anyString());
}
@Test
void migrationWithoutSqlFileStillAppliesAndRecords() {
List<String> applied = new ArrayList<>();
LtsMigrationService service = service(List.of(migration("4.2.2.3", applied)));
service.applyMigrations("4.2.2.2", "4.2.2.3");
assertEquals(List.of("4.2.2.3"), applied);
verify(jdbcTemplate, never()).execute(anyString());
verify(schemaSettingsService).updateSchemaVersion("4.2.2.3");
}
@Test
void failsLoudOnDuplicateVersion() {
List<String> applied = new ArrayList<>();
assertThrows(IllegalStateException.class, () -> service(List.of(
migration("4.2.2.3", applied), migration("4.2.2.3", applied))));
}
@Test
void failsLoudOnUnparseableVersion() {
List<String> applied = new ArrayList<>();
assertThrows(IllegalArgumentException.class, () -> service(List.of(migration("nope", applied))));
}
}
Loading…
Cancel
Save