资源备份恢复
This commit is contained in:
@@ -9,6 +9,14 @@ public class ResourceBackupProperties {
|
||||
private String precheckStoreDir = "/home/tms/tmp/resource-restore-precheck";
|
||||
private String restoreTaskRootDir = "/home/tms/tmp/resource-restore";
|
||||
private String tmsScriptPath = "/home/tms/scripts/tms.sh";
|
||||
private String runnerLaunchScriptPath = "/home/tms/bin/resource-restore/start-runner.sh";
|
||||
private String mysqldumpPath = "mysqldump";
|
||||
private String mysqlPath = "mysql";
|
||||
private String restoreDbScriptPath = "/home/tms/bin/resource-restore/restore-db.sh";
|
||||
private String tmsDatabaseName = "";
|
||||
private String cmepDatabaseName = "CMEP";
|
||||
private String healthCheckUrl = "http://127.0.0.1:8080/actuator/health";
|
||||
private int healthCheckTimeoutSeconds = 5;
|
||||
private boolean runnerAutoLaunchEnabled = false;
|
||||
private int runnerHeartbeatTimeoutSeconds = 300;
|
||||
private int storageEncKeyIndex = 1;
|
||||
@@ -46,6 +54,70 @@ public class ResourceBackupProperties {
|
||||
this.tmsScriptPath = tmsScriptPath;
|
||||
}
|
||||
|
||||
public String getRunnerLaunchScriptPath() {
|
||||
return runnerLaunchScriptPath;
|
||||
}
|
||||
|
||||
public void setRunnerLaunchScriptPath(String runnerLaunchScriptPath) {
|
||||
this.runnerLaunchScriptPath = runnerLaunchScriptPath;
|
||||
}
|
||||
|
||||
public String getMysqldumpPath() {
|
||||
return mysqldumpPath;
|
||||
}
|
||||
|
||||
public void setMysqldumpPath(String mysqldumpPath) {
|
||||
this.mysqldumpPath = mysqldumpPath;
|
||||
}
|
||||
|
||||
public String getMysqlPath() {
|
||||
return mysqlPath;
|
||||
}
|
||||
|
||||
public void setMysqlPath(String mysqlPath) {
|
||||
this.mysqlPath = mysqlPath;
|
||||
}
|
||||
|
||||
public String getRestoreDbScriptPath() {
|
||||
return restoreDbScriptPath;
|
||||
}
|
||||
|
||||
public void setRestoreDbScriptPath(String restoreDbScriptPath) {
|
||||
this.restoreDbScriptPath = restoreDbScriptPath;
|
||||
}
|
||||
|
||||
public String getTmsDatabaseName() {
|
||||
return tmsDatabaseName;
|
||||
}
|
||||
|
||||
public void setTmsDatabaseName(String tmsDatabaseName) {
|
||||
this.tmsDatabaseName = tmsDatabaseName;
|
||||
}
|
||||
|
||||
public String getCmepDatabaseName() {
|
||||
return cmepDatabaseName;
|
||||
}
|
||||
|
||||
public void setCmepDatabaseName(String cmepDatabaseName) {
|
||||
this.cmepDatabaseName = cmepDatabaseName;
|
||||
}
|
||||
|
||||
public String getHealthCheckUrl() {
|
||||
return healthCheckUrl;
|
||||
}
|
||||
|
||||
public void setHealthCheckUrl(String healthCheckUrl) {
|
||||
this.healthCheckUrl = healthCheckUrl;
|
||||
}
|
||||
|
||||
public int getHealthCheckTimeoutSeconds() {
|
||||
return healthCheckTimeoutSeconds;
|
||||
}
|
||||
|
||||
public void setHealthCheckTimeoutSeconds(int healthCheckTimeoutSeconds) {
|
||||
this.healthCheckTimeoutSeconds = healthCheckTimeoutSeconds;
|
||||
}
|
||||
|
||||
public boolean getRunnerAutoLaunchEnabled() {
|
||||
return runnerAutoLaunchEnabled;
|
||||
}
|
||||
|
||||
@@ -29,6 +29,11 @@ public class ResourceDiscoveryServiceImpl implements ResourceDiscoveryService {
|
||||
|
||||
List<DiscoveredResourceItem> items = new ArrayList<>();
|
||||
items.add(item("FILE", "TMS_CONFIG", initExecutorProperties.getTmsConfigDir(), true));
|
||||
items.add(item("DB", "TMS_DB_DUMP", "TMS", true));
|
||||
if (!"DIRECT".equals(productType)) {
|
||||
// 第一版标准版迁移需要把收发器业务库一起带走,DIRECT 暂时不要求这份库。
|
||||
items.add(item("DB", "CMEP_DB_DUMP", "CMEP", true));
|
||||
}
|
||||
items.add(item("FILE", "CP_CONFIG", resolveCpConfigPath(productType), true));
|
||||
items.add(item("FILE", "RECEIVER_LICENSE", resolveReceiverLicensePath(productType), true));
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.cisd.tms.modules.backup.packagex;
|
||||
|
||||
public interface DatabaseBackupCollector {
|
||||
|
||||
CollectedDatabaseDump collectTmsDatabaseDump();
|
||||
|
||||
CollectedDatabaseDump collectCmepDatabaseDump();
|
||||
|
||||
class CollectedDatabaseDump {
|
||||
|
||||
private String databaseName;
|
||||
private String fileName;
|
||||
private byte[] content;
|
||||
|
||||
public String getDatabaseName() {
|
||||
return databaseName;
|
||||
}
|
||||
|
||||
public void setDatabaseName(String databaseName) {
|
||||
this.databaseName = databaseName;
|
||||
}
|
||||
|
||||
public String getFileName() {
|
||||
return fileName;
|
||||
}
|
||||
|
||||
public void setFileName(String fileName) {
|
||||
this.fileName = fileName;
|
||||
}
|
||||
|
||||
public byte[] getContent() {
|
||||
return content;
|
||||
}
|
||||
|
||||
public void setContent(byte[] content) {
|
||||
this.content = content;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
package com.cisd.tms.modules.backup.packagex;
|
||||
|
||||
import com.cisd.tms.modules.backup.config.ResourceBackupProperties;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class MysqlDatabaseBackupCollector implements DatabaseBackupCollector {
|
||||
|
||||
private final ResourceBackupProperties resourceBackupProperties;
|
||||
private final String datasourceUrl;
|
||||
private final String datasourceUsername;
|
||||
private final String datasourcePassword;
|
||||
|
||||
public MysqlDatabaseBackupCollector(
|
||||
ResourceBackupProperties resourceBackupProperties,
|
||||
@Value("${spring.datasource.url:}") String datasourceUrl,
|
||||
@Value("${spring.datasource.username:}") String datasourceUsername,
|
||||
@Value("${spring.datasource.password:}") String datasourcePassword
|
||||
) {
|
||||
this.resourceBackupProperties = resourceBackupProperties;
|
||||
this.datasourceUrl = datasourceUrl;
|
||||
this.datasourceUsername = datasourceUsername;
|
||||
this.datasourcePassword = datasourcePassword;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CollectedDatabaseDump collectTmsDatabaseDump() {
|
||||
MysqlConnectionTarget target = resolveTarget(resourceBackupProperties.getTmsDatabaseName(), "TMS");
|
||||
return collectDatabaseDump(target, "TMS.sql");
|
||||
}
|
||||
|
||||
@Override
|
||||
public CollectedDatabaseDump collectCmepDatabaseDump() {
|
||||
MysqlConnectionTarget target = resolveTarget(resourceBackupProperties.getCmepDatabaseName(), "CMEP");
|
||||
return collectDatabaseDump(target, "CMEP.sql");
|
||||
}
|
||||
|
||||
private CollectedDatabaseDump collectDatabaseDump(MysqlConnectionTarget target, String fileName) {
|
||||
List<String> command = new ArrayList<>();
|
||||
command.add(resourceBackupProperties.getMysqldumpPath());
|
||||
command.add("--host=" + target.host());
|
||||
command.add("--port=" + target.port());
|
||||
command.add("--user=" + target.username());
|
||||
if (!target.password().isEmpty()) {
|
||||
command.add("--password=" + target.password());
|
||||
}
|
||||
command.add("--single-transaction");
|
||||
command.add("--skip-lock-tables");
|
||||
command.add("--databases");
|
||||
command.add(target.databaseName());
|
||||
|
||||
try {
|
||||
Process process = new ProcessBuilder(command).start();
|
||||
byte[] stdout = readAllBytes(process.getInputStream());
|
||||
byte[] stderr = readAllBytes(process.getErrorStream());
|
||||
int exitCode = process.waitFor();
|
||||
if (exitCode != 0) {
|
||||
String error = new String(stderr, StandardCharsets.UTF_8).trim();
|
||||
throw new IllegalStateException("mysqldump failed with exitCode=" + exitCode
|
||||
+ (error.isEmpty() ? "" : ", error=" + error));
|
||||
}
|
||||
if (stdout.length == 0) {
|
||||
throw new IllegalStateException("mysqldump produced empty output");
|
||||
}
|
||||
CollectedDatabaseDump dump = new CollectedDatabaseDump();
|
||||
dump.setDatabaseName(target.databaseName());
|
||||
dump.setFileName(fileName);
|
||||
dump.setContent(stdout);
|
||||
return dump;
|
||||
} catch (InterruptedException ex) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IllegalStateException("mysqldump interrupted", ex);
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("failed to run mysqldump", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private MysqlConnectionTarget resolveTarget(String configuredDatabaseName, String fallbackDatabaseName) {
|
||||
String url = trim(datasourceUrl);
|
||||
if (url.isEmpty()) {
|
||||
throw new IllegalStateException("spring.datasource.url is required for database backup");
|
||||
}
|
||||
if (!url.startsWith("jdbc:mysql://")) {
|
||||
throw new IllegalStateException("unsupported datasource url for backup: " + url);
|
||||
}
|
||||
try {
|
||||
String normalizedUrl = url.substring("jdbc:".length());
|
||||
String[] segments = normalizedUrl.split("\\?", 2);
|
||||
URI uri = URI.create(segments[0]);
|
||||
String databaseName = trim(configuredDatabaseName);
|
||||
if (databaseName.isEmpty()) {
|
||||
String path = trim(uri.getPath());
|
||||
databaseName = path.startsWith("/") ? path.substring(1) : path;
|
||||
}
|
||||
if (databaseName.isEmpty()) {
|
||||
databaseName = fallbackDatabaseName;
|
||||
}
|
||||
return new MysqlConnectionTarget(
|
||||
uri.getHost() == null ? "127.0.0.1" : uri.getHost(),
|
||||
uri.getPort() <= 0 ? 3306 : uri.getPort(),
|
||||
trim(datasourceUsername),
|
||||
trim(datasourcePassword),
|
||||
databaseName
|
||||
);
|
||||
} catch (IllegalArgumentException ex) {
|
||||
throw new IllegalStateException("invalid datasource url for backup: " + url, ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] readAllBytes(InputStream inputStream) throws IOException {
|
||||
try (inputStream; ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
|
||||
inputStream.transferTo(outputStream);
|
||||
return outputStream.toByteArray();
|
||||
}
|
||||
}
|
||||
|
||||
private static String trim(String value) {
|
||||
return value == null ? "" : value.trim();
|
||||
}
|
||||
|
||||
private record MysqlConnectionTarget(String host, int port, String username, String password, String databaseName) {
|
||||
}
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
package com.cisd.tms.modules.backup.packagex;
|
||||
|
||||
public class RequiredBackupResourceMissingException extends IllegalStateException {
|
||||
|
||||
public RequiredBackupResourceMissingException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -36,15 +36,18 @@ public class ResourcePackageServiceImpl implements ResourcePackageService {
|
||||
|
||||
private final ResourceBackupProperties resourceBackupProperties;
|
||||
private final ResourceEnvelopeCryptoService resourceEnvelopeCryptoService;
|
||||
private final DatabaseBackupCollector databaseBackupCollector;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public ResourcePackageServiceImpl(
|
||||
ResourceBackupProperties resourceBackupProperties,
|
||||
ResourceEnvelopeCryptoService resourceEnvelopeCryptoService,
|
||||
DatabaseBackupCollector databaseBackupCollector,
|
||||
ObjectMapper objectMapper
|
||||
) {
|
||||
this.resourceBackupProperties = resourceBackupProperties;
|
||||
this.resourceEnvelopeCryptoService = resourceEnvelopeCryptoService;
|
||||
this.databaseBackupCollector = databaseBackupCollector;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
@@ -243,13 +246,18 @@ public class ResourcePackageServiceImpl implements ResourcePackageService {
|
||||
DiscoveredResourceItem item,
|
||||
Map<String, Object> resourceEntry
|
||||
) throws IOException {
|
||||
if ("DB".equalsIgnoreCase(trim(item.getResourceType()))) {
|
||||
return collectDatabaseItem(zipOutputStream, item, resourceEntry);
|
||||
}
|
||||
if (!"FILE".equalsIgnoreCase(trim(item.getResourceType()))) {
|
||||
assertOptionalResource(item, "RESOURCE_TYPE_NOT_SUPPORTED_YET");
|
||||
resourceEntry.put("collected", Boolean.FALSE);
|
||||
resourceEntry.put("skipReason", "RESOURCE_TYPE_NOT_SUPPORTED_YET");
|
||||
return 0;
|
||||
}
|
||||
Path resolvedPath = toResolvedPath(item.getResolvedPath());
|
||||
if (resolvedPath == null || !Files.exists(resolvedPath)) {
|
||||
assertOptionalResource(item, "PATH_NOT_FOUND");
|
||||
resourceEntry.put("collected", Boolean.FALSE);
|
||||
resourceEntry.put("skipReason", "PATH_NOT_FOUND");
|
||||
return 0;
|
||||
@@ -275,6 +283,7 @@ public class ResourcePackageServiceImpl implements ResourcePackageService {
|
||||
)));
|
||||
}
|
||||
} else {
|
||||
assertOptionalResource(item, "UNSUPPORTED_FILE_TYPE");
|
||||
resourceEntry.put("collected", Boolean.FALSE);
|
||||
resourceEntry.put("skipReason", "UNSUPPORTED_FILE_TYPE");
|
||||
return 0;
|
||||
@@ -289,6 +298,73 @@ public class ResourcePackageServiceImpl implements ResourcePackageService {
|
||||
return archivedFiles.size();
|
||||
}
|
||||
|
||||
private int collectDatabaseItem(
|
||||
ZipOutputStream zipOutputStream,
|
||||
DiscoveredResourceItem item,
|
||||
Map<String, Object> resourceEntry
|
||||
) throws IOException {
|
||||
DatabaseBackupCollector.CollectedDatabaseDump dump = resolveDatabaseDump(item);
|
||||
if (dump == null) {
|
||||
assertOptionalResource(item, "RESOURCE_TYPE_NOT_SUPPORTED_YET");
|
||||
resourceEntry.put("collected", Boolean.FALSE);
|
||||
resourceEntry.put("skipReason", "RESOURCE_TYPE_NOT_SUPPORTED_YET");
|
||||
return 0;
|
||||
}
|
||||
try {
|
||||
byte[] content = dump.getContent();
|
||||
if (content == null || content.length == 0) {
|
||||
throw new IllegalStateException("database backup collector returned empty dump");
|
||||
}
|
||||
String databaseName = trim(dump.getDatabaseName()).isEmpty() ? "TMS" : trim(dump.getDatabaseName());
|
||||
String fileName = trim(dump.getFileName()).isEmpty() ? "TMS.sql" : trim(dump.getFileName());
|
||||
String entryPath = "db/" + fileName;
|
||||
addZipEntry(zipOutputStream, entryPath, content);
|
||||
|
||||
Map<String, Object> fileEntry = new LinkedHashMap<>();
|
||||
fileEntry.put("entryPath", entryPath);
|
||||
fileEntry.put("sourcePath", "database:" + databaseName);
|
||||
fileEntry.put("relativeName", fileName);
|
||||
fileEntry.put("size", content.length);
|
||||
fileEntry.put("contentSm3", sm3Hex(content));
|
||||
|
||||
resourceEntry.put("collected", Boolean.TRUE);
|
||||
resourceEntry.put("fileCount", 1);
|
||||
resourceEntry.put("entryPath", entryPath);
|
||||
resourceEntry.put("databaseName", databaseName);
|
||||
resourceEntry.put("files", List.of(fileEntry));
|
||||
return 1;
|
||||
} catch (RuntimeException ex) {
|
||||
assertOptionalResource(item, "DB_DUMP_FAILED");
|
||||
resourceEntry.put("collected", Boolean.FALSE);
|
||||
resourceEntry.put("skipReason", "DB_DUMP_FAILED");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private DatabaseBackupCollector.CollectedDatabaseDump resolveDatabaseDump(DiscoveredResourceItem item) {
|
||||
String resourceCode = trim(item.getResourceCode());
|
||||
if ("TMS_DB_DUMP".equalsIgnoreCase(resourceCode)) {
|
||||
return databaseBackupCollector.collectTmsDatabaseDump();
|
||||
}
|
||||
if ("CMEP_DB_DUMP".equalsIgnoreCase(resourceCode)) {
|
||||
return databaseBackupCollector.collectCmepDatabaseDump();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void assertOptionalResource(DiscoveredResourceItem item, String reason) {
|
||||
if (!item.isRequired()) {
|
||||
return;
|
||||
}
|
||||
String resourceCode = trim(item.getResourceCode()).isEmpty() ? "UNKNOWN" : trim(item.getResourceCode());
|
||||
String resolvedPath = trim(item.getResolvedPath());
|
||||
String message = "required backup resource is unavailable: code=" + resourceCode + ", reason=" + reason;
|
||||
if (!resolvedPath.isEmpty()) {
|
||||
message += ", path=" + resolvedPath;
|
||||
}
|
||||
throw new RequiredBackupResourceMissingException(message);
|
||||
}
|
||||
|
||||
private Map<String, Object> writeRegularFileEntry(
|
||||
ZipOutputStream zipOutputStream,
|
||||
DiscoveredResourceItem item,
|
||||
|
||||
-30
@@ -1,30 +0,0 @@
|
||||
package com.cisd.tms.modules.backup.runner;
|
||||
|
||||
import com.cisd.tms.modules.backup.config.ResourceBackupProperties;
|
||||
import java.nio.file.Path;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class AsyncResourceRestoreRunnerLauncher implements ResourceRestoreRunnerLauncher {
|
||||
|
||||
private final ResourceRestoreRunner resourceRestoreRunner;
|
||||
private final ResourceBackupProperties resourceBackupProperties;
|
||||
|
||||
public AsyncResourceRestoreRunnerLauncher(
|
||||
ResourceRestoreRunner resourceRestoreRunner,
|
||||
ResourceBackupProperties resourceBackupProperties
|
||||
) {
|
||||
this.resourceRestoreRunner = resourceRestoreRunner;
|
||||
this.resourceBackupProperties = resourceBackupProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void launch(Path handoffFile) {
|
||||
if (!resourceBackupProperties.getRunnerAutoLaunchEnabled()) {
|
||||
return;
|
||||
}
|
||||
Thread thread = new Thread(() -> resourceRestoreRunner.run(handoffFile), "resource-restore-runner");
|
||||
thread.setDaemon(true);
|
||||
thread.start();
|
||||
}
|
||||
}
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
package com.cisd.tms.modules.backup.runner;
|
||||
|
||||
import com.cisd.tms.modules.backup.config.ResourceBackupProperties;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class ConsistencyVerifyRunnerStepExecutor implements RunnerStepExecutor {
|
||||
|
||||
private static final List<String> REQUIRED_SUCCESS_STEPS = List.of(
|
||||
"RESTORE_FILES",
|
||||
"RESTORE_DATABASES",
|
||||
"START_SERVICES"
|
||||
);
|
||||
|
||||
private final ResourceBackupProperties resourceBackupProperties;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final HttpClient httpClient;
|
||||
|
||||
public ConsistencyVerifyRunnerStepExecutor(
|
||||
ResourceBackupProperties resourceBackupProperties,
|
||||
ObjectMapper objectMapper
|
||||
) {
|
||||
this(resourceBackupProperties, objectMapper, HttpClient.newBuilder()
|
||||
.connectTimeout(Duration.ofSeconds(5))
|
||||
.build());
|
||||
}
|
||||
|
||||
ConsistencyVerifyRunnerStepExecutor(
|
||||
ResourceBackupProperties resourceBackupProperties,
|
||||
ObjectMapper objectMapper,
|
||||
HttpClient httpClient
|
||||
) {
|
||||
this.resourceBackupProperties = resourceBackupProperties;
|
||||
this.objectMapper = objectMapper;
|
||||
this.httpClient = httpClient;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(String stepCode) {
|
||||
return "CONSISTENCY_VERIFY".equals(stepCode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RunnerStepExecutionResult execute(RunnerStepContext context) {
|
||||
Path stepOutputDir = context.taskDir().resolve("step-output");
|
||||
for (String requiredStep : REQUIRED_SUCCESS_STEPS) {
|
||||
assertStepSucceeded(stepOutputDir, requiredStep);
|
||||
}
|
||||
verifyHealthEndpoint();
|
||||
return RunnerStepExecutionResult.success("required steps succeeded and health endpoint is UP");
|
||||
}
|
||||
|
||||
private void assertStepSucceeded(Path stepOutputDir, String stepCode) {
|
||||
try {
|
||||
Path stepFile = resolveRequiredStepOutput(stepOutputDir, stepCode);
|
||||
Map<String, Object> payload = objectMapper.readValue(stepFile.toFile(), new TypeReference<Map<String, Object>>() {
|
||||
});
|
||||
String result = String.valueOf(payload.get("result"));
|
||||
if (!"SUCCESS".equalsIgnoreCase(result)) {
|
||||
throw new IllegalStateException("required step is not successful: " + stepCode);
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("failed to read step output for " + stepCode, ex);
|
||||
}
|
||||
}
|
||||
|
||||
private Path resolveRequiredStepOutput(Path stepOutputDir, String stepCode) throws IOException {
|
||||
if (!Files.isDirectory(stepOutputDir)) {
|
||||
throw new IllegalStateException("step output directory missing: " + stepOutputDir);
|
||||
}
|
||||
try (var pathStream = Files.list(stepOutputDir)) {
|
||||
return pathStream
|
||||
.filter(path -> path.getFileName().toString().endsWith("-" + stepCode + ".json"))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new IllegalStateException("step output missing for " + stepCode));
|
||||
}
|
||||
}
|
||||
|
||||
private void verifyHealthEndpoint() {
|
||||
String healthUrl = resourceBackupProperties.getHealthCheckUrl();
|
||||
try {
|
||||
HttpRequest request = HttpRequest.newBuilder(URI.create(healthUrl))
|
||||
.timeout(Duration.ofSeconds(resourceBackupProperties.getHealthCheckTimeoutSeconds()))
|
||||
.GET()
|
||||
.build();
|
||||
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
if (response.statusCode() != 200 || !response.body().contains("\"status\":\"UP\"")) {
|
||||
throw new IllegalStateException("health endpoint is not UP: statusCode=" + response.statusCode());
|
||||
}
|
||||
} catch (InterruptedException ex) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IllegalStateException("health verification interrupted", ex);
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalStateException("failed to verify health endpoint: " + healthUrl, ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
package com.cisd.tms.modules.backup.runner;
|
||||
|
||||
import com.cisd.tms.modules.backup.config.ResourceBackupProperties;
|
||||
import java.net.URI;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class DatabaseRestoreRunnerStepExecutor implements RunnerStepExecutor {
|
||||
|
||||
private final ResourceBackupProperties resourceBackupProperties;
|
||||
private final RunnerCommandInvoker runnerCommandInvoker;
|
||||
private final String datasourceUrl;
|
||||
private final String datasourceUsername;
|
||||
private final String datasourcePassword;
|
||||
|
||||
public DatabaseRestoreRunnerStepExecutor(
|
||||
ResourceBackupProperties resourceBackupProperties,
|
||||
RunnerCommandInvoker runnerCommandInvoker,
|
||||
@Value("${spring.datasource.url:}") String datasourceUrl,
|
||||
@Value("${spring.datasource.username:}") String datasourceUsername,
|
||||
@Value("${spring.datasource.password:}") String datasourcePassword
|
||||
) {
|
||||
this.resourceBackupProperties = resourceBackupProperties;
|
||||
this.runnerCommandInvoker = runnerCommandInvoker;
|
||||
this.datasourceUrl = datasourceUrl;
|
||||
this.datasourceUsername = datasourceUsername;
|
||||
this.datasourcePassword = datasourcePassword;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(String stepCode) {
|
||||
return "RESTORE_DATABASES".equals(stepCode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RunnerStepExecutionResult execute(RunnerStepContext context) {
|
||||
if (!supports(context.stepCode())) {
|
||||
throw new IllegalArgumentException("unsupported runner step: " + context.stepCode());
|
||||
}
|
||||
List<String> restoredDatabases = new ArrayList<>();
|
||||
restoreDatabase(context, "TMS.sql", resourceBackupProperties.getTmsDatabaseName(), "TMS", true, restoredDatabases);
|
||||
// 标准版第一版还需要恢复收发器业务库;DIRECT 当前没有这份库时允许跳过。
|
||||
restoreDatabase(context, "CMEP.sql", resourceBackupProperties.getCmepDatabaseName(), "CMEP", false, restoredDatabases);
|
||||
return RunnerStepExecutionResult.success("restored databases " + String.join(",", restoredDatabases));
|
||||
}
|
||||
|
||||
private void restoreDatabase(
|
||||
RunnerStepContext context,
|
||||
String sqlFileName,
|
||||
String configuredDatabaseName,
|
||||
String fallbackDatabaseName,
|
||||
boolean required,
|
||||
List<String> restoredDatabases
|
||||
) {
|
||||
Path sqlFile = context.taskDir().resolve("staging").resolve("payload").resolve("db").resolve(sqlFileName).normalize();
|
||||
if (!Files.isRegularFile(sqlFile)) {
|
||||
if (required) {
|
||||
throw new IllegalStateException("restore database sql file missing: " + sqlFile);
|
||||
}
|
||||
return;
|
||||
}
|
||||
MysqlTarget target = resolveTarget(configuredDatabaseName, fallbackDatabaseName);
|
||||
List<String> command = new ArrayList<>();
|
||||
command.add("/bin/bash");
|
||||
command.add(resourceBackupProperties.getRestoreDbScriptPath());
|
||||
command.add("--mysql-path");
|
||||
command.add(resourceBackupProperties.getMysqlPath());
|
||||
command.add("--host");
|
||||
command.add(target.host());
|
||||
command.add("--port");
|
||||
command.add(String.valueOf(target.port()));
|
||||
command.add("--username");
|
||||
command.add(target.username());
|
||||
command.add("--password");
|
||||
command.add(target.password());
|
||||
command.add("--db-name");
|
||||
command.add(target.databaseName());
|
||||
command.add("--sql-file");
|
||||
command.add(sqlFile.toString());
|
||||
|
||||
int exitCode = runnerCommandInvoker.run(command, resolveLogFile(context), context.taskDir());
|
||||
if (exitCode != 0) {
|
||||
throw new IllegalStateException("runner step RESTORE_DATABASES failed for " + target.databaseName() + ", exitCode=" + exitCode);
|
||||
}
|
||||
restoredDatabases.add(target.databaseName());
|
||||
}
|
||||
|
||||
private MysqlTarget resolveTarget(String configuredDatabaseName, String fallbackDatabaseName) {
|
||||
String url = trim(datasourceUrl);
|
||||
if (url.isEmpty()) {
|
||||
throw new IllegalStateException("spring.datasource.url is required for database restore");
|
||||
}
|
||||
if (!url.startsWith("jdbc:mysql://")) {
|
||||
throw new IllegalStateException("unsupported datasource url for restore: " + url);
|
||||
}
|
||||
try {
|
||||
String normalizedUrl = url.substring("jdbc:".length());
|
||||
String[] segments = normalizedUrl.split("\\?", 2);
|
||||
URI uri = URI.create(segments[0]);
|
||||
String databaseName = trim(configuredDatabaseName);
|
||||
if (databaseName.isEmpty()) {
|
||||
String path = trim(uri.getPath());
|
||||
databaseName = path.startsWith("/") ? path.substring(1) : path;
|
||||
}
|
||||
if (databaseName.isEmpty()) {
|
||||
databaseName = fallbackDatabaseName;
|
||||
}
|
||||
return new MysqlTarget(
|
||||
uri.getHost() == null ? "127.0.0.1" : uri.getHost(),
|
||||
uri.getPort() <= 0 ? 3306 : uri.getPort(),
|
||||
trim(datasourceUsername),
|
||||
trim(datasourcePassword),
|
||||
databaseName
|
||||
);
|
||||
} catch (IllegalArgumentException ex) {
|
||||
throw new IllegalStateException("invalid datasource url for restore: " + url, ex);
|
||||
}
|
||||
}
|
||||
|
||||
private Path resolveLogFile(RunnerStepContext context) {
|
||||
return context.taskDir().resolve("steps").resolve(String.format("%02d-%s.log", context.stepNo(), context.stepCode()));
|
||||
}
|
||||
|
||||
private static String trim(String value) {
|
||||
return value == null ? "" : value.trim();
|
||||
}
|
||||
|
||||
private record MysqlTarget(String host, int port, String username, String password, String databaseName) {
|
||||
}
|
||||
}
|
||||
+70
-15
@@ -10,10 +10,12 @@ import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
import org.springframework.stereotype.Component;
|
||||
@@ -21,6 +23,9 @@ import org.springframework.stereotype.Component;
|
||||
@Component
|
||||
public class PayloadArchiveRunnerStepExecutor implements RunnerStepExecutor {
|
||||
|
||||
private static final Set<String> RESTORE_FILE_RESOURCE_CODES =
|
||||
Set.of("TMS_CONFIG", "CP_CONFIG", "RECEIVER_LICENSE");
|
||||
|
||||
private final HandoffFileStore handoffFileStore;
|
||||
private final ResourcePackageService resourcePackageService;
|
||||
private final ObjectMapper objectMapper;
|
||||
@@ -37,7 +42,7 @@ public class PayloadArchiveRunnerStepExecutor implements RunnerStepExecutor {
|
||||
|
||||
@Override
|
||||
public boolean supports(String stepCode) {
|
||||
return "RESTORE_FILES".equals(stepCode) || "CONSISTENCY_VERIFY".equals(stepCode);
|
||||
return "RESTORE_FILES".equals(stepCode);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -45,9 +50,6 @@ public class PayloadArchiveRunnerStepExecutor implements RunnerStepExecutor {
|
||||
if ("RESTORE_FILES".equals(context.stepCode())) {
|
||||
return restoreFiles(context);
|
||||
}
|
||||
if ("CONSISTENCY_VERIFY".equals(context.stepCode())) {
|
||||
return verifyExtractedPayload(context);
|
||||
}
|
||||
throw new IllegalArgumentException("unsupported runner step: " + context.stepCode());
|
||||
}
|
||||
|
||||
@@ -60,17 +62,12 @@ public class PayloadArchiveRunnerStepExecutor implements RunnerStepExecutor {
|
||||
Path payloadDir = context.taskDir().resolve("staging").resolve("payload").normalize();
|
||||
extractPayloadArchive(payloadArchiveBytes, payloadDir);
|
||||
|
||||
int resourceCount = readResourceCount(payloadDir.resolve("payload-manifest.json"));
|
||||
return RunnerStepExecutionResult.success("payload extracted to staging, resourceCount=" + resourceCount);
|
||||
}
|
||||
|
||||
private RunnerStepExecutionResult verifyExtractedPayload(RunnerStepContext context) {
|
||||
Path manifestFile = context.taskDir().resolve("staging").resolve("payload").resolve("payload-manifest.json").normalize();
|
||||
if (!Files.isRegularFile(manifestFile)) {
|
||||
throw new IllegalStateException("payload manifest missing: " + manifestFile);
|
||||
}
|
||||
int resourceCount = readResourceCount(manifestFile);
|
||||
return RunnerStepExecutionResult.success("payload manifest verified, resourceCount=" + resourceCount);
|
||||
Path payloadManifestFile = payloadDir.resolve("payload-manifest.json");
|
||||
int resourceCount = readResourceCount(payloadManifestFile);
|
||||
int restoredFileCount = restoreSupportedFiles(payloadDir, payloadManifestFile);
|
||||
return RunnerStepExecutionResult.success(
|
||||
"payload extracted to staging, resourceCount=" + resourceCount + ", restoredFileCount=" + restoredFileCount
|
||||
);
|
||||
}
|
||||
|
||||
private ResourceRestoreHandoffSnapshot readRequiredHandoff(Path handoffFile) {
|
||||
@@ -133,6 +130,64 @@ public class PayloadArchiveRunnerStepExecutor implements RunnerStepExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
private int restoreSupportedFiles(Path payloadDir, Path manifestFile) {
|
||||
try {
|
||||
Map<String, Object> manifest = objectMapper.readValue(manifestFile.toFile(), new TypeReference<Map<String, Object>>() {
|
||||
});
|
||||
Object resourcesValue = manifest.get("resources");
|
||||
if (!(resourcesValue instanceof List<?> resources)) {
|
||||
return 0;
|
||||
}
|
||||
int restoredFileCount = 0;
|
||||
for (Object resourceObject : resources) {
|
||||
if (!(resourceObject instanceof Map<?, ?> rawResource)) {
|
||||
continue;
|
||||
}
|
||||
Map<String, Object> resource = castMap(rawResource);
|
||||
String resourceCode = String.valueOf(resource.get("resourceCode"));
|
||||
if (!RESTORE_FILE_RESOURCE_CODES.contains(resourceCode)) {
|
||||
continue;
|
||||
}
|
||||
Object filesValue = resource.get("files");
|
||||
if (!(filesValue instanceof List<?> rawFiles)) {
|
||||
continue;
|
||||
}
|
||||
for (Object fileObject : rawFiles) {
|
||||
if (!(fileObject instanceof Map<?, ?> rawFile)) {
|
||||
continue;
|
||||
}
|
||||
Map<String, Object> fileEntry = castMap(rawFile);
|
||||
Path sourceFile = payloadDir.resolve(String.valueOf(fileEntry.get("entryPath"))).normalize();
|
||||
if (!sourceFile.startsWith(payloadDir) || !Files.isRegularFile(sourceFile)) {
|
||||
throw new IllegalStateException("restored file source missing: " + sourceFile);
|
||||
}
|
||||
Path targetFile = resolveTargetFile(resourceCode, String.valueOf(resource.get("resolvedPath")), String.valueOf(fileEntry.get("relativeName")));
|
||||
Files.createDirectories(targetFile.getParent());
|
||||
// 第一版只回写最关键的配置/license 文件,不做目录合并或增量策略。
|
||||
Files.copy(sourceFile, targetFile, StandardCopyOption.REPLACE_EXISTING);
|
||||
restoredFileCount++;
|
||||
}
|
||||
}
|
||||
return restoredFileCount;
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("failed to restore payload files", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private Path resolveTargetFile(String resourceCode, String resolvedPath, String relativeName) {
|
||||
Path targetPath = Path.of(resolvedPath).normalize();
|
||||
if ("TMS_CONFIG".equals(resourceCode)) {
|
||||
return targetPath.resolve(relativeName).normalize();
|
||||
}
|
||||
return targetPath;
|
||||
}
|
||||
|
||||
private static Map<String, Object> castMap(Map<?, ?> rawMap) {
|
||||
Map<String, Object> result = new java.util.LinkedHashMap<>();
|
||||
rawMap.forEach((key, value) -> result.put(String.valueOf(key), value));
|
||||
return result;
|
||||
}
|
||||
|
||||
private static byte[] readAllBytes(InputStream inputStream) throws IOException {
|
||||
return inputStream.readAllBytes();
|
||||
}
|
||||
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package com.cisd.tms.modules.backup.runner;
|
||||
|
||||
import com.cisd.tms.modules.backup.config.ResourceBackupProperties;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class ProcessResourceRestoreRunnerLauncher implements ResourceRestoreRunnerLauncher {
|
||||
|
||||
private final ResourceBackupProperties resourceBackupProperties;
|
||||
private final RunnerCommandInvoker runnerCommandInvoker;
|
||||
|
||||
public ProcessResourceRestoreRunnerLauncher(
|
||||
ResourceBackupProperties resourceBackupProperties,
|
||||
RunnerCommandInvoker runnerCommandInvoker
|
||||
) {
|
||||
this.resourceBackupProperties = resourceBackupProperties;
|
||||
this.runnerCommandInvoker = runnerCommandInvoker;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void launch(Path handoffFile) {
|
||||
if (!resourceBackupProperties.getRunnerAutoLaunchEnabled()) {
|
||||
return;
|
||||
}
|
||||
Path normalizedHandoffFile = handoffFile.normalize().toAbsolutePath();
|
||||
Path taskDir = normalizedHandoffFile.getParent();
|
||||
Path launcherLog = taskDir.resolve("runner-launch.log");
|
||||
List<String> command = List.of(
|
||||
"/bin/bash",
|
||||
resourceBackupProperties.getRunnerLaunchScriptPath(),
|
||||
"--handoff-file",
|
||||
normalizedHandoffFile.toString()
|
||||
);
|
||||
int exitCode = runnerCommandInvoker.run(command, launcherLog, taskDir);
|
||||
if (exitCode != 0) {
|
||||
throw new IllegalStateException("failed to launch external restore runner, exitCode=" + exitCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.cisd.tms.modules.backup.runner;
|
||||
|
||||
import com.cisd.tms.TmsApplication;
|
||||
import java.nio.file.Path;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.WebApplicationType;
|
||||
import org.springframework.boot.builder.SpringApplicationBuilder;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.core.env.Environment;
|
||||
|
||||
public final class ResourceRestoreRunnerMain {
|
||||
|
||||
private ResourceRestoreRunnerMain() {
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
ConfigurableApplicationContext context = new SpringApplicationBuilder(TmsApplication.class)
|
||||
.web(WebApplicationType.NONE)
|
||||
.logStartupInfo(false)
|
||||
.run(args);
|
||||
int exitCode = 0;
|
||||
try {
|
||||
Environment environment = context.getEnvironment();
|
||||
String handoffFile = environment.getProperty("tms.backup.runner-handoff-file", "");
|
||||
if (handoffFile == null || handoffFile.isBlank()) {
|
||||
throw new IllegalStateException("tms.backup.runner-handoff-file is required");
|
||||
}
|
||||
context.getBean(ResourceRestoreRunner.class).run(Path.of(handoffFile));
|
||||
} catch (RuntimeException ex) {
|
||||
exitCode = 1;
|
||||
throw ex;
|
||||
} finally {
|
||||
final int finalExitCode = exitCode;
|
||||
int springExitCode = SpringApplication.exit(context, () -> finalExitCode);
|
||||
System.exit(springExitCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
+32
-20
@@ -10,6 +10,7 @@ import com.cisd.tms.modules.backup.dto.response.ResourceKeysetStatusResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceTaskStepItemResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceTaskStepLogResponse;
|
||||
import com.cisd.tms.modules.backup.entity.ResourceBackupTaskEntity;
|
||||
import com.cisd.tms.modules.backup.packagex.RequiredBackupResourceMissingException;
|
||||
import com.cisd.tms.modules.backup.packagex.ResourcePackageResult;
|
||||
import com.cisd.tms.modules.backup.packagex.ResourcePackageService;
|
||||
import com.cisd.tms.modules.backup.repository.ResourceBackupTaskRepository;
|
||||
@@ -62,45 +63,56 @@ public class ResourceBackupServiceImpl implements ResourceBackupService {
|
||||
|
||||
String taskId = resourceTaskIdGenerator.nextBackupTaskId();
|
||||
String backupId = resourceTaskIdGenerator.nextBackupId();
|
||||
// 先根据初始化快照收敛资源范围,再交给打包服务生成本地可见的 .tmsbak 文件。
|
||||
ResourcePackageResult packageResult = resourcePackageService.createPackage(
|
||||
taskId,
|
||||
backupId,
|
||||
initTask,
|
||||
resourceDiscoveryService.discover(initTask),
|
||||
request
|
||||
);
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
ResourceBackupTaskEntity entity = new ResourceBackupTaskEntity();
|
||||
// 第一版先按同步成功模型落库,后续切换为真正异步任务时继续复用这张任务表。
|
||||
entity.setTaskId(taskId);
|
||||
entity.setBackupId(backupId);
|
||||
entity.setStatus("SUCCESS");
|
||||
entity.setStatus("RUNNING");
|
||||
entity.setProductType(initTask.getProductType());
|
||||
entity.setMqType(initTask.getMqType());
|
||||
entity.setOrgCode(initTask.getOrgCode());
|
||||
entity.setSourceDeviceId(resolveSourceDeviceId());
|
||||
entity.setPackageName(packageResult.getPackageName());
|
||||
entity.setPackagePath(packageResult.getPackagePath());
|
||||
entity.setPackageSize(packageResult.getPackageSize());
|
||||
entity.setPackageHash(packageResult.getPackageHash());
|
||||
entity.setStorageEncKeyFingerprint(packageResult.getStorageEncKeyFingerprint());
|
||||
entity.setAuthenticityKeyFingerprint(packageResult.getAuthenticityKeyFingerprint());
|
||||
entity.setManifestSummaryJson(packageResult.getManifestSummaryJson());
|
||||
entity.setRemark(request == null ? null : request.getRemark());
|
||||
entity.setCreateBy("SYSTEM");
|
||||
entity.setCreateTime(now);
|
||||
entity.setUpdateTime(now);
|
||||
entity.setStartTime(now);
|
||||
entity.setFinishTime(now);
|
||||
|
||||
try {
|
||||
// 先根据初始化快照收敛资源范围,再交给打包服务生成本地可见的 .tmsbak 文件。
|
||||
ResourcePackageResult packageResult = resourcePackageService.createPackage(
|
||||
taskId,
|
||||
backupId,
|
||||
initTask,
|
||||
resourceDiscoveryService.discover(initTask),
|
||||
request
|
||||
);
|
||||
// 第一版先按同步模型直接落最终结果,后续切换异步任务时仍复用同一张任务表。
|
||||
entity.setStatus("SUCCESS");
|
||||
entity.setPackageName(packageResult.getPackageName());
|
||||
entity.setPackagePath(packageResult.getPackagePath());
|
||||
entity.setPackageSize(packageResult.getPackageSize());
|
||||
entity.setPackageHash(packageResult.getPackageHash());
|
||||
entity.setStorageEncKeyFingerprint(packageResult.getStorageEncKeyFingerprint());
|
||||
entity.setAuthenticityKeyFingerprint(packageResult.getAuthenticityKeyFingerprint());
|
||||
entity.setManifestSummaryJson(packageResult.getManifestSummaryJson());
|
||||
} catch (RequiredBackupResourceMissingException ex) {
|
||||
entity.setStatus("FAILED");
|
||||
entity.setErrorCode("REQUIRED_RESOURCE_MISSING");
|
||||
entity.setErrorMessage(ex.getMessage());
|
||||
} catch (RuntimeException ex) {
|
||||
entity.setStatus("FAILED");
|
||||
entity.setErrorCode("PACKAGE_CREATE_FAILED");
|
||||
entity.setErrorMessage(ex.getMessage());
|
||||
}
|
||||
entity.setFinishTime(LocalDateTime.now());
|
||||
resourceBackupTaskRepository.save(entity);
|
||||
|
||||
CreateResourceBackupResponse response = new CreateResourceBackupResponse();
|
||||
response.setTaskId(taskId);
|
||||
response.setBackupId(backupId);
|
||||
response.setStatus(entity.getStatus());
|
||||
response.setCreatedAt(now.toString());
|
||||
response.setCreatedAt(entity.getCreateTime().toString());
|
||||
return response;
|
||||
}
|
||||
|
||||
|
||||
@@ -186,6 +186,22 @@ tms:
|
||||
output-dir: ${TMS_BACKUP_OUTPUT_DIR:/home/tms/tmp/resource-backup-packages}
|
||||
# 恢复 runner 启停 TMS 时使用的脚本路径。
|
||||
tms-script-path: ${TMS_BACKUP_TMS_SCRIPT_PATH:/home/tms/scripts/tms.sh}
|
||||
# 外部恢复 runner 启动脚本路径;创建恢复任务后由主进程调用该脚本拉起独立 runner。
|
||||
runner-launch-script-path: ${TMS_BACKUP_RUNNER_LAUNCH_SCRIPT_PATH:/home/tms/bin/resource-restore/start-runner.sh}
|
||||
# 导出 TMS 库时使用的 mysqldump 命令;现场可按实际安装路径覆盖。
|
||||
mysqldump-path: ${TMS_BACKUP_MYSQLDUMP_PATH:mysqldump}
|
||||
# 恢复 TMS 库时使用的 mysql 客户端命令。
|
||||
mysql-path: ${TMS_BACKUP_MYSQL_PATH:mysql}
|
||||
# 数据库恢复脚本路径;runner 在 RESTORE_DATABASES 阶段调用。
|
||||
restore-db-script-path: ${TMS_BACKUP_RESTORE_DB_SCRIPT_PATH:/home/tms/bin/resource-restore/restore-db.sh}
|
||||
# 备份 TMS 库名;为空时默认从 spring.datasource.url 中解析。
|
||||
tms-database-name: ${TMS_BACKUP_TMS_DATABASE_NAME:}
|
||||
# 备份/恢复标准收发器业务库名;第一版默认使用 CMEP。
|
||||
cmep-database-name: ${TMS_BACKUP_CMEP_DATABASE_NAME:CMEP}
|
||||
# 恢复完成后的最小健康检查地址。
|
||||
health-check-url: ${TMS_BACKUP_HEALTH_CHECK_URL:http://127.0.0.1:8080/actuator/health}
|
||||
# 健康检查请求超时(秒)。
|
||||
health-check-timeout-seconds: ${TMS_BACKUP_HEALTH_CHECK_TIMEOUT_SECONDS:5}
|
||||
# 是否在创建恢复任务后自动拉起本机 runner;默认关闭,避免开发环境误触停机恢复链。
|
||||
runner-auto-launch-enabled: ${TMS_BACKUP_RUNNER_AUTO_LAUNCH_ENABLED:false}
|
||||
# 恢复 runner 心跳失联超时(秒)。
|
||||
|
||||
Reference in New Issue
Block a user