This is an automated email from the ASF dual-hosted git repository. dataroaring pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/master by this push: new 365fdd2f4db [feature](backup) add property to remove snapshot before creating repo (#25847) 365fdd2f4db is described below commit 365fdd2f4dbf034ea32e51ebd76d43bec3d6a49b Author: walter <w41te...@gmail.com> AuthorDate: Fri Oct 27 21:03:26 2023 +0800 [feature](backup) add property to remove snapshot before creating repo (#25847) Doris is not responsible for managing snapshots, but it needs to clear all snapshots before doing backup/restore regression testing, so a property is added to indicate that existing snapshots need to be cleared when creating a repo. In addition, a regression test case for backup/restore has been added. --- .../Backup-and-Restore/CREATE-REPOSITORY.md | 19 +++++ .../Backup-and-Restore/CREATE-REPOSITORY.md | 19 +++++ .../doris/analysis/CreateRepositoryStmt.java | 12 +++ .../java/org/apache/doris/backup/Repository.java | 24 +++++- .../java/org/apache/doris/fs/obj/ObjStorage.java | 2 + .../java/org/apache/doris/fs/obj/S3ObjStorage.java | 74 ++++++++++++++++- .../org/apache/doris/fs/remote/S3FileSystem.java | 4 + .../org/apache/doris/fs/obj/S3ObjStorageTest.java | 90 +++++++++++++++++--- .../apache/doris/regression/suite/Syncer.groovy | 95 +++++++++++++++++++++- .../backup_restore/test_backup_restore.groovy | 66 ++++++++++++++- .../test_create_and_drop_repository.groovy | 60 ++++++++++++-- 11 files changed, 440 insertions(+), 25 deletions(-) diff --git a/docs/en/docs/sql-manual/sql-reference/Data-Definition-Statements/Backup-and-Restore/CREATE-REPOSITORY.md b/docs/en/docs/sql-manual/sql-reference/Data-Definition-Statements/Backup-and-Restore/CREATE-REPOSITORY.md index 2308fd6b9c3..f5571c4c441 100644 --- a/docs/en/docs/sql-manual/sql-reference/Data-Definition-Statements/Backup-and-Restore/CREATE-REPOSITORY.md +++ b/docs/en/docs/sql-manual/sql-reference/Data-Definition-Statements/Backup-and-Restore/CREATE-REPOSITORY.md @@ -174,6 +174,25 @@ PROPERTIES ); ``` +9. Create repository and delete snapshots if exists. + +```sql +CREATE REPOSITORY `s3_repo` +WITH S3 +ON LOCATION "s3://s3-repo" +PROPERTIES +( + "s3.endpoint" = "http://s3-REGION.amazonaws.com", + "s3.region" = "s3-REGION", + "s3.access_key" = "AWS_ACCESS_KEY", + "s3.secret_key"="AWS_SECRET_KEY", + "s3.region" = "REGION", + "delete_if_exists" = "true" +); +``` + +Note: only the s3 service supports the "delete_if_exists" property. + ### Keywords CREATE, REPOSITORY diff --git a/docs/zh-CN/docs/sql-manual/sql-reference/Data-Definition-Statements/Backup-and-Restore/CREATE-REPOSITORY.md b/docs/zh-CN/docs/sql-manual/sql-reference/Data-Definition-Statements/Backup-and-Restore/CREATE-REPOSITORY.md index 4057d67b00d..972f0b7f6d7 100644 --- a/docs/zh-CN/docs/sql-manual/sql-reference/Data-Definition-Statements/Backup-and-Restore/CREATE-REPOSITORY.md +++ b/docs/zh-CN/docs/sql-manual/sql-reference/Data-Definition-Statements/Backup-and-Restore/CREATE-REPOSITORY.md @@ -170,6 +170,25 @@ PROPERTIES ); ``` +9. 创建仓库并删除已经存在的 snapshot + +```sql +CREATE REPOSITORY `s3_repo` +WITH S3 +ON LOCATION "s3://s3-repo" +PROPERTIES +( + "s3.endpoint" = "http://s3-REGION.amazonaws.com", + "s3.region" = "s3-REGION", + "s3.access_key" = "AWS_ACCESS_KEY", + "s3.secret_key"="AWS_SECRET_KEY", + "s3.region" = "REGION", + "delete_if_exists" = "true" +); +``` + +注:目前只有 s3 支持 "delete_if_exists" 属性。 + ### Keywords CREATE, REPOSITORY diff --git a/fe/fe-core/src/main/java/org/apache/doris/analysis/CreateRepositoryStmt.java b/fe/fe-core/src/main/java/org/apache/doris/analysis/CreateRepositoryStmt.java index 539a2618eee..0cb30dd7a36 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/analysis/CreateRepositoryStmt.java +++ b/fe/fe-core/src/main/java/org/apache/doris/analysis/CreateRepositoryStmt.java @@ -28,6 +28,8 @@ import org.apache.doris.qe.ConnectContext; import java.util.Map; public class CreateRepositoryStmt extends DdlStmt { + public static String PROP_DELETE_IF_EXISTS = "delete_if_exists"; + private boolean isReadOnly; private String name; private StorageBackend storage; @@ -71,6 +73,16 @@ public class CreateRepositoryStmt extends DdlStmt { ErrorReport.reportAnalysisException(ErrorCode.ERR_SPECIFIC_ACCESS_DENIED_ERROR, "ADMIN"); } FeNameFormat.checkCommonName("repository", name); + + // check delete_if_exists, this property will be used by Repository.initRepository. + Map<String, String> properties = getProperties(); + String deleteIfExistsStr = properties.get(PROP_DELETE_IF_EXISTS); + if (deleteIfExistsStr != null) { + if (!deleteIfExistsStr.equalsIgnoreCase("true") && !deleteIfExistsStr.equalsIgnoreCase("false")) { + ErrorReport.reportAnalysisException(ErrorCode.ERR_COMMON_ERROR, + "'" + PROP_DELETE_IF_EXISTS + "' in properties, you should set it false or true"); + } + } } @Override diff --git a/fe/fe-core/src/main/java/org/apache/doris/backup/Repository.java b/fe/fe-core/src/main/java/org/apache/doris/backup/Repository.java index dbe4d5afb7a..27ce489948a 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/backup/Repository.java +++ b/fe/fe-core/src/main/java/org/apache/doris/backup/Repository.java @@ -17,6 +17,7 @@ package org.apache.doris.backup; +import org.apache.doris.analysis.CreateRepositoryStmt; import org.apache.doris.analysis.StorageBackend; import org.apache.doris.backup.Status.ErrCode; import org.apache.doris.catalog.Env; @@ -215,6 +216,27 @@ public class Repository implements Writable { if (FeConstants.runningUnitTest) { return Status.OK; } + + // A temporary solution is to delete all stale snapshots before creating an S3 repository + // so that we can add regression tests about backup/restore. + // + // TODO: support hdfs/brokers + if (fileSystem instanceof S3FileSystem) { + String deleteStaledSnapshots = fileSystem.getProperties() + .getOrDefault(CreateRepositoryStmt.PROP_DELETE_IF_EXISTS, "false"); + if (deleteStaledSnapshots.equalsIgnoreCase("true")) { + // delete with prefix: + // eg. __palo_repository_repo_name/ + String snapshotPrefix = Joiner.on(PATH_DELIMITER).join(location, joinPrefix(PREFIX_REPO, name)); + LOG.info("property {} is set, delete snapshots with prefix: {}", + CreateRepositoryStmt.PROP_DELETE_IF_EXISTS, snapshotPrefix); + Status st = ((S3FileSystem) fileSystem).deleteDirectory(snapshotPrefix); + if (!st.ok()) { + return st; + } + } + } + String repoInfoFilePath = assembleRepoInfoFilePath(); // check if the repo is already exist in remote List<RemoteFile> remoteFiles = Lists.newArrayList(); @@ -245,8 +267,8 @@ public class Repository implements Writable { return new Status(ErrCode.COMMON_ERROR, "failed to parse create time of repository: " + root.get("create_time")); } - return Status.OK; + return Status.OK; } catch (IOException e) { return new Status(ErrCode.COMMON_ERROR, "failed to read repo info file: " + e.getMessage()); } finally { diff --git a/fe/fe-core/src/main/java/org/apache/doris/fs/obj/ObjStorage.java b/fe/fe-core/src/main/java/org/apache/doris/fs/obj/ObjStorage.java index 31f9c065cc7..b964e3022ac 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/fs/obj/ObjStorage.java +++ b/fe/fe-core/src/main/java/org/apache/doris/fs/obj/ObjStorage.java @@ -44,6 +44,8 @@ public interface ObjStorage<C> { Status deleteObject(String remotePath); + Status deleteObjects(String remotePath); + Status copyObject(String origFilePath, String destFilePath); RemoteObjects listObjects(String remotePath, String continuationToken) throws DdlException; diff --git a/fe/fe-core/src/main/java/org/apache/doris/fs/obj/S3ObjStorage.java b/fe/fe-core/src/main/java/org/apache/doris/fs/obj/S3ObjStorage.java index 930988e54b8..d1e8e74b49a 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/fs/obj/S3ObjStorage.java +++ b/fe/fe-core/src/main/java/org/apache/doris/fs/obj/S3ObjStorage.java @@ -37,14 +37,18 @@ import software.amazon.awssdk.core.sync.RequestBody; import software.amazon.awssdk.services.s3.S3Client; import software.amazon.awssdk.services.s3.model.CopyObjectRequest; import software.amazon.awssdk.services.s3.model.CopyObjectResponse; +import software.amazon.awssdk.services.s3.model.Delete; import software.amazon.awssdk.services.s3.model.DeleteObjectRequest; import software.amazon.awssdk.services.s3.model.DeleteObjectResponse; +import software.amazon.awssdk.services.s3.model.DeleteObjectsRequest; +import software.amazon.awssdk.services.s3.model.DeleteObjectsResponse; import software.amazon.awssdk.services.s3.model.GetObjectRequest; import software.amazon.awssdk.services.s3.model.GetObjectResponse; import software.amazon.awssdk.services.s3.model.HeadObjectRequest; import software.amazon.awssdk.services.s3.model.HeadObjectResponse; import software.amazon.awssdk.services.s3.model.ListObjectsV2Request; import software.amazon.awssdk.services.s3.model.ListObjectsV2Response; +import software.amazon.awssdk.services.s3.model.ObjectIdentifier; import software.amazon.awssdk.services.s3.model.PutObjectRequest; import software.amazon.awssdk.services.s3.model.PutObjectResponse; import software.amazon.awssdk.services.s3.model.S3Exception; @@ -56,6 +60,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.TreeMap; +import java.util.stream.Collectors; public class S3ObjStorage implements ObjStorage<S3Client> { private static final Logger LOG = LogManager.getLogger(S3ObjStorage.class); @@ -223,6 +228,52 @@ public class S3ObjStorage implements ObjStorage<S3Client> { } } + @Override + public Status deleteObjects(String absolutePath) { + try { + S3URI baseUri = S3URI.create(absolutePath, forceHostedStyle); + String continuationToken = ""; + boolean isTruncated = false; + long totalObjects = 0; + do { + RemoteObjects objects = listObjects(absolutePath, continuationToken); + List<RemoteObject> objectList = objects.getObjectList(); + if (!objectList.isEmpty()) { + Delete delete = Delete.builder() + .objects(objectList.stream() + .map(RemoteObject::getKey) + .map(k -> ObjectIdentifier.builder().key(k).build()) + .collect(Collectors.toList())) + .build(); + DeleteObjectsRequest req = DeleteObjectsRequest.builder() + .bucket(baseUri.getBucket()) + .delete(delete) + .build(); + + DeleteObjectsResponse resp = getClient(baseUri.getVirtualBucket()).deleteObjects(req); + if (resp.errors().size() > 0) { + LOG.warn("{} errors returned while deleting {} objects for dir {}", + resp.errors().size(), objectList.size(), absolutePath); + } + LOG.info("{} of {} objects deleted for dir {}", + resp.deleted().size(), objectList.size(), absolutePath); + totalObjects += objectList.size(); + } + + isTruncated = objects.isTruncated(); + continuationToken = objects.getContinuationToken(); + } while (isTruncated); + LOG.info("total delete {} objects for dir {}", totalObjects, absolutePath); + return Status.OK; + } catch (DdlException e) { + return new Status(Status.ErrCode.COMMON_ERROR, "list objects for delete objects failed: " + e.getMessage()); + } catch (Exception e) { + LOG.warn("delete objects {} failed, force visual host style {}", absolutePath, e, forceHostedStyle); + return new Status(Status.ErrCode.COMMON_ERROR, "delete objects failed: " + e.getMessage()); + } + } + + @Override public Status copyObject(String origFilePath, String destFilePath) { try { S3URI origUri = S3URI.create(origFilePath); @@ -249,9 +300,26 @@ public class S3ObjStorage implements ObjStorage<S3Client> { public RemoteObjects listObjects(String absolutePath, String continuationToken) throws DdlException { try { S3URI uri = S3URI.create(absolutePath, forceHostedStyle); + String bucket = uri.getBucket(); String prefix = uri.getKey(); - ListObjectsV2Request.Builder requestBuilder = ListObjectsV2Request.builder().bucket(uri.getBucket()) - .prefix(normalizePrefix(prefix)); + if (!StringUtils.isEmpty(uri.getVirtualBucket())) { + // Support s3 compatible service. The generated HTTP request for list objects likes: + // + // GET /<bucket-name>?list-type=2&prefix=<prefix> + prefix = bucket + "/" + prefix; + String endpoint = properties.get(S3Properties.ENDPOINT); + if (endpoint.contains("cos.")) { + bucket = "/"; + } else if (endpoint.contains("oss-")) { + bucket = uri.getVirtualBucket(); + } else if (endpoint.contains("obs.")) { + // FIXME: unlike cos and oss, the obs will report 'The specified key does not exist'. + throw new DdlException("obs does not support list objects via s3 sdk. path: " + absolutePath); + } + } + ListObjectsV2Request.Builder requestBuilder = ListObjectsV2Request.builder() + .bucket(bucket) + .prefix(normalizePrefix(prefix)); if (!StringUtils.isEmpty(continuationToken)) { requestBuilder.continuationToken(continuationToken); } @@ -263,7 +331,7 @@ public class S3ObjStorage implements ObjStorage<S3Client> { } return new RemoteObjects(remoteObjects, response.isTruncated(), response.nextContinuationToken()); } catch (Exception e) { - LOG.warn("Failed to list objects for S3", e); + LOG.warn("Failed to list objects for S3: {}", absolutePath, e); throw new DdlException("Failed to list objects for S3, Error message: " + e.getMessage(), e); } } diff --git a/fe/fe-core/src/main/java/org/apache/doris/fs/remote/S3FileSystem.java b/fe/fe-core/src/main/java/org/apache/doris/fs/remote/S3FileSystem.java index 6f1daf0ae96..f91c50d7099 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/fs/remote/S3FileSystem.java +++ b/fe/fe-core/src/main/java/org/apache/doris/fs/remote/S3FileSystem.java @@ -107,5 +107,9 @@ public class S3FileSystem extends ObjFileSystem { } return Status.OK; } + + public Status deleteDirectory(String absolutePath) { + return ((S3ObjStorage) objStorage).deleteObjects(absolutePath); + } } diff --git a/fe/fe-core/src/test/java/org/apache/doris/fs/obj/S3ObjStorageTest.java b/fe/fe-core/src/test/java/org/apache/doris/fs/obj/S3ObjStorageTest.java index 523a812fd8e..c4dce56c578 100644 --- a/fe/fe-core/src/test/java/org/apache/doris/fs/obj/S3ObjStorageTest.java +++ b/fe/fe-core/src/test/java/org/apache/doris/fs/obj/S3ObjStorageTest.java @@ -21,8 +21,8 @@ import org.apache.doris.backup.Status; import org.apache.doris.common.UserException; import org.apache.doris.common.util.S3URI; +import org.apache.commons.lang3.StringUtils; import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import software.amazon.awssdk.core.sync.RequestBody; @@ -36,26 +36,85 @@ import java.util.Map; @TestInstance(TestInstance.Lifecycle.PER_CLASS) class S3ObjStorageTest { - private S3ObjStorage storage; + @Test + public void testS3BaseOp() throws UserException { + String ak = System.getenv("S3_ACCESS_KEY"); + String sk = System.getenv("S3_SECRET_KEY"); + String endpoint = System.getenv("S3_ENDPOINT"); + String region = System.getenv("S3_REGION"); + String bucket = System.getenv("S3_BUCKET"); + String prefix = System.getenv("S3_PREFIX"); + + // Skip this test if ENV variables are not set. + if (StringUtils.isEmpty(endpoint) || StringUtils.isEmpty(ak) + || StringUtils.isEmpty(sk) || StringUtils.isEmpty(region) + || StringUtils.isEmpty(bucket) || StringUtils.isEmpty(prefix)) { + return; + } + + Map<String, String> properties = new HashMap<>(); + properties.put("s3.endpoint", endpoint); + properties.put("s3.access_key", ak); + properties.put("s3.secret_key", sk); + properties.put("s3.region", region); + S3ObjStorage storage = new S3ObjStorage(properties); + + String baseUrl = "s3://" + bucket + "/" + prefix + "/"; + for (int i = 0; i < 5; ++i) { + Status st = storage.putObject(baseUrl + "key" + i, RequestBody.fromString("mocked")); + Assertions.assertEquals(Status.OK, st); + } + + RemoteObjects remoteObjects = storage.listObjects(baseUrl, null); + Assertions.assertEquals(5, remoteObjects.getObjectList().size()); + Assertions.assertFalse(remoteObjects.isTruncated()); + Assertions.assertEquals(null, remoteObjects.getContinuationToken()); + + List<RemoteObject> objectList = remoteObjects.getObjectList(); + for (int i = 0; i < objectList.size(); i++) { + RemoteObject remoteObject = objectList.get(i); + Assertions.assertEquals("key" + i, remoteObject.getRelativePath()); + } + + Status st = storage.headObject(baseUrl + "key" + 0); + Assertions.assertEquals(Status.OK, st); - private MockedS3Client mockedClient; + File file = new File("test-file.txt"); + file.delete(); + st = storage.getObject(baseUrl + "key" + 0, file); + Assertions.assertEquals(Status.OK, st); + + st = storage.deleteObject(baseUrl + "key" + 0); + Assertions.assertEquals(Status.OK, st); + + file.delete(); + st = storage.getObject(baseUrl + "key" + 0, file); + Assertions.assertEquals(Status.ErrCode.COMMON_ERROR, st.getErrCode()); + Assertions.assertTrue(st.getErrMsg().contains("The specified key does not exist")); + file.delete(); + + st = storage.deleteObjects(baseUrl); + Assertions.assertEquals(Status.OK, st); + + remoteObjects = storage.listObjects(baseUrl, null); + Assertions.assertEquals(0, remoteObjects.getObjectList().size()); + Assertions.assertFalse(remoteObjects.isTruncated()); + Assertions.assertEquals(null, remoteObjects.getContinuationToken()); + } - @BeforeAll - public void beforeAll() throws Exception { + @Test + public void testBaseOp() throws Exception { Map<String, String> properties = new HashMap<>(); properties.put("s3.endpoint", "s3.e.c"); properties.put("s3.access_key", "abc"); properties.put("s3.secret_key", "123"); - storage = new S3ObjStorage(properties); + S3ObjStorage storage = new S3ObjStorage(properties); Field client = storage.getClass().getDeclaredField("client"); client.setAccessible(true); - mockedClient = new MockedS3Client(); + MockedS3Client mockedClient = new MockedS3Client(); client.set(storage, mockedClient); Assertions.assertTrue(storage.getClient("mocked") instanceof MockedS3Client); - } - @Test - public void testBaseOp() throws UserException { S3URI vUri = S3URI.create("s3://bucket/key", true); S3URI uri = S3URI.create("s3://bucket/key", false); Assertions.assertEquals(vUri.getVirtualBucket(), "bucket"); @@ -98,7 +157,16 @@ class S3ObjStorageTest { List<RemoteObject> list = remoteObjectsVBucket.getObjectList(); for (int i = 0; i < list.size(); i++) { RemoteObject remoteObject = list.get(i); - Assertions.assertTrue(remoteObject.getRelativePath().startsWith("keys/key" + i)); + Assertions.assertTrue(remoteObject.getRelativePath().startsWith("key" + i)); + } + + storage.properties.put("use_path_style", "true"); + storage.setProperties(storage.properties); + remoteObjectsVBucket = storage.listObjects("oss://bucket/keys", null); + list = remoteObjectsVBucket.getObjectList(); + for (int i = 0; i < list.size(); i++) { + RemoteObject remoteObject = list.get(i); + Assertions.assertTrue(remoteObject.getRelativePath().startsWith("key" + i)); } } } diff --git a/regression-test/framework/src/main/groovy/org/apache/doris/regression/suite/Syncer.groovy b/regression-test/framework/src/main/groovy/org/apache/doris/regression/suite/Syncer.groovy index 08d224c330e..c7f5ccdcb8d 100644 --- a/regression-test/framework/src/main/groovy/org/apache/doris/regression/suite/Syncer.groovy +++ b/regression-test/framework/src/main/groovy/org/apache/doris/regression/suite/Syncer.groovy @@ -352,10 +352,50 @@ class Syncer { Boolean checkSnapshotFinish() { String checkSQL = "SHOW BACKUP FROM " + context.db - List<Object> row = suite.sql(checkSQL)[0] - logger.info("Now row is ${row}") + def records = suite.sql(checkSQL) + for (row in records) { + logger.info("BACKUP row is ${row}") + String state = (row[3] as String); + if (state != "FINISHED" && state != "CANCELLED") { + return false + } + } + true + } - return (row[3] as String) == "FINISHED" + String getSnapshotTimestamp(String repoName, String snapshotName) { + def filterShowSnapshot = { records, name -> + for (row in records) { + logger.info("Snapshot row is ${row}") + if (row[0] == name && row[1] != "null") { + return row + } + } + null + } + + for (int i = 0; i < 3; ++i) { + def result = suite.sql "SHOW SNAPSHOT ON ${repoName}" + def snapshot = filterShowSnapshot(result, snapshotName) + if (snapshot != null) { + return snapshot[1].split('\n').last() + } + Thread.sleep(3000); + } + null + } + + Boolean checkAllRestoreFinish() { + String checkSQL = "SHOW RESTORE FROM ${context.db}" + def records = suite.sql(checkSQL) + for (row in records) { + logger.info("Restore row is ${row}") + String state = row[4] + if (state != "FINISHED" && state != "CANCELLED") { + return false + } + } + true } Boolean checkRestoreFinish() { @@ -742,4 +782,53 @@ class Syncer { TCommitTxnResult result = SyncerUtils.commitTxn(clientImpl, context) return checkCommitTxn(result) } + + String externalStoragePrefix() { + String feAddr = "${context.config.feTargetThriftNetworkAddress}" + int code = feAddr.hashCode(); + ((code < 0) ? -code : code).toString() + } + + void createS3Repository(String name, boolean readOnly = false) { + String ak = suite.getS3AK() + String sk = suite.getS3SK() + String endpoint = suite.getS3Endpoint() + String region = suite.getS3Region() + String bucket = suite.getS3BucketName() + String prefix = externalStoragePrefix() + + suite.try_sql "DROP REPOSITORY `${name}`" + suite.sql """ + CREATE ${readOnly ? "READ ONLY" : ""} REPOSITORY `${name}` + WITH S3 + ON LOCATION "s3://${bucket}/${prefix}/${name}" + PROPERTIES + ( + "s3.endpoint" = "http://${endpoint}", + "s3.region" = "${region}", + "s3.access_key" = "${ak}", + "s3.secret_key" = "${sk}", + "delete_if_exists" = "true" + ) + """ + } + + void createHdfsRepository(String name, boolean readOnly = false) { + String hdfsFs = suite.getHdfsFs() + String hdfsUser = suite.getHdfsUser() + String dataDir = suite.getHdfsDataDir() + String prefix = externalStoragePrefix() + + suite.try_sql "DROP REPOSITORY `${name}`" + suite.sql """ + CREATE REPOSITORY `${name}` + WITH hdfs + ON LOCATION "${dataDir}/${prefix}/${name}" + PROPERTIES + ( + "fs.defaultFS" = "${hdfsFs}", + "hadoop.username" = "${hdfsUser}" + ) + """ + } } diff --git a/regression-test/suites/backup_restore/test_backup_restore.groovy b/regression-test/suites/backup_restore/test_backup_restore.groovy index 7eba03260a5..3e5aa92ffbe 100644 --- a/regression-test/suites/backup_restore/test_backup_restore.groovy +++ b/regression-test/suites/backup_restore/test_backup_restore.groovy @@ -16,6 +16,68 @@ // under the License. suite("test_backup_restore", "backup_restore") { - // todo: test repository/backup/restore/cancel backup ... - sql "SHOW REPOSITORIES" + String repoName = "test_backup_restore_repo" + + def syncer = getSyncer() + syncer.createS3Repository(repoName) + + String tableName = "test_backup_restore_table" + sql "DROP TABLE IF EXISTS ${tableName}" + sql """ + CREATE TABLE ${tableName} ( + `id` LARGEINT NOT NULL, + `count` LARGEINT SUM DEFAULT "0") + AGGREGATE KEY(`id`) + DISTRIBUTED BY HASH(`id`) BUCKETS 2 + PROPERTIES + ( + "replication_num" = "1" + ) + """ + + List<String> values = [] + for (i = 1; i <= 10; ++i) { + values.add("(${i}, ${i})") + } + sql "INSERT INTO ${tableName} VALUES ${values.join(",")}" + def result = sql "SELECT * FROM ${tableName}" + assertEquals(result.size(), values.size()); + + String snapshotName = "test_backup_restore_snapshot" + sql """ + BACKUP SNAPSHOT ${snapshotName} + TO `${repoName}` + ON (${tableName}) + """ + + while (!syncer.checkSnapshotFinish()) { + Thread.sleep(3000) + } + + snapshot = syncer.getSnapshotTimestamp(repoName, snapshotName) + assertTrue(snapshot != null) + + sql "TRUNCATE TABLE ${tableName}" + + sql """ + RESTORE SNAPSHOT ${snapshotName} + FROM `${repoName}` + ON ( `${tableName}`) + PROPERTIES + ( + "backup_timestamp" = "${snapshot}", + "replication_num" = "1" + ) + """ + + while (!syncer.checkAllRestoreFinish()) { + Thread.sleep(3000) + } + + result = sql "SELECT * FROM ${tableName}" + assertEquals(result.size(), values.size()); + + sql "DROP TABLE ${tableName} FORCE" + sql "DROP REPOSITORY `${repoName}`" } + diff --git a/regression-test/suites/backup_restore/test_create_and_drop_repository.groovy b/regression-test/suites/backup_restore/test_create_and_drop_repository.groovy index b291703e733..3a5ff6f64cb 100644 --- a/regression-test/suites/backup_restore/test_create_and_drop_repository.groovy +++ b/regression-test/suites/backup_restore/test_create_and_drop_repository.groovy @@ -26,7 +26,7 @@ suite("test_create_and_drop_repository", "backup_restore") { String region = getS3Region() String bucket = context.config.otherConfigs.get("s3BucketName"); - def filter_show_repo_result = { result, name -> + def filterShowRepoResult = { result, name -> for (record in result) { if (record[1] == name) return record @@ -49,13 +49,13 @@ suite("test_create_and_drop_repository", "backup_restore") { """ def result = sql """ SHOW REPOSITORIES """ - def repo = filter_show_repo_result(result, repoName) + def repo = filterShowRepoResult(result, repoName) assertTrue(repo != null) sql "DROP REPOSITORY `${repoName}`" result = sql """ SHOW REPOSITORIES """ - repo = filter_show_repo_result(result, repoName) + repo = filterShowRepoResult(result, repoName) assertTrue(repo == null) // case 2. S3 read only repo @@ -73,12 +73,62 @@ suite("test_create_and_drop_repository", "backup_restore") { """ result = sql """ SHOW REPOSITORIES """ - repo = filter_show_repo_result(result, repoName) + repo = filterShowRepoResult(result, repoName) assertTrue(repo != null) sql "DROP REPOSITORY `${repoName}`" result = sql """ SHOW REPOSITORIES """ - repo = filter_show_repo_result(result, repoName) + repo = filterShowRepoResult(result, repoName) assertTrue(repo == null) + + if (enableHdfs()) { + // case 3. hdfs repo + String hdfsFs = getHdfsFs() + String hdfsUser = getHdfsUser() + String dataDir = getHdfsDataDir() + + sql """ + CREATE REPOSITORY `${repoName}` + WITH hdfs + ON LOCATION "${dataDir}${repoName}" + PROPERTIES + ( + "fs.defaultFS" = "${hdfsFs}", + "hadoop.username" = "${hdfsUser}" + ) + """ + + result = sql """ SHOW REPOSITORIES """ + repo = filterShowRepoResult(result, repoName) + assertTrue(repo != null) + + sql "DROP REPOSITORY `${repoName}`" + + result = sql """ SHOW REPOSITORIES """ + repo = filterShowRepoResult(result, repoName) + assertTrue(repo == null) + + // case 4. hdfs read only repo + sql """ + CREATE READ ONLY REPOSITORY `${repoName}` + WITH hdfs + ON LOCATION "${dataDir}${repoName}" + PROPERTIES + ( + "fs.defaultFS" = "${hdfsFs}", + "hadoop.username" = "${hdfsUser}" + ) + """ + + result = sql """ SHOW REPOSITORIES """ + repo = filterShowRepoResult(result, repoName) + assertTrue(repo != null) + + sql "DROP REPOSITORY `${repoName}`" + + result = sql """ SHOW REPOSITORIES """ + repo = filterShowRepoResult(result, repoName) + assertTrue(repo == null) + } } --------------------------------------------------------------------- To unsubscribe, e-mail: commits-unsubscr...@doris.apache.org For additional commands, e-mail: commits-h...@doris.apache.org