diff --git a/CreateSnapshot/src/main/java/com/rfs/CreateSnapshot.java b/CreateSnapshot/src/main/java/com/rfs/CreateSnapshot.java index 51ebb508b..dd380c4f5 100644 --- a/CreateSnapshot/src/main/java/com/rfs/CreateSnapshot.java +++ b/CreateSnapshot/src/main/java/com/rfs/CreateSnapshot.java @@ -3,92 +3,83 @@ import com.beust.jcommander.JCommander; import com.beust.jcommander.Parameter; +import com.rfs.common.UsernamePassword; +import lombok.AllArgsConstructor; +import lombok.Getter; import lombok.extern.slf4j.Slf4j; -import org.apache.logging.log4j.Level; -import org.apache.logging.log4j.Logger; -import org.apache.logging.log4j.LogManager; -import com.rfs.cms.CmsClient; -import com.rfs.cms.OpenSearchCmsClient; import com.rfs.common.ConnectionDetails; -import com.rfs.common.Logging; import com.rfs.common.OpenSearchClient; import com.rfs.common.SnapshotCreator; import com.rfs.common.TryHandlePhaseFailure; import com.rfs.common.S3SnapshotCreator; -import com.rfs.worker.GlobalState; import com.rfs.worker.SnapshotRunner; +import java.util.Optional; +import java.util.function.Function; + @Slf4j public class CreateSnapshot { public static class Args { - @Parameter(names = {"--snapshot-name"}, description = "The name of the snapshot to migrate", required = true) + @Parameter(names = {"--snapshot-name"}, + required = true, + description = "The name of the snapshot to migrate") public String snapshotName; - @Parameter(names = {"--s3-repo-uri"}, description = "The S3 URI of the snapshot repo, like: s3://my-bucket/dir1/dir2", required = true) + @Parameter(names = {"--s3-repo-uri"}, + required = true, + description = "The S3 URI of the snapshot repo, like: s3://my-bucket/dir1/dir2") public String s3RepoUri; - @Parameter(names = {"--s3-region"}, description = "The AWS Region the S3 bucket is in, like: us-east-2", required = true) + @Parameter(names = {"--s3-region"}, + required = true, + description = "The AWS Region the S3 bucket is in, like: us-east-2" + ) public String s3Region; - @Parameter(names = {"--source-host"}, description = "The source host and port (e.g. http://localhost:9200)", required = true) + @Parameter(names = {"--source-host"}, + required = true, + description = "The source host and port (e.g. http://localhost:9200)") public String sourceHost; - @Parameter(names = {"--source-username"}, description = "Optional. The source username; if not provided, will assume no auth on source", required = false) + @Parameter(names = {"--source-username"}, + description = "Optional. The source username; if not provided, will assume no auth on source") public String sourceUser = null; - @Parameter(names = {"--source-password"}, description = "Optional. The source password; if not provided, will assume no auth on source", required = false) + @Parameter(names = {"--source-password"}, + description = "Optional. The source password; if not provided, will assume no auth on source") public String sourcePass = null; - @Parameter(names = {"--source-insecure"}, description = "Allow untrusted SSL certificates for source", required = false) + @Parameter(names = {"--source-insecure"}, + description = "Allow untrusted SSL certificates for source") public boolean sourceInsecure = false; + } - @Parameter(names = {"--target-host"}, description = "The target host and port (e.g. http://localhost:9200)", required = true) - public String targetHost; - - @Parameter(names = {"--target-username"}, description = "Optional. The target username; if not provided, will assume no auth on target", required = false) - public String targetUser = null; - - @Parameter(names = {"--target-password"}, description = "Optional. The target password; if not provided, will assume no auth on target", required = false) - public String targetPass = null; - - @Parameter(names = {"--target-insecure"}, description = "Allow untrusted SSL certificates for target", required = false) - public boolean targetInsecure = false; + @Getter + @AllArgsConstructor + public static class S3RepoInfo { + String awsRegion; + String repoUri; } public static void main(String[] args) throws Exception { // Grab out args Args arguments = new Args(); JCommander.newBuilder() - .addObject(arguments) - .build() - .parse(args); - - final String snapshotName = arguments.snapshotName; - final String s3RepoUri = arguments.s3RepoUri; - final String s3Region = arguments.s3Region; - final String sourceHost = arguments.sourceHost; - final String sourceUser = arguments.sourceUser; - final String sourcePass = arguments.sourcePass; - final String targetHost = arguments.targetHost; - final String targetUser = arguments.targetUser; - final String targetPass = arguments.targetPass; - final boolean sourceInsecure = arguments.sourceInsecure; - final boolean targetInsecure = arguments.targetInsecure; + .addObject(arguments) + .build() + .parse(args); - final ConnectionDetails sourceConnection = new ConnectionDetails(sourceHost, sourceUser, sourcePass, sourceInsecure); - final ConnectionDetails targetConnection = new ConnectionDetails(targetHost, targetUser, targetPass, targetInsecure); + log.info("Running CreateSnapshot with " + String.join(" ", args)); + run(c -> new S3SnapshotCreator(arguments.snapshotName, c, arguments.s3RepoUri, arguments.s3Region), + new OpenSearchClient(arguments.sourceHost, arguments.sourceUser, arguments.sourcePass, arguments.sourceInsecure)); + } + public static void run(Function snapshotCreatorFactory, + OpenSearchClient openSearchClient) + throws Exception { TryHandlePhaseFailure.executeWithTryCatch(() -> { - log.info("Running RfsWorker"); - GlobalState globalState = GlobalState.getInstance(); - OpenSearchClient sourceClient = new OpenSearchClient(sourceConnection); - OpenSearchClient targetClient = new OpenSearchClient(targetConnection); - final CmsClient cmsClient = new OpenSearchCmsClient(targetClient); - - final SnapshotCreator snapshotCreator = new S3SnapshotCreator(snapshotName, sourceClient, s3RepoUri, s3Region); - final SnapshotRunner snapshotWorker = new SnapshotRunner(globalState, cmsClient, snapshotCreator); - snapshotWorker.run(); + SnapshotRunner.runAndWaitForCompletion(snapshotCreatorFactory.apply(openSearchClient)); }); } } diff --git a/RFS/build-preloaded-source-image.gradle b/DocumentsFromSnapshotMigration/build-preloaded-source-image.gradle similarity index 99% rename from RFS/build-preloaded-source-image.gradle rename to DocumentsFromSnapshotMigration/build-preloaded-source-image.gradle index 8138bd459..695754562 100644 --- a/RFS/build-preloaded-source-image.gradle +++ b/DocumentsFromSnapshotMigration/build-preloaded-source-image.gradle @@ -22,6 +22,7 @@ def createNetworkTask = task createNetwork(type: Exec) { println 'Network created' } } + task createInitialElasticsearchContainer(type: DockerCreateContainer) { dependsOn createNetwork, buildDockerImage_emptyElasticsearchSource_7_10 targetImageId 'migrations/empty_elasticsearch_source_7_10:latest' @@ -70,7 +71,7 @@ def sourceContainerCommitTask = task commitSourceContainer() { } task removeClientContainer(type: DockerRemoveContainer) { - dependsOn commitSourceContainer + dependsOn waitClientContainer targetContainerId createClientContainer.getContainerId() } startClientTask.finalizedBy(removeClientContainer) diff --git a/DocumentsFromSnapshotMigration/build.gradle b/DocumentsFromSnapshotMigration/build.gradle index 06ad89c24..521799ae8 100644 --- a/DocumentsFromSnapshotMigration/build.gradle +++ b/DocumentsFromSnapshotMigration/build.gradle @@ -3,30 +3,73 @@ plugins { id 'java' id 'jacoco' id 'io.freefair.lombok' version '8.6' + id "com.avast.gradle.docker-compose" version "0.17.4" + id 'com.bmuschko.docker-remote-api' } +import com.bmuschko.gradle.docker.tasks.image.DockerBuildImage +import groovy.transform.Canonical import org.opensearch.migrations.common.CommonUtils java.sourceCompatibility = JavaVersion.VERSION_11 java.targetCompatibility = JavaVersion.VERSION_11 +@Canonical +class DockerServiceProps { + String projectName = "" + String dockerImageName = "" + String inputDir = "" + Map buildArgs = [:] + List taskDependencies = [] +} + repositories { mavenCentral() } dependencies { implementation project(":commonDependencyVersionConstraints") + implementation platform('io.projectreactor:reactor-bom:2023.0.5') + testImplementation platform('io.projectreactor:reactor-bom:2023.0.5') implementation project(":RFS") + implementation group: 'org.apache.logging.log4j', name: 'log4j-api' + implementation group: 'org.apache.logging.log4j', name: 'log4j-core' implementation group: 'com.beust', name: 'jcommander' + implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind' + implementation group: 'com.fasterxml.jackson.core', name: 'jackson-annotations' + implementation group: 'com.fasterxml.jackson.core', name: 'jackson-core' + implementation group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-smile' + implementation group: 'io.projectreactor.netty', name: 'reactor-netty-core' + implementation group: 'io.projectreactor.netty', name:'reactor-netty-http' implementation group: 'org.slf4j', name: 'slf4j-api' implementation group: 'org.apache.logging.log4j', name: 'log4j-slf4j2-impl' + + + testImplementation testFixtures(project(":RFS")) + testImplementation project(":CreateSnapshot") + testImplementation project(":MetadataMigration") + testImplementation group: 'org.apache.lucene', name: 'lucene-core' + testImplementation group: 'org.hamcrest', name: 'hamcrest' + testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-api' + testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-params' + testImplementation group: 'org.opensearch', name: 'opensearch-testcontainers' + testImplementation group: 'org.testcontainers', name: 'testcontainers' + + testImplementation platform('io.projectreactor:reactor-bom:2023.0.5') + testRuntimeOnly group: 'org.junit.jupiter', name: 'junit-jupiter-engine' + } application { mainClassName = 'com.rfs.RfsMigrateDocuments' } +// Cleanup additional docker build directory +clean.doFirst { + delete project.file("./docker/build") +} + // Utility task to allow copying required libraries into a 'dependencies' folder for security scanning tasks.register('copyDependencies', Sync) { duplicatesStrategy = DuplicatesStrategy.EXCLUDE @@ -35,15 +78,96 @@ tasks.register('copyDependencies', Sync) { into "${buildDir}/dependencies" } +task copyDockerRuntimeJars (type: Sync) { + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + description = 'Copy runtime JARs and app jar to docker build directory' + + // Define the destination directory + def buildDir = project.file("./docker/build/runtimeJars") + into buildDir + + // Add all the required runtime JARs to be copied + from configurations.runtimeClasspath + from tasks.named('jar') + include '*.jar' +} + +DockerServiceProps[] dockerServices = [ + new DockerServiceProps([projectName:"reindexFromSnapshot", + dockerImageName:"reindex_from_snapshot", + inputDir:"./docker", + taskDependencies:["copyDockerRuntimeJars"]]), + new DockerServiceProps([projectName:"emptyElasticsearchSource_7_10", + dockerImageName:"empty_elasticsearch_source_7_10", + inputDir:"./docker/TestSource_ES_7_10"]), + new DockerServiceProps([projectName:"emptyElasticsearchSource_7_17", + dockerImageName:"empty_elasticsearch_source_7_17", + inputDir:"./docker/TestSource_ES_7_17"]), + new DockerServiceProps([projectName:"trafficGenerator", + dockerImageName:"osb_traffic_generator", + inputDir:"./docker/TrafficGenerator", + taskDependencies:[":TrafficCapture:dockerSolution:buildDockerImage_elasticsearchTestConsole"]]), +] as DockerServiceProps[] + +for (dockerService in dockerServices) { + task "buildDockerImage_${dockerService.projectName}" (type: DockerBuildImage) { + def hash = CommonUtils.calculateDockerHash(project.fileTree("docker/${dockerService.projectName}")) + for (dep in dockerService.taskDependencies) { + dependsOn dep + } + inputDir = project.file(dockerService.inputDir) + buildArgs = dockerService.buildArgs + images.add("migrations/${dockerService.dockerImageName}:${hash}") + images.add("migrations/${dockerService.dockerImageName}:${version}") + images.add("migrations/${dockerService.dockerImageName}:latest") + } +} + +apply from: 'build-preloaded-source-image.gradle' + +dockerCompose { + useComposeFiles = ['docker/docker-compose.yml'] + projectName = 'rfs-compose' +} + +// ../gradlew buildDockerImages +task buildDockerImages { + for (dockerService in dockerServices) { + dependsOn "buildDockerImage_${dockerService.projectName}" + } + dependsOn buildDockerImage_elasticsearchRFSSource +} + +tasks.named("buildDockerImage_elasticsearchRFSSource") { + dependsOn(':TrafficCapture:dockerSolution:buildDockerImage_elasticsearchTestConsole') +} +tasks.getByName('composeUp') + .dependsOn(tasks.getByName('buildDockerImages')) + + +test { + useJUnitPlatform { + excludeTags 'longTest' + } + jacoco { + enabled = false + } +} + +task slowTest(type: Test) { + useJUnitPlatform() + dependsOn buildDockerImage_elasticsearchRFSSource + jacoco { + enabled = true + } +} + jacocoTestReport { + dependsOn slowTest reports { xml.required = true xml.destination file("${buildDir}/reports/jacoco/test/jacocoTestReport.xml") html.required = true html.destination file("${buildDir}/reports/jacoco/test/html") } -} - -test { - useJUnitPlatform() } \ No newline at end of file diff --git a/RFS/docker/Dockerfile b/DocumentsFromSnapshotMigration/docker/Dockerfile similarity index 100% rename from RFS/docker/Dockerfile rename to DocumentsFromSnapshotMigration/docker/Dockerfile diff --git a/RFS/docker/TestSource_ES_7_10/Dockerfile b/DocumentsFromSnapshotMigration/docker/TestSource_ES_7_10/Dockerfile similarity index 100% rename from RFS/docker/TestSource_ES_7_10/Dockerfile rename to DocumentsFromSnapshotMigration/docker/TestSource_ES_7_10/Dockerfile diff --git a/RFS/docker/TestSource_ES_7_10/container-start.sh b/DocumentsFromSnapshotMigration/docker/TestSource_ES_7_10/container-start.sh similarity index 100% rename from RFS/docker/TestSource_ES_7_10/container-start.sh rename to DocumentsFromSnapshotMigration/docker/TestSource_ES_7_10/container-start.sh diff --git a/RFS/docker/TestSource_ES_7_17/Dockerfile b/DocumentsFromSnapshotMigration/docker/TestSource_ES_7_17/Dockerfile similarity index 100% rename from RFS/docker/TestSource_ES_7_17/Dockerfile rename to DocumentsFromSnapshotMigration/docker/TestSource_ES_7_17/Dockerfile diff --git a/RFS/docker/TestSource_ES_7_17/container-start.sh b/DocumentsFromSnapshotMigration/docker/TestSource_ES_7_17/container-start.sh similarity index 100% rename from RFS/docker/TestSource_ES_7_17/container-start.sh rename to DocumentsFromSnapshotMigration/docker/TestSource_ES_7_17/container-start.sh diff --git a/RFS/docker/TrafficGenerator/Dockerfile b/DocumentsFromSnapshotMigration/docker/TrafficGenerator/Dockerfile similarity index 100% rename from RFS/docker/TrafficGenerator/Dockerfile rename to DocumentsFromSnapshotMigration/docker/TrafficGenerator/Dockerfile diff --git a/RFS/docker/TrafficGenerator/generateDataset.sh b/DocumentsFromSnapshotMigration/docker/TrafficGenerator/generateDataset.sh similarity index 100% rename from RFS/docker/TrafficGenerator/generateDataset.sh rename to DocumentsFromSnapshotMigration/docker/TrafficGenerator/generateDataset.sh diff --git a/RFS/docker/docker-compose.yml b/DocumentsFromSnapshotMigration/docker/docker-compose.yml similarity index 100% rename from RFS/docker/docker-compose.yml rename to DocumentsFromSnapshotMigration/docker/docker-compose.yml diff --git a/DocumentsFromSnapshotMigration/src/main/java/com/rfs/RfsMigrateDocuments.java b/DocumentsFromSnapshotMigration/src/main/java/com/rfs/RfsMigrateDocuments.java index 4d5b716e5..d8007c971 100644 --- a/DocumentsFromSnapshotMigration/src/main/java/com/rfs/RfsMigrateDocuments.java +++ b/DocumentsFromSnapshotMigration/src/main/java/com/rfs/RfsMigrateDocuments.java @@ -3,22 +3,25 @@ import com.beust.jcommander.JCommander; import com.beust.jcommander.Parameter; +import java.io.IOException; +import java.net.URI; import java.nio.file.Path; import java.nio.file.Paths; +import java.time.Clock; +import java.util.UUID; +import java.util.function.Function; +import com.rfs.cms.IWorkCoordinator; import lombok.extern.slf4j.Slf4j; -import org.apache.logging.log4j.Level; -import org.apache.logging.log4j.Logger; -import org.apache.logging.log4j.LogManager; +import com.rfs.cms.ApacheHttpClient; +import com.rfs.cms.OpenSearchWorkCoordinator; +import com.rfs.cms.LeaseExpireTrigger; +import com.rfs.cms.ScopedWorkCoordinator; +import com.rfs.worker.ShardWorkPreparer; - -import com.rfs.cms.CmsClient; -import com.rfs.cms.OpenSearchCmsClient; -import com.rfs.common.ConnectionDetails; import com.rfs.common.DefaultSourceRepoAccessor; import com.rfs.common.DocumentReindexer; import com.rfs.common.IndexMetadata; -import com.rfs.common.Logging; import com.rfs.common.LuceneDocumentsReader; import com.rfs.common.OpenSearchClient; import com.rfs.common.S3Uri; @@ -33,33 +36,49 @@ import com.rfs.version_es_7_10.ShardMetadataFactory_ES_7_10; import com.rfs.version_es_7_10.SnapshotRepoProvider_ES_7_10; import com.rfs.worker.DocumentsRunner; -import com.rfs.worker.GlobalState; @Slf4j public class RfsMigrateDocuments { + public static final int PROCESS_TIMED_OUT = 1; + public static final int TOLERABLE_CLIENT_SERVER_CLOCK_DIFFERENCE_SECONDS = 5; + public static class Args { - @Parameter(names = {"--snapshot-name"}, description = "The name of the snapshot to migrate", required = true) + @Parameter(names = {"--snapshot-name"}, + required = true, + description = "The name of the snapshot to migrate") public String snapshotName; - @Parameter(names = {"--s3-local-dir"}, description = "The absolute path to the directory on local disk to download S3 files to", required = true) + @Parameter(names = {"--s3-local-dir"}, + required = true, + description = "The absolute path to the directory on local disk to download S3 files to") public String s3LocalDirPath; - @Parameter(names = {"--s3-repo-uri"}, description = "The S3 URI of the snapshot repo, like: s3://my-bucket/dir1/dir2", required = true) + @Parameter(names = {"--s3-repo-uri"}, + required = true, + description = "The S3 URI of the snapshot repo, like: s3://my-bucket/dir1/dir2") public String s3RepoUri; - @Parameter(names = {"--s3-region"}, description = "The AWS Region the S3 bucket is in, like: us-east-2", required = true) + @Parameter(names = {"--s3-region"}, + required = true, + description = "The AWS Region the S3 bucket is in, like: us-east-2") public String s3Region; - @Parameter(names = {"--lucene-dir"}, description = "The absolute path to the directory where we'll put the Lucene docs", required = true) + @Parameter(names = {"--lucene-dir"}, + required = true, + description = "The absolute path to the directory where we'll put the Lucene docs") public String luceneDirPath; - @Parameter(names = {"--target-host"}, description = "The target host and port (e.g. http://localhost:9200)", required = true) + @Parameter(names = {"--target-host"}, + required = true, + description = "The target host and port (e.g. http://localhost:9200)") public String targetHost; - @Parameter(names = {"--target-username"}, description = "Optional. The target username; if not provided, will assume no auth on target", required = false) + @Parameter(names = {"--target-username"}, + description = "Optional. The target username; if not provided, will assume no auth on target") public String targetUser = null; - @Parameter(names = {"--target-password"}, description = "Optional. The target password; if not provided, will assume no auth on target", required = false) + @Parameter(names = {"--target-password"}, + description = "Optional. The target password; if not provided, will assume no auth on target") public String targetPass = null; @Parameter(names = {"--max-shard-size-bytes"}, description = ("Optional. The maximum shard size, in bytes, to allow when" @@ -67,45 +86,101 @@ public static class Args { public long maxShardSizeBytes = 50 * 1024 * 1024 * 1024L; } + public static class NoWorkLeftException extends Exception { + public NoWorkLeftException(String message) { + super(message); + } + } + public static void main(String[] args) throws Exception { // Grab out args Args arguments = new Args(); JCommander.newBuilder() - .addObject(arguments) - .build() - .parse(args); - - final String snapshotName = arguments.snapshotName; - final Path s3LocalDirPath = Paths.get(arguments.s3LocalDirPath); - final String s3RepoUri = arguments.s3RepoUri; - final String s3Region = arguments.s3Region; - final Path luceneDirPath = Paths.get(arguments.luceneDirPath); - final String targetHost = arguments.targetHost; - final String targetUser = arguments.targetUser; - final String targetPass = arguments.targetPass; - final long maxShardSizeBytes = arguments.maxShardSizeBytes; - - final ConnectionDetails targetConnection = new ConnectionDetails(targetHost, targetUser, targetPass); + .addObject(arguments) + .build() + .parse(args); + + var luceneDirPath = Paths.get(arguments.luceneDirPath); + var processManager = new LeaseExpireTrigger(workItemId->{ + log.error("terminating RunRfsWorker because its lease has expired for " + workItemId); + System.exit(PROCESS_TIMED_OUT); + }, Clock.systemUTC()); + var workCoordinator = new OpenSearchWorkCoordinator(new ApacheHttpClient(new URI(arguments.targetHost)), + TOLERABLE_CLIENT_SERVER_CLOCK_DIFFERENCE_SECONDS, UUID.randomUUID().toString()); TryHandlePhaseFailure.executeWithTryCatch(() -> { log.info("Running RfsWorker"); - GlobalState globalState = GlobalState.getInstance(); - OpenSearchClient targetClient = new OpenSearchClient(targetConnection); - final CmsClient cmsClient = new OpenSearchCmsClient(targetClient); - - final SourceRepo sourceRepo = S3Repo.create(s3LocalDirPath, new S3Uri(s3RepoUri), s3Region); - final SnapshotRepo.Provider repoDataProvider = new SnapshotRepoProvider_ES_7_10(sourceRepo); - - final IndexMetadata.Factory indexMetadataFactory = new IndexMetadataFactory_ES_7_10(repoDataProvider); - final ShardMetadata.Factory shardMetadataFactory = new ShardMetadataFactory_ES_7_10(repoDataProvider); - final DefaultSourceRepoAccessor repoAccessor = new DefaultSourceRepoAccessor(sourceRepo); - final SnapshotShardUnpacker.Factory unpackerFactory = new SnapshotShardUnpacker.Factory(repoAccessor, luceneDirPath, ElasticsearchConstants_ES_7_10.BUFFER_SIZE_IN_BYTES); - final LuceneDocumentsReader reader = new LuceneDocumentsReader(luceneDirPath); - final DocumentReindexer reindexer = new DocumentReindexer(targetClient); - - DocumentsRunner documentsWorker = new DocumentsRunner(globalState, cmsClient, snapshotName, maxShardSizeBytes, indexMetadataFactory, shardMetadataFactory, unpackerFactory, reader, reindexer); - documentsWorker.run(); + OpenSearchClient targetClient = + new OpenSearchClient(arguments.targetHost, arguments.targetUser, arguments.targetPass, false); + DocumentReindexer reindexer = new DocumentReindexer(targetClient); + + SourceRepo sourceRepo = S3Repo.create(Paths.get(arguments.s3LocalDirPath), + new S3Uri(arguments.s3RepoUri), arguments.s3Region); + SnapshotRepo.Provider repoDataProvider = new SnapshotRepoProvider_ES_7_10(sourceRepo); + + IndexMetadata.Factory indexMetadataFactory = new IndexMetadataFactory_ES_7_10(repoDataProvider); + ShardMetadata.Factory shardMetadataFactory = new ShardMetadataFactory_ES_7_10(repoDataProvider); + DefaultSourceRepoAccessor repoAccessor = new DefaultSourceRepoAccessor(sourceRepo); + SnapshotShardUnpacker.Factory unpackerFactory = new SnapshotShardUnpacker.Factory(repoAccessor, + luceneDirPath, ElasticsearchConstants_ES_7_10.BUFFER_SIZE_IN_BYTES); + + run(LuceneDocumentsReader::new, reindexer, workCoordinator, processManager, indexMetadataFactory, + arguments.snapshotName, shardMetadataFactory, unpackerFactory, arguments.maxShardSizeBytes); }); } + + public static DocumentsRunner.CompletionStatus run(Function readerFactory, + DocumentReindexer reindexer, + IWorkCoordinator workCoordinator, + LeaseExpireTrigger leaseExpireTrigger, + IndexMetadata.Factory indexMetadataFactory, + String snapshotName, + ShardMetadata.Factory shardMetadataFactory, + SnapshotShardUnpacker.Factory unpackerFactory, + long maxShardSizeBytes) + throws IOException, InterruptedException, NoWorkLeftException { + var scopedWorkCoordinator = new ScopedWorkCoordinator(workCoordinator, leaseExpireTrigger); + confirmShardPrepIsComplete(indexMetadataFactory, snapshotName, scopedWorkCoordinator); + if (!workCoordinator.workItemsArePending()) { + throw new NoWorkLeftException("No work items are pending/all work items have been processed. Returning."); + } + return new DocumentsRunner(scopedWorkCoordinator, + (name, shard) -> { + var shardMetadata = shardMetadataFactory.fromRepo(snapshotName, name, shard); + log.info("Shard size: " + shardMetadata.getTotalSizeBytes()); + if (shardMetadata.getTotalSizeBytes() > maxShardSizeBytes) { + throw new DocumentsRunner.ShardTooLargeException(shardMetadata.getTotalSizeBytes(), maxShardSizeBytes); + } + return shardMetadata; + }, + unpackerFactory, readerFactory, reindexer).migrateNextShard(); + } + + private static void confirmShardPrepIsComplete(IndexMetadata.Factory indexMetadataFactory, + String snapshotName, + ScopedWorkCoordinator scopedWorkCoordinator) + throws IOException, InterruptedException + { + // assume that the shard setup work will be done quickly, much faster than its lease in most cases. + // in cases where that isn't true, doing random backoff across the fleet should guarantee that eventually, + // these workers will attenuate enough that it won't cause an impact on the coordination server + long lockRenegotiationMillis = 1000; + for (int shardSetupAttemptNumber=0; ; ++shardSetupAttemptNumber) { + try { + new ShardWorkPreparer().run(scopedWorkCoordinator, indexMetadataFactory, snapshotName); + return; + } catch (IWorkCoordinator.LeaseLockHeldElsewhereException e) { + long finalLockRenegotiationMillis = lockRenegotiationMillis; + int finalShardSetupAttemptNumber = shardSetupAttemptNumber; + log.atInfo().setMessage(() -> + "After " + finalShardSetupAttemptNumber + "another process holds the lock" + + " for setting up the shard work items. " + + "Waiting " + finalLockRenegotiationMillis + "ms before trying again.").log(); + Thread.sleep(lockRenegotiationMillis); + lockRenegotiationMillis *= 2; + continue; + } + } + } } diff --git a/DocumentsFromSnapshotMigration/src/test/java/com/rfs/FullTest.java b/DocumentsFromSnapshotMigration/src/test/java/com/rfs/FullTest.java new file mode 100644 index 000000000..1d1b11cc3 --- /dev/null +++ b/DocumentsFromSnapshotMigration/src/test/java/com/rfs/FullTest.java @@ -0,0 +1,287 @@ +package com.rfs; + +import com.rfs.cms.ApacheHttpClient; +import com.rfs.cms.OpenSearchWorkCoordinator; +import com.rfs.cms.LeaseExpireTrigger; +import com.rfs.common.ClusterVersion; +import com.rfs.common.ConnectionDetails; +import com.rfs.common.DefaultSourceRepoAccessor; +import com.rfs.common.DocumentReindexer; +import com.rfs.common.FileSystemRepo; +import com.rfs.common.FileSystemSnapshotCreator; +import com.rfs.common.GlobalMetadata; +import com.rfs.common.IndexMetadata; +import com.rfs.common.LuceneDocumentsReader; +import com.rfs.common.OpenSearchClient; +import com.rfs.common.RestClient; +import com.rfs.common.ShardMetadata; +import com.rfs.common.SnapshotRepo; +import com.rfs.common.SnapshotShardUnpacker; +import com.rfs.common.SourceRepo; +import com.rfs.framework.ElasticsearchContainer; +import com.rfs.transformers.TransformFunctions; +import com.rfs.transformers.Transformer; +import com.rfs.version_es_7_10.ElasticsearchConstants_ES_7_10; +import com.rfs.version_es_7_10.GlobalMetadataFactory_ES_7_10; +import com.rfs.version_es_7_10.IndexMetadataFactory_ES_7_10; +import com.rfs.version_es_7_10.ShardMetadataFactory_ES_7_10; +import com.rfs.version_es_7_10.SnapshotRepoProvider_ES_7_10; +import com.rfs.version_os_2_11.GlobalMetadataCreator_OS_2_11; +import com.rfs.version_os_2_11.IndexCreator_OS_2_11; +import com.rfs.worker.DocumentsRunner; +import com.rfs.worker.IndexRunner; +import com.rfs.worker.MetadataRunner; +import lombok.Lombok; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.apache.lucene.document.Document; +import org.hamcrest.MatcherAssert; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.opensearch.testcontainers.OpensearchContainer; +import org.slf4j.event.Level; +import reactor.core.publisher.Flux; + +import java.io.IOException; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.UnaryOperator; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +@Tag("longTest") +@Slf4j +public class FullTest { + final static long TOLERABLE_CLIENT_SERVER_CLOCK_DIFFERENCE_SECONDS = 3600; + final static Pattern CAT_INDICES_INDEX_COUNT_PATTERN = + Pattern.compile("(?:\\S+\\s+){2}(\\S+)\\s+(?:\\S+\\s+){3}(\\S+)"); + + public static Stream makeArgs() { + var sourceImageNames = List.of("migrations/elasticsearch_rfs_source"); + var targetImageNames = List.of("opensearchproject/opensearch:2.13.0", "opensearchproject/opensearch:1.3.0"); + var numWorkers = List.of(1, 3, 40); + return sourceImageNames.stream() + .flatMap(a-> + targetImageNames.stream().flatMap(b-> + numWorkers.stream().map(c->Arguments.of(a, b, c)))); + } + + @ParameterizedTest + @MethodSource("makeArgs") + public void test(String sourceImageName, String targetImageName, int numWorkers) throws Exception { + try (ElasticsearchContainer esSourceContainer = + new ElasticsearchContainer(new ElasticsearchContainer.Version(sourceImageName, + "preloaded-ES_7_10")); + OpensearchContainer osTargetContainer = + new OpensearchContainer<>(targetImageName)) { + esSourceContainer.start(); + osTargetContainer.start(); + + final var SNAPSHOT_NAME = "test_snapshot"; + CreateSnapshot.run( + c -> new FileSystemSnapshotCreator(SNAPSHOT_NAME, c, ElasticsearchContainer.CLUSTER_SNAPSHOT_DIR), + new OpenSearchClient(esSourceContainer.getUrl(), null)); + var tempDir = Files.createTempDirectory("opensearchMigrationReindexFromSnapshot_test_snapshot"); + try { + esSourceContainer.copySnapshotData(tempDir.toString()); + + var targetClient = new OpenSearchClient(osTargetContainer.getHttpHostAddress(), null); + var sourceRepo = new FileSystemRepo(tempDir); + migrateMetadata(sourceRepo, targetClient, SNAPSHOT_NAME); + + var workerFutures = new ArrayList>(); + var runCounter = new AtomicInteger(); + for (int i = 0; i < numWorkers; ++i) { + workerFutures.add(CompletableFuture.supplyAsync(() -> + migrateDocumentsSequentially(sourceRepo, SNAPSHOT_NAME, + osTargetContainer.getHttpHostAddress(), runCounter))); + } + var thrownException = Assertions.assertThrows(ExecutionException.class, () -> + CompletableFuture.allOf(workerFutures.toArray(CompletableFuture[]::new)).get()); + var exceptionResults = + workerFutures.stream().map(cf -> { + try { + return cf.handle((v, t) -> + Optional.ofNullable(t).map(Throwable::getCause).orElse(null)) + .get(); + } catch (Exception e) { + throw Lombok.sneakyThrow(e); + } + }).filter(Objects::nonNull).collect(Collectors.toList()); + exceptionResults.forEach(e -> + log.atLevel(e instanceof RfsMigrateDocuments.NoWorkLeftException ? Level.INFO : Level.ERROR) + .setMessage(() -> "First exception for run").setCause(thrownException.getCause()).log()); + exceptionResults.forEach(e -> Assertions.assertInstanceOf(RfsMigrateDocuments.NoWorkLeftException.class, e)); + + // for now, lets make sure that we got all of the + Assertions.assertInstanceOf(RfsMigrateDocuments.NoWorkLeftException.class, thrownException.getCause(), + "expected at least one worker to notice that all work was completed."); + checkClusterMigrationOnFinished(esSourceContainer, osTargetContainer); + var totalCompletedWorkRuns = runCounter.get(); + Assertions.assertTrue(totalCompletedWorkRuns >= numWorkers, + "Expected to make more runs (" + totalCompletedWorkRuns + ") than the number of workers " + + "(" + numWorkers + "). Increase the number of shards so that there is more work to do."); + } finally { + deleteTree(tempDir); + } + } + } + + private void checkClusterMigrationOnFinished(ElasticsearchContainer esSourceContainer, + OpensearchContainer osTargetContainer) { + var targetClient = new RestClient(new ConnectionDetails(osTargetContainer.getHttpHostAddress(), null, null)); + var sourceMap = getIndexToCountMap(new RestClient(new ConnectionDetails(esSourceContainer.getUrl(), + null, null))); + var refreshResponse = targetClient.get("_refresh"); + Assertions.assertEquals(200, refreshResponse.code); + var targetMap = getIndexToCountMap(targetClient); + MatcherAssert.assertThat(targetMap, Matchers.equalTo(sourceMap)); + } + + private Map getIndexToCountMap(RestClient client) {; + var lines = Optional.ofNullable(client.get("_cat/indices")) + .flatMap(r->Optional.ofNullable(r.body)) + .map(b->b.split("\n")) + .orElse(new String[0]); + return Arrays.stream(lines) + .map(line -> { + var matcher = CAT_INDICES_INDEX_COUNT_PATTERN.matcher(line); + return !matcher.find() ? null : + new AbstractMap.SimpleEntry<>(matcher.group(1), matcher.group(2)); + }) + .filter(Objects::nonNull) + .filter(kvp->!kvp.getKey().startsWith(".")) + .collect(Collectors.toMap(AbstractMap.SimpleEntry::getKey, kvp -> Integer.parseInt(kvp.getValue()))); + } + + @SneakyThrows + private Void migrateDocumentsSequentially(FileSystemRepo sourceRepo, + String snapshotName, + String targetAddress, + AtomicInteger runCounter) { + for (int runNumber=0; ; ++runNumber) { + try { + var workResult = migrateDocumentsWithOneWorker(sourceRepo, snapshotName, targetAddress); + if (workResult == DocumentsRunner.CompletionStatus.NOTHING_DONE) { + return null; + } else { + runCounter.incrementAndGet(); + } + } catch (RfsMigrateDocuments.NoWorkLeftException e) { + log.info("No work at all was found. " + + "Presuming that work was complete and that all worker processes should terminate"); + throw e; + } catch (Exception e) { + log.atError().setCause(e).setMessage(()->"Caught an exception, " + + "but just going to run again with this worker to simulate task/container recycling").log(); + } + } + } + + private static void migrateMetadata(SourceRepo sourceRepo, OpenSearchClient targetClient, String snapshotName) { + SnapshotRepo.Provider repoDataProvider = new SnapshotRepoProvider_ES_7_10(sourceRepo); + GlobalMetadata.Factory metadataFactory = new GlobalMetadataFactory_ES_7_10(repoDataProvider); + GlobalMetadataCreator_OS_2_11 metadataCreator = new GlobalMetadataCreator_OS_2_11(targetClient, + List.of(), List.of(), List.of()); + Transformer transformer = + TransformFunctions.getTransformer(ClusterVersion.ES_7_10, ClusterVersion.OS_2_11, 1); + new MetadataRunner(snapshotName, metadataFactory, metadataCreator, transformer).migrateMetadata(); + + IndexMetadata.Factory indexMetadataFactory = new IndexMetadataFactory_ES_7_10(repoDataProvider); + IndexCreator_OS_2_11 indexCreator = new IndexCreator_OS_2_11(targetClient); + new IndexRunner(snapshotName, indexMetadataFactory, indexCreator, transformer).migrateIndices(); + } + + private static class FilteredLuceneDocumentsReader extends LuceneDocumentsReader { + private final UnaryOperator docTransformer; + + public FilteredLuceneDocumentsReader(Path luceneFilesBasePath, UnaryOperator docTransformer) { + super(luceneFilesBasePath); + this.docTransformer = docTransformer; + } + + @Override + public Flux readDocuments() { + return super.readDocuments().map(docTransformer::apply); + } + } + + static class LeasePastError extends Error { } + + @SneakyThrows + private DocumentsRunner.CompletionStatus migrateDocumentsWithOneWorker(SourceRepo sourceRepo, + String snapshotName, + String targetAddress) + throws RfsMigrateDocuments.NoWorkLeftException + { + var tempDir = Files.createTempDirectory("opensearchMigrationReindexFromSnapshot_test_lucene"); + try { + var shouldThrow = new AtomicBoolean(); + UnaryOperator terminatingDocumentFilter = d -> { + if (shouldThrow.get()) { + throw new LeasePastError(); + } + return d; + }; + var processManager = new LeaseExpireTrigger(workItemId->{ + log.atDebug().setMessage("Lease expired for " + workItemId + " making next document get throw").log(); + shouldThrow.set(true); + }); + + DefaultSourceRepoAccessor repoAccessor = new DefaultSourceRepoAccessor(sourceRepo); + SnapshotShardUnpacker.Factory unpackerFactory = new SnapshotShardUnpacker.Factory(repoAccessor, + tempDir, ElasticsearchConstants_ES_7_10.BUFFER_SIZE_IN_BYTES); + + SnapshotRepo.Provider repoDataProvider = new SnapshotRepoProvider_ES_7_10(sourceRepo); + IndexMetadata.Factory indexMetadataFactory = new IndexMetadataFactory_ES_7_10(repoDataProvider); + ShardMetadata.Factory shardMetadataFactory = new ShardMetadataFactory_ES_7_10(repoDataProvider); + + return RfsMigrateDocuments.run(path -> new FilteredLuceneDocumentsReader(path, terminatingDocumentFilter), + new DocumentReindexer(new OpenSearchClient(targetAddress, null)), + new OpenSearchWorkCoordinator( + new ApacheHttpClient(new URI(targetAddress)), +// new ReactorHttpClient(new ConnectionDetails(osTargetContainer.getHttpHostAddress(), +// null, null)), + TOLERABLE_CLIENT_SERVER_CLOCK_DIFFERENCE_SECONDS, UUID.randomUUID().toString()), + processManager, + indexMetadataFactory, + snapshotName, + shardMetadataFactory, + unpackerFactory, + 16*1024*1024); + } finally { + deleteTree(tempDir); + } + } + + private static void deleteTree(Path path) throws IOException { + try (var walk = Files.walk(path)) { + walk.sorted(Comparator.reverseOrder()).forEach(p -> { + try { + Files.delete(p); + } catch (IOException e) { + throw Lombok.sneakyThrow(e); + } + }); + } + } +} diff --git a/DocumentsFromSnapshotMigration/src/test/resources/log4j2.properties b/DocumentsFromSnapshotMigration/src/test/resources/log4j2.properties new file mode 100644 index 000000000..f3774570f --- /dev/null +++ b/DocumentsFromSnapshotMigration/src/test/resources/log4j2.properties @@ -0,0 +1,14 @@ +status = ERROR + +appender.console.type = Console +appender.console.name = Console +appender.console.target = SYSTEM_OUT +appender.console.layout.type = PatternLayout +appender.console.layout.pattern = %d{HH:mm:ss.SSS} %threadName %-5p %c{1}:%L - %m%n + +rootLogger.level = info +rootLogger.appenderRef.console.ref = Console + +logger.wireLogger.name = org.apache.http.wire +logger.wireLogger.level = OFF +logger.wireLogger.additivity = false \ No newline at end of file diff --git a/MetadataMigration/src/main/java/com/rfs/MetadataMigration.java b/MetadataMigration/src/main/java/com/rfs/MetadataMigration.java index 35cc8bbca..9f9d79540 100644 --- a/MetadataMigration/src/main/java/com/rfs/MetadataMigration.java +++ b/MetadataMigration/src/main/java/com/rfs/MetadataMigration.java @@ -7,8 +7,6 @@ import java.nio.file.Paths; import java.util.List; -import com.rfs.cms.CmsClient; -import com.rfs.cms.OpenSearchCmsClient; import com.rfs.common.ClusterVersion; import com.rfs.common.ConnectionDetails; import com.rfs.common.GlobalMetadata; @@ -26,14 +24,13 @@ import com.rfs.version_es_7_10.SnapshotRepoProvider_ES_7_10; import com.rfs.version_os_2_11.GlobalMetadataCreator_OS_2_11; import com.rfs.version_os_2_11.IndexCreator_OS_2_11; -import com.rfs.worker.GlobalState; import com.rfs.worker.IndexRunner; import com.rfs.worker.MetadataRunner; import lombok.extern.slf4j.Slf4j; @Slf4j public class MetadataMigration { - + public static class Args { @Parameter(names = {"--snapshot-name"}, description = "The name of the snapshot to migrate", required = true) public String snapshotName; @@ -79,9 +76,9 @@ public static void main(String[] args) throws Exception { // Grab out args Args arguments = new Args(); JCommander.newBuilder() - .addObject(arguments) - .build() - .parse(args); + .addObject(arguments) + .build() + .parse(args); final String snapshotName = arguments.snapshotName; final Path s3LocalDirPath = Paths.get(arguments.s3LocalDirPath); @@ -98,22 +95,18 @@ public static void main(String[] args) throws Exception { TryHandlePhaseFailure.executeWithTryCatch(() -> { log.info("Running RfsWorker"); - GlobalState globalState = GlobalState.getInstance(); OpenSearchClient targetClient = new OpenSearchClient(targetConnection); - final CmsClient cmsClient = new OpenSearchCmsClient(targetClient); final SourceRepo sourceRepo = S3Repo.create(s3LocalDirPath, new S3Uri(s3RepoUri), s3Region); final SnapshotRepo.Provider repoDataProvider = new SnapshotRepoProvider_ES_7_10(sourceRepo); final GlobalMetadata.Factory metadataFactory = new GlobalMetadataFactory_ES_7_10(repoDataProvider); final GlobalMetadataCreator_OS_2_11 metadataCreator = new GlobalMetadataCreator_OS_2_11(targetClient, List.of(), componentTemplateAllowlist, indexTemplateAllowlist); final Transformer transformer = TransformFunctions.getTransformer(ClusterVersion.ES_7_10, ClusterVersion.OS_2_11, awarenessDimensionality); - MetadataRunner metadataWorker = new MetadataRunner(globalState, cmsClient, snapshotName, metadataFactory, metadataCreator, transformer); - metadataWorker.run(); + new MetadataRunner(snapshotName, metadataFactory, metadataCreator, transformer).migrateMetadata(); final IndexMetadata.Factory indexMetadataFactory = new IndexMetadataFactory_ES_7_10(repoDataProvider); final IndexCreator_OS_2_11 indexCreator = new IndexCreator_OS_2_11(targetClient); - final IndexRunner indexWorker = new IndexRunner(globalState, cmsClient, snapshotName, indexMetadataFactory, indexCreator, transformer); - indexWorker.run(); + new IndexRunner(snapshotName, indexMetadataFactory, indexCreator, transformer).migrateIndices(); }); } } diff --git a/RFS/README.md b/RFS/README.md index 51e896cbb..da08f57d4 100644 --- a/RFS/README.md +++ b/RFS/README.md @@ -72,13 +72,13 @@ gradle run --args='-n global_state_snapshot --source-host $SOURCE_HOST --source- ``` ### Using Docker -RFS has support for packaging its java application as a Docker image by using the Dockerfile located in the `RFS/docker` directory. This support is directly built into Gradle as well so that a user can perform the below action, and generate a fresh Docker image (`migrations/reindex_from_snapshot:latest`) with the latest local code changes available. +DocumentsFromSnaphotMigration has support for packaging its java application as a Docker image by using the Dockerfile located in the `../DocumentsFromSnapshot/docker` directory. This support is directly built into Gradle as well so that a user can perform the below action, and generate a fresh Docker image (`migrations/reindex_from_snapshot:latest`) with the latest local code changes available. ```shell ../gradlew buildDockerImages ``` -Also built into this Docker/Gradle support is the ability to spin up a testing RFS environment using Docker compose. This compose file can be seen [here](./docker/docker-compose.yml) and includes the RFS container, a source cluster container, and a target cluster container. +Also built into this Docker/Gradle support is the ability to spin up a testing RFS environment using Docker compose. This compose file can be seen [here](../DocumentsFromSnapshotMigration/docker/docker-compose.yml) and includes the RFS container, a source cluster container, and a target cluster container. -This environment can be spun up with the Gradle command, and use the optional `-Pdataset` flag to preload a dataset from the `generateDatasetStage` in the multi-stage Docker [here](docker/TestSource_ES_7_10/Dockerfile). This stage will take a few minutes to run on its first attempt if it is generating data, as it will be making requests with OSB. This will be cached for future runs. +This environment can be spun up with the Gradle command, and use the optional `-Pdataset` flag to preload a dataset from the `generateDatasetStage` in the multi-stage Docker [here](../DocumentsFromSnapshotMigration/docker/TestSource_ES_7_10/Dockerfile). This stage will take a few minutes to run on its first attempt if it is generating data, as it will be making requests with OSB. This will be cached for future runs. ```shell ../gradlew composeUp -Pdataset=default_osb_test_workloads ``` diff --git a/RFS/build.gradle b/RFS/build.gradle index e03c2445b..5d3b314dd 100644 --- a/RFS/build.gradle +++ b/RFS/build.gradle @@ -2,26 +2,13 @@ plugins { id 'application' id 'java' id 'jacoco' - id "com.avast.gradle.docker-compose" version "0.17.4" - id 'com.bmuschko.docker-remote-api' id 'io.freefair.lombok' version '8.6' + id 'java-test-fixtures' } -import com.bmuschko.gradle.docker.tasks.image.DockerBuildImage -import groovy.transform.Canonical -import org.opensearch.migrations.common.CommonUtils - java.sourceCompatibility = JavaVersion.VERSION_11 java.targetCompatibility = JavaVersion.VERSION_11 -@Canonical -class DockerServiceProps { - String projectName = "" - String dockerImageName = "" - String inputDir = "" - Map buildArgs = [:] - List taskDependencies = [] -} repositories { mavenCentral() @@ -39,46 +26,47 @@ dependencies { implementation group: 'com.beust', name: 'jcommander' implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind' - implementation group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-smile' implementation group: 'com.fasterxml.jackson.core', name: 'jackson-annotations' implementation group: 'com.fasterxml.jackson.core', name: 'jackson-core' + implementation group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-smile' implementation group: 'io.netty', name: 'netty-codec-http' + implementation group: 'org.apache.httpcomponents.client5', name: 'httpclient5' implementation group: 'org.apache.logging.log4j', name: 'log4j-api' implementation group: 'org.apache.logging.log4j', name: 'log4j-core' + implementation group: 'org.apache.logging.log4j', name: 'log4j-slf4j2-impl' implementation group: 'org.apache.lucene', name: 'lucene-core' implementation group: 'org.apache.lucene', name: 'lucene-analyzers-common' implementation group: 'org.apache.lucene', name: 'lucene-backward-codecs' + implementation group: 'software.amazon.awssdk', name: 's3' + implementation group: 'software.amazon.awssdk', name: 's3-transfer-manager' + implementation group: 'software.amazon.awssdk.crt', name: 'aws-crt' implementation platform('io.projectreactor:reactor-bom:2023.0.5') implementation 'io.projectreactor.netty:reactor-netty-core' implementation 'io.projectreactor.netty:reactor-netty-http' - implementation group: 'software.amazon.awssdk', name: 's3' - implementation group: 'software.amazon.awssdk', name: 's3-transfer-manager' - implementation group: 'software.amazon.awssdk.crt', name: 'aws-crt' - + testImplementation testFixtures(project(path: ':RFS')) testImplementation group: 'io.projectreactor', name: 'reactor-test' testImplementation group: 'org.apache.logging.log4j', name: 'log4j-core' + testImplementation group: 'org.apache.logging.log4j', name: 'log4j-slf4j2-impl' testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-api' testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-params' testImplementation group: 'org.mockito', name: 'mockito-core' testImplementation group: 'org.mockito', name: 'mockito-junit-jupiter' - testRuntimeOnly group: 'org.junit.jupiter', name: 'junit-jupiter-engine' + testRuntimeOnly group: 'org.junit.jupiter', name: 'junit-jupiter-engine' // Integration tests + testImplementation group: 'org.opensearch', name: 'opensearch-testcontainers' testImplementation group: 'org.testcontainers', name: 'testcontainers' testImplementation group: 'org.hamcrest', name: 'hamcrest' - testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-api' - testRuntimeOnly group: 'org.junit.jupiter', name: 'junit-jupiter-engine' - implementation group: 'org.apache.httpcomponents.client5', name: 'httpclient5' - implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind' - implementation group: 'com.fasterxml.jackson.core', name: 'jackson-core' - implementation group: 'com.fasterxml.jackson.core', name: 'jackson-annotations' + + testFixturesImplementation project(":commonDependencyVersionConstraints") + testFixturesImplementation group: 'org.testcontainers', name: 'testcontainers' } application { - mainClassName = 'com.rfs.RfsMigrateDocuments' + mainClassName = 'com.rfs.ReindexFromSnapshot' } task runRfsWorker (type: JavaExec) { @@ -101,11 +89,6 @@ task migrateDocuments (type: JavaExec) { mainClass = 'com.rfs.RfsMigrateDocuments' } -// Cleanup additional docker build directory -clean.doFirst { - delete project.file("./docker/build") -} - // Utility task to allow copying required libraries into a 'dependencies' folder for security scanning tasks.register('copyDependencies', Sync) { duplicatesStrategy = DuplicatesStrategy.EXCLUDE @@ -123,74 +106,10 @@ jacocoTestReport { } } -task copyDockerRuntimeJars (type: Sync) { - duplicatesStrategy = DuplicatesStrategy.EXCLUDE - description = 'Copy runtime JARs and app jar to docker build directory' - - // Define the destination directory - def buildDir = project.file("./docker/build/runtimeJars") - into buildDir - - // Add all the required runtime JARs to be copied - from configurations.runtimeClasspath - from tasks.named('jar') - include '*.jar' -} - -DockerServiceProps[] dockerServices = [ - new DockerServiceProps([projectName:"reindexFromSnapshot", - dockerImageName:"reindex_from_snapshot", - inputDir:"./docker", - taskDependencies:["copyDockerRuntimeJars"]]), - new DockerServiceProps([projectName:"emptyElasticsearchSource_7_10", - dockerImageName:"empty_elasticsearch_source_7_10", - inputDir:"./docker/TestSource_ES_7_10"]), - new DockerServiceProps([projectName:"emptyElasticsearchSource_7_17", - dockerImageName:"empty_elasticsearch_source_7_17", - inputDir:"./docker/TestSource_ES_7_17"]), - new DockerServiceProps([projectName:"trafficGenerator", - dockerImageName:"osb_traffic_generator", - inputDir:"./docker/TrafficGenerator", - taskDependencies:[":TrafficCapture:dockerSolution:buildDockerImage_elasticsearchTestConsole"]]), -] as DockerServiceProps[] - -for (dockerService in dockerServices) { - task "buildDockerImage_${dockerService.projectName}" (type: DockerBuildImage) { - def hash = CommonUtils.calculateDockerHash(project.fileTree("docker/${dockerService.projectName}")) - for (dep in dockerService.taskDependencies) { - dependsOn dep - } - inputDir = project.file(dockerService.inputDir) - buildArgs = dockerService.buildArgs - images.add("migrations/${dockerService.dockerImageName}:${hash}") - images.add("migrations/${dockerService.dockerImageName}:${version}") - images.add("migrations/${dockerService.dockerImageName}:latest") - } -} - -apply from: 'build-preloaded-source-image.gradle' - -dockerCompose { - useComposeFiles = ['docker/docker-compose.yml'] - projectName = 'rfs-compose' -} - -// ../gradlew buildDockerImages -task buildDockerImages { - for (dockerService in dockerServices) { - dependsOn "buildDockerImage_${dockerService.projectName}" - } - dependsOn buildDockerImage_elasticsearchRFSSource -} - -tasks.named("buildDockerImage_elasticsearchRFSSource") { - dependsOn(':TrafficCapture:dockerSolution:buildDockerImage_elasticsearchTestConsole') -} -tasks.getByName('composeUp') - .dependsOn(tasks.getByName('buildDockerImages')) - test { - useJUnitPlatform() + useJUnitPlatform { + excludeTags 'longTest' + } testLogging { exceptionFormat = 'full' @@ -200,4 +119,11 @@ test { showStackTraces true showStandardStreams = true } +} + +task slowTest(type: Test) { + // include longTest + jacoco { + enabled = true + } } \ No newline at end of file diff --git a/RFS/src/main/java/com/rfs/ReindexFromSnapshot.java b/RFS/src/main/java/com/rfs/ReindexFromSnapshot.java index f8efcc61b..f5e00b85a 100644 --- a/RFS/src/main/java/com/rfs/ReindexFromSnapshot.java +++ b/RFS/src/main/java/com/rfs/ReindexFromSnapshot.java @@ -367,7 +367,7 @@ public static void main(String[] args) throws InterruptedException { for (int shardId = 0; shardId < indexMetadata.getNumberOfShards(); shardId++) { logger.info("=== Index Id: " + indexMetadata.getName() + ", Shard ID: " + shardId + " ==="); - Flux documents = reader.readDocuments(indexMetadata.getName(), shardId); + Flux documents = reader.readDocuments(); String targetIndex = indexMetadata.getName() + indexSuffix; final int finalShardId = shardId; // Define in local context for the lambda diff --git a/RFS/src/main/java/com/rfs/RunRfsWorker.java b/RFS/src/main/java/com/rfs/RunRfsWorker.java index 2bf8ea83c..768f2f851 100644 --- a/RFS/src/main/java/com/rfs/RunRfsWorker.java +++ b/RFS/src/main/java/com/rfs/RunRfsWorker.java @@ -3,20 +3,24 @@ import com.beust.jcommander.JCommander; import com.beust.jcommander.Parameter; +import java.net.URI; import java.nio.file.Path; import java.nio.file.Paths; +import java.time.Clock; import java.util.List; +import java.util.UUID; +import com.rfs.cms.ApacheHttpClient; +import com.rfs.cms.OpenSearchWorkCoordinator; +import com.rfs.cms.LeaseExpireTrigger; +import com.rfs.cms.ScopedWorkCoordinator; +import com.rfs.common.DefaultSourceRepoAccessor; +import com.rfs.worker.ShardWorkPreparer; +import lombok.extern.slf4j.Slf4j; import org.apache.logging.log4j.Level; -import org.apache.logging.log4j.Logger; -import org.apache.logging.log4j.LogManager; - -import com.rfs.cms.CmsClient; -import com.rfs.cms.OpenSearchCmsClient; import com.rfs.common.ClusterVersion; import com.rfs.common.ConnectionDetails; -import com.rfs.common.DefaultSourceRepoAccessor; import com.rfs.common.DocumentReindexer; import com.rfs.common.GlobalMetadata; import com.rfs.common.IndexMetadata; @@ -28,7 +32,6 @@ import com.rfs.common.S3Repo; import com.rfs.common.SnapshotCreator; import com.rfs.common.SourceRepo; -import com.rfs.common.TryHandlePhaseFailure; import com.rfs.common.S3SnapshotCreator; import com.rfs.common.SnapshotRepo; import com.rfs.common.SnapshotShardUnpacker; @@ -42,13 +45,13 @@ import com.rfs.version_os_2_11.GlobalMetadataCreator_OS_2_11; import com.rfs.version_os_2_11.IndexCreator_OS_2_11; import com.rfs.worker.DocumentsRunner; -import com.rfs.worker.GlobalState; import com.rfs.worker.IndexRunner; import com.rfs.worker.MetadataRunner; import com.rfs.worker.SnapshotRunner; +@Slf4j public class RunRfsWorker { - private static final Logger logger = LogManager.getLogger(RunRfsWorker.class); + public static final int PROCESS_TIMED_OUT = 1; public static class Args { @Parameter(names = {"--snapshot-name"}, description = "The name of the snapshot to migrate", required = true) @@ -99,7 +102,7 @@ public static class Args { @Parameter(names = {"--max-shard-size-bytes"}, description = ("Optional. The maximum shard size, in bytes, to allow when" + " performing the document migration. Useful for preventing disk overflow. Default: 50 * 1024 * 1024 * 1024 (50 GB)"), required = false) public long maxShardSizeBytes = 50 * 1024 * 1024 * 1024L; - + //https://opensearch.org/docs/2.11/api-reference/cluster-api/cluster-awareness/ @Parameter(names = {"--min-replicas"}, description = ("Optional. The minimum number of replicas configured for migrated indices on the target." + " This can be useful for migrating to targets which use zonal deployments and require additional replicas to meet zone requirements. Default: 0") @@ -129,7 +132,7 @@ public static void main(String[] args) throws Exception { final String targetHost = arguments.targetHost; final String targetUser = arguments.targetUser; final String targetPass = arguments.targetPass; - final List indexTemplateAllowlist = arguments.indexTemplateAllowlist; + final List indexTemplateAllowlist = arguments.indexAllowlist; final List componentTemplateAllowlist = arguments.componentTemplateAllowlist; final long maxShardSizeBytes = arguments.maxShardSizeBytes; final int awarenessDimensionality = arguments.minNumberOfReplicas + 1; @@ -140,37 +143,47 @@ public static void main(String[] args) throws Exception { final ConnectionDetails sourceConnection = new ConnectionDetails(sourceHost, sourceUser, sourcePass); final ConnectionDetails targetConnection = new ConnectionDetails(targetHost, targetUser, targetPass); - TryHandlePhaseFailure.executeWithTryCatch(() -> { - logger.info("Running RfsWorker"); - GlobalState globalState = GlobalState.getInstance(); + try { + log.info("Running RfsWorker"); OpenSearchClient sourceClient = new OpenSearchClient(sourceConnection); OpenSearchClient targetClient = new OpenSearchClient(targetConnection); - final CmsClient cmsClient = new OpenSearchCmsClient(targetClient); - - final SnapshotCreator snapshotCreator = new S3SnapshotCreator(snapshotName, sourceClient, s3RepoUri, s3Region); - final SnapshotRunner snapshotWorker = new SnapshotRunner(globalState, cmsClient, snapshotCreator); - snapshotWorker.run(); - - final SourceRepo sourceRepo = S3Repo.create(s3LocalDirPath, new S3Uri(s3RepoUri), s3Region); - final SnapshotRepo.Provider repoDataProvider = new SnapshotRepoProvider_ES_7_10(sourceRepo); - final GlobalMetadata.Factory metadataFactory = new GlobalMetadataFactory_ES_7_10(repoDataProvider); - final GlobalMetadataCreator_OS_2_11 metadataCreator = new GlobalMetadataCreator_OS_2_11(targetClient, List.of(), componentTemplateAllowlist, indexTemplateAllowlist); - final Transformer transformer = TransformFunctions.getTransformer(ClusterVersion.ES_7_10, ClusterVersion.OS_2_11, awarenessDimensionality); - MetadataRunner metadataWorker = new MetadataRunner(globalState, cmsClient, snapshotName, metadataFactory, metadataCreator, transformer); - metadataWorker.run(); - - final IndexMetadata.Factory indexMetadataFactory = new IndexMetadataFactory_ES_7_10(repoDataProvider); - final IndexCreator_OS_2_11 indexCreator = new IndexCreator_OS_2_11(targetClient); - final IndexRunner indexWorker = new IndexRunner(globalState, cmsClient, snapshotName, indexMetadataFactory, indexCreator, transformer); - indexWorker.run(); - - final ShardMetadata.Factory shardMetadataFactory = new ShardMetadataFactory_ES_7_10(repoDataProvider); - final DefaultSourceRepoAccessor repoAccessor = new DefaultSourceRepoAccessor(sourceRepo); - final SnapshotShardUnpacker.Factory unpackerFactory = new SnapshotShardUnpacker.Factory(repoAccessor, luceneDirPath, ElasticsearchConstants_ES_7_10.BUFFER_SIZE_IN_BYTES); - final LuceneDocumentsReader reader = new LuceneDocumentsReader(luceneDirPath); - final DocumentReindexer reindexer = new DocumentReindexer(targetClient); - DocumentsRunner documentsWorker = new DocumentsRunner(globalState, cmsClient, snapshotName, maxShardSizeBytes, indexMetadataFactory, shardMetadataFactory, unpackerFactory, reader, reindexer); - documentsWorker.run(); - }); + + SnapshotCreator snapshotCreator = new S3SnapshotCreator(snapshotName, sourceClient, s3RepoUri, s3Region); + SnapshotRunner.runAndWaitForCompletion(snapshotCreator); + + SourceRepo sourceRepo = S3Repo.create(s3LocalDirPath, new S3Uri(s3RepoUri), s3Region); + SnapshotRepo.Provider repoDataProvider = new SnapshotRepoProvider_ES_7_10(sourceRepo); + GlobalMetadata.Factory metadataFactory = new GlobalMetadataFactory_ES_7_10(repoDataProvider); + GlobalMetadataCreator_OS_2_11 metadataCreator = new GlobalMetadataCreator_OS_2_11(targetClient, List.of(), componentTemplateAllowlist, indexTemplateAllowlist); + Transformer transformer = TransformFunctions.getTransformer(ClusterVersion.ES_7_10, ClusterVersion.OS_2_11, awarenessDimensionality); + new MetadataRunner(snapshotName, metadataFactory, metadataCreator, transformer).migrateMetadata(); + + IndexMetadata.Factory indexMetadataFactory = new IndexMetadataFactory_ES_7_10(repoDataProvider); + IndexCreator_OS_2_11 indexCreator = new IndexCreator_OS_2_11(targetClient); + new IndexRunner(snapshotName, indexMetadataFactory, indexCreator, transformer).migrateIndices(); + + ShardMetadata.Factory shardMetadataFactory = new ShardMetadataFactory_ES_7_10(repoDataProvider); + DefaultSourceRepoAccessor repoAccessor = new DefaultSourceRepoAccessor(sourceRepo); + var unpackerFactory = new SnapshotShardUnpacker.Factory(repoAccessor, + luceneDirPath, ElasticsearchConstants_ES_7_10.BUFFER_SIZE_IN_BYTES); + DocumentReindexer reindexer = new DocumentReindexer(targetClient); + var processManager = new LeaseExpireTrigger(workItemId->{ + log.error("terminating RunRfsWorker because its lease has expired for "+workItemId); + System.exit(PROCESS_TIMED_OUT); + }, Clock.systemUTC()); + var workCoordinator = new OpenSearchWorkCoordinator(new ApacheHttpClient(new URI(targetHost)), + 5, UUID.randomUUID().toString()); + var scopedWorkCoordinator = new ScopedWorkCoordinator(workCoordinator, processManager); + new ShardWorkPreparer().run(scopedWorkCoordinator, indexMetadataFactory, snapshotName); + new DocumentsRunner(scopedWorkCoordinator, + (name,shard) -> shardMetadataFactory.fromRepo(snapshotName,name,shard), + unpackerFactory, + path -> new LuceneDocumentsReader(path), + reindexer) + .migrateNextShard(); + } catch (Exception e) { + log.error("Unexpected error running RfsWorker", e); + throw e; + } } } diff --git a/RFS/src/main/java/com/rfs/cms/AbstractedHttpClient.java b/RFS/src/main/java/com/rfs/cms/AbstractedHttpClient.java new file mode 100644 index 000000000..7b2ba2890 --- /dev/null +++ b/RFS/src/main/java/com/rfs/cms/AbstractedHttpClient.java @@ -0,0 +1,62 @@ +package com.rfs.cms; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public interface AbstractedHttpClient extends AutoCloseable { + interface AbstractHttpResponse { + Stream> getHeaders(); + + default byte[] getPayloadBytes() throws IOException { + return getPayloadStream().readAllBytes(); + } + + default InputStream getPayloadStream() throws IOException { + return new ByteArrayInputStream(getPayloadBytes()); + } + + String getStatusText(); + + int getStatusCode(); + + default String toDiagnosticString() { + String payloadStr; + try { + payloadStr = Arrays.toString(getPayloadBytes()); + } catch (IOException e) { + payloadStr = "[EXCEPTION EVALUATING PAYLOAD]: " + e; + } + return getStatusText() + "/" + getStatusCode() + + getHeaders().map(kvp -> kvp.getKey() + ": " + kvp.getValue()) + .collect(Collectors.joining(";", "[", "]")) + + payloadStr; + } + } + + AbstractHttpResponse makeRequest(String method, + String path, + Map headers, + String payload) + throws IOException; + + default AbstractHttpResponse makeJsonRequest(String method, + String path, + Map extraHeaders, + String body) + throws IOException + { + var combinedHeaders = new LinkedHashMap(); + combinedHeaders.put("Content-Type", "application/json"); + combinedHeaders.put("Accept-Encoding", "identity"); + if (extraHeaders != null) { + combinedHeaders.putAll(extraHeaders); + } + return makeRequest(method, path, combinedHeaders, body); + } +} diff --git a/RFS/src/main/java/com/rfs/cms/ApacheHttpClient.java b/RFS/src/main/java/com/rfs/cms/ApacheHttpClient.java new file mode 100644 index 000000000..35950dbf6 --- /dev/null +++ b/RFS/src/main/java/com/rfs/cms/ApacheHttpClient.java @@ -0,0 +1,98 @@ +package com.rfs.cms; + +import lombok.Getter; +import lombok.Lombok; +import lombok.extern.slf4j.Slf4j; +import org.apache.hc.client5.http.classic.methods.HttpDelete; +import org.apache.hc.client5.http.classic.methods.HttpGet; +import org.apache.hc.client5.http.classic.methods.HttpHead; +import org.apache.hc.client5.http.classic.methods.HttpOptions; +import org.apache.hc.client5.http.classic.methods.HttpPatch; +import org.apache.hc.client5.http.classic.methods.HttpPost; +import org.apache.hc.client5.http.classic.methods.HttpPut; +import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.impl.classic.HttpClients; +import org.apache.hc.core5.http.io.entity.StringEntity; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.util.AbstractMap; +import java.util.Arrays; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Stream; + +@Slf4j +public class ApacheHttpClient implements AbstractedHttpClient { + private final CloseableHttpClient client = HttpClients.createDefault(); + private final URI baseUri; + + public ApacheHttpClient(URI baseUri) { + this.baseUri = baseUri; + } + + private static HttpUriRequestBase makeRequestBase(URI baseUri, String method, String path) { + switch (method.toUpperCase()) { + case "GET": + return new HttpGet(baseUri + "/" + path); + case OpenSearchWorkCoordinator.POST_METHOD: + return new HttpPost(baseUri + "/" + path); + case OpenSearchWorkCoordinator.PUT_METHOD: + return new HttpPut(baseUri + "/" + path); + case "PATCH": + return new HttpPatch(baseUri + "/" + path); + case "HEAD": + return new HttpHead(baseUri + "/" + path); + case "OPTIONS": + return new HttpOptions(baseUri + "/" + path); + case "DELETE": + return new HttpDelete(baseUri + "/" + path); + default: + throw new IllegalArgumentException("Cannot map method to an Apache Http Client request: " + method); + } + } + + @Override + public AbstractHttpResponse makeRequest(String method, String path, + Map headers, String payload) throws IOException { + var request = makeRequestBase(baseUri, method, path); + headers.entrySet().forEach(kvp->request.setHeader(kvp.getKey(), kvp.getValue())); + if (payload != null) { + request.setEntity(new StringEntity(payload)); + } + return client.execute(request, fr -> new AbstractHttpResponse() { + @Getter + final byte[] payloadBytes = Optional.ofNullable(fr.getEntity()) + .map(e-> { + try { + return e.getContent().readAllBytes(); + } catch (IOException ex) { + throw Lombok.sneakyThrow(ex); + } + }).orElse(null); + + @Override + public String getStatusText() { + return fr.getReasonPhrase(); + } + + @Override + public int getStatusCode() { + return fr.getCode(); + } + + @Override + public Stream> getHeaders() { + return Arrays.stream(fr.getHeaders()) + .map(h -> new AbstractMap.SimpleEntry<>(h.getName(), h.getValue())); + } + }); + } + + @Override + public void close() throws Exception { + client.close(); + } +} diff --git a/RFS/src/main/java/com/rfs/cms/CmsClient.java b/RFS/src/main/java/com/rfs/cms/CmsClient.java deleted file mode 100644 index 142d572f2..000000000 --- a/RFS/src/main/java/com/rfs/cms/CmsClient.java +++ /dev/null @@ -1,128 +0,0 @@ -package com.rfs.cms; - -import java.util.List; -import java.util.Optional; - -/* - * Client to connect to and work with the Coordinating Metadata Store. The CMS could be implemented by any reasonable - * data store option (Postgres, AWS DynamoDB, Elasticsearch/Opensearch, etc). - */ -public interface CmsClient { - /* - * Creates a new entry in the CMS for the Snapshot's progress. Returns an Optional; if the document was created, it - * will be the created object and empty otherwise. - */ - public Optional createSnapshotEntry(String snapshotName); - - /* - * Attempt to retrieve the Snapshot entry from the CMS. Returns an Optional; if the document exists, it will be the - * retrieved entry and empty otherwise. - */ - public Optional getSnapshotEntry(String snapshotName); - - /* - * Updates the Snapshot entry in the CMS. Returns an Optional; if the document was updated, it will be - * the updated entry and empty otherwise. - */ - public Optional updateSnapshotEntry(CmsEntry.Snapshot newEntry, CmsEntry.Snapshot lastEntry); - - /* - * Creates a new entry in the CMS for the Metadata Migration's progress. Returns an Optional; if the document was - * created, it will be the created entry and empty otherwise. - */ - public Optional createMetadataEntry(); - - /* - * Attempt to retrieve the Metadata Migration entry from the CMS, if it exists. Returns an Optional; if the document - * exists, it will be the retrieved entry and empty otherwise. - */ - public Optional getMetadataEntry(); - - /* - * Updates the Metadata Migration entry in the CMS. Returns an Optional; if the document was updated, - * it will be the updated entry and empty otherwise. - */ - public Optional updateMetadataEntry(CmsEntry.Metadata newEntry, CmsEntry.Metadata lastEntry); - - /* - * Creates a new entry in the CMS for the Index Migration's progress. Returns an Optional; if the document was - * created, it will be the created entry and empty otherwise. - */ - public Optional createIndexEntry(); - - /* - * Attempt to retrieve the Index Migration entry from the CMS, if it exists. Returns an Optional; if the document - * exists, it will be the retrieved entry and empty otherwise. - */ - public Optional getIndexEntry(); - - /* - * Updates the Index Migration entry in the CMS. Returns an Optional; if the document was updated, - * it will be the updated entry and empty otherwise. - */ - public Optional updateIndexEntry(CmsEntry.Index newEntry, CmsEntry.Index lastEntry); - - /* - * Creates a new entry in the CMS for an Index Work Item. Returns an Optional; if the document was - * created, it will be the created entry and empty otherwise. - */ - public Optional createIndexWorkItem(String name, int numShards); - - /* - * Updates the Index Work Item in the CMS. Returns an Optional; if the document was updated, - * it will be the updated entry and empty otherwise. - */ - public Optional updateIndexWorkItem(CmsEntry.IndexWorkItem newEntry, CmsEntry.IndexWorkItem lastEntry); - - /* - * Forcefully updates the Index Work Item in the CMS. This method should be used when you don't care about collisions - * and just want to overwrite the existing entry no matter what. Returns the updated entry. - */ - public CmsEntry.IndexWorkItem updateIndexWorkItemForceful(CmsEntry.IndexWorkItem newEntry); - - /* - * Retrieves a set of Index Work Items from the CMS that appear ready to be worked on, up to the specified limit. - */ - public List getAvailableIndexWorkItems(int maxItems); - - /* - * Creates a new entry in the CMS for the Documents Migration's progress. Returns an Optional; if the document was - * created, it will be the created entry and empty otherwise. - */ - public Optional createDocumentsEntry(); - - /* - * Attempt to retrieve the Documents Migration entry from the CMS, if it exists. Returns an Optional; if the document - * exists, it will be the retrieved entry and empty otherwise. - */ - public Optional getDocumentsEntry(); - - /* - * Updates the Documents Migration entry in the CMS. Returns an Optional; if the document was updated, - * it will be the updated entry and empty otherwise. - */ - public Optional updateDocumentsEntry(CmsEntry.Documents newEntry, CmsEntry.Documents lastEntry); - - /* - * Creates a new entry in the CMS for an Documents Work Item. Returns an Optional; if the document was - * created, it will be the created entry and empty otherwise. - */ - public Optional createDocumentsWorkItem(String indexName, int shardId); - - /* - * Updates the Documents Work Item in the CMS. Returns an Optional; if the document was updated, - * it will be the updated entry and empty otherwise. - */ - public Optional updateDocumentsWorkItem(CmsEntry.DocumentsWorkItem newEntry, CmsEntry.DocumentsWorkItem lastEntry); - - /* - * Forcefully updates the Documents Work Item in the CMS. This method should be used when you don't care about collisions - * and just want to overwrite the existing entry no matter what. Returns the updated entry. - */ - public CmsEntry.DocumentsWorkItem updateDocumentsWorkItemForceful(CmsEntry.DocumentsWorkItem newEntry); - - /* - * Retrieves a Documents Work Items from the CMS that appears ready to be worked on. - */ - public Optional getAvailableDocumentsWorkItem(); -} diff --git a/RFS/src/main/java/com/rfs/cms/CmsEntry.java b/RFS/src/main/java/com/rfs/cms/CmsEntry.java deleted file mode 100644 index 7debaddbf..000000000 --- a/RFS/src/main/java/com/rfs/cms/CmsEntry.java +++ /dev/null @@ -1,245 +0,0 @@ -package com.rfs.cms; - - -import com.rfs.common.RfsException; - -import lombok.RequiredArgsConstructor; - -public class CmsEntry { - public static enum EntryType { - SNAPSHOT, - METADATA, - INDEX, - INDEX_WORK_ITEM, - DOCUMENTS, - DOCUMENTS_WORK_ITEM - } - - public abstract static class Base { - protected Base() {} - - // Implementations of this method should provide a string version of the object that fully represents its contents - public abstract String toRepresentationString(); - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null || !(obj instanceof Base)) { - return false; - } - Base other = (Base) obj; - return this.toRepresentationString().equals(other.toRepresentationString()); - } - - @Override - public int hashCode() { - return this.toRepresentationString().hashCode(); - } - } - - /* - * Provides a base class for leasable entry types. Doesn't allow for customization of lease mechanics, but it's - * unclear how to achieve that in Java given the constraints around static methods. - */ - public abstract static class Leasable extends Base { - public static final int LEASE_MS = 1 * 60 * 1000; // 1 minute, arbitrarily chosen - public static final int MAX_ATTEMPTS = 3; // arbitrarily chosen - - protected Leasable() {} - - public static int getLeaseDurationMs(int numAttempts) { - if (numAttempts > MAX_ATTEMPTS) { - throw new CouldNotGenerateNextLeaseDuration("numAttempts=" + numAttempts + " is greater than MAX_ATTEMPTS=" + MAX_ATTEMPTS); - } else if (numAttempts < 1) { - throw new CouldNotGenerateNextLeaseDuration("numAttempts=" + numAttempts + " is less than 1"); - } - return LEASE_MS * numAttempts; // Arbitratily chosen algorithm - } - - // TODO: We should be ideally setting the lease expiry using the server's clock, but it's unclear on the best - // way to do this. For now, we'll just use the client's clock. - public static String getLeaseExpiry(long currentTime, int numAttempts) { - return Long.toString(currentTime + getLeaseDurationMs(numAttempts)); - } - } - - public static enum SnapshotStatus { - NOT_STARTED, - IN_PROGRESS, - COMPLETED, - FAILED, - } - - /* - * Used to track the progress of taking a snapshot of the source cluster - */ - @RequiredArgsConstructor - public static class Snapshot extends Base { - public final EntryType type = EntryType.SNAPSHOT; - public final String name; - public final SnapshotStatus status; - - @Override - public String toRepresentationString() { - return "Snapshot(" - + "type='" + type.toString() + "," - + "name='" + name + "," - + "status=" + status.toString() + - ")"; - } - } - - public static enum MetadataStatus { - IN_PROGRESS, - COMPLETED, - FAILED, - } - - /* - * Used to track the progress of migrating all the templates from the source cluster - */ - @RequiredArgsConstructor - public static class Metadata extends Leasable { - public final EntryType type = EntryType.METADATA; - public final MetadataStatus status; - public final String leaseExpiry; - public final Integer numAttempts; - - @Override - public String toRepresentationString() { - return "Metadata(" - + "type='" + type.toString() + "," - + "status=" + status.toString() + "," - + "leaseExpiry=" + leaseExpiry + "," - + "numAttempts=" + numAttempts.toString() + - ")"; - } - } - - public static enum IndexStatus { - SETUP, - IN_PROGRESS, - COMPLETED, - FAILED, - } - - /* - * Used to track the progress of migrating all the indices from the soruce cluster - */ - @RequiredArgsConstructor - public static class Index extends Leasable { - public final EntryType type = EntryType.INDEX; - public final IndexStatus status; - public final String leaseExpiry; - public final Integer numAttempts; - - @Override - public String toRepresentationString() { - return "Index(" - + "type='" + type.toString() + "," - + "status=" + status.toString() + "," - + "leaseExpiry=" + leaseExpiry + "," - + "numAttempts=" + numAttempts.toString() + - ")"; - } - } - - public static enum IndexWorkItemStatus { - NOT_STARTED, - COMPLETED, - FAILED, - } - - /* - * Used to track the migration of a particular index from the source cluster - */ - @RequiredArgsConstructor - public static class IndexWorkItem extends Base { - public final EntryType type = EntryType.INDEX_WORK_ITEM; - public static final int ATTEMPTS_SOFT_LIMIT = 3; // will make at least this many attempts; arbitrarily chosen - - public final String name; - public final IndexWorkItemStatus status; - public final Integer numAttempts; - public final Integer numShards; - - @Override - public String toRepresentationString() { - return "IndexWorkItem(" - + "type='" + type.toString() + "," - + "name=" + name.toString() + "," - + "status=" + status.toString() + "," - + "numAttempts=" + numAttempts.toString() + "," - + "numShards=" + numShards.toString() + - ")"; - } - } - - public static enum DocumentsStatus { - SETUP, - IN_PROGRESS, - COMPLETED, - FAILED, - } - - /* - * Used to track the progress of migrating all the documents from the soruce cluster - */ - @RequiredArgsConstructor - public static class Documents extends Leasable { - public final EntryType type = EntryType.DOCUMENTS; - public final DocumentsStatus status; - public final String leaseExpiry; - public final Integer numAttempts; - - @Override - public String toRepresentationString() { - return "Documents(" - + "type='" + type.toString() + "," - + "status=" + status.toString() + "," - + "leaseExpiry=" + leaseExpiry + "," - + "numAttempts=" + numAttempts.toString() + - ")"; - } - } - - public static enum DocumentsWorkItemStatus { - NOT_STARTED, - COMPLETED, - FAILED, - } - - /* - * Used to track the migration of a particular index from the source cluster - */ - @RequiredArgsConstructor - public static class DocumentsWorkItem extends Leasable { - public final EntryType type = EntryType.DOCUMENTS_WORK_ITEM; - - public final String indexName; - public final Integer shardId; - public final DocumentsWorkItemStatus status; - public final String leaseExpiry; - public final Integer numAttempts; - - @Override - public String toRepresentationString() { - return "DocumentsWorkItem(" - + "type='" + type.toString() + "," - + "indexName=" + indexName.toString() + "," - + "shardId=" + shardId.toString() + "," - + "status=" + status.toString() + "," - + "leaseExpiry=" + leaseExpiry.toString() + "," - + "numAttempts=" + numAttempts.toString() + - ")"; - } - } - - public static class CouldNotGenerateNextLeaseDuration extends RfsException { - public CouldNotGenerateNextLeaseDuration(String message) { - super("Could not find next lease duration. Reason: " + message); - } - } -} diff --git a/RFS/src/main/java/com/rfs/cms/IWorkCoordinator.java b/RFS/src/main/java/com/rfs/cms/IWorkCoordinator.java new file mode 100644 index 000000000..7af6b7f8e --- /dev/null +++ b/RFS/src/main/java/com/rfs/cms/IWorkCoordinator.java @@ -0,0 +1,155 @@ +package com.rfs.cms; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NonNull; +import lombok.ToString; + +import java.io.IOException; +import java.time.Duration; +import java.time.Instant; + +/** + * Multiple workers can create an instance of this class to coordinate what work each of them + * should be handling. Implementations of this class must be thread-safe, even when the threads + * are run in a distributed environment on different hosts. + * + * This class allows for the creation of work items that can be claimed by a single worker. + * Workers will complete the work or let their leases lapse by default with the passage of time. + * The class guarantees that only one worker may own a lease on a work item at a given time. + * + * Work items being acquired may be specific, or from a pool of unassigned items. The latter is + * used to distribute large quantities of work, with one phase adding many work items that are + * unassigned, followed by workers grabbing (leasing) items from that pool and completing them. + */ +public interface IWorkCoordinator extends AutoCloseable { + + /** + * Initialize any external and internal state so that the subsequent calls will work appropriately. + * This method must be resilient if there are multiple callers and act as if there were only one. + * After this method returns, all of the other methods in this class are valid. There is no way + * to reverse any stateful actions that were performed by setup. This is a one-way function. + * @throws IOException + * @throws InterruptedException + */ + void setup() throws IOException, InterruptedException; + + /** + * @param workItemId - the name of the document/resource to create. + * This value will be used as a key to other methods that update leases and to close work out. + * @return true if the document was created and false if it was already present + * @throws IOException if the document was not successfully create for any other reason + */ + boolean createUnassignedWorkItem(String workItemId) throws IOException; + + /** + * @param workItemId the item that the caller is trying to take ownership of + * @param leaseDuration the initial amount of time that the caller would like to own the lease for. + * Notice if other attempts have been made on this workItem, the lease will be + * greater than the requested amount. + * @return a tuple that contains the expiration time of the lease, at which point, + * this process must completely yield all work on the item + * @throws IOException if there was an error resolving the lease ownership + * @throws LeaseLockHeldElsewhereException if the lease is owned by another process + */ + @NonNull WorkAcquisitionOutcome createOrUpdateLeaseForWorkItem(String workItemId, Duration leaseDuration) + throws IOException; + + /** + * Scan the created work items that have not yet had leases acquired and have not yet finished. + * One of those work items will be returned along with a lease for how long this process may continue + * to work on it. There is no way to extend a lease. After the caller has completed the work, + * completeWorkItem should be called. If completeWorkItem isn't called and the lease expires, the + * caller must ensure that no more work will be undertaken for this work item and the work item + * itself will be leased out to a future caller of acquireNextWorkItem. Each subsequent time that + * a lease is acquired for a work item, the lease period will be doubled. + * @param leaseDuration + * @return + * @throws IOException + * @throws InterruptedException + */ + WorkAcquisitionOutcome acquireNextWorkItem(Duration leaseDuration) throws IOException, InterruptedException; + + /** + * Mark the work item as completed. After this succeeds, the work item will never be leased out + * to any callers. + * @param workItemId + * @throws IOException + */ + void completeWorkItem(String workItemId) throws IOException; + + /** + * @return the number of items that are not yet complete. This will include items with and without claimed leases. + * @throws IOException + * @throws InterruptedException + */ + int numWorkItemsArePending() throws IOException, InterruptedException; + + /** + * @return true if there are any work items that are not yet complete. + * @throws IOException + * @throws InterruptedException + */ + boolean workItemsArePending() throws IOException, InterruptedException; + + + + + /** + * Used as a discriminated union of different outputs that can be returned from acquiring a lease. + * Exceptions are too difficult/unsafe to deal with when going across lambdas and lambdas seem + * critical to glue different components together in a readable fashion. + */ + interface WorkAcquisitionOutcome { + T visit(WorkAcquisitionOutcomeVisitor v) throws IOException; + } + + interface WorkAcquisitionOutcomeVisitor { + T onAlreadyCompleted() throws IOException; + T onNoAvailableWorkToBeDone() throws IOException; + T onAcquiredWork(WorkItemAndDuration workItem) throws IOException; + } + + /** + * This represents that a work item was already completed. + */ + class AlreadyCompleted implements WorkAcquisitionOutcome { + @Override public T visit(WorkAcquisitionOutcomeVisitor v) throws IOException { + return v.onAlreadyCompleted(); + } + } + + /** + * This will occur when some other process is holding a lease and there's no other work item(s) + * available for this proocess to take on given the request. + */ + class NoAvailableWorkToBeDone implements WorkAcquisitionOutcome { + @Override + public T visit(WorkAcquisitionOutcomeVisitor v) throws IOException { + return v.onNoAvailableWorkToBeDone(); + } + } + /** + * This represents when the lease wasn't acquired because another process already owned the + * lease. + */ + class LeaseLockHeldElsewhereException extends RuntimeException { } + + /** + * What's the id of the work item (which is determined by calls to createUnassignedWorkItem or + * createOrUpdateLeaseForWorkItem) and at what time should this worker that has obtained the + * lease need to relinquish control? After the leaseExpirationTime, other processes may be + * able to acquire their own lease on this work item. + */ + @Getter + @AllArgsConstructor + @ToString + class WorkItemAndDuration implements WorkAcquisitionOutcome { + final String workItemId; + final Instant leaseExpirationTime; + @Override public T visit(WorkAcquisitionOutcomeVisitor v) throws IOException { + return v.onAcquiredWork(this); + } + } + +} diff --git a/RFS/src/main/java/com/rfs/cms/LeaseExpireTrigger.java b/RFS/src/main/java/com/rfs/cms/LeaseExpireTrigger.java new file mode 100644 index 000000000..d9b23888f --- /dev/null +++ b/RFS/src/main/java/com/rfs/cms/LeaseExpireTrigger.java @@ -0,0 +1,50 @@ +package com.rfs.cms; + +import io.netty.util.concurrent.DefaultThreadFactory; + +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +/** + * This class takes an expiration time and guarantees that the process will NOT run past that expiration + * time unless the work is marked as complete before that expiration. This class may, but does not need to, + * synchronize its clock with an external source of truth for better accuracy. + */ +public class LeaseExpireTrigger { + private final ScheduledExecutorService scheduledExecutorService; + final ConcurrentHashMap workItemToLeaseMap; + final Consumer onLeaseExpired; + final Clock currentTimeSupplier; + + public LeaseExpireTrigger(Consumer onLeaseExpired) { + this(onLeaseExpired, Clock.systemUTC()); + } + + public LeaseExpireTrigger(Consumer onLeaseExpired, Clock currentTimeSupplier) { + scheduledExecutorService = Executors.newScheduledThreadPool(1, + new DefaultThreadFactory("leaseWatchingProcessKillerThread")); + this.workItemToLeaseMap = new ConcurrentHashMap<>(); + this.onLeaseExpired = onLeaseExpired; + this.currentTimeSupplier = currentTimeSupplier; + } + + public void registerExpiration(String workItemId, Instant killTime) { + workItemToLeaseMap.put(workItemId, killTime); + final var killDuration = Duration.between(currentTimeSupplier.instant(), killTime); + scheduledExecutorService.schedule(()->{ + if (workItemToLeaseMap.containsKey(workItemId)) { + onLeaseExpired.accept(workItemId); + } + }, killDuration.toSeconds(), TimeUnit.SECONDS); + } + + public void markWorkAsCompleted(String workItemId) { + workItemToLeaseMap.remove(workItemId); + } +} diff --git a/RFS/src/main/java/com/rfs/cms/OpenSearchCmsClient.java b/RFS/src/main/java/com/rfs/cms/OpenSearchCmsClient.java deleted file mode 100644 index 57697ccd8..000000000 --- a/RFS/src/main/java/com/rfs/cms/OpenSearchCmsClient.java +++ /dev/null @@ -1,317 +0,0 @@ -package com.rfs.cms; - -import java.util.List; -import java.util.Optional; -import java.util.stream.Collectors; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import com.fasterxml.jackson.databind.node.ObjectNode; -import com.rfs.common.OpenSearchClient; -import com.rfs.common.RfsException; - -public class OpenSearchCmsClient implements CmsClient { - private static final Logger logger = LogManager.getLogger(OpenSearchCmsClient.class); - - public static final String CMS_INDEX_NAME = "cms-reindex-from-snapshot"; - public static final String CMS_SNAPSHOT_DOC_ID = "snapshot_status"; - public static final String CMS_METADATA_DOC_ID = "metadata_status"; - public static final String CMS_INDEX_DOC_ID = "index_status"; - public static final String CMS_DOCUMENTS_DOC_ID = "documents_status"; - - public static String getIndexWorkItemDocId(String name) { - // iwi => index work item - return "iwi_" + name; - } - - public static String getDocumentsWorkItemDocId(String indexName, int shardId) { - // dwi => documents work item - return "dwi_" + indexName + "_" + shardId; - } - - private final OpenSearchClient client; - - public OpenSearchCmsClient(OpenSearchClient client) { - this.client = client; - } - - @Override - public Optional createSnapshotEntry(String snapshotName) { - OpenSearchCmsEntry.Snapshot newEntry = OpenSearchCmsEntry.Snapshot.getInitial(snapshotName); - Optional createdEntry = client.createDocument(CMS_INDEX_NAME, CMS_SNAPSHOT_DOC_ID, newEntry.toJson()); - return createdEntry.map(OpenSearchCmsEntry.Snapshot::fromJson); - } - - @Override - public Optional getSnapshotEntry(String snapshotName) { - Optional document = client.getDocument(CMS_INDEX_NAME, CMS_SNAPSHOT_DOC_ID); - return document.map(doc -> (ObjectNode) doc.get("_source")) - .map(OpenSearchCmsEntry.Snapshot::fromJson); - } - - @Override - public Optional updateSnapshotEntry(CmsEntry.Snapshot newEntry, CmsEntry.Snapshot lastEntry) { - // Pull the existing entry to ensure that it hasn't changed since we originally retrieved it - ObjectNode currentEntryRaw = client.getDocument(CMS_INDEX_NAME, CMS_SNAPSHOT_DOC_ID) - .orElseThrow(() -> new RfsException("Failed to update snapshot entry: " + CMS_SNAPSHOT_DOC_ID + " does not exist")); - - OpenSearchCmsEntry.Snapshot currentEntry = OpenSearchCmsEntry.Snapshot.fromJson((ObjectNode) currentEntryRaw.get("_source")); - if (!currentEntry.equals(new OpenSearchCmsEntry.Snapshot(lastEntry))) { - logger.info("Failed to update snapshot entry: " + CMS_SNAPSHOT_DOC_ID + " has changed we first retrieved it"); - return Optional.empty(); - } - - // Now attempt the update - ObjectNode newEntryJson = new OpenSearchCmsEntry.Snapshot(newEntry).toJson(); - Optional updatedEntry = client.updateDocument(CMS_INDEX_NAME, CMS_SNAPSHOT_DOC_ID, newEntryJson, currentEntryRaw); - return updatedEntry.map(OpenSearchCmsEntry.Snapshot::fromJson); - } - - @Override - public Optional createMetadataEntry() { - OpenSearchCmsEntry.Metadata entry = OpenSearchCmsEntry.Metadata.getInitial(); - Optional createdEntry = client.createDocument(CMS_INDEX_NAME, CMS_METADATA_DOC_ID, entry.toJson()); - return createdEntry.map(OpenSearchCmsEntry.Metadata::fromJson); - - } - - @Override - public Optional getMetadataEntry() { - Optional document = client.getDocument(CMS_INDEX_NAME, CMS_METADATA_DOC_ID); - return document.map(doc -> (ObjectNode) doc.get("_source")) - .map(OpenSearchCmsEntry.Metadata::fromJson); - } - - @Override - public Optional updateMetadataEntry(CmsEntry.Metadata newEntry, CmsEntry.Metadata lastEntry) { - // Pull the existing entry to ensure that it hasn't changed since we originally retrieved it - ObjectNode currentEntryRaw = client.getDocument(CMS_INDEX_NAME, CMS_METADATA_DOC_ID) - .orElseThrow(() -> new RfsException("Failed to update metadata entry: " + CMS_METADATA_DOC_ID + " does not exist")); - - OpenSearchCmsEntry.Metadata currentEntry = OpenSearchCmsEntry.Metadata.fromJson((ObjectNode) currentEntryRaw.get("_source")); - if (!currentEntry.equals(new OpenSearchCmsEntry.Metadata(lastEntry))) { - logger.info("Failed to update metadata entry: " + CMS_METADATA_DOC_ID + " has changed we first retrieved it"); - return Optional.empty(); - } - - // Now attempt the update - ObjectNode newEntryJson = new OpenSearchCmsEntry.Metadata(newEntry).toJson(); - Optional updatedEntry = client.updateDocument(CMS_INDEX_NAME, CMS_METADATA_DOC_ID, newEntryJson, currentEntryRaw); - return updatedEntry.map(OpenSearchCmsEntry.Metadata::fromJson); - } - - @Override - public Optional createIndexEntry() { - OpenSearchCmsEntry.Index entry = OpenSearchCmsEntry.Index.getInitial(); - Optional createdEntry = client.createDocument(CMS_INDEX_NAME, CMS_INDEX_DOC_ID, entry.toJson()); - return createdEntry.map(OpenSearchCmsEntry.Index::fromJson); - - } - - @Override - public Optional getIndexEntry() { - Optional document = client.getDocument(CMS_INDEX_NAME, CMS_INDEX_DOC_ID); - return document.map(doc -> (ObjectNode) doc.get("_source")) - .map(OpenSearchCmsEntry.Index::fromJson); - } - - @Override - public Optional updateIndexEntry(CmsEntry.Index newEntry, CmsEntry.Index lastEntry) { - // Pull the existing entry to ensure that it hasn't changed since we originally retrieved it - ObjectNode currentEntryRaw = client.getDocument(CMS_INDEX_NAME, CMS_INDEX_DOC_ID) - .orElseThrow(() -> new RfsException("Failed to update index entry: " + CMS_INDEX_DOC_ID + " does not exist")); - - OpenSearchCmsEntry.Index currentEntry = OpenSearchCmsEntry.Index.fromJson((ObjectNode) currentEntryRaw.get("_source")); - if (!currentEntry.equals(new OpenSearchCmsEntry.Index(lastEntry))) { - logger.info("Failed to update index entry: " + CMS_INDEX_DOC_ID + " has changed we first retrieved it"); - return Optional.empty(); - } - - // Now attempt the update - ObjectNode newEntryJson = new OpenSearchCmsEntry.Index(newEntry).toJson(); - Optional updatedEntry = client.updateDocument(CMS_INDEX_NAME, CMS_INDEX_DOC_ID, newEntryJson, currentEntryRaw); - return updatedEntry.map(OpenSearchCmsEntry.Index::fromJson); - } - - @Override - public Optional createIndexWorkItem(String name, int numShards) { - OpenSearchCmsEntry.IndexWorkItem entry = OpenSearchCmsEntry.IndexWorkItem.getInitial(name, numShards); - Optional createdEntry = client.createDocument(CMS_INDEX_NAME, getIndexWorkItemDocId(entry.name), entry.toJson()); - return createdEntry.map(OpenSearchCmsEntry.IndexWorkItem::fromJson); - } - - @Override - public Optional updateIndexWorkItem(CmsEntry.IndexWorkItem newEntry, CmsEntry.IndexWorkItem lastEntry) { - // Pull the existing entry to ensure that it hasn't changed since we originally retrieved it - ObjectNode currentEntryRaw = client.getDocument(CMS_INDEX_NAME, getIndexWorkItemDocId(lastEntry.name)) - .orElseThrow(() -> new RfsException("Failed to update index work item: " + getIndexWorkItemDocId(lastEntry.name) + " does not exist")); - - OpenSearchCmsEntry.IndexWorkItem currentEntry = OpenSearchCmsEntry.IndexWorkItem.fromJson((ObjectNode) currentEntryRaw.get("_source")); - if (!currentEntry.equals(new OpenSearchCmsEntry.IndexWorkItem(lastEntry))) { - logger.info("Failed to update index work item: " + getIndexWorkItemDocId(lastEntry.name) + " has changed we first retrieved it"); - return Optional.empty(); - } - - // Now attempt the update - ObjectNode newEntryJson = new OpenSearchCmsEntry.IndexWorkItem(newEntry).toJson(); - Optional updatedEntry = client.updateDocument(CMS_INDEX_NAME, getIndexWorkItemDocId(newEntry.name), newEntryJson, currentEntryRaw); - return updatedEntry.map(OpenSearchCmsEntry.IndexWorkItem::fromJson); - } - - @Override - public CmsEntry.IndexWorkItem updateIndexWorkItemForceful(CmsEntry.IndexWorkItem newEntry) { - // Now attempt the update - ObjectNode newEntryJson = new OpenSearchCmsEntry.IndexWorkItem(newEntry).toJson(); - ObjectNode updatedEntry = client.updateDocumentForceful(CMS_INDEX_NAME, getIndexWorkItemDocId(newEntry.name), newEntryJson); - return OpenSearchCmsEntry.IndexWorkItem.fromJson(updatedEntry); - } - - @Override - public List getAvailableIndexWorkItems(int maxItems) { - // Ensure we have a relatively fresh view of the index - client.refresh(); - - // Pull the docs - String queryBody = "{\n" + - " \"query\": {\n" + - " \"function_score\": {\n" + - " \"query\": {\n" + - " \"bool\": {\n" + - " \"must\": [\n" + - " {\n" + - " \"match\": {\n" + - " \"type\": \"INDEX_WORK_ITEM\"\n" + - " }\n" + - " },\n" + - " {\n" + - " \"match\": {\n" + - " \"status\": \"NOT_STARTED\"\n" + - " }\n" + - " }\n" + - " ]\n" + - " }\n" + - " },\n" + - " \"random_score\": {}\n" + // Try to avoid the workers fighting for the same work items - " }\n" + - " },\n" + - " \"size\": " + maxItems + "\n" + - "}"; - - List hits = client.searchDocuments(CMS_INDEX_NAME, queryBody); - List workItems = hits.stream() - .map(hit -> (ObjectNode) hit.get("_source")) - .map(OpenSearchCmsEntry.IndexWorkItem::fromJson) - .collect(Collectors.toList()); - - return workItems; - } - - @Override - public Optional createDocumentsEntry() { - OpenSearchCmsEntry.Documents entry = OpenSearchCmsEntry.Documents.getInitial(); - Optional createdEntry = client.createDocument(CMS_INDEX_NAME, CMS_DOCUMENTS_DOC_ID, entry.toJson()); - return createdEntry.map(OpenSearchCmsEntry.Documents::fromJson); - - } - - @Override - public Optional getDocumentsEntry() { - Optional document = client.getDocument(CMS_INDEX_NAME, CMS_DOCUMENTS_DOC_ID); - return document.map(doc -> (ObjectNode) doc.get("_source")) - .map(OpenSearchCmsEntry.Documents::fromJson); - } - - @Override - public Optional updateDocumentsEntry(CmsEntry.Documents newEntry, CmsEntry.Documents lastEntry) { - // Pull the existing entry to ensure that it hasn't changed since we originally retrieved it - ObjectNode currentEntryRaw = client.getDocument(CMS_INDEX_NAME, CMS_DOCUMENTS_DOC_ID) - .orElseThrow(() -> new RfsException("Failed to update documents entry: " + CMS_DOCUMENTS_DOC_ID + " does not exist")); - - OpenSearchCmsEntry.Documents currentEntry = OpenSearchCmsEntry.Documents.fromJson((ObjectNode) currentEntryRaw.get("_source")); - if (!currentEntry.equals(new OpenSearchCmsEntry.Documents(lastEntry))) { - logger.info("Failed to documents index entry: " + CMS_DOCUMENTS_DOC_ID + " has changed we first retrieved it"); - return Optional.empty(); - } - - // Now attempt the update - ObjectNode newEntryJson = new OpenSearchCmsEntry.Documents(newEntry).toJson(); - Optional updatedEntry = client.updateDocument(CMS_INDEX_NAME, CMS_DOCUMENTS_DOC_ID, newEntryJson, currentEntryRaw); - return updatedEntry.map(OpenSearchCmsEntry.Documents::fromJson); - } - - @Override - public Optional createDocumentsWorkItem(String indexName, int shardId) { - OpenSearchCmsEntry.DocumentsWorkItem entry = OpenSearchCmsEntry.DocumentsWorkItem.getInitial(indexName, shardId); - Optional createdEntry = client.createDocument(CMS_INDEX_NAME, getDocumentsWorkItemDocId(indexName, shardId), entry.toJson()); - return createdEntry.map(OpenSearchCmsEntry.DocumentsWorkItem::fromJson); - } - - @Override - public Optional updateDocumentsWorkItem(CmsEntry.DocumentsWorkItem newEntry, CmsEntry.DocumentsWorkItem lastEntry) { - String docId = getDocumentsWorkItemDocId(lastEntry.indexName, lastEntry.shardId); - - // Pull the existing entry to ensure that it hasn't changed since we originally retrieved it - ObjectNode currentEntryRaw = client.getDocument(CMS_INDEX_NAME, docId) - .orElseThrow(() -> new RfsException("Failed to update documents work item: " + docId + " does not exist")); - - OpenSearchCmsEntry.DocumentsWorkItem currentEntry = OpenSearchCmsEntry.DocumentsWorkItem.fromJson((ObjectNode) currentEntryRaw.get("_source")); - if (!currentEntry.equals(new OpenSearchCmsEntry.DocumentsWorkItem(lastEntry))) { - logger.info("Failed to update documents work item: " + docId + " has changed we first retrieved it"); - return Optional.empty(); - } - - // Now attempt the update - ObjectNode newEntryJson = new OpenSearchCmsEntry.DocumentsWorkItem(newEntry).toJson(); - Optional updatedEntry = client.updateDocument(CMS_INDEX_NAME, docId, newEntryJson, currentEntryRaw); - return updatedEntry.map(OpenSearchCmsEntry.DocumentsWorkItem::fromJson); - } - - @Override - public CmsEntry.DocumentsWorkItem updateDocumentsWorkItemForceful(CmsEntry.DocumentsWorkItem newEntry) { - // Now attempt the update - ObjectNode newEntryJson = new OpenSearchCmsEntry.DocumentsWorkItem(newEntry).toJson(); - ObjectNode updatedEntry = client.updateDocumentForceful(CMS_INDEX_NAME, getDocumentsWorkItemDocId(newEntry.indexName, newEntry.shardId), newEntryJson); - return OpenSearchCmsEntry.DocumentsWorkItem.fromJson(updatedEntry); - } - - @Override - public Optional getAvailableDocumentsWorkItem() { - // Ensure we have a relatively fresh view of the index - client.refresh(); - - // Pull the docs - String queryBody = "{\n" + - " \"query\": {\n" + - " \"function_score\": {\n" + - " \"query\": {\n" + - " \"bool\": {\n" + - " \"must\": [\n" + - " {\n" + - " \"match\": {\n" + - " \"type\": \"DOCUMENTS_WORK_ITEM\"\n" + - " }\n" + - " },\n" + - " {\n" + - " \"match\": {\n" + - " \"status\": \"NOT_STARTED\"\n" + - " }\n" + - " }\n" + - " ]\n" + - " }\n" + - " },\n" + - " \"random_score\": {}\n" + // Try to avoid the workers fighting for the same work items - " }\n" + - " },\n" + - " \"size\": 1\n" + // At most one result - "}"; - - List hits = client.searchDocuments(CMS_INDEX_NAME, queryBody); - List workItems = hits.stream() - .map(hit -> (ObjectNode) hit.get("_source")) - .map(OpenSearchCmsEntry.DocumentsWorkItem::fromJson) - .collect(Collectors.toList()); - - return workItems.isEmpty() ? Optional.empty() : Optional.of(workItems.get(0)); - } -} diff --git a/RFS/src/main/java/com/rfs/cms/OpenSearchCmsEntry.java b/RFS/src/main/java/com/rfs/cms/OpenSearchCmsEntry.java deleted file mode 100644 index 2be150867..000000000 --- a/RFS/src/main/java/com/rfs/cms/OpenSearchCmsEntry.java +++ /dev/null @@ -1,336 +0,0 @@ -package com.rfs.cms; - -import java.time.Instant; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ObjectNode; -import com.rfs.common.RfsException; - -public class OpenSearchCmsEntry { - private static final ObjectMapper objectMapper = new ObjectMapper(); - - public static class Snapshot extends CmsEntry.Snapshot { - public static final String FIELD_TYPE = "type"; - public static final String FIELD_NAME = "name"; - public static final String FIELD_STATUS = "status"; - - public static Snapshot getInitial(String name) { - return new Snapshot(name, CmsEntry.SnapshotStatus.NOT_STARTED); - } - - public static Snapshot fromJson(ObjectNode node) { - try { - return new Snapshot( - node.get(FIELD_STATUS).asText(), - CmsEntry.SnapshotStatus.valueOf(node.get(FIELD_STATUS).asText()) - ); - } catch (Exception e) { - throw new CantParseCmsEntryFromJson(Snapshot.class, node.toString(), e); - } - } - - public Snapshot(String name, CmsEntry.SnapshotStatus status) { - super(name, status); - } - - public Snapshot(CmsEntry.Snapshot entry) { - this(entry.name, entry.status); - } - - public ObjectNode toJson() { - ObjectNode node = objectMapper.createObjectNode(); - node.put(FIELD_TYPE, type.toString()); - node.put(FIELD_STATUS, name); - node.put(FIELD_STATUS, status.toString()); - return node; - } - - @Override - public String toRepresentationString() { - return this.toJson().toString(); - } - } - - public static class Metadata extends CmsEntry.Metadata { - public static final String FIELD_TYPE = "type"; - public static final String FIELD_STATUS = "status"; - public static final String FIELD_LEASE_EXPIRY = "leaseExpiry"; - public static final String FIELD_NUM_ATTEMPTS = "numAttempts"; - - public static Metadata getInitial() { - return new Metadata( - CmsEntry.MetadataStatus.IN_PROGRESS, - // TODO: We should be ideally setting the lease using the server's clock, but it's unclear on the best way - // to do this. For now, we'll just use the client's clock. - CmsEntry.Metadata.getLeaseExpiry(Instant.now().toEpochMilli(), 1), - 1 - ); - } - - public static Metadata fromJson(ObjectNode node) { - try { - return new Metadata( - CmsEntry.MetadataStatus.valueOf(node.get(FIELD_STATUS).asText()), - node.get(FIELD_LEASE_EXPIRY).asText(), - node.get(FIELD_NUM_ATTEMPTS).asInt() - ); - } catch (Exception e) { - throw new CantParseCmsEntryFromJson(Metadata.class, node.toString(), e); - } - } - - public Metadata(CmsEntry.MetadataStatus status, String leaseExpiry, Integer numAttempts) { - super(status, leaseExpiry, numAttempts); - } - - public Metadata(CmsEntry.Metadata entry) { - this(entry.status, entry.leaseExpiry, entry.numAttempts); - } - - public ObjectNode toJson() { - ObjectNode node = objectMapper.createObjectNode(); - node.put(FIELD_TYPE, type.toString()); - node.put(FIELD_STATUS, status.toString()); - node.put(FIELD_LEASE_EXPIRY, leaseExpiry); - node.put(FIELD_NUM_ATTEMPTS, numAttempts); - return node; - } - - @Override - public String toRepresentationString() { - return this.toJson().toString(); - } - } - - public static class Index extends CmsEntry.Index { - public static final String FIELD_TYPE = "type"; - public static final String FIELD_STATUS = "status"; - public static final String FIELD_LEASE_EXPIRY = "leaseExpiry"; - public static final String FIELD_NUM_ATTEMPTS = "numAttempts"; - - public static Index getInitial() { - return new Index( - CmsEntry.IndexStatus.SETUP, - // TODO: We should be ideally setting the lease using the server's clock, but it's unclear on the best way - // to do this. For now, we'll just use the client's clock. - CmsEntry.Index.getLeaseExpiry(Instant.now().toEpochMilli(), 1), - 1 - ); - } - - public static Index fromJson(ObjectNode node) { - try { - return new Index( - CmsEntry.IndexStatus.valueOf(node.get(FIELD_STATUS).asText()), - node.get(FIELD_LEASE_EXPIRY).asText(), - node.get(FIELD_NUM_ATTEMPTS).asInt() - ); - } catch (Exception e) { - throw new CantParseCmsEntryFromJson(Index.class, node.toString(), e); - } - } - - public Index(CmsEntry.IndexStatus status, String leaseExpiry, Integer numAttempts) { - super(status, leaseExpiry, numAttempts); - } - - public Index(CmsEntry.Index entry) { - this(entry.status, entry.leaseExpiry, entry.numAttempts); - } - - public ObjectNode toJson() { - ObjectNode node = objectMapper.createObjectNode(); - node.put(FIELD_TYPE, type.toString()); - node.put(FIELD_STATUS, status.toString()); - node.put(FIELD_LEASE_EXPIRY, leaseExpiry); - node.put(FIELD_NUM_ATTEMPTS, numAttempts); - return node; - } - - @Override - public String toRepresentationString() { - return this.toJson().toString(); - } - } - - public static class IndexWorkItem extends CmsEntry.IndexWorkItem { - public static final String FIELD_TYPE = "type"; - public static final String FIELD_NAME = "name"; - public static final String FIELD_STATUS = "status"; - public static final String FIELD_NUM_ATTEMPTS = "numAttempts"; - public static final String FIELD_NUM_SHARDS = "numShards"; - - public static IndexWorkItem getInitial(String name, int numShards) { - return new IndexWorkItem( - name, - CmsEntry.IndexWorkItemStatus.NOT_STARTED, - 1, - numShards - ); - } - - public static IndexWorkItem fromJson(ObjectNode node) { - try { - return new IndexWorkItem( - node.get(FIELD_NAME).asText(), - CmsEntry.IndexWorkItemStatus.valueOf(node.get(FIELD_STATUS).asText()), - node.get(FIELD_NUM_ATTEMPTS).asInt(), - node.get(FIELD_NUM_SHARDS).asInt() - ); - } catch (Exception e) { - throw new CantParseCmsEntryFromJson(IndexWorkItem.class, node.toString(), e); - } - } - - public IndexWorkItem(String name, CmsEntry.IndexWorkItemStatus status, Integer numAttempts, Integer numShards) { - super(name, status, numAttempts, numShards); - } - - public IndexWorkItem(CmsEntry.IndexWorkItem entry) { - this(entry.name, entry.status, entry.numAttempts, entry.numShards); - } - - public ObjectNode toJson() { - ObjectNode node = objectMapper.createObjectNode(); - node.put(FIELD_TYPE, type.toString()); - node.put(FIELD_NAME, name); - node.put(FIELD_STATUS, status.toString()); - node.put(FIELD_NUM_ATTEMPTS, numAttempts); - node.put(FIELD_NUM_SHARDS, numShards); - return node; - } - - @Override - public String toRepresentationString() { - return this.toJson().toString(); - } - } - - public static class Documents extends CmsEntry.Documents { - public static final String FIELD_TYPE = "type"; - public static final String FIELD_STATUS = "status"; - public static final String FIELD_LEASE_EXPIRY = "leaseExpiry"; - public static final String FIELD_NUM_ATTEMPTS = "numAttempts"; - - public static Documents getInitial() { - return new Documents( - CmsEntry.DocumentsStatus.SETUP, - // TODO: We should be ideally setting the lease using the server's clock, but it's unclear on the best way - // to do this. For now, we'll just use the client's clock. - CmsEntry.Documents.getLeaseExpiry(Instant.now().toEpochMilli(), 1), - 1 - ); - } - - public static Documents fromJson(ObjectNode node) { - try { - return new Documents( - CmsEntry.DocumentsStatus.valueOf(node.get(FIELD_STATUS).asText()), - node.get(FIELD_LEASE_EXPIRY).asText(), - node.get(FIELD_NUM_ATTEMPTS).asInt() - ); - } catch (Exception e) { - throw new CantParseCmsEntryFromJson(Documents.class, node.toString(), e); - } - } - - public Documents(CmsEntry.DocumentsStatus status, String leaseExpiry, Integer numAttempts) { - super(status, leaseExpiry, numAttempts); - } - - public Documents(CmsEntry.Documents entry) { - this(entry.status, entry.leaseExpiry, entry.numAttempts); - } - - public ObjectNode toJson() { - ObjectNode node = objectMapper.createObjectNode(); - node.put(FIELD_TYPE, type.toString()); - node.put(FIELD_STATUS, status.toString()); - node.put(FIELD_LEASE_EXPIRY, leaseExpiry); - node.put(FIELD_NUM_ATTEMPTS, numAttempts); - return node; - } - - @Override - public String toRepresentationString() { - return this.toJson().toString(); - } - } - - public static class DocumentsWorkItem extends CmsEntry.DocumentsWorkItem { - public static final String FIELD_TYPE = "type"; - public static final String FIELD_INDEX_NAME = "indexName"; - public static final String FIELD_SHARD_ID = "shardId"; - public static final String FIELD_STATUS = "status"; - public static final String FIELD_LEASE_EXPIRY = "leaseExpiry"; - public static final String FIELD_NUM_ATTEMPTS = "numAttempts"; - - public static DocumentsWorkItem getInitial(String indexName, int shardId) { - return new DocumentsWorkItem( - indexName, - shardId, - CmsEntry.DocumentsWorkItemStatus.NOT_STARTED, - // TODO: We should be ideally setting the lease using the server's clock, but it's unclear on the best way - // to do this. For now, we'll just use the client's clock. - CmsEntry.Documents.getLeaseExpiry(Instant.now().toEpochMilli(), 1), - 1 - ); - } - - public static DocumentsWorkItem fromJson(ObjectNode node) { - try { - return new DocumentsWorkItem( - node.get(FIELD_INDEX_NAME).asText(), - node.get(FIELD_SHARD_ID).asInt(), - CmsEntry.DocumentsWorkItemStatus.valueOf(node.get(FIELD_STATUS).asText()), - node.get(FIELD_LEASE_EXPIRY).asText(), - node.get(FIELD_NUM_ATTEMPTS).asInt() - ); - } catch (Exception e) { - throw new CantParseCmsEntryFromJson(DocumentsWorkItem.class, node.toString(), e); - } - } - - public DocumentsWorkItem(String indexName, int shardId, CmsEntry.DocumentsWorkItemStatus status, String leaseExpiry, int numAttempts) { - super(indexName, shardId, status, leaseExpiry, numAttempts); - } - - public DocumentsWorkItem(CmsEntry.DocumentsWorkItem entry) { - this(entry.indexName, entry.shardId, entry.status, entry.leaseExpiry, entry.numAttempts); - } - - public ObjectNode toJson() { - ObjectNode node = objectMapper.createObjectNode(); - node.put(FIELD_TYPE, type.toString()); - node.put(FIELD_INDEX_NAME, indexName); - node.put(FIELD_SHARD_ID, shardId); - node.put(FIELD_STATUS, status.toString()); - node.put(FIELD_LEASE_EXPIRY, leaseExpiry); - node.put(FIELD_NUM_ATTEMPTS, numAttempts); - return node; - } - - @Override - public String toRepresentationString() { - return this.toJson().toString(); - } - } - - - - - - - - - - - - - - public static class CantParseCmsEntryFromJson extends RfsException { - public CantParseCmsEntryFromJson(Class entryClass, String json, Exception e) { - super("Failed to parse CMS entry of type " + entryClass.getName() + " from JSON: " + json, e); - } - } -} diff --git a/RFS/src/main/java/com/rfs/cms/OpenSearchWorkCoordinator.java b/RFS/src/main/java/com/rfs/cms/OpenSearchWorkCoordinator.java new file mode 100644 index 000000000..c9c7a7e40 --- /dev/null +++ b/RFS/src/main/java/com/rfs/cms/OpenSearchWorkCoordinator.java @@ -0,0 +1,517 @@ +package com.rfs.cms; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.AllArgsConstructor; +import lombok.Lombok; +import lombok.NonNull; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.util.Optional; +import java.util.function.BiPredicate; +import java.util.function.Function; +import java.util.function.Supplier; + +@Slf4j +public class OpenSearchWorkCoordinator implements IWorkCoordinator { + public static final String INDEX_NAME = ".migrations_working_state"; + public static final int MAX_RETRIES = 6; // at 100ms, the total delay will be 105s + + public static final String PUT_METHOD = "PUT"; + public static final String POST_METHOD = "POST"; + public static final String GET_METHOD = "GET"; + public static final String HEAD_METHOD = "HEAD"; + + public static final String SCRIPT_VERSION_TEMPLATE = "{SCRIPT_VERSION}"; + public static final String WORKER_ID_TEMPLATE = "{WORKER_ID}"; + public static final String CLIENT_TIMESTAMP_TEMPLATE = "{CLIENT_TIMESTAMP}"; + public static final String EXPIRATION_WINDOW_TEMPLATE = "{EXPIRATION_WINDOW}"; + public static final String CLOCK_DEVIATION_SECONDS_THRESHOLD_TEMPLATE = "{CLOCK_DEVIATION_SECONDS_THRESHOLD}"; + public static final String OLD_EXPIRATION_THRESHOLD_TEMPLATE = "OLD_EXPIRATION_THRESHOLD"; + + public static final String RESULT_OPENSSEARCH_FIELD_NAME = "result"; + public static final String EXPIRATION_FIELD_NAME = "expiration"; + public static final String UPDATED_COUNT_FIELD_NAME = "updated"; + public static final String LEASE_HOLDER_ID_FIELD_NAME = "leaseHolderId"; + public static final String VERSION_CONFLICTS_FIELD_NAME = "version_conflicts"; + public static final String COMPLETED_AT_FIELD_NAME = "completedAt"; + public static final String SOURCE_FIELD_NAME = "_source"; + public static final String ERROR_OPENSEARCH_FIELD_NAME = "error"; + + private final long tolerableClientServerClockDifferenceSeconds; + private final AbstractedHttpClient httpClient; + private final String workerId; + private final ObjectMapper objectMapper; + private final Clock clock; + + public OpenSearchWorkCoordinator(AbstractedHttpClient httpClient, long tolerableClientServerClockDifferenceSeconds, + String workerId) { + this(httpClient, tolerableClientServerClockDifferenceSeconds, workerId, Clock.systemUTC()); + } + + public OpenSearchWorkCoordinator(AbstractedHttpClient httpClient, long tolerableClientServerClockDifferenceSeconds, + String workerId, Clock clock) { + this.tolerableClientServerClockDifferenceSeconds = tolerableClientServerClockDifferenceSeconds; + this.httpClient = httpClient; + this.workerId = workerId; + this.clock = clock; + this.objectMapper = new ObjectMapper(); + } + + @Override + public void close() throws Exception { + httpClient.close(); + } + + public void setup() throws IOException, InterruptedException { + var indexCheckResponse = httpClient.makeJsonRequest(HEAD_METHOD, INDEX_NAME, null, null); + if (indexCheckResponse.getStatusCode() == 200) { + log.info("Not creating " + INDEX_NAME + " because it already exists"); + return; + } + log.atInfo().setMessage("Creating " + INDEX_NAME + " because it's HEAD check returned " + + indexCheckResponse.getStatusCode()).log(); + var body = "{\n" + + " \"settings\": {\n" + + " \"index\": {" + + " \"number_of_shards\": 1,\n" + + " \"number_of_replicas\": 1\n" + + " }\n" + + " },\n" + + " \"mappings\": {\n" + + " \"properties\": {\n" + + " \"" + EXPIRATION_FIELD_NAME + "\": {\n" + + " \"type\": \"long\"\n" + + " },\n" + + " \"" + COMPLETED_AT_FIELD_NAME + "\": {\n" + + " \"type\": \"long\"\n" + + " },\n" + + " \"leaseHolderId\": {\n" + + " \"type\": \"keyword\",\n" + + " \"norms\": false\n" + + " },\n" + + " \"status\": {\n" + + " \"type\": \"keyword\",\n" + + " \"norms\": false\n" + + " }\n" + + " }\n" + + " }\n" + + "}\n"; + + try { + doUntil("setup-" + INDEX_NAME, 100, MAX_RETRIES, + () -> { + try { + return httpClient.makeJsonRequest(PUT_METHOD, INDEX_NAME, null, body); + } catch (Exception e) { + throw Lombok.sneakyThrow(e); + } + }, + r -> new Object() { + @Override + @SneakyThrows + public String toString() { + var payloadStr = Optional.ofNullable(r.getPayloadBytes()) + .map(bytes -> (new String(bytes, StandardCharsets.UTF_8))).orElse("[NULL]"); + return "[ statusCode: " + r.getStatusCode() + ", payload: " + payloadStr + "]"; + } + }, + (response, ignored) -> (response.getStatusCode() / 100) != 2); + } catch (MaxTriesExceededException e) { + throw new IOException(e); + } + } + + enum DocumentModificationResult { + IGNORED, CREATED, UPDATED; + static DocumentModificationResult parse(String s) { + switch (Optional.ofNullable(s).orElse("")/*let default handle this*/) { + case "noop": return DocumentModificationResult.IGNORED; + case "created": return DocumentModificationResult.CREATED; + case UPDATED_COUNT_FIELD_NAME: return DocumentModificationResult.UPDATED; + default: + throw new IllegalArgumentException("Unknown result " + s); + } + } + } + + AbstractedHttpClient.AbstractHttpResponse createOrUpdateLeaseForDocument(String workItemId, + long expirationWindowSeconds) + throws IOException { + // the notion of 'now' isn't supported with painless scripts + // https://www.elastic.co/guide/en/elasticsearch/painless/current/painless-datetime.html#_datetime_now + final var upsertLeaseBodyTemplate = "{\n" + + " \"scripted_upsert\": true,\n" + + " \"upsert\": {\n" + + " \"scriptVersion\": \"" + SCRIPT_VERSION_TEMPLATE + "\",\n" + + " \"" + EXPIRATION_FIELD_NAME + "\": 0,\n" + + " \"creatorId\": \"" + WORKER_ID_TEMPLATE + "\",\n" + + " \"numAttempts\": 0\n" + + " },\n" + + " \"script\": {\n" + + " \"lang\": \"painless\",\n" + + " \"params\": { \n" + + " \"clientTimestamp\": " + CLIENT_TIMESTAMP_TEMPLATE + ",\n" + + " \"expirationWindow\": " + EXPIRATION_WINDOW_TEMPLATE + ",\n" + + " \"workerId\": \"" + WORKER_ID_TEMPLATE + "\"\n" + + " },\n" + + " \"source\": \"" + + " if (ctx._source.scriptVersion != \\\"" + SCRIPT_VERSION_TEMPLATE + "\\\") {" + + " throw new IllegalArgumentException(\\\"scriptVersion mismatch. Not all participants are using the same script: sourceVersion=\\\" + ctx.source.scriptVersion);" + + " } " + + " long serverTimeSeconds = System.currentTimeMillis() / 1000;" + + " if (Math.abs(params.clientTimestamp - serverTimeSeconds) > {CLOCK_DEVIATION_SECONDS_THRESHOLD}) {" + + " throw new IllegalArgumentException(\\\"The current times indicated between the client and server are too different.\\\");" + + " }" + + " long newExpiration = params.clientTimestamp + (((long)Math.pow(2, ctx._source.numAttempts)) * params.expirationWindow);" + + " if (params.expirationWindow > 0 && " + // don't obtain a lease lock + " ctx._source." + COMPLETED_AT_FIELD_NAME + " == null) {" + // already done + " if (ctx._source." + LEASE_HOLDER_ID_FIELD_NAME + " == params.workerId && " + + " ctx._source." + EXPIRATION_FIELD_NAME + " > serverTimeSeconds) {" + // count as an update to force the caller to lookup the expiration time, but no need to modify it + " ctx.op = \\\"update\\\";" + + " } else if (ctx._source." + EXPIRATION_FIELD_NAME + " < serverTimeSeconds && " + // is expired + " ctx._source." + EXPIRATION_FIELD_NAME + " < newExpiration) {" + // sanity check + " ctx._source." + EXPIRATION_FIELD_NAME + " = newExpiration;" + + " ctx._source." + LEASE_HOLDER_ID_FIELD_NAME + " = params.workerId;" + + " ctx._source.numAttempts += 1;" + + " } else {" + + " ctx.op = \\\"noop\\\";" + + " }" + + " } else if (params.expirationWindow != 0) {" + + " ctx.op = \\\"noop\\\";" + + " }" + + "\"\n" + + " }\n" + // close script + "}"; // close top-level + + var body = upsertLeaseBodyTemplate + .replace(SCRIPT_VERSION_TEMPLATE, "poc") + .replace(WORKER_ID_TEMPLATE, workerId) + .replace(CLIENT_TIMESTAMP_TEMPLATE, Long.toString(clock.instant().toEpochMilli() / 1000)) + .replace(EXPIRATION_WINDOW_TEMPLATE, Long.toString(expirationWindowSeconds)) + .replace(CLOCK_DEVIATION_SECONDS_THRESHOLD_TEMPLATE, Long.toString(tolerableClientServerClockDifferenceSeconds)); + + return httpClient.makeJsonRequest(POST_METHOD, INDEX_NAME + "/_update/" + workItemId, + null, body); + } + + DocumentModificationResult getResult(AbstractedHttpClient.AbstractHttpResponse response) throws IOException { + if (response.getStatusCode() == 409) { + return DocumentModificationResult.IGNORED; + } + final var resultDoc = objectMapper.readTree(response.getPayloadStream()); + var resultStr = resultDoc.path(RESULT_OPENSSEARCH_FIELD_NAME).textValue(); + return DocumentModificationResult.parse(resultStr); + } + + @Override + public boolean createUnassignedWorkItem(String workItemId) throws IOException { + var response = createOrUpdateLeaseForDocument(workItemId, 0); + try { + return getResult(response) == DocumentModificationResult.CREATED; + } catch (IllegalArgumentException e) { + log.error("Error parsing resposne: " + response); + throw e; + } + } + + @Override + @NonNull + public WorkAcquisitionOutcome createOrUpdateLeaseForWorkItem(String workItemId, Duration leaseDuration) + throws IOException { + var startTime = Instant.now(); + var updateResponse = createOrUpdateLeaseForDocument(workItemId, leaseDuration.toSeconds()); + var resultFromUpdate = getResult(updateResponse); + + if (resultFromUpdate == DocumentModificationResult.CREATED) { + return new WorkItemAndDuration(workItemId, startTime.plus(leaseDuration)); + } else { + final var httpResponse = httpClient.makeJsonRequest(GET_METHOD, INDEX_NAME + "/_doc/" + workItemId, + null, null); + final var responseDoc = objectMapper.readTree(httpResponse.getPayloadStream()).path(SOURCE_FIELD_NAME); + if (resultFromUpdate == DocumentModificationResult.UPDATED) { + return new WorkItemAndDuration(workItemId, Instant.ofEpochMilli(1000 * + responseDoc.path(EXPIRATION_FIELD_NAME).longValue())); + } else if (!responseDoc.path(COMPLETED_AT_FIELD_NAME).isMissingNode()) { + return new AlreadyCompleted(); + } else if (resultFromUpdate == DocumentModificationResult.IGNORED) { + throw new LeaseLockHeldElsewhereException(); + } else { + throw new IllegalStateException("Unknown result: " + resultFromUpdate); + } + } + } + + public void completeWorkItem(String workItemId) throws IOException { + final var markWorkAsCompleteBodyTemplate = "{\n" + + " \"script\": {\n" + + " \"lang\": \"painless\",\n" + + " \"params\": { \n" + + " \"clientTimestamp\": " + CLIENT_TIMESTAMP_TEMPLATE + ",\n" + + " \"workerId\": \"" + WORKER_ID_TEMPLATE + "\"\n" + + " },\n" + + " \"source\": \"" + + " if (ctx._source.scriptVersion != \\\"" + SCRIPT_VERSION_TEMPLATE + "\\\") {" + + " throw new IllegalArgumentException(\\\"scriptVersion mismatch. Not all participants are using the same script: sourceVersion=\\\" + ctx.source.scriptVersion);" + + " } " + + " if (ctx._source." + LEASE_HOLDER_ID_FIELD_NAME + " != params.workerId) {" + + " throw new IllegalArgumentException(\\\"work item was owned by \\\" + ctx._source." + LEASE_HOLDER_ID_FIELD_NAME + " + \\\" not \\\" + params.workerId);" + + " } else {" + + " ctx._source." + COMPLETED_AT_FIELD_NAME + " = System.currentTimeMillis() / 1000;" + + " }" + + "\"\n" + + " }\n" + + "}"; + + var body = markWorkAsCompleteBodyTemplate + .replace(SCRIPT_VERSION_TEMPLATE, "poc") + .replace(WORKER_ID_TEMPLATE, workerId) + .replace(CLIENT_TIMESTAMP_TEMPLATE, Long.toString(clock.instant().toEpochMilli()/1000)); + + var response = httpClient.makeJsonRequest(POST_METHOD, INDEX_NAME + "/_update/" + workItemId, + null, body); + final var resultStr = objectMapper.readTree(response.getPayloadStream()).get(RESULT_OPENSSEARCH_FIELD_NAME).textValue(); + if (DocumentModificationResult.UPDATED != DocumentModificationResult.parse(resultStr)) { + throw new IllegalStateException("Unexpected response for workItemId: " + workItemId + ". Response: " + + response.toDiagnosticString()); + } + } + + private int numWorkItemsArePending(int maxItemsToCheckFor) throws IOException, InterruptedException { + refresh(); + // TODO: Switch this to use _count + log.warn("Switch this to use _count"); + final var queryBody = "{\n" + + "\"query\": {" + + " \"bool\": {" + + " \"must\": [" + + " { \"exists\":" + + " { \"field\": \"" + EXPIRATION_FIELD_NAME + "\"}" + + " }" + + " ]," + + " \"must_not\": [" + + " { \"exists\":" + + " { \"field\": \"" + COMPLETED_AT_FIELD_NAME + "\"}" + + " }" + + " ]" + + " }" + + "}" + + "}"; + + var path = INDEX_NAME + "/_search" + (maxItemsToCheckFor <= 0 ? "" : "?size=" + maxItemsToCheckFor); + var response = httpClient.makeJsonRequest(POST_METHOD, path, null, queryBody); + + final var resultHitsUpper = objectMapper.readTree(response.getPayloadStream()).path("hits"); + var statusCode = response.getStatusCode(); + if (statusCode != 200) { + throw new IllegalStateException("Querying for pending (expired or not) work, " + + "returned an unexpected status code " + statusCode + " instead of 200"); + } + return resultHitsUpper.path("hits").size(); + } + + @Override + public int numWorkItemsArePending() throws IOException, InterruptedException { + return numWorkItemsArePending(-1); + } + + @Override + public boolean workItemsArePending() throws IOException, InterruptedException { + return numWorkItemsArePending(1) >= 1; + } + + enum UpdateResult { + SUCCESSFUL_ACQUISITION, + VERSION_CONFLICT, + NOTHING_TO_ACQUIRE + } + + /** + * @param expirationWindowSeconds + * @return true if a work item entry was assigned w/ a lease and false otherwise + * @throws IOException if the request couldn't be made + */ + UpdateResult assignOneWorkItem(long expirationWindowSeconds) throws IOException { + // the random_score reduces the number of version conflicts from ~1200 for 40 concurrent requests + // to acquire 40 units of work to around 800 + final var queryUpdateTemplate = "{\n" + + "\"query\": {" + + " \"function_score\": {\n" + + " \"query\": {\n" + + " \"bool\": {" + + " \"must\": [" + + " {" + + " \"range\": {" + + " \"" + EXPIRATION_FIELD_NAME + "\": { \"lt\": " + OLD_EXPIRATION_THRESHOLD_TEMPLATE + " }" + + " }" + + " }" + + " ]," + + " \"must_not\": [" + + " { \"exists\":" + + " { \"field\": \"" + COMPLETED_AT_FIELD_NAME + "\"}" + + " }" + + " ]" + + " }" + + " }," + + " \"random_score\": {},\n" + + " \"boost_mode\": \"replace\"\n" + // Try to avoid the workers fighting for the same work items + " }" + + "}," + + "\"size\": 1,\n" + + "\"script\": {" + + " \"params\": { \n" + + " \"clientTimestamp\": " + CLIENT_TIMESTAMP_TEMPLATE + ",\n" + + " \"expirationWindow\": " + EXPIRATION_WINDOW_TEMPLATE + ",\n" + + " \"workerId\": \"" + WORKER_ID_TEMPLATE + "\",\n" + + " \"counter\": 0\n" + + " },\n" + + " \"source\": \"" + + " if (ctx._source.scriptVersion != \\\"" + SCRIPT_VERSION_TEMPLATE + "\\\") {" + + " throw new IllegalArgumentException(\\\"scriptVersion mismatch. Not all participants are using the same script: sourceVersion=\\\" + ctx.source.scriptVersion);" + + " } " + + " long serverTimeSeconds = System.currentTimeMillis() / 1000;" + + " if (Math.abs(params.clientTimestamp - serverTimeSeconds) > {CLOCK_DEVIATION_SECONDS_THRESHOLD}) {" + + " throw new IllegalArgumentException(\\\"The current times indicated between the client and server are too different.\\\");" + + " }" + + " long newExpiration = params.clientTimestamp + (((long)Math.pow(2, ctx._source.numAttempts)) * params.expirationWindow);" + + " if (ctx._source." + EXPIRATION_FIELD_NAME + " < serverTimeSeconds && " + // is expired + " ctx._source." + EXPIRATION_FIELD_NAME + " < newExpiration) {" + // sanity check + " ctx._source." + EXPIRATION_FIELD_NAME + " = newExpiration;" + + " ctx._source." + LEASE_HOLDER_ID_FIELD_NAME + " = params.workerId;" + + " ctx._source.numAttempts += 1;" + + " }" + + "\" " + // end of source script contents + "}" + // end of script block + "}"; + + final var timestampEpochSeconds = clock.instant().toEpochMilli()/1000; + final var body = queryUpdateTemplate + .replace(SCRIPT_VERSION_TEMPLATE, "poc") + .replace(WORKER_ID_TEMPLATE, workerId) + .replace(CLIENT_TIMESTAMP_TEMPLATE, Long.toString(timestampEpochSeconds)) + .replace(OLD_EXPIRATION_THRESHOLD_TEMPLATE, Long.toString(timestampEpochSeconds+expirationWindowSeconds)) + .replace(EXPIRATION_WINDOW_TEMPLATE, Long.toString(expirationWindowSeconds)) + .replace(CLOCK_DEVIATION_SECONDS_THRESHOLD_TEMPLATE, Long.toString(tolerableClientServerClockDifferenceSeconds)); + + var response = httpClient.makeJsonRequest(POST_METHOD, INDEX_NAME + "/_update_by_query?refresh=true&max_docs=1", + null, body); + var resultTree = objectMapper.readTree(response.getPayloadStream()); + final var numUpdated = resultTree.path(UPDATED_COUNT_FIELD_NAME).longValue(); + assert numUpdated <= 1; + if (numUpdated > 0) { + return UpdateResult.SUCCESSFUL_ACQUISITION; + } else if (resultTree.path(VERSION_CONFLICTS_FIELD_NAME).longValue() > 0) { + return UpdateResult.VERSION_CONFLICT; + } else if (resultTree.path("total").longValue() == 0) { + return UpdateResult.NOTHING_TO_ACQUIRE; + } else { + throw new IllegalStateException("Unexpected response for update: " + resultTree); + } + } + + private WorkItemAndDuration getAssignedWorkItem() throws IOException { + final var queryWorkersAssignedItemsTemplate = "{\n" + + " \"query\": {\n" + + " \"bool\": {" + + " \"must\": [" + + " {" + + " \"term\": { \"" + LEASE_HOLDER_ID_FIELD_NAME + "\": \"" + WORKER_ID_TEMPLATE + "\"}\n" + + " }" + + " ]," + + " \"must_not\": [" + + " {" + + " \"exists\": { \"field\": \"" + COMPLETED_AT_FIELD_NAME + "\"}\n" + + " }" + + " ]" + + " }" + + " }" + + "}"; + final var body = queryWorkersAssignedItemsTemplate.replace(WORKER_ID_TEMPLATE, workerId); + var response = httpClient.makeJsonRequest(POST_METHOD, INDEX_NAME + "/_search", + null, body); + + final var resultHitsUpper = objectMapper.readTree(response.getPayloadStream()).path("hits"); + if (resultHitsUpper.isMissingNode()) { + log.warn("Couldn't find the top level 'hits' field, returning null"); + return null; + } + final var numDocs = resultHitsUpper.path("total").path("value").longValue(); + if (numDocs != 1) { + throw new IllegalStateException("The query for the assigned work document returned " + numDocs + + " instead of one item"); + } + var resultHitInner = resultHitsUpper.path("hits").path(0); + var expiration = resultHitInner.path(SOURCE_FIELD_NAME).path(EXPIRATION_FIELD_NAME).longValue(); + if (expiration == 0) { + log.warn("Expiration wasn't found or wasn't set to > 0. Returning null."); + return null; + } + return new WorkItemAndDuration(resultHitInner.get("_id").asText(), Instant.ofEpochMilli(1000*expiration)); + } + + @AllArgsConstructor + private static class MaxTriesExceededException extends Exception { + Object suppliedValue; + Object transformedValue; + } + + public static U doUntil(String labelThatShouldBeAContext, long initialRetryDelayMs, int maxTries, + Supplier supplier, Function transformer, BiPredicate test) + throws InterruptedException, MaxTriesExceededException + { + var sleepMillis = initialRetryDelayMs; + for (var attempt = 1; ; ++attempt) { + var suppliedVal = supplier.get(); + var transformedVal = transformer.apply(suppliedVal); + if (test.test(suppliedVal, transformedVal)) { + return transformedVal; + } else { + log.atWarn().setMessage(() -> "Retrying " + labelThatShouldBeAContext + + " because the predicate failed for: (" + suppliedVal + "," + transformedVal + ")").log(); + if (attempt >= maxTries) { + throw new MaxTriesExceededException(suppliedVal, transformedVal); + } + Thread.sleep(sleepMillis); + sleepMillis *= 2; + } + } + } + + private void refresh() throws IOException, InterruptedException { + try { + doUntil("refresh", 100, MAX_RETRIES, () -> { + try { + return httpClient.makeJsonRequest(GET_METHOD, INDEX_NAME + "/_refresh",null,null); + } catch (IOException e) { + throw Lombok.sneakyThrow(e); + } + }, + AbstractedHttpClient.AbstractHttpResponse::getStatusCode, (r, statusCode) -> statusCode == 200); + } catch (MaxTriesExceededException e) { + throw new IOException(e); + } + } + + public WorkAcquisitionOutcome acquireNextWorkItem(Duration leaseDuration) throws IOException, InterruptedException { + refresh(); + while (true) { + final var obtainResult = assignOneWorkItem(leaseDuration.toSeconds()); + switch (obtainResult) { + case SUCCESSFUL_ACQUISITION: + return getAssignedWorkItem(); + case NOTHING_TO_ACQUIRE: + return new NoAvailableWorkToBeDone(); + case VERSION_CONFLICT: + continue; + default: + throw new IllegalStateException("unknown result from the assignOneWorkItem: " + obtainResult); + } + } + } + +} \ No newline at end of file diff --git a/RFS/src/main/java/com/rfs/cms/ReactorHttpClient.java b/RFS/src/main/java/com/rfs/cms/ReactorHttpClient.java new file mode 100644 index 000000000..a1eea5474 --- /dev/null +++ b/RFS/src/main/java/com/rfs/cms/ReactorHttpClient.java @@ -0,0 +1,97 @@ +package com.rfs.cms; + +import com.rfs.common.ConnectionDetails; +import com.rfs.common.RestClient; +import io.netty.handler.codec.http.HttpMethod; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import reactor.core.publisher.Mono; +import reactor.netty.ByteBufMono; +import reactor.netty.http.client.HttpClient; +import reactor.netty.http.client.HttpClientResponse; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; +import java.util.Map; +import java.util.function.BiFunction; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +@Slf4j +public class ReactorHttpClient implements AbstractedHttpClient { + + private HttpClient client; + + @Getter + @AllArgsConstructor + public static class Response implements AbstractedHttpClient.AbstractHttpResponse { + List> headersList; + String statusText; + int statusCode; + byte[] payloadBytes; + + @Override + public Stream> getHeaders() { + return headersList.stream(); + } + } + + public ReactorHttpClient(ConnectionDetails connectionDetails) { + this.client = HttpClient.create() + .baseUrl(connectionDetails.url) + .headers(h -> { + h.add("Content-Type", "application/json"); + h.add("User-Agent", "RfsWorker-1.0"); + if (connectionDetails.authType == ConnectionDetails.AuthType.BASIC) { + String credentials = connectionDetails.username + ":" + connectionDetails.password; + String encodedCredentials = Base64.getEncoder().encodeToString(credentials.getBytes()); + h.add("Authorization", "Basic " + encodedCredentials); + } + }); + } + + @Override + public AbstractHttpResponse makeRequest(String method, String path, Map headers, String payload) + throws IOException { + var requestSender = client.request(HttpMethod.valueOf(method)) + .uri("/"+path); + BiFunction> responseWrapperFunction = + (response, bytes) -> { + try { + log.info("Received response with status: " + response.status()); + log.info("Response headers: " + response.responseHeaders().entries()); + + return bytes.asByteArray() + .map(b -> { + try { + log.info("Making wrapped response with status: " + response.status()); + + return new Response(new ArrayList<>(response.responseHeaders().entries()), + response.status().reasonPhrase(), + response.status().code(), + b); + } catch (Exception e) { + log.atError().setCause(e).setMessage("Caught exception").log(); + throw e; + } + }) + .or(Mono.fromSupplier(()-> + new Response(new ArrayList<>(response.responseHeaders().entries()), + response.status().reasonPhrase(), response.status().code(), null))); + } catch (Exception e) { + log.atError().setCause(e).setMessage("Caught exception").log(); + throw e; + } + }; + var monoResponse = payload != null ? + requestSender.send(ByteBufMono.fromString(Mono.just(payload))).responseSingle(responseWrapperFunction) : + requestSender.responseSingle(responseWrapperFunction); + return monoResponse.block(); + } + + @Override + public void close() throws Exception {} +} diff --git a/RFS/src/main/java/com/rfs/cms/ScopedWorkCoordinator.java b/RFS/src/main/java/com/rfs/cms/ScopedWorkCoordinator.java new file mode 100644 index 000000000..42d31242b --- /dev/null +++ b/RFS/src/main/java/com/rfs/cms/ScopedWorkCoordinator.java @@ -0,0 +1,48 @@ +package com.rfs.cms; + +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; + +import java.io.IOException; + +@Slf4j +public class ScopedWorkCoordinator { + + public final IWorkCoordinator workCoordinator; + final LeaseExpireTrigger leaseExpireTrigger; + + public ScopedWorkCoordinator(IWorkCoordinator workCoordinator, LeaseExpireTrigger leaseExpireTrigger) { + this.workCoordinator = workCoordinator; + this.leaseExpireTrigger = leaseExpireTrigger; + } + + public interface WorkItemGetter { + @NonNull IWorkCoordinator.WorkAcquisitionOutcome tryAcquire(IWorkCoordinator wc); + } + + public T ensurePhaseCompletion(WorkItemGetter workItemIdSupplier, + IWorkCoordinator.WorkAcquisitionOutcomeVisitor visitor) throws IOException { + var acquisitionResult = workItemIdSupplier.tryAcquire(workCoordinator); + return acquisitionResult.visit(new IWorkCoordinator.WorkAcquisitionOutcomeVisitor() { + @Override + public T onAlreadyCompleted() throws IOException { + return visitor.onAlreadyCompleted(); + } + + @Override + public T onNoAvailableWorkToBeDone() throws IOException { + return visitor.onNoAvailableWorkToBeDone(); + } + + @Override + public T onAcquiredWork(IWorkCoordinator.WorkItemAndDuration workItem) throws IOException { + var workItemId = workItem.getWorkItemId(); + leaseExpireTrigger.registerExpiration(workItem.workItemId, workItem.leaseExpirationTime); + var rval = visitor.onAcquiredWork(workItem); + workCoordinator.completeWorkItem(workItemId); + leaseExpireTrigger.markWorkAsCompleted(workItemId); + return rval; + } + }); + } +} diff --git a/RFS/src/main/java/com/rfs/common/FileSystemSnapshotCreator.java b/RFS/src/main/java/com/rfs/common/FileSystemSnapshotCreator.java index ea230a560..ac41ae2b2 100644 --- a/RFS/src/main/java/com/rfs/common/FileSystemSnapshotCreator.java +++ b/RFS/src/main/java/com/rfs/common/FileSystemSnapshotCreator.java @@ -6,14 +6,10 @@ public class FileSystemSnapshotCreator extends SnapshotCreator { private static final ObjectMapper mapper = new ObjectMapper(); - private final OpenSearchClient client; - private final String snapshotName; private final String snapshotRepoDirectoryPath; public FileSystemSnapshotCreator(String snapshotName, OpenSearchClient client, String snapshotRepoDirectoryPath) { super(snapshotName, client); - this.snapshotName = snapshotName; - this.client = client; this.snapshotRepoDirectoryPath = snapshotRepoDirectoryPath; } diff --git a/RFS/src/main/java/com/rfs/common/LuceneDocumentsReader.java b/RFS/src/main/java/com/rfs/common/LuceneDocumentsReader.java index ba34be8c6..6d882ae79 100644 --- a/RFS/src/main/java/com/rfs/common/LuceneDocumentsReader.java +++ b/RFS/src/main/java/com/rfs/common/LuceneDocumentsReader.java @@ -2,9 +2,13 @@ import java.io.IOException; import java.nio.file.Path; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; import org.apache.lucene.document.Document; import org.apache.lucene.index.DirectoryReader; import org.apache.lucene.index.IndexReader; @@ -16,34 +20,33 @@ import reactor.core.publisher.Flux; @RequiredArgsConstructor +@Slf4j public class LuceneDocumentsReader { - private static final Logger logger = LogManager.getLogger(LuceneDocumentsReader.class); - protected final Path luceneFilesBasePath; - - public Flux readDocuments(String indexName, int shardId) { - Path indexDirectoryPath = luceneFilesBasePath.resolve(indexName).resolve(String.valueOf(shardId)); + public static final int NUM_DOCUMENTS_BUFFERED = 1024; + protected final Path indexDirectoryPath; + public Flux readDocuments() { return Flux.using( - () -> openIndexReader(indexDirectoryPath), - reader -> { - logger.info(reader.maxDoc() + " documents found in the current Lucene index"); + () -> openIndexReader(indexDirectoryPath), + reader -> { + log.info(reader.maxDoc() + " documents found in the current Lucene index"); - return Flux.range(0, reader.maxDoc()) // Extract all the Documents in the IndexReader - .handle((i, sink) -> { - Document doc = getDocument(reader, i); - if (doc != null) { // Skip malformed docs - sink.next(doc); + return Flux.range(0, reader.maxDoc()) // Extract all the Documents in the IndexReader + .handle((i, sink) -> { + Document doc = getDocument(reader, i); + if (doc != null) { // Skip malformed docs + sink.next(doc); + } + }).cast(Document.class); + }, + reader -> { // Close the IndexReader when done + try { + reader.close(); + } catch (IOException e) { + log.error("Failed to close IndexReader", e); + throw Lombok.sneakyThrow(e); } - }).cast(Document.class); - }, - reader -> { // Close the IndexReader when done - try { - reader.close(); - } catch (IOException e) { - logger.error("Failed to close IndexReader", e); - throw Lombok.sneakyThrow(e); } - } ); } @@ -62,18 +65,18 @@ protected Document getDocument(IndexReader reader, int docId) { StringBuilder errorMessage = new StringBuilder(); errorMessage.append("Unable to parse Document id from Document. The Document's Fields: "); document.getFields().forEach(f -> errorMessage.append(f.name()).append(", ")); - logger.error(errorMessage.toString()); + log.error(errorMessage.toString()); return null; // Skip documents with missing id } if (source_bytes == null || source_bytes.bytes.length == 0) { - logger.warn("Document " + id + " is deleted or doesn't have the _source field enabled"); + log.warn("Document " + id + " is deleted or doesn't have the _source field enabled"); return null; // Skip these too } - logger.debug("Document " + id + " read successfully"); + log.debug("Document " + id + " read successfully"); return document; } catch (Exception e) { - logger.error("Failed to read document at Lucene index location " + docId, e); + log.error("Failed to read document at Lucene index location " + docId, e); return null; } } diff --git a/RFS/src/main/java/com/rfs/common/OpenSearchClient.java b/RFS/src/main/java/com/rfs/common/OpenSearchClient.java index abf3431a5..b28f0bfb6 100644 --- a/RFS/src/main/java/com/rfs/common/OpenSearchClient.java +++ b/RFS/src/main/java/com/rfs/common/OpenSearchClient.java @@ -8,6 +8,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import lombok.NonNull; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -26,6 +27,14 @@ public class OpenSearchClient { public final ConnectionDetails connectionDetails; private final RestClient client; + public OpenSearchClient(@NonNull String url, UsernamePassword p) { + this(url, p == null ? null : p.getUsername(), p == null ? null : p.getPassword(), false); + } + + public OpenSearchClient(@NonNull String url, String username, String password, boolean insecure) { + this(new ConnectionDetails(url, username, password, insecure)); + } + public OpenSearchClient(ConnectionDetails connectionDetails) { this.connectionDetails = connectionDetails; this.client = new RestClient(connectionDetails); @@ -164,137 +173,6 @@ public Optional getSnapshotStatus(String repoName, String snapshotNa } } - /* - * Create a document if it does not already exist. Returns an Optional; if the document was created, it - * will be the created object and empty otherwise. - */ - public Optional createDocument(String indexName, String documentId, ObjectNode body) { - String targetPath = indexName + "/_doc/" + documentId + "?op_type=create"; - RestClient.Response response = client.putAsync(targetPath, body.toString()) - .flatMap(resp -> { - if (resp.code == HttpURLConnection.HTTP_CREATED || resp.code == HttpURLConnection.HTTP_CONFLICT) { - return Mono.just(resp); - } else { - String errorMessage = ("Could not create document: " + indexName + "/" + documentId + ". Response Code: " + resp.code - + ", Response Message: " + resp.message + ", Response Body: " + resp.body); - return Mono.error(new OperationFailed(errorMessage, resp)); - } - }) - .doOnError(e -> logger.error(e.getMessage())) - .retryWhen(Retry.backoff(3, Duration.ofSeconds(1)).maxBackoff(Duration.ofSeconds(10))) - .block(); - if (response.code == HttpURLConnection.HTTP_CREATED) { - return Optional.of(body); - } else { - // The only response code that can end up here is HTTP_CONFLICT, as everything is an error above - // This indicates that the document already exists - return Optional.empty(); - } - } - - /* - * Retrieve a document. Returns an Optional; if the document was found, it will be the document and empty otherwise. - */ - public Optional getDocument(String indexName, String documentId) { - String targetPath = indexName + "/_doc/" + documentId; - RestClient.Response response = client.getAsync(targetPath) - .flatMap(resp -> { - if (resp.code == HttpURLConnection.HTTP_OK || resp.code == HttpURLConnection.HTTP_NOT_FOUND) { - return Mono.just(resp); - } else { - String errorMessage = ("Could not retrieve document: " + indexName + "/" + documentId + ". Response Code: " + resp.code - + ", Response Message: " + resp.message + ", Response Body: " + resp.body); - return Mono.error(new OperationFailed(errorMessage, resp)); - } - }) - .doOnError(e -> logger.error(e.getMessage())) - .retryWhen(Retry.backoff(3, Duration.ofSeconds(1)).maxBackoff(Duration.ofSeconds(10))) - .block(); - - if (response.code == HttpURLConnection.HTTP_OK) { - try { - return Optional.of(objectMapper.readValue(response.body, ObjectNode.class)); - } catch (Exception e) { - String errorMessage = "Could not parse response for: " + indexName + "/" + documentId; - throw new OperationFailed(errorMessage, response); - } - } else if (response.code == HttpURLConnection.HTTP_NOT_FOUND) { - return Optional.empty(); - } else { - String errorMessage = "Should not have gotten here while parsing response for: " + indexName + "/" + documentId; - throw new OperationFailed(errorMessage, response); - } - } - - /* - * Update a document using optimistic locking with the versioning info in an original copy of the doc that is passed in. - * Returns an Optional; if the document was updated, it will be the new value and empty otherwise. - */ - public Optional updateDocument(String indexName, String documentId, ObjectNode newBody, ObjectNode originalCopy) { - String currentSeqNum; - String currentPrimaryTerm; - try { - currentSeqNum = originalCopy.get("_seq_no").asText(); - currentPrimaryTerm = originalCopy.get("_primary_term").asText(); - } catch (Exception e) { - String errorMessage = "Could not update document: " + indexName + "/" + documentId; - throw new RfsException(errorMessage, e); - } - - ObjectNode upsertBody = new ObjectMapper().createObjectNode(); - upsertBody.set("doc", newBody); - - String targetPath = indexName + "/_update/" + documentId + "?if_seq_no=" + currentSeqNum + "&if_primary_term=" + currentPrimaryTerm; - RestClient.Response response = client.postAsync(targetPath, upsertBody.toString()) - .flatMap(resp -> { - if (resp.code == HttpURLConnection.HTTP_OK || resp.code == HttpURLConnection.HTTP_CONFLICT) { - return Mono.just(resp); - } else { - - String errorMessage = ("Could not update document: " + indexName + "/" + documentId + ". Response Code: " + resp.code - + ", Response Message: " + resp.message + ", Response Body: " + resp.body); - return Mono.error(new OperationFailed(errorMessage, resp)); - } - }) - .doOnError(e -> logger.error(e.getMessage())) - .retryWhen(Retry.backoff(3, Duration.ofSeconds(1)).maxBackoff(Duration.ofSeconds(10))) - .block(); - if (response.code == HttpURLConnection.HTTP_OK) { - return Optional.of(newBody); - } else { - // The only response code that can end up here is HTTP_CONFLICT, as everything is an error above - // This indicates that we didn't acquire the optimistic lock - return Optional.empty(); - } - } - - /* - * Update a document forcefully by skipping optimistic locking and overwriting the document regardless of versioning. - * Returns the new body used in the update. - */ - public ObjectNode updateDocumentForceful(String indexName, String documentId, ObjectNode newBody) { - ObjectNode upsertBody = new ObjectMapper().createObjectNode(); - upsertBody.set("doc", newBody); - - String targetPath = indexName + "/_update/" + documentId; - client.postAsync(targetPath, upsertBody.toString()) - .flatMap(resp -> { - if (resp.code == HttpURLConnection.HTTP_OK) { - return Mono.just(resp); - } else { - - String errorMessage = ("Could not update document: " + indexName + "/" + documentId + ". Response Code: " + resp.code - + ", Response Message: " + resp.message + ", Response Body: " + resp.body); - return Mono.error(new OperationFailed(errorMessage, resp)); - } - }) - .doOnError(e -> logger.error(e.getMessage())) - .retryWhen(Retry.backoff(3, Duration.ofSeconds(1)).maxBackoff(Duration.ofSeconds(10))) - .block(); - - return newBody; - } - public Mono sendBulkRequest(String indexName, String body) { String targetPath = indexName + "/_bulk"; @@ -316,39 +194,6 @@ public RestClient.Response refresh() { return client.get(targetPath); } - /* - * Retrieves documents from the specified index with the specified query. Returns a list of the hits. - */ - public List searchDocuments(String indexName, String queryBody) { - String targetPath = indexName + "/_search"; - RestClient.Response response = client.postAsync(targetPath, queryBody.toString()) - .flatMap(resp -> { - if (resp.code == HttpURLConnection.HTTP_OK) { - return Mono.just(resp); - } else { - String errorMessage = ("Could not retrieve documents from index " + indexName + ". Response Code: " + resp.code - + ", Response Message: " + resp.message + ", Response Body: " + resp.body); - return Mono.error(new OperationFailed(errorMessage, resp)); - } - }) - .doOnError(e -> logger.error(e.getMessage())) - .retryWhen(Retry.backoff(3, Duration.ofSeconds(1)).maxBackoff(Duration.ofSeconds(10))) - .block(); - - try { - // Pull the hits out of the surrounding response and return them - ObjectNode responseJson = objectMapper.readValue(response.body, ObjectNode.class); - ArrayNode hits = (ArrayNode) responseJson.get("hits").get("hits"); - - JavaType type = objectMapper.getTypeFactory().constructCollectionType(ArrayList.class, ObjectNode.class); - List docs = objectMapper.convertValue(hits, type); - return docs; - } catch (Exception e) { - String errorMessage = "Could not parse response for: " + indexName; - throw new OperationFailed(errorMessage, response); - } - } - public static class BulkResponse extends RestClient.Response { public BulkResponse(int responseCode, String responseBody, String responseMessage) { super(responseCode, responseBody, responseMessage); @@ -381,12 +226,6 @@ public String getFailureMessage() { } } - public static class UpdateFailed extends RfsException { - public UpdateFailed(String message) { - super(message); - } - } - public static class OperationFailed extends RfsException { public final RestClient.Response response; diff --git a/RFS/src/main/java/com/rfs/common/SnapshotShardUnpacker.java b/RFS/src/main/java/com/rfs/common/SnapshotShardUnpacker.java index 073080e44..3eac5686c 100644 --- a/RFS/src/main/java/com/rfs/common/SnapshotShardUnpacker.java +++ b/RFS/src/main/java/com/rfs/common/SnapshotShardUnpacker.java @@ -36,7 +36,7 @@ public SnapshotShardUnpacker create(ShardMetadata.Data shardMetadata) { } } - public void unpack() { + public Path unpack() { try { // Some constants NativeFSLockFactory lockFactory = NativeFSLockFactory.INSTANCE; @@ -66,6 +66,7 @@ public void unpack() { } } } + return luceneIndexDir; } catch (Exception e) { throw new CouldNotUnpackShard("Could not unpack shard: Index " + shardMetadata.getIndexId() + ", Shard " + shardMetadata.getShardId(), e); } diff --git a/RFS/src/main/java/com/rfs/common/TryHandlePhaseFailure.java b/RFS/src/main/java/com/rfs/common/TryHandlePhaseFailure.java index c8e231748..f217de844 100644 --- a/RFS/src/main/java/com/rfs/common/TryHandlePhaseFailure.java +++ b/RFS/src/main/java/com/rfs/common/TryHandlePhaseFailure.java @@ -1,53 +1,22 @@ package com.rfs.common; -import java.util.Arrays; -import java.util.Optional; - +import lombok.extern.slf4j.Slf4j; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ObjectNode; -import com.rfs.cms.CmsEntry; -import com.rfs.worker.GlobalState; -import com.rfs.worker.Runner; -import com.rfs.worker.WorkerStep; - +@Slf4j public class TryHandlePhaseFailure { - private static final Logger logger = LogManager.getLogger(TryHandlePhaseFailure.class); - @FunctionalInterface - public static interface TryBlock { + public interface TryBlock { void run() throws Exception; } public static void executeWithTryCatch(TryBlock tryBlock) throws Exception { try { tryBlock.run(); - } catch (Runner.PhaseFailed e) { - logPhaseFailureRecord(e.phase, e.nextStep, e.cmsEntry, e.getCause()); - throw e; } catch (Exception e) { - logger.error("Unexpected error running RfsWorker", e); + log.atError().setMessage("Unexpected error running RfsWorker").setCause(e).log(); throw e; } } - - public static void logPhaseFailureRecord(GlobalState.Phase phase, WorkerStep nextStep, Optional cmsEntry, Throwable e) { - ObjectNode errorBlob = new ObjectMapper().createObjectNode(); - errorBlob.put("exceptionMessage", e.getMessage()); - errorBlob.put("exceptionClass", e.getClass().getSimpleName()); - errorBlob.put("exceptionTrace", Arrays.toString(e.getStackTrace())); - - errorBlob.put("phase", phase.toString()); - - String currentStep = (nextStep != null) ? nextStep.getClass().getSimpleName() : "null"; - errorBlob.put("currentStep", currentStep); - - String currentEntry = (cmsEntry.isPresent()) ? cmsEntry.get().toRepresentationString() : "null"; - errorBlob.put("cmsEntry", currentEntry); - - - logger.error(errorBlob.toString()); - } } diff --git a/RFS/src/main/java/com/rfs/common/UsernamePassword.java b/RFS/src/main/java/com/rfs/common/UsernamePassword.java new file mode 100644 index 000000000..b3333bc9e --- /dev/null +++ b/RFS/src/main/java/com/rfs/common/UsernamePassword.java @@ -0,0 +1,11 @@ +package com.rfs.common; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class UsernamePassword { + String username; + String password; +} diff --git a/RFS/src/main/java/com/rfs/worker/DocumentsRunner.java b/RFS/src/main/java/com/rfs/worker/DocumentsRunner.java index 758236e55..2ace29632 100644 --- a/RFS/src/main/java/com/rfs/worker/DocumentsRunner.java +++ b/RFS/src/main/java/com/rfs/worker/DocumentsRunner.java @@ -1,74 +1,94 @@ package com.rfs.worker; -import java.util.Optional; +import java.io.IOException; +import java.nio.file.Path; +import java.time.Duration; +import java.util.function.BiFunction; +import java.util.function.Function; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; +import com.rfs.cms.IWorkCoordinator; +import com.rfs.cms.ScopedWorkCoordinator; +import com.rfs.common.RfsException; +import lombok.AllArgsConstructor; +import lombok.Lombok; +import lombok.extern.slf4j.Slf4j; -import com.rfs.cms.CmsClient; -import com.rfs.cms.CmsEntry; import com.rfs.common.DocumentReindexer; -import com.rfs.common.IndexMetadata; import com.rfs.common.LuceneDocumentsReader; import com.rfs.common.ShardMetadata; import com.rfs.common.SnapshotShardUnpacker; +import org.apache.lucene.document.Document; +import reactor.core.publisher.Flux; -public class DocumentsRunner implements Runner { - private static final Logger logger = LogManager.getLogger(DocumentsRunner.class); +@Slf4j +@AllArgsConstructor +public class DocumentsRunner { + public static final String ALL_INDEX_MANIFEST = "all_index_manifest"; - private final DocumentsStep.SharedMembers members; + ScopedWorkCoordinator workCoordinator; + private final BiFunction shardMetadataFactory; + private final SnapshotShardUnpacker.Factory unpackerFactory; + private final Function readerFactory; + private final DocumentReindexer reindexer; - public DocumentsRunner( - GlobalState globalState, CmsClient cmsClient, String snapshotName, long maxShardSizeBytes, - IndexMetadata.Factory metadataFactory, ShardMetadata.Factory shardMetadataFactory, - SnapshotShardUnpacker.Factory unpackerFactory, LuceneDocumentsReader reader, DocumentReindexer reindexer) { - this.members = new DocumentsStep.SharedMembers( - globalState, - cmsClient, - snapshotName, - maxShardSizeBytes, - metadataFactory, - shardMetadataFactory, - unpackerFactory, - reader, - reindexer - ); + public enum CompletionStatus { + NOTHING_DONE, + WORK_COMPLETED } - @Override - public void runInternal() { - WorkerStep nextStep = null; - try { - nextStep = new DocumentsStep.EnterPhase(members); + /** + * @return true if it did work, false if there was no available work at this time. + * @throws IOException + */ + public CompletionStatus migrateNextShard() throws IOException { + return workCoordinator.ensurePhaseCompletion(wc -> { + try { + return wc.acquireNextWorkItem(Duration.ofMinutes(10)); + } catch (Exception e) { + throw Lombok.sneakyThrow(e); + } + }, + new IWorkCoordinator.WorkAcquisitionOutcomeVisitor<>() { + @Override + public CompletionStatus onAlreadyCompleted() throws IOException { + return CompletionStatus.NOTHING_DONE; + } - while (nextStep != null) { - nextStep.run(); - nextStep = nextStep.nextStep(); - } - } catch (Exception e) { - throw new DocumentsMigrationPhaseFailed( - members.globalState.getPhase(), - nextStep, - members.cmsEntry.map(bar -> (CmsEntry.Base) bar), - e - ); - } - } + @Override + public CompletionStatus onAcquiredWork(IWorkCoordinator.WorkItemAndDuration workItem) throws IOException { + doDocumentsMigration(IndexAndShard.valueFromWorkItemString(workItem.getWorkItemId())); + return CompletionStatus.WORK_COMPLETED; + } - @Override - public String getPhaseName() { - return "Documents Migration"; + @Override + public CompletionStatus onNoAvailableWorkToBeDone() throws IOException { + return CompletionStatus.NOTHING_DONE; + } + }); } - @Override - public Logger getLogger() { - return logger; + public static class ShardTooLargeException extends RfsException { + public ShardTooLargeException(long shardSizeBytes, long maxShardSize) { + super("The shard size of " + shardSizeBytes + " bytes exceeds the maximum shard size of " + maxShardSize + " bytes"); + } } - public static class DocumentsMigrationPhaseFailed extends Runner.PhaseFailed { - public DocumentsMigrationPhaseFailed(GlobalState.Phase phase, WorkerStep nextStep, Optional cmsEntry, Exception e) { - super("Documents Migration Phase failed", phase, nextStep, cmsEntry, e); + private void doDocumentsMigration(IndexAndShard indexAndShard) { + log.info("Migrating docs for " + indexAndShard); + ShardMetadata.Data shardMetadata = shardMetadataFactory.apply(indexAndShard.indexName, indexAndShard.shard); + + try (var unpacker = unpackerFactory.create(shardMetadata)) { + var reader = readerFactory.apply(unpacker.unpack()); + Flux documents = reader.readDocuments(); + + reindexer.reindex(shardMetadata.getIndexName(), documents) + .doOnError(error -> log.error("Error during reindexing: " + error)) + .doOnSuccess(done -> log.atInfo() + .setMessage(()->"Reindexing completed for Index " + shardMetadata.getIndexName() + + ", Shard " + shardMetadata.getShardId()).log()) + // Wait for the reindexing to complete before proceeding + .block(); + log.info("Docs migrated"); } - } - + } } diff --git a/RFS/src/main/java/com/rfs/worker/DocumentsStep.java b/RFS/src/main/java/com/rfs/worker/DocumentsStep.java deleted file mode 100644 index add57d363..000000000 --- a/RFS/src/main/java/com/rfs/worker/DocumentsStep.java +++ /dev/null @@ -1,550 +0,0 @@ -package com.rfs.worker; - -import java.time.Instant; -import java.util.Optional; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.apache.lucene.document.Document; - -import com.rfs.cms.CmsClient; -import com.rfs.cms.CmsEntry; -import com.rfs.cms.OpenSearchCmsClient; -import com.rfs.common.DocumentReindexer; -import com.rfs.common.IndexMetadata; -import com.rfs.common.LuceneDocumentsReader; -import com.rfs.common.RfsException; -import com.rfs.common.ShardMetadata; -import com.rfs.common.SnapshotRepo; -import com.rfs.common.SnapshotShardUnpacker; - -import lombok.RequiredArgsConstructor; -import reactor.core.publisher.Flux; - -public class DocumentsStep { - @RequiredArgsConstructor - public static class SharedMembers { - protected final GlobalState globalState; - protected final CmsClient cmsClient; - protected final String snapshotName; - protected final long maxShardSizeBytes; - protected final IndexMetadata.Factory metadataFactory; - protected final ShardMetadata.Factory shardMetadataFactory; - protected final SnapshotShardUnpacker.Factory unpackerFactory; - protected final LuceneDocumentsReader reader; - protected final DocumentReindexer reindexer; - protected Optional cmsEntry = Optional.empty(); - protected Optional cmsWorkEntry = Optional.empty(); - - // Convient ways to check if the CMS entries are present before retrieving them. In some places, it's fine/expected - // for the CMS entry to be missing, but in others, it's a problem. - public CmsEntry.Documents getCmsEntryNotMissing() { - return cmsEntry.orElseThrow( - () -> new MissingDocumentsEntry() - ); - } - public CmsEntry.DocumentsWorkItem getCmsWorkEntryNotMissing() { - return cmsWorkEntry.orElseThrow( - () -> new MissingDocumentsEntry() - ); - } - } - - public static abstract class Base implements WorkerStep { - protected final Logger logger = LogManager.getLogger(getClass()); - protected final SharedMembers members; - - public Base(SharedMembers members) { - this.members = members; - } - } - - /* - * Updates the Worker's phase to indicate we're doing work on an Index Migration - */ - public static class EnterPhase extends Base { - public EnterPhase(SharedMembers members) { - super(members); - } - - @Override - public void run() { - logger.info("Documents Migration not yet completed, entering Documents Phase..."); - members.globalState.updatePhase(GlobalState.Phase.DOCUMENTS_IN_PROGRESS); - } - - @Override - public WorkerStep nextStep() { - return new GetEntry(members); - } - } - - /* - * Gets the current Documents Migration entry from the CMS, if it exists - */ - public static class GetEntry extends Base { - - public GetEntry(SharedMembers members) { - super(members); - } - - @Override - public void run() { - logger.info("Pulling the Documents Migration entry from the CMS, if it exists..."); - members.cmsEntry = members.cmsClient.getDocumentsEntry(); - } - - @Override - public WorkerStep nextStep() { - if (members.cmsEntry.isEmpty()) { - return new CreateEntry(members); - } - - CmsEntry.Documents currentEntry = members.cmsEntry.get(); - switch (currentEntry.status) { - case SETUP: - // TODO: This uses the client-side clock to evaluate the lease expiration, when we should - // ideally be using the server-side clock. Consider this a temporary solution until we find - // out how to use the server-side clock. - long leaseExpiryMillis = Long.parseLong(currentEntry.leaseExpiry); - Instant leaseExpiryInstant = Instant.ofEpochMilli(leaseExpiryMillis); - boolean leaseExpired = leaseExpiryInstant.isBefore(Instant.now()); - - // Don't try to acquire the lease if we're already at the max number of attempts - if (currentEntry.numAttempts >= CmsEntry.Documents.MAX_ATTEMPTS && leaseExpired) { - return new ExitPhaseFailed(members, new MaxAttemptsExceeded()); - } - - if (leaseExpired) { - return new AcquireLease(members, currentEntry); - } - - logger.info("Documents Migration entry found, but there's already a valid work lease on it"); - return new RandomWait(members); - - case IN_PROGRESS: - return new GetDocumentsToMigrate(members); - case COMPLETED: - return new ExitPhaseSuccess(members); - case FAILED: - return new ExitPhaseFailed(members, new FoundFailedDocumentsMigration()); - default: - throw new IllegalStateException("Unexpected documents migration status: " + currentEntry.status); - } - } - } - - public static class CreateEntry extends Base { - - public CreateEntry(SharedMembers members) { - super(members); - } - - @Override - public void run() { - logger.info("Documents Migration CMS Entry not found, attempting to create it..."); - members.cmsEntry = members.cmsClient.createDocumentsEntry(); - logger.info("Documents Migration CMS Entry created"); - } - - @Override - public WorkerStep nextStep() { - // Set up the documents work entries if we successfully created the CMS entry; otherwise, circle back to the beginning - if (members.cmsEntry.isPresent()) { - return new SetupDocumentsWorkEntries(members); - } else { - return new GetEntry(members); - } - } - } - - public static class AcquireLease extends Base { - protected final CmsEntry.Base entry; - protected Optional leasedEntry = Optional.empty(); - - public AcquireLease(SharedMembers members, CmsEntry.Base entry) { - super(members); - this.entry = entry; - } - - protected long getNowMs() { - return Instant.now().toEpochMilli(); - } - - @Override - public void run() { - - if (entry instanceof CmsEntry.Documents) { - CmsEntry.Documents currentCmsEntry = (CmsEntry.Documents) entry; - logger.info("Attempting to acquire lease on Documents Migration entry..."); - CmsEntry.Documents updatedEntry = new CmsEntry.Documents( - currentCmsEntry.status, - // Set the next CMS entry based on the current one - // TODO: Should be using the server-side clock here - CmsEntry.Documents.getLeaseExpiry(getNowMs(), currentCmsEntry.numAttempts + 1), - currentCmsEntry.numAttempts + 1 - ); - members.cmsEntry = members.cmsClient.updateDocumentsEntry(updatedEntry, currentCmsEntry); - leasedEntry = members.cmsEntry.map(bar -> (CmsEntry.Base) bar); - } else if (entry instanceof CmsEntry.DocumentsWorkItem) { - CmsEntry.DocumentsWorkItem currentCmsEntry = (CmsEntry.DocumentsWorkItem) entry; - logger.info("Attempting to acquire lease on Documents Work Item entry..."); - CmsEntry.DocumentsWorkItem updatedEntry = new CmsEntry.DocumentsWorkItem( - currentCmsEntry.indexName, - currentCmsEntry.shardId, - currentCmsEntry.status, - // Set the next CMS entry based on the current one - // TODO: Should be using the server-side clock here - CmsEntry.DocumentsWorkItem.getLeaseExpiry(getNowMs(), currentCmsEntry.numAttempts + 1), - currentCmsEntry.numAttempts + 1 - ); - members.cmsWorkEntry = members.cmsClient.updateDocumentsWorkItem(updatedEntry, currentCmsEntry); - leasedEntry = members.cmsWorkEntry.map(bar -> (CmsEntry.Base) bar); - } else { - throw new IllegalStateException("Unexpected CMS entry type: " + entry.getClass().getName()); - } - - if (leasedEntry.isPresent()) { - logger.info("Lease acquired"); - } else { - logger.info("Failed to acquire lease"); - } - } - - @Override - public WorkerStep nextStep() { - // Do work if we acquired the lease; otherwise, circle back to the beginning after a backoff - if (leasedEntry.isPresent()) { - if (leasedEntry.get() instanceof CmsEntry.Documents) { - return new SetupDocumentsWorkEntries(members); - } else if (entry instanceof CmsEntry.DocumentsWorkItem) { - return new MigrateDocuments(members); - } else { - throw new IllegalStateException("Unexpected CMS entry type: " + entry.getClass().getName()); - } - } else { - return new RandomWait(members); - } - } - } - - public static class SetupDocumentsWorkEntries extends Base { - - public SetupDocumentsWorkEntries(SharedMembers members) { - super(members); - } - - @Override - public void run() { - CmsEntry.Documents leasedCmsEntry = members.getCmsEntryNotMissing(); - - logger.info("Setting the worker's current work item to be creating the documents work entries..."); - members.globalState.updateWorkItem(new OpenSearchWorkItem(OpenSearchCmsClient.CMS_INDEX_NAME, OpenSearchCmsClient.CMS_DOCUMENTS_DOC_ID)); - logger.info("Work item set"); - - logger.info("Setting up the Documents Work Items..."); - SnapshotRepo.Provider repoDataProvider = members.metadataFactory.getRepoDataProvider(); - for (SnapshotRepo.Index index : repoDataProvider.getIndicesInSnapshot(members.snapshotName)) { - IndexMetadata.Data indexMetadata = members.metadataFactory.fromRepo(members.snapshotName, index.getName()); - logger.info("Index " + indexMetadata.getName() + " has " + indexMetadata.getNumberOfShards() + " shards"); - for (int shardId = 0; shardId < indexMetadata.getNumberOfShards(); shardId++) { - logger.info("Creating Documents Work Item for index: " + indexMetadata.getName() + ", shard: " + shardId); - members.cmsClient.createDocumentsWorkItem(indexMetadata.getName(), shardId); - } - } - logger.info("Finished setting up the Documents Work Items."); - - logger.info("Updating the Documents Migration entry to indicate setup has been completed..."); - CmsEntry.Documents updatedEntry = new CmsEntry.Documents( - CmsEntry.DocumentsStatus.IN_PROGRESS, - leasedCmsEntry.leaseExpiry, - leasedCmsEntry.numAttempts - ); - - members.cmsEntry = members.cmsClient.updateDocumentsEntry(updatedEntry, leasedCmsEntry); - logger.info("Documents Migration entry updated"); - - logger.info("Clearing the worker's current work item..."); - members.globalState.updateWorkItem(null); - logger.info("Work item cleared"); - } - - @Override - public WorkerStep nextStep() { - if (members.cmsEntry.isEmpty()) { - // In this scenario, we've done all the work, but failed to update the CMS entry so that we know we've - // done the work. We circle back around to try again, which is made more reasonable by the fact we - // don't re-migrate templates that already exist on the target cluster. If we didn't circle back - // around, there would be a chance that the CMS entry would never be marked as completed. - // - // The CMS entry's retry limit still applies in this case, so there's a limiting factor here. - logger.warn("Completed creating the documents work entries but failed to update the Documents Migration entry; retrying..."); - return new GetEntry(members); - } - - return new GetDocumentsToMigrate(members); - } - } - - public static class GetDocumentsToMigrate extends Base { - - public GetDocumentsToMigrate(SharedMembers members) { - super(members); - } - - @Override - public void run() { - logger.info("Seeing if there are any docs left to migration according to the CMS..."); - members.cmsWorkEntry = members.cmsClient.getAvailableDocumentsWorkItem(); - members.cmsWorkEntry.ifPresentOrElse( - (item) -> logger.info("Found some docs to migrate"), - () -> logger.info("No docs found to migrate") - ); - } - - @Override - public WorkerStep nextStep() { - // No work left to do - if (members.cmsWorkEntry.isEmpty()) { - return new ExitPhaseSuccess(members); - } - return new MigrateDocuments(members); - } - } - - public static class MigrateDocuments extends Base { - - public MigrateDocuments(SharedMembers members) { - super(members); - } - - @Override - public void run() { - CmsEntry.DocumentsWorkItem workItem = members.getCmsWorkEntryNotMissing(); - - /* - * Try to migrate the documents. We should have a unique lease on the entry that guarantees that we're the only - * one working on it. However, we apply some care to ensure that even if that's not the case, something fairly - * reasonable happens. - * - * If we succeed, we forcefully mark it as completed. When we do so, we don't care if someone else has changed - * the record in the meantime; *we* completed it successfully and that's what matters. Because this is the only - * forceful operation on the entry, the other operations are safe to be non-forceful. - * - * If it's already exceeded the number of attempts, we attempt to mark it as failed. If someone else - * has updated the entry in the meantime, we just move on to the next work item. This is safe because - * it means someone else has either marked it as completed or failed, and either is fine. - * - * If we fail to migrate it, we attempt to increment the attempt count. It's fine if the increment - * fails because . - */ - if (workItem.numAttempts > CmsEntry.DocumentsWorkItem.MAX_ATTEMPTS) { - logger.warn("Documents Work Item (Index: " + workItem.indexName + ", Shard: " + workItem.shardId + ") has exceeded the maximum number of attempts; marking it as failed..."); - CmsEntry.DocumentsWorkItem updatedEntry = new CmsEntry.DocumentsWorkItem( - workItem.indexName, - workItem.shardId, - CmsEntry.DocumentsWorkItemStatus.FAILED, - workItem.leaseExpiry, - workItem.numAttempts - ); - - // We use optimistic locking here in the unlikely event someone else is working on this task despite the - // leasing system and managed to complete the task; in that case we want this update to bounce. - members.cmsWorkEntry = members.cmsClient.updateDocumentsWorkItem(updatedEntry, workItem); - members.cmsWorkEntry.ifPresentOrElse( - value -> logger.info("Documents Work Item (Index: " + workItem.indexName + ", Shard: " + workItem.shardId + ") marked as failed"), - () ->logger.warn("Unable to mark Documents Work Item (Index: " + workItem.indexName + ", Shard: " + workItem.shardId + ") as failed") - ); - return; - } - - logger.info("Setting the worker's current work item to be migrating the docs..."); - members.globalState.updateWorkItem(new OpenSearchWorkItem( - OpenSearchCmsClient.CMS_INDEX_NAME, - OpenSearchCmsClient.getDocumentsWorkItemDocId(workItem.indexName, workItem.shardId) - )); - logger.info("Work item set"); - - ShardMetadata.Data shardMetadata = null; - try { - logger.info("Migrating docs: Index " + workItem.indexName + ", Shard " + workItem.shardId); - shardMetadata = members.shardMetadataFactory.fromRepo(members.snapshotName, workItem.indexName, workItem.shardId); - - logger.info("Shard size: " + shardMetadata.getTotalSizeBytes() + " bytes"); - if (shardMetadata.getTotalSizeBytes() > members.maxShardSizeBytes) { - throw new ShardTooLarge(shardMetadata.getTotalSizeBytes(), members.maxShardSizeBytes); - } - - try (SnapshotShardUnpacker unpacker = members.unpackerFactory.create(shardMetadata)) { - unpacker.unpack(); - - Flux documents = members.reader.readDocuments(shardMetadata.getIndexName(), shardMetadata.getShardId()); - final ShardMetadata.Data finalShardMetadata = shardMetadata; // Define in local context for the lambda - members.reindexer.reindex(shardMetadata.getIndexName(), documents) - .doOnError(error -> logger.error("Error during reindexing: " + error)) - .doOnSuccess(done -> logger.info("Reindexing completed for Index " + finalShardMetadata.getIndexName() + ", Shard " + finalShardMetadata.getShardId())) - // Wait for the reindexing to complete before proceeding - .block(); - logger.info("Docs migrated"); - } - - logger.info("Updating the Documents Work Item to indicate it has been completed..."); - CmsEntry.DocumentsWorkItem updatedEntry = new CmsEntry.DocumentsWorkItem( - workItem.indexName, - workItem.shardId, - CmsEntry.DocumentsWorkItemStatus.COMPLETED, - workItem.leaseExpiry, - workItem.numAttempts - ); - - members.cmsWorkEntry = Optional.of(members.cmsClient.updateDocumentsWorkItemForceful(updatedEntry)); - logger.info("Documents Work Item updated"); - } catch (Exception e) { - logger.info("Failed to documents: Index " + workItem.indexName + ", Shard " + workItem.shardId, e); - logger.info("Updating the Documents Work Item with incremented attempt count..."); - CmsEntry.DocumentsWorkItem updatedEntry = new CmsEntry.DocumentsWorkItem( - workItem.indexName, - workItem.shardId, - workItem.status, - workItem.leaseExpiry, - workItem.numAttempts + 1 - ); - - // We use optimistic locking here in the unlikely event someone else is working on this task despite the - // leasing system and managed to complete the task; in that case we want this update to bounce. - members.cmsWorkEntry = members.cmsClient.updateDocumentsWorkItem(updatedEntry, workItem); - members.cmsWorkEntry.ifPresentOrElse( - value -> logger.info("Documents Work Item (Index: " + workItem.indexName + ", Shard: " + workItem.shardId + ") attempt count was incremented"), - () ->logger.info("Unable to increment attempt count of Documents Work Item (Index: " + workItem.indexName + ", Shard: " + workItem.shardId + ")") - ); - } - - logger.info("Clearing the worker's current work item..."); - members.globalState.updateWorkItem(null); - logger.info("Work item cleared"); - } - - @Override - public WorkerStep nextStep() { - return new GetDocumentsToMigrate(members); - } - } - - public static class RandomWait extends Base { - private final static int WAIT_TIME_MS = 5 * 1000; // arbitrarily chosen - - public RandomWait(SharedMembers members) { - super(members); - } - - protected void waitABit() { - try { - Thread.sleep(WAIT_TIME_MS); - } catch (InterruptedException e) { - logger.error("Interrupted while performing a wait", e); - throw new DocumentsMigrationFailed("Interrupted"); - } - } - - @Override - public void run() { - logger.info("Backing off for " + WAIT_TIME_MS + " milliseconds before checking the Index Migration entry again..."); - waitABit(); - } - - @Override - public WorkerStep nextStep() { - return new GetEntry(members); - } - } - - public static class ExitPhaseSuccess extends Base { - public ExitPhaseSuccess(SharedMembers members) { - super(members); - } - - @Override - public void run() { - logger.info("Marking the Documents Migration as completed..."); - CmsEntry.Documents lastCmsEntry = members.getCmsEntryNotMissing(); - CmsEntry.Documents updatedEntry = new CmsEntry.Documents( - CmsEntry.DocumentsStatus.COMPLETED, - lastCmsEntry.leaseExpiry, - lastCmsEntry.numAttempts - ); - members.cmsClient.updateDocumentsEntry(updatedEntry, lastCmsEntry); - logger.info("Documents Migration marked as completed"); - - logger.info("Documents Migration completed, exiting Documents Phase..."); - members.globalState.updatePhase(GlobalState.Phase.DOCUMENTS_COMPLETED); - } - - @Override - public WorkerStep nextStep() { - return null; - } - } - - public static class ExitPhaseFailed extends Base { - protected final DocumentsMigrationFailed e; - - public ExitPhaseFailed(SharedMembers members, DocumentsMigrationFailed e) { - super(members); - this.e = e; - } - - @Override - public void run() { - // We either failed the Documents Migration or found it had already been failed; either way this - // should not be missing - CmsEntry.Documents lastCmsEntry = members.getCmsEntryNotMissing(); - - logger.error("Documents Migration failed"); - CmsEntry.Documents updatedEntry = new CmsEntry.Documents( - CmsEntry.DocumentsStatus.FAILED, - lastCmsEntry.leaseExpiry, - lastCmsEntry.numAttempts - ); - members.cmsClient.updateDocumentsEntry(updatedEntry, lastCmsEntry); - members.globalState.updatePhase(GlobalState.Phase.DOCUMENTS_FAILED); - } - - @Override - public WorkerStep nextStep() { - throw e; - } - } - - public static class ShardTooLarge extends RfsException { - public ShardTooLarge(long shardSizeBytes, long maxShardSize) { - super("The shard size of " + shardSizeBytes + " bytes exceeds the maximum shard size of " + maxShardSize + " bytes"); - } - } - - public static class DocumentsMigrationFailed extends RfsException { - public DocumentsMigrationFailed(String message) { - super("The Documents Migration has failed. Reason: " + message); - } - } - - public static class MissingDocumentsEntry extends RfsException { - public MissingDocumentsEntry() { - super("The Documents Migration CMS entry we expected to be stored in local memory was null." - + " This should never happen." - ); - } - } - - public static class FoundFailedDocumentsMigration extends DocumentsMigrationFailed { - public FoundFailedDocumentsMigration() { - super("We checked the status in the CMS and found it had failed. Aborting."); - } - } - - public static class MaxAttemptsExceeded extends DocumentsMigrationFailed { - public MaxAttemptsExceeded() { - super("We reached the limit of " + CmsEntry.Documents.MAX_ATTEMPTS + " attempts to complete the Documents Migration"); - } - } - -} diff --git a/RFS/src/main/java/com/rfs/worker/GlobalState.java b/RFS/src/main/java/com/rfs/worker/GlobalState.java deleted file mode 100644 index 679af0b6c..000000000 --- a/RFS/src/main/java/com/rfs/worker/GlobalState.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.rfs.worker; - -import java.util.concurrent.atomic.AtomicReference; - -/* - * This class is a Singleton that contains global process state. - */ -public class GlobalState { - private static final AtomicReference instance = new AtomicReference<>(); - - public enum Phase { - UNSET, - SNAPSHOT_IN_PROGRESS, - SNAPSHOT_COMPLETED, - SNAPSHOT_FAILED, - METADATA_IN_PROGRESS, - METADATA_COMPLETED, - METADATA_FAILED, - INDEX_IN_PROGRESS, - INDEX_COMPLETED, - INDEX_FAILED, - DOCUMENTS_IN_PROGRESS, - DOCUMENTS_COMPLETED, - DOCUMENTS_FAILED - } - - private AtomicReference phase = new AtomicReference<>(Phase.UNSET); - private AtomicReference workItem = new AtomicReference<>(null); - - private GlobalState() {} - - public static GlobalState getInstance() { - instance.updateAndGet(existingInstance -> { - if (existingInstance == null) { - return new GlobalState(); - } else { - return existingInstance; - } - }); - return instance.get(); - } - - public void updatePhase(Phase newValue) { - phase.set(newValue); - } - - public Phase getPhase() { - return phase.get(); - } - - public void updateWorkItem(OpenSearchWorkItem newValue) { - workItem.set(newValue); - } - - public OpenSearchWorkItem getWorkItem() { - return workItem.get(); - } -} diff --git a/RFS/src/main/java/com/rfs/worker/IndexAndShard.java b/RFS/src/main/java/com/rfs/worker/IndexAndShard.java new file mode 100644 index 000000000..980b64657 --- /dev/null +++ b/RFS/src/main/java/com/rfs/worker/IndexAndShard.java @@ -0,0 +1,28 @@ +package com.rfs.worker; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.ToString; + +@AllArgsConstructor +@Getter +@ToString +public class IndexAndShard { + public static final String SEPARATOR = "__"; + String indexName; + int shard; + + public static String formatAsWorkItemString(String name, int shardId) { + if (name.contains(SEPARATOR)) { + throw new IllegalArgumentException("Illegal work item name: '" + name +"'. " + + "Work item names cannot contain '" + SEPARATOR + "'"); + } + return name + SEPARATOR + shardId; + } + + public static IndexAndShard valueFromWorkItemString(String input) { + int lastIndex = input.lastIndexOf(SEPARATOR); + return new IndexAndShard(input.substring(0, lastIndex), + Integer.parseInt(input.substring(lastIndex + 2))); + } +} diff --git a/RFS/src/main/java/com/rfs/worker/IndexRunner.java b/RFS/src/main/java/com/rfs/worker/IndexRunner.java index 0b8885527..a88304071 100644 --- a/RFS/src/main/java/com/rfs/worker/IndexRunner.java +++ b/RFS/src/main/java/com/rfs/worker/IndexRunner.java @@ -2,58 +2,35 @@ import java.util.Optional; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.rfs.common.SnapshotRepo; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; -import com.rfs.cms.CmsClient; -import com.rfs.cms.CmsEntry; import com.rfs.common.IndexMetadata; import com.rfs.transformers.Transformer; import com.rfs.version_os_2_11.IndexCreator_OS_2_11; -public class IndexRunner implements Runner { - private static final Logger logger = LogManager.getLogger(IndexRunner.class); - - private final IndexStep.SharedMembers members; - - public IndexRunner(GlobalState globalState, CmsClient cmsClient, String snapshotName, IndexMetadata.Factory metadataFactory, - IndexCreator_OS_2_11 indexCreator, Transformer transformer) { - this.members = new IndexStep.SharedMembers(globalState, cmsClient, snapshotName, metadataFactory, indexCreator, transformer); - } - - @Override - public void runInternal() { - WorkerStep nextStep = null; - try { - nextStep = new IndexStep.EnterPhase(members); - - while (nextStep != null) { - nextStep.run(); - nextStep = nextStep.nextStep(); - } - } catch (Exception e) { - throw new IndexMigrationPhaseFailed( - members.globalState.getPhase(), - nextStep, - members.cmsEntry.map(bar -> (CmsEntry.Base) bar), - e +@Slf4j +@AllArgsConstructor +public class IndexRunner { + + private final String snapshotName; + private final IndexMetadata.Factory metadataFactory; + private final IndexCreator_OS_2_11 indexCreator; + private final Transformer transformer; + + public void migrateIndices() { + SnapshotRepo.Provider repoDataProvider = metadataFactory.getRepoDataProvider(); + // TODO - parallelize this, maybe ~400-1K requests per thread and do it asynchronously + for (SnapshotRepo.Index index : repoDataProvider.getIndicesInSnapshot(snapshotName)) { + var indexMetadata = metadataFactory.fromRepo(snapshotName, index.getName()); + var root = indexMetadata.toObjectNode(); + var transformedRoot = transformer.transformIndexMetadata(root); + var resultOp = indexCreator.create(transformedRoot, index.getName(), indexMetadata.getId()); + resultOp.ifPresentOrElse(value -> log.info("Index " + index.getName() + " created successfully"), + () -> log.info("Index " + index.getName() + " already existed; no work required") ); } } - - @Override - public String getPhaseName() { - return "Index Migration"; - } - - @Override - public Logger getLogger() { - return logger; - } - - public static class IndexMigrationPhaseFailed extends Runner.PhaseFailed { - public IndexMigrationPhaseFailed(GlobalState.Phase phase, WorkerStep nextStep, Optional cmsEntry, Exception e) { - super("Index Migration Phase failed", phase, nextStep, cmsEntry, e); - } - } } diff --git a/RFS/src/main/java/com/rfs/worker/IndexStep.java b/RFS/src/main/java/com/rfs/worker/IndexStep.java deleted file mode 100644 index 8f2ec68c8..000000000 --- a/RFS/src/main/java/com/rfs/worker/IndexStep.java +++ /dev/null @@ -1,479 +0,0 @@ -package com.rfs.worker; - -import java.time.Instant; -import java.util.List; -import java.util.Optional; -import java.util.stream.Collectors; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import com.fasterxml.jackson.databind.node.ObjectNode; -import com.rfs.cms.CmsClient; -import com.rfs.cms.CmsEntry; -import com.rfs.cms.OpenSearchCmsClient; -import com.rfs.common.IndexMetadata; -import com.rfs.common.RfsException; -import com.rfs.common.SnapshotRepo; -import com.rfs.transformers.Transformer; -import com.rfs.version_os_2_11.IndexCreator_OS_2_11; - -import lombok.RequiredArgsConstructor; - -public class IndexStep { - - @RequiredArgsConstructor - public static class SharedMembers { - protected final GlobalState globalState; - protected final CmsClient cmsClient; - protected final String snapshotName; - protected final IndexMetadata.Factory metadataFactory; - protected final IndexCreator_OS_2_11 indexCreator; - protected final Transformer transformer; - protected Optional cmsEntry = Optional.empty(); - - // A convient way to check if the CMS entry is present before retrieving it. In some places, it's fine/expected - // for the CMS entry to be missing, but in others, it's a problem. - public CmsEntry.Index getCmsEntryNotMissing() { - return cmsEntry.orElseThrow( - () -> new MissingIndexEntry() - ); - } - } - - public static abstract class Base implements WorkerStep { - protected final Logger logger = LogManager.getLogger(getClass()); - protected final SharedMembers members; - - public Base(SharedMembers members) { - this.members = members; - } - } - - /* - * Updates the Worker's phase to indicate we're doing work on an Index Migration - */ - public static class EnterPhase extends Base { - public EnterPhase(SharedMembers members) { - super(members); - } - - @Override - public void run() { - logger.info("Index Migration not yet completed, entering Index Phase..."); - members.globalState.updatePhase(GlobalState.Phase.INDEX_IN_PROGRESS); - } - - @Override - public WorkerStep nextStep() { - return new GetEntry(members); - } - } - - /* - * Gets the current Index Migration entry from the CMS, if it exists - */ - public static class GetEntry extends Base { - - public GetEntry(SharedMembers members) { - super(members); - } - - @Override - public void run() { - logger.info("Pulling the Index Migration entry from the CMS, if it exists..."); - members.cmsEntry = members.cmsClient.getIndexEntry(); - } - - @Override - public WorkerStep nextStep() { - if (members.cmsEntry.isEmpty()) { - return new CreateEntry(members); - } - - CmsEntry.Index currentEntry = members.cmsEntry.get(); - switch (currentEntry.status) { - case SETUP: - // TODO: This uses the client-side clock to evaluate the lease expiration, when we should - // ideally be using the server-side clock. Consider this a temporary solution until we find - // out how to use the server-side clock. - long leaseExpiryMillis = Long.parseLong(currentEntry.leaseExpiry); - Instant leaseExpiryInstant = Instant.ofEpochMilli(leaseExpiryMillis); - boolean leaseExpired = leaseExpiryInstant.isBefore(Instant.now()); - - // Don't try to acquire the lease if we're already at the max number of attempts - if (currentEntry.numAttempts >= CmsEntry.Index.MAX_ATTEMPTS && leaseExpired) { - return new ExitPhaseFailed(members, new MaxAttemptsExceeded()); - } - - if (leaseExpired) { - return new AcquireLease(members); - } - - logger.info("Index Migration entry found, but there's already a valid work lease on it"); - return new RandomWait(members); - - case IN_PROGRESS: - return new GetIndicesToMigrate(members); - case COMPLETED: - return new ExitPhaseSuccess(members); - case FAILED: - return new ExitPhaseFailed(members, new FoundFailedIndexMigration()); - default: - throw new IllegalStateException("Unexpected index migration status: " + currentEntry.status); - } - } - } - - public static class CreateEntry extends Base { - - public CreateEntry(SharedMembers members) { - super(members); - } - - @Override - public void run() { - logger.info("Index Migration CMS Entry not found, attempting to create it..."); - members.cmsEntry = members.cmsClient.createIndexEntry(); - logger.info("Index Migration CMS Entry created"); - } - - @Override - public WorkerStep nextStep() { - // Set up the index work entries if we successfully created the CMS entry; otherwise, circle back to the beginning - if (members.cmsEntry.isPresent()) { - return new SetupIndexWorkEntries(members); - } else { - return new GetEntry(members); - } - } - } - - public static class AcquireLease extends Base { - - public AcquireLease(SharedMembers members) { - super(members); - } - - protected long getNowMs() { - return Instant.now().toEpochMilli(); - } - - @Override - public void run() { - // We only get here if we know we want to acquire the lock, so we know the CMS entry should not be null - CmsEntry.Index lastCmsEntry = members.getCmsEntryNotMissing(); - - logger.info("Current Index Migration work lease appears to have expired; attempting to acquire it..."); - - CmsEntry.Index updatedEntry = new CmsEntry.Index( - lastCmsEntry.status, - // Set the next CMS entry based on the current one - // TODO: Should be using the server-side clock here - CmsEntry.Index.getLeaseExpiry(getNowMs(), lastCmsEntry.numAttempts + 1), - lastCmsEntry.numAttempts + 1 - ); - members.cmsEntry = members.cmsClient.updateIndexEntry(updatedEntry, lastCmsEntry); - - if (members.cmsEntry.isPresent()) { - logger.info("Lease acquired"); - } else { - logger.info("Failed to acquire lease"); - } - } - - @Override - public WorkerStep nextStep() { - // Set up the index work entries if we acquired the lease; otherwise, circle back to the beginning after a backoff - if (members.cmsEntry.isPresent()) { - return new SetupIndexWorkEntries(members); - } else { - return new RandomWait(members); - } - } - } - - public static class SetupIndexWorkEntries extends Base { - - public SetupIndexWorkEntries(SharedMembers members) { - super(members); - } - - @Override - public void run() { - // We only get here if we acquired the lock, so we know the CMS entry should not be missing - CmsEntry.Index lastCmsEntry = members.getCmsEntryNotMissing(); - - logger.info("Setting the worker's current work item to be creating the index work entries..."); - members.globalState.updateWorkItem(new OpenSearchWorkItem(OpenSearchCmsClient.CMS_INDEX_NAME, OpenSearchCmsClient.CMS_INDEX_DOC_ID)); - logger.info("Work item set"); - - logger.info("Setting up the Index Work Items..."); - SnapshotRepo.Provider repoDataProvider = members.metadataFactory.getRepoDataProvider(); - for (SnapshotRepo.Index index : repoDataProvider.getIndicesInSnapshot(members.snapshotName)) { - IndexMetadata.Data indexMetadata = members.metadataFactory.fromRepo(members.snapshotName, index.getName()); - logger.info("Creating Index Work Item for index: " + indexMetadata.getName()); - members.cmsClient.createIndexWorkItem(indexMetadata.getName(), indexMetadata.getNumberOfShards()); - } - logger.info("Finished setting up the Index Work Items."); - - logger.info("Updating the Index Migration entry to indicate setup has been completed..."); - CmsEntry.Index updatedEntry = new CmsEntry.Index( - CmsEntry.IndexStatus.IN_PROGRESS, - lastCmsEntry.leaseExpiry, - lastCmsEntry.numAttempts - ); - - members.cmsEntry = members.cmsClient.updateIndexEntry(updatedEntry, lastCmsEntry); - logger.info("Index Migration entry updated"); - - logger.info("Clearing the worker's current work item..."); - members.globalState.updateWorkItem(null); - logger.info("Work item cleared"); - } - - @Override - public WorkerStep nextStep() { - if (members.cmsEntry.isEmpty()) { - // In this scenario, we've done all the work, but failed to update the CMS entry so that we know we've - // done the work. We circle back around to try again, which is made more reasonable by the fact we - // don't re-migrate templates that already exist on the target cluster. If we didn't circle back - // around, there would be a chance that the CMS entry would never be marked as completed. - // - // The CMS entry's retry limit still applies in this case, so there's a limiting factor here. - logger.warn("Completed creating the index work entries but failed to update the Index Migration entry; retrying..."); - return new GetEntry(members); - } - return new GetIndicesToMigrate(members); - } - } - - public static class GetIndicesToMigrate extends Base { - public static final int MAX_WORK_ITEMS = 10; //Arbitrarily chosen - - protected List workItems; - - public GetIndicesToMigrate(SharedMembers members) { - super(members); - workItems = List.of(); - } - - @Override - public void run() { - logger.info("Pulling a list of indices to migrate from the CMS..."); - workItems = members.cmsClient.getAvailableIndexWorkItems(MAX_WORK_ITEMS); - logger.info("Pulled " + workItems.size() + " indices to migrate:"); - List representationStrings = workItems.stream() - .map(CmsEntry.IndexWorkItem::toRepresentationString) - .collect(Collectors.toList()); - logger.info(representationStrings); - } - - @Override - public WorkerStep nextStep() { - if (workItems.isEmpty()) { - return new ExitPhaseSuccess(members); - } else { - return new MigrateIndices(members, workItems); - } - } - } - - public static class MigrateIndices extends Base { - protected final List workItems; - - public MigrateIndices(SharedMembers members, List workItems) { - super(members); - this.workItems = workItems; - } - - @Override - public void run() { - logger.info("Migrating current batch of indices..."); - for (CmsEntry.IndexWorkItem workItem : workItems) { - /* - * Try to migrate the index. - * - * If we succeed, we forcefully mark it as completed. When we do so, we don't care if someone else has changed - * the record in the meantime; *we* completed it successfully and that's what matters. Because this is the only - * forceful operation on the entry, the other operations are safe to be non-forceful. - * - * If it's already exceeded the number of attempts, we attempt to mark it as failed. If someone else - * has updated the entry in the meantime, we just move on to the next work item. This is safe because - * it means someone else has either marked it as completed or failed, and either is fine. - * - * If we fail to migrate it, we attempt to increment the attempt count. It's fine if the increment - * fails because we guarantee that we'll attempt the work at least N times, not exactly N times. - */ - if (workItem.numAttempts > CmsEntry.IndexWorkItem.ATTEMPTS_SOFT_LIMIT) { - logger.warn("Index Work Item " + workItem.name + " has exceeded the maximum number of attempts; marking it as failed..."); - CmsEntry.IndexWorkItem updatedEntry = new CmsEntry.IndexWorkItem( - workItem.name, - CmsEntry.IndexWorkItemStatus.FAILED, - workItem.numAttempts, - workItem.numShards - ); - - members.cmsClient.updateIndexWorkItem(updatedEntry, workItem).ifPresentOrElse( - value -> logger.info("Index Work Item " + workItem.name + " marked as failed"), - () ->logger.info("Unable to mark Index Work Item " + workItem.name + " as failed") - ); - continue; - } - - try { - logger.info("Migrating index: " + workItem.name); - IndexMetadata.Data indexMetadata = members.metadataFactory.fromRepo(members.snapshotName, workItem.name); - - ObjectNode root = indexMetadata.toObjectNode(); - ObjectNode transformedRoot = members.transformer.transformIndexMetadata(root); - - members.indexCreator.create(transformedRoot, workItem.name, indexMetadata.getId()).ifPresentOrElse( - value -> logger.info("Index " + workItem.name + " created successfully"), - () -> logger.info("Index " + workItem.name + " already existed; no work required") - ); - - logger.info("Forcefully updating the Index Work Item to indicate it has been completed..."); - CmsEntry.IndexWorkItem updatedEntry = new CmsEntry.IndexWorkItem( - workItem.name, - CmsEntry.IndexWorkItemStatus.COMPLETED, - workItem.numAttempts, - workItem.numShards - ); - members.cmsClient.updateIndexWorkItemForceful(updatedEntry); - logger.info("Index Work Item updated"); - } catch (Exception e) { - logger.info("Failed to migrate index: " + workItem.name, e); - logger.info("Updating the Index Work Item with incremented attempt count..."); - CmsEntry.IndexWorkItem updatedEntry = new CmsEntry.IndexWorkItem( - workItem.name, - workItem.status, - workItem.numAttempts + 1, - workItem.numShards - ); - - members.cmsClient.updateIndexWorkItem(updatedEntry, workItem).ifPresentOrElse( - value -> logger.info("Index Work Item " + workItem.name + " attempt count was incremented"), - () ->logger.info("Unable to increment attempt count of Index Work Item " + workItem.name) - ); - } - } - } - - @Override - public WorkerStep nextStep() { - return new GetIndicesToMigrate(members); - } - } - - public static class RandomWait extends Base { - private final static int WAIT_TIME_MS = 5 * 1000; // arbitrarily chosen - - public RandomWait(SharedMembers members) { - super(members); - } - - protected void waitABit() { - try { - Thread.sleep(WAIT_TIME_MS); - } catch (InterruptedException e) { - logger.error("Interrupted while performing a wait", e); - throw new IndexMigrationFailed("Interrupted"); - } - } - - @Override - public void run() { - logger.info("Backing off for " + WAIT_TIME_MS + " milliseconds before checking the Index Migration entry again..."); - waitABit(); - } - - @Override - public WorkerStep nextStep() { - return new GetEntry(members); - } - } - - public static class ExitPhaseSuccess extends Base { - public ExitPhaseSuccess(SharedMembers members) { - super(members); - } - - @Override - public void run() { - logger.info("Marking the Index Migration as completed..."); - CmsEntry.Index lastCmsEntry = members.getCmsEntryNotMissing(); - CmsEntry.Index updatedEntry = new CmsEntry.Index( - CmsEntry.IndexStatus.COMPLETED, - lastCmsEntry.leaseExpiry, - lastCmsEntry.numAttempts - ); - members.cmsClient.updateIndexEntry(updatedEntry, lastCmsEntry); - logger.info("Index Migration marked as completed"); - - logger.info("Index Migration completed, exiting Index Phase..."); - members.globalState.updatePhase(GlobalState.Phase.INDEX_COMPLETED); - } - - @Override - public WorkerStep nextStep() { - return null; - } - } - - public static class ExitPhaseFailed extends Base { - private final IndexMigrationFailed e; - - public ExitPhaseFailed(SharedMembers members, IndexMigrationFailed e) { - super(members); - this.e = e; - } - - @Override - public void run() { - // We either failed the Index Migration or found it had already been failed; either way this - // should not be missing - CmsEntry.Index lastCmsEntry = members.getCmsEntryNotMissing(); - - logger.error("Index Migration failed"); - CmsEntry.Index updatedEntry = new CmsEntry.Index( - CmsEntry.IndexStatus.FAILED, - lastCmsEntry.leaseExpiry, - lastCmsEntry.numAttempts - ); - members.cmsClient.updateIndexEntry(updatedEntry, lastCmsEntry); - members.globalState.updatePhase(GlobalState.Phase.INDEX_FAILED); - } - - @Override - public WorkerStep nextStep() { - throw e; - } - } - - public static class IndexMigrationFailed extends RfsException { - public IndexMigrationFailed(String message) { - super("The Index Migration has failed. Reason: " + message); - } - } - - public static class MissingIndexEntry extends RfsException { - public MissingIndexEntry() { - super("The Index Migration CMS entry we expected to be stored in local memory was null." - + " This should never happen." - ); - } - } - - public static class FoundFailedIndexMigration extends IndexMigrationFailed { - public FoundFailedIndexMigration() { - super("We checked the status in the CMS and found it had failed. Aborting."); - } - } - - public static class MaxAttemptsExceeded extends IndexMigrationFailed { - public MaxAttemptsExceeded() { - super("We reached the limit of " + CmsEntry.Index.MAX_ATTEMPTS + " attempts to complete the Index Migration"); - } - } -} diff --git a/RFS/src/main/java/com/rfs/worker/MetadataRunner.java b/RFS/src/main/java/com/rfs/worker/MetadataRunner.java index ceefc2092..6f874538c 100644 --- a/RFS/src/main/java/com/rfs/worker/MetadataRunner.java +++ b/RFS/src/main/java/com/rfs/worker/MetadataRunner.java @@ -1,65 +1,28 @@ package com.rfs.worker; -import org.apache.logging.log4j.Logger; +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; -import java.util.Optional; - -import org.apache.logging.log4j.LogManager; - -import com.rfs.cms.CmsClient; -import com.rfs.cms.CmsEntry; -import com.rfs.cms.CmsEntry.Metadata; -import com.rfs.cms.CmsEntry.MetadataStatus; import com.rfs.common.GlobalMetadata; import com.rfs.transformers.Transformer; import com.rfs.version_os_2_11.GlobalMetadataCreator_OS_2_11; -public class MetadataRunner implements Runner { - private static final Logger logger = LogManager.getLogger(MetadataRunner.class); - private final MetadataStep.SharedMembers members; - - public MetadataRunner(GlobalState globalState, CmsClient cmsClient, String snapshotName, GlobalMetadata.Factory metadataFactory, - GlobalMetadataCreator_OS_2_11 metadataCreator, Transformer transformer) { - this.members = new MetadataStep.SharedMembers(globalState, cmsClient, snapshotName, metadataFactory, metadataCreator, transformer); - } - - @Override - public void runInternal() { - WorkerStep nextStep = null; - try { - Optional metadataEntry = members.cmsClient.getMetadataEntry(); - - if (metadataEntry.isEmpty() || metadataEntry.get().status != MetadataStatus.COMPLETED) { - nextStep = new MetadataStep.EnterPhase(members); - - while (nextStep != null) { - nextStep.run(); - nextStep = nextStep.nextStep(); - } - } - } catch (Exception e) { - throw new MetadataMigrationPhaseFailed( - members.globalState.getPhase(), - nextStep, - members.cmsEntry.map(bar -> (CmsEntry.Base) bar), - e - ); - } - } - - @Override - public String getPhaseName() { - return "Metadata Migration"; - } - - @Override - public Logger getLogger() { - return logger; - } - - public static class MetadataMigrationPhaseFailed extends Runner.PhaseFailed { - public MetadataMigrationPhaseFailed(GlobalState.Phase phase, WorkerStep nextStep, Optional cmsEntry, Exception e) { - super("Metadata Migration Phase failed", phase, nextStep, cmsEntry, e); - } +@Slf4j +@AllArgsConstructor +public class MetadataRunner { + + private final String snapshotName; + private final GlobalMetadata.Factory metadataFactory; + private final GlobalMetadataCreator_OS_2_11 metadataCreator; + private final Transformer transformer; + + public void migrateMetadata() { + log.info("Migrating the Templates..."); + var globalMetadata = metadataFactory.fromRepo(snapshotName); + var root = globalMetadata.toObjectNode(); + var transformedRoot = transformer.transformGlobalMetadata(root); + metadataCreator.create(transformedRoot); + log.info("Templates migration complete"); } } \ No newline at end of file diff --git a/RFS/src/main/java/com/rfs/worker/MetadataStep.java b/RFS/src/main/java/com/rfs/worker/MetadataStep.java deleted file mode 100644 index 60ed29cac..000000000 --- a/RFS/src/main/java/com/rfs/worker/MetadataStep.java +++ /dev/null @@ -1,356 +0,0 @@ -package com.rfs.worker; - -import java.time.Instant; -import java.util.Optional; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import com.fasterxml.jackson.databind.node.ObjectNode; -import com.rfs.cms.CmsClient; -import com.rfs.cms.CmsEntry; -import com.rfs.cms.OpenSearchCmsClient; -import com.rfs.common.GlobalMetadata; -import com.rfs.common.RfsException; -import com.rfs.transformers.Transformer; -import com.rfs.version_os_2_11.GlobalMetadataCreator_OS_2_11; - - -public class MetadataStep { - public static class SharedMembers { - protected final GlobalState globalState; - protected final CmsClient cmsClient; - protected final String snapshotName; - protected final GlobalMetadata.Factory metadataFactory; - protected final GlobalMetadataCreator_OS_2_11 metadataCreator; - protected final Transformer transformer; - protected Optional cmsEntry; - - public SharedMembers(GlobalState globalState, CmsClient cmsClient, String snapshotName, GlobalMetadata.Factory metadataFactory, - GlobalMetadataCreator_OS_2_11 metadataCreator, Transformer transformer) { - this.globalState = globalState; - this.cmsClient = cmsClient; - this.snapshotName = snapshotName; - this.metadataFactory = metadataFactory; - this.metadataCreator = metadataCreator; - this.transformer = transformer; - this.cmsEntry = Optional.empty(); - } - - // A convient way to check if the CMS entry is present before retrieving it. In some places, it's fine/expected - // for the CMS entry to be missing, but in others, it's a problem. - public CmsEntry.Metadata getCmsEntryNotMissing() { - return cmsEntry.orElseThrow( - () -> new MissingMigrationEntry() - ); - } - } - - public static abstract class Base implements WorkerStep { - protected final Logger logger = LogManager.getLogger(getClass()); - protected final SharedMembers members; - - public Base(SharedMembers members) { - this.members = members; - } - - @Override - public abstract void run(); - - @Override - public abstract WorkerStep nextStep(); - } - - /* - * Updates the Worker's phase to indicate we're doing work on a Metadata Migration - */ - public static class EnterPhase extends Base { - public EnterPhase(SharedMembers members) { - super(members); - } - - @Override - public void run() { - logger.info("Metadata Migration not yet completed, entering Metadata Phase..."); - members.globalState.updatePhase(GlobalState.Phase.METADATA_IN_PROGRESS); - } - - @Override - public WorkerStep nextStep() { - return new GetEntry(members); - } - } - - /* - * Gets the current Metadata Migration entry from the CMS, if it exists - */ - public static class GetEntry extends Base { - - public GetEntry(SharedMembers members) { - super(members); - } - - @Override - public void run() { - logger.info("Pulling the Metadata Migration entry from the CMS, if it exists..."); - members.cmsEntry = members.cmsClient.getMetadataEntry(); - } - - @Override - public WorkerStep nextStep() { - if (members.cmsEntry.isEmpty()) { - return new CreateEntry(members); - } - - CmsEntry.Metadata currentEntry = members.cmsEntry.get(); - switch (currentEntry.status) { - case IN_PROGRESS: - // TODO: This uses the client-side clock to evaluate the lease expiration, when we should - // ideally be using the server-side clock. Consider this a temporary solution until we find - // out how to use the server-side clock. - long leaseExpiryMillis = Long.parseLong(currentEntry.leaseExpiry); - Instant leaseExpiryInstant = Instant.ofEpochMilli(leaseExpiryMillis); - boolean leaseExpired = leaseExpiryInstant.isBefore(Instant.now()); - - // Don't try to acquire the lease if we're already at the max number of attempts - if (currentEntry.numAttempts >= CmsEntry.Metadata.MAX_ATTEMPTS && leaseExpired) { - return new ExitPhaseFailed(members, new MaxAttemptsExceeded()); - } - - if (leaseExpired) { - return new AcquireLease(members); - } - - logger.info("Metadata Migration entry found, but there's already a valid work lease on it"); - return new RandomWait(members); - case COMPLETED: - return new ExitPhaseSuccess(members); - case FAILED: - return new ExitPhaseFailed(members, new FoundFailedMetadataMigration()); - default: - throw new IllegalStateException("Unexpected metadata migration status: " + currentEntry.status); - } - } - } - - public static class CreateEntry extends Base { - - public CreateEntry(SharedMembers members) { - super(members); - } - - @Override - public void run() { - logger.info("Metadata Migration CMS Entry not found, attempting to create it..."); - members.cmsEntry = members.cmsClient.createMetadataEntry(); - logger.info("Metadata Migration CMS Entry created"); - } - - @Override - public WorkerStep nextStep() { - // Migrate the templates if we successfully created the CMS entry; otherwise, circle back to the beginning - if (members.cmsEntry.isPresent()) { - return new MigrateTemplates(members); - } else { - return new GetEntry(members); - } - } - } - - public static class AcquireLease extends Base { - - public AcquireLease(SharedMembers members) { - super(members); - } - - protected long getNowMs() { - return Instant.now().toEpochMilli(); - } - - @Override - public void run() { - // We only get here if we know we want to acquire the lock, so we know the CMS entry should not be null - CmsEntry.Metadata lastCmsEntry = members.getCmsEntryNotMissing(); - - logger.info("Current Metadata Migration work lease appears to have expired; attempting to acquire it..."); - - CmsEntry.Metadata updatedEntry = new CmsEntry.Metadata( - lastCmsEntry.status, - // Set the next CMS entry based on the current one - // TODO: Should be using the server-side clock here - CmsEntry.Metadata.getLeaseExpiry(getNowMs(), lastCmsEntry.numAttempts + 1), - lastCmsEntry.numAttempts + 1 - ); - members.cmsEntry = members.cmsClient.updateMetadataEntry(updatedEntry, lastCmsEntry); - - if (members.cmsEntry.isPresent()) { - logger.info("Lease acquired"); - } else { - logger.info("Failed to acquire lease"); - } - } - - @Override - public WorkerStep nextStep() { - // Migrate the templates if we acquired the lease; otherwise, circle back to the beginning after a backoff - if (members.cmsEntry.isPresent()) { - return new MigrateTemplates(members); - } else { - return new RandomWait(members); - } - } - } - - public static class MigrateTemplates extends Base { - - public MigrateTemplates(SharedMembers members) { - super(members); - } - - @Override - public void run() { - // We only get here if we acquired the lock, so we know the CMS entry should not be missing - CmsEntry.Metadata lastCmsEntry = members.getCmsEntryNotMissing(); - - logger.info("Setting the worker's current work item to be the Metadata Migration..."); - members.globalState.updateWorkItem(new OpenSearchWorkItem(OpenSearchCmsClient.CMS_INDEX_NAME, OpenSearchCmsClient.CMS_METADATA_DOC_ID)); - logger.info("Work item set"); - - logger.info("Migrating the Templates..."); - GlobalMetadata.Data globalMetadata = members.metadataFactory.fromRepo(members.snapshotName); - ObjectNode root = globalMetadata.toObjectNode(); - ObjectNode transformedRoot = members.transformer.transformGlobalMetadata(root); - members.metadataCreator.create(transformedRoot); - logger.info("Templates migration complete"); - - logger.info("Updating the Metadata Migration entry to indicate completion..."); - CmsEntry.Metadata updatedEntry = new CmsEntry.Metadata( - CmsEntry.MetadataStatus.COMPLETED, - lastCmsEntry.leaseExpiry, - lastCmsEntry.numAttempts - ); - members.cmsEntry = members.cmsClient.updateMetadataEntry(updatedEntry, lastCmsEntry); - logger.info("Metadata Migration entry updated"); - - logger.info("Clearing the worker's current work item..."); - members.globalState.updateWorkItem(null); - logger.info("Work item cleared"); - } - - @Override - public WorkerStep nextStep() { - if (members.cmsEntry.isEmpty()) { - // In this scenario, we've done all the work, but failed to update the CMS entry so that we know we've - // done the work. We circle back around to try again, which is made more reasonable by the fact we - // don't re-migrate templates that already exist on the target cluster. If we didn't circle back - // around, there would be a chance that the CMS entry would never be marked as completed. - // - // The CMS entry's retry limit still applies in this case, so there's a limiting factor here. - logger.warn("Completed migrating the templates but failed to update the Metadata Migration entry; retrying..."); - return new GetEntry(members); - } - return new ExitPhaseSuccess(members); - } - } - - public static class RandomWait extends Base { - private final static int WAIT_TIME_MS = 5 * 1000; // arbitrarily chosen - - public RandomWait(SharedMembers members) { - super(members); - } - - protected void waitABit() { - try { - Thread.sleep(WAIT_TIME_MS); - } catch (InterruptedException e) { - logger.error("Interrupted while performing a wait", e); - throw new MetadataMigrationFailed("Interrupted"); - } - } - - @Override - public void run() { - logger.info("Backing off for " + WAIT_TIME_MS + " milliseconds before checking the Metadata Migration entry again..."); - waitABit(); - } - - @Override - public WorkerStep nextStep() { - return new GetEntry(members); - } - } - - public static class ExitPhaseSuccess extends Base { - public ExitPhaseSuccess(SharedMembers members) { - super(members); - } - - @Override - public void run() { - logger.info("Metadata Migration completed, exiting Metadata Phase..."); - members.globalState.updatePhase(GlobalState.Phase.METADATA_COMPLETED); - } - - @Override - public WorkerStep nextStep() { - return null; - } - } - - public static class ExitPhaseFailed extends Base { - private final MetadataMigrationFailed e; - - public ExitPhaseFailed(SharedMembers members, MetadataMigrationFailed e) { - super(members); - this.e = e; - } - - @Override - public void run() { - // We either failed the Metadata Migration or found it had already been failed; either way this - // should not be missing - CmsEntry.Metadata lastCmsEntry = members.getCmsEntryNotMissing(); - - logger.error("Metadata Migration failed"); - CmsEntry.Metadata updatedEntry = new CmsEntry.Metadata( - CmsEntry.MetadataStatus.FAILED, - lastCmsEntry.leaseExpiry, - lastCmsEntry.numAttempts - ); - members.cmsClient.updateMetadataEntry(updatedEntry, lastCmsEntry); - members.globalState.updatePhase(GlobalState.Phase.METADATA_FAILED); - } - - @Override - public WorkerStep nextStep() { - throw e; - } - } - - public static class MissingMigrationEntry extends RfsException { - public MissingMigrationEntry() { - super("The Metadata Migration CMS entry we expected to be stored in local memory was null." - + " This should never happen." - ); - } - } - - public static class MetadataMigrationFailed extends RfsException { - public MetadataMigrationFailed(String message) { - super("The Metadata Migration has failed. Reason: " + message); - } - } - - public static class FoundFailedMetadataMigration extends MetadataMigrationFailed { - public FoundFailedMetadataMigration() { - super("We checked the status in the CMS and found it had failed. Aborting."); - } - } - - public static class MaxAttemptsExceeded extends MetadataMigrationFailed { - public MaxAttemptsExceeded() { - super("We reached the limit of " + CmsEntry.Metadata.MAX_ATTEMPTS + " attempts to complete the Metadata Migration"); - } - } -} diff --git a/RFS/src/main/java/com/rfs/worker/Runner.java b/RFS/src/main/java/com/rfs/worker/Runner.java deleted file mode 100644 index 5b12dce07..000000000 --- a/RFS/src/main/java/com/rfs/worker/Runner.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.rfs.worker; - -import org.apache.logging.log4j.Logger; - -import java.util.Optional; - -import com.rfs.cms.CmsEntry; -import com.rfs.common.RfsException; - -public abstract interface Runner { - abstract void runInternal(); - abstract String getPhaseName(); - abstract Logger getLogger(); - - default void run() { - try { - getLogger().info("Checking if work remains in the " + getPhaseName() +" Phase..."); - runInternal(); - getLogger().info(getPhaseName() + " Phase is complete"); - } catch (Exception e) { - getLogger().error(getPhaseName() + " Phase failed w/ an exception ", e); - - throw e; - } - } - - public static class PhaseFailed extends RfsException { - public final GlobalState.Phase phase; - public final WorkerStep nextStep; - public final Optional cmsEntry; - - public PhaseFailed(String message, GlobalState.Phase phase, WorkerStep nextStep, Optional cmsEntry, Exception e) { - super(message, e); - this.phase = phase; - this.nextStep = nextStep; - this.cmsEntry = cmsEntry; - } - } -} diff --git a/RFS/src/main/java/com/rfs/worker/ShardWorkPreparer.java b/RFS/src/main/java/com/rfs/worker/ShardWorkPreparer.java new file mode 100644 index 000000000..4149cde22 --- /dev/null +++ b/RFS/src/main/java/com/rfs/worker/ShardWorkPreparer.java @@ -0,0 +1,73 @@ +package com.rfs.worker; + +import com.rfs.cms.IWorkCoordinator; +import com.rfs.cms.ScopedWorkCoordinator; +import com.rfs.common.IndexMetadata; +import com.rfs.common.SnapshotRepo; +import lombok.Lombok; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; + +import java.io.IOException; +import java.time.Duration; + +/** + * This class adds workitemes (leasable mutexes) via the WorkCoordinator so that future + * runs of the DocumentsRunner can pick one of those items and migrate the documents for + * that section of work. + */ +@Slf4j +public class ShardWorkPreparer { + + public static final String SHARD_SETUP_WORK_ITEM_ID = "shard_setup"; + + public void run(ScopedWorkCoordinator scopedWorkCoordinator, IndexMetadata.Factory metadataFactory, + String snapshotName) + throws IOException, InterruptedException { + + // ensure that there IS an index to house the shared state that we're going to be manipulating + scopedWorkCoordinator.workCoordinator.setup(); + + scopedWorkCoordinator.ensurePhaseCompletion( + wc -> { + try { + return wc.createOrUpdateLeaseForWorkItem(SHARD_SETUP_WORK_ITEM_ID, Duration.ofMinutes(5)); + } catch (Exception e) { + throw Lombok.sneakyThrow(e); + } + }, + new IWorkCoordinator.WorkAcquisitionOutcomeVisitor() { + @Override + public Void onAlreadyCompleted() throws IOException { + return null; + } + + @Override + public Void onAcquiredWork(IWorkCoordinator.WorkItemAndDuration workItem) throws IOException { + prepareShardWorkItems(scopedWorkCoordinator.workCoordinator, metadataFactory, snapshotName); + return null; + } + + @Override + public Void onNoAvailableWorkToBeDone() throws IOException { + return null; + } + }); + } + + @SneakyThrows + private static void prepareShardWorkItems(IWorkCoordinator workCoordinator, + IndexMetadata.Factory metadataFactory, String snapshotName) { + log.info("Setting up the Documents Work Items..."); + SnapshotRepo.Provider repoDataProvider = metadataFactory.getRepoDataProvider(); + for (SnapshotRepo.Index index : repoDataProvider.getIndicesInSnapshot(snapshotName)) { + IndexMetadata.Data indexMetadata = metadataFactory.fromRepo(snapshotName, index.getName()); + log.info("Index " + indexMetadata.getName() + " has " + indexMetadata.getNumberOfShards() + " shards"); + for (int shardId = 0; shardId < indexMetadata.getNumberOfShards(); shardId++) { + log.info("Creating Documents Work Item for index: " + indexMetadata.getName() + ", shard: " + shardId); + workCoordinator.createUnassignedWorkItem(IndexAndShard.formatAsWorkItemString(indexMetadata.getName(), shardId)); + } + } + log.info("Finished setting up the Documents Work Items."); + } +} diff --git a/RFS/src/main/java/com/rfs/worker/SnapshotRunner.java b/RFS/src/main/java/com/rfs/worker/SnapshotRunner.java index 1b961c2c2..4cdfe40ba 100644 --- a/RFS/src/main/java/com/rfs/worker/SnapshotRunner.java +++ b/RFS/src/main/java/com/rfs/worker/SnapshotRunner.java @@ -1,63 +1,33 @@ package com.rfs.worker; -import org.apache.logging.log4j.Logger; +import lombok.extern.slf4j.Slf4j; -import java.util.Optional; - -import org.apache.logging.log4j.LogManager; - -import com.rfs.cms.CmsClient; -import com.rfs.cms.CmsEntry; -import com.rfs.cms.CmsEntry.Snapshot; -import com.rfs.cms.CmsEntry.SnapshotStatus; import com.rfs.common.SnapshotCreator; -public class SnapshotRunner implements Runner { - private static final Logger logger = LogManager.getLogger(SnapshotRunner.class); - private final SnapshotStep.SharedMembers members; +@Slf4j +public class SnapshotRunner { + private SnapshotRunner() {} - public SnapshotRunner(GlobalState globalState, CmsClient cmsClient, SnapshotCreator snapshotCreator) { - this.members = new SnapshotStep.SharedMembers(globalState, cmsClient, snapshotCreator); + protected static void waitForSnapshotToFinish(SnapshotCreator snapshotCreator) throws InterruptedException { + while (!snapshotCreator.isSnapshotFinished()) { + var waitPeriodMs = 1000; + log.info("Snapshot not finished yet; sleeping for " + waitPeriodMs + "ms..."); + Thread.sleep(waitPeriodMs); + } } - @Override - public void runInternal() { - WorkerStep nextStep = null; - + public static void runAndWaitForCompletion(SnapshotCreator snapshotCreator) { try { - Optional snapshotEntry = members.cmsClient.getSnapshotEntry(members.snapshotCreator.getSnapshotName()); - - if (snapshotEntry.isEmpty() || snapshotEntry.get().status != SnapshotStatus.COMPLETED) { - nextStep = new SnapshotStep.EnterPhase(members, snapshotEntry); - - while (nextStep != null) { - nextStep.run(); - nextStep = nextStep.nextStep(); - } - } - } catch (Exception e) { - throw new SnapshotPhaseFailed( - members.globalState.getPhase(), - nextStep, - members.cmsEntry.map(bar -> (CmsEntry.Base) bar), - e - ); - } - } - - @Override - public String getPhaseName() { - return "Snapshot"; - } - - @Override - public Logger getLogger() { - return logger; - } - - public static class SnapshotPhaseFailed extends Runner.PhaseFailed { - public SnapshotPhaseFailed(GlobalState.Phase phase, WorkerStep nextStep, Optional cmsEntry, Exception e) { - super("Snapshot Phase failed", phase, nextStep, cmsEntry, e); + log.info("Attempting to initiate the snapshot..."); + snapshotCreator.registerRepo(); + snapshotCreator.createSnapshot(); + + log.info("Snapshot in progress..."); + waitForSnapshotToFinish(snapshotCreator); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + log.error("Interrupted while waiting for Snapshot to complete", e); + throw new SnapshotCreator.SnapshotCreationFailed(snapshotCreator.getSnapshotName()); } } } diff --git a/RFS/src/main/java/com/rfs/worker/SnapshotStep.java b/RFS/src/main/java/com/rfs/worker/SnapshotStep.java deleted file mode 100644 index 6b0e38f3f..000000000 --- a/RFS/src/main/java/com/rfs/worker/SnapshotStep.java +++ /dev/null @@ -1,236 +0,0 @@ -package com.rfs.worker; - -import java.util.Optional; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import com.rfs.cms.CmsClient; -import com.rfs.cms.CmsEntry; -import com.rfs.cms.CmsEntry.SnapshotStatus; -import com.rfs.common.RfsException; -import com.rfs.common.SnapshotCreator; -import com.rfs.common.SnapshotCreator.SnapshotCreationFailed; - - -public class SnapshotStep { - public static class SharedMembers { - protected final CmsClient cmsClient; - protected final GlobalState globalState; - protected final SnapshotCreator snapshotCreator; - protected Optional cmsEntry; - - public SharedMembers(GlobalState globalState, CmsClient cmsClient, SnapshotCreator snapshotCreator) { - this.globalState = globalState; - this.cmsClient = cmsClient; - this.snapshotCreator = snapshotCreator; - this.cmsEntry = Optional.empty(); - } - - // A convient way to check if the CMS entry is present before retrieving it. In some places, it's fine/expected - // for the CMS entry to be missing, but in others, it's a problem. - public CmsEntry.Snapshot getCmsEntryNotMissing() { - return cmsEntry.orElseThrow( - () -> new MissingSnapshotEntry() - ); - } - } - - public static abstract class Base implements WorkerStep { - protected final Logger logger = LogManager.getLogger(getClass()); - protected final SharedMembers members; - - public Base(SharedMembers members) { - this.members = members; - } - - @Override - public abstract void run(); - - @Override - public abstract WorkerStep nextStep(); - } - - /* - * Updates the Worker's phase to indicate we're doing work on a Snapshot - */ - public static class EnterPhase extends Base { - - public EnterPhase(SharedMembers members, Optional currentEntry) { - super(members); - this.members.cmsEntry = currentEntry; - } - - @Override - public void run() { - logger.info("Snapshot not yet completed, entering Snapshot Phase..."); - members.globalState.updatePhase(GlobalState.Phase.SNAPSHOT_IN_PROGRESS); - } - - @Override - public WorkerStep nextStep() { - if (members.cmsEntry.isEmpty()) { - return new CreateEntry(members); - } - - CmsEntry.Snapshot currentEntry = members.cmsEntry.get(); - switch (currentEntry.status) { - case NOT_STARTED: - return new InitiateSnapshot(members); - case IN_PROGRESS: - return new WaitForSnapshot(members); - default: - throw new IllegalStateException("Unexpected snapshot status: " + currentEntry.status); - } - } - } - - /* - * Idempotently create a CMS Entry for the Snapshot - */ - public static class CreateEntry extends Base { - public CreateEntry(SharedMembers members) { - super(members); - } - - @Override - public void run() { - logger.info("Snapshot CMS Entry not found, attempting to create it..."); - members.cmsEntry = members.cmsClient.createSnapshotEntry(members.snapshotCreator.getSnapshotName()); - logger.info("Snapshot CMS Entry created"); - } - - @Override - public WorkerStep nextStep() { - return new InitiateSnapshot(members); - } - } - - /* - * Idempotently initiate the Snapshot creation process - */ - public static class InitiateSnapshot extends Base { - public InitiateSnapshot(SharedMembers members) { - super(members); - } - - @Override - public void run() { - // We only get here if we know we want to create a snapshot, so we know the CMS entry should not be null - CmsEntry.Snapshot lastCmsEntry = members.getCmsEntryNotMissing(); - - logger.info("Attempting to initiate the snapshot..."); - members.snapshotCreator.registerRepo(); - members.snapshotCreator.createSnapshot(); - - logger.info("Snapshot in progress..."); - CmsEntry.Snapshot updatedEntry = new CmsEntry.Snapshot(members.snapshotCreator.getSnapshotName(), SnapshotStatus.IN_PROGRESS); - members.cmsEntry = members.cmsClient.updateSnapshotEntry(updatedEntry, lastCmsEntry); - } - - @Override - public WorkerStep nextStep() { - return new WaitForSnapshot(members); - } - } - - /* - * Wait for the Snapshot to complete, regardless of whether we initiated it or not - */ - public static class WaitForSnapshot extends Base { - private SnapshotCreationFailed e = null; - - public WaitForSnapshot(SharedMembers members) { - super(members); - } - - protected void waitABit() throws InterruptedException { - logger.info("Snapshot not finished yet; sleeping for 5 seconds..."); - Thread.sleep(5000); - } - - @Override - public void run() { - try{ - while (!members.snapshotCreator.isSnapshotFinished()) { - waitABit(); - } - } catch (InterruptedException e) { - logger.error("Interrupted while waiting for Snapshot to complete", e); - throw new SnapshotCreationFailed(members.snapshotCreator.getSnapshotName()); - } catch (SnapshotCreationFailed e) { - this.e = e; - } - } - - @Override - public WorkerStep nextStep() { - if (e == null) { - return new ExitPhaseSuccess(members); - } else { - return new ExitPhaseSnapshotFailed(members, e); - } - } - } - - /* - * Update the CMS Entry and the Worker's phase to indicate the Snapshot completed successfully - */ - public static class ExitPhaseSuccess extends Base { - public ExitPhaseSuccess(SharedMembers members) { - super(members); - } - - @Override - public void run() { - CmsEntry.Snapshot lastCmsEntry = members.getCmsEntryNotMissing(); - CmsEntry.Snapshot updatedEntry = new CmsEntry.Snapshot(members.snapshotCreator.getSnapshotName(), SnapshotStatus.COMPLETED); - members.cmsClient.updateSnapshotEntry(updatedEntry, lastCmsEntry); - - members.globalState.updatePhase(GlobalState.Phase.SNAPSHOT_COMPLETED); - logger.info("Snapshot completed, exiting Snapshot Phase..."); - } - - @Override - public WorkerStep nextStep() { - return null; - } - } - - /* - * Update the CMS Entry and the Worker's phase to indicate the Snapshot completed unsuccessfully - */ - public static class ExitPhaseSnapshotFailed extends Base { - private final SnapshotCreationFailed e; - - public ExitPhaseSnapshotFailed(SharedMembers members, SnapshotCreationFailed e) { - super(members); - this.e = e; - } - - @Override - public void run() { - logger.error("Snapshot creation failed"); - CmsEntry.Snapshot lastCmsEntry = members.getCmsEntryNotMissing(); - CmsEntry.Snapshot updatedEntry = new CmsEntry.Snapshot(members.snapshotCreator.getSnapshotName(), SnapshotStatus.FAILED); - members.cmsClient.updateSnapshotEntry(updatedEntry, lastCmsEntry); - - members.globalState.updatePhase(GlobalState.Phase.SNAPSHOT_FAILED); - } - - @Override - public WorkerStep nextStep() { - throw e; - } - } - - - - public static class MissingSnapshotEntry extends RfsException { - public MissingSnapshotEntry() { - super("The Snapshot CMS entry we expected to be stored in local memory was null." - + " This should never happen." - ); - } - } -} diff --git a/RFS/src/main/java/com/rfs/worker/WorkerStep.java b/RFS/src/main/java/com/rfs/worker/WorkerStep.java deleted file mode 100644 index a77c8158e..000000000 --- a/RFS/src/main/java/com/rfs/worker/WorkerStep.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.rfs.worker; - -public interface WorkerStep { - public void run(); - public WorkerStep nextStep(); -} diff --git a/RFS/src/test/java/com/rfs/cms/CmsEntryTest.java b/RFS/src/test/java/com/rfs/cms/CmsEntryTest.java deleted file mode 100644 index a3e511b34..000000000 --- a/RFS/src/test/java/com/rfs/cms/CmsEntryTest.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.rfs.cms; - -import java.util.stream.Stream; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; - -public class CmsEntryTest { - - static Stream provide_Metadata_getLeaseExpiry_HappyPath_args() { - // Generate an argument for each possible number of attempts - Stream argStream = Stream.of(); - for (int i = 1; i <= CmsEntry.Metadata.MAX_ATTEMPTS; i++) { - argStream = Stream.concat(argStream, Stream.of(Arguments.of(i))); - } - return argStream; - } - - @ParameterizedTest - @MethodSource("provide_Metadata_getLeaseExpiry_HappyPath_args") - void Metadata_getLeaseExpiry_HappyPath(int numAttempts) { - // Run the test - String result = CmsEntry.Metadata.getLeaseExpiry(0, numAttempts); - - // Check the results - assertEquals(Long.toString(CmsEntry.Metadata.LEASE_MS * numAttempts), result); - } - - static Stream provide_Metadata_getLeaseExpiry_UnhappyPath_args() { - return Stream.of( - Arguments.of(0), - Arguments.of(CmsEntry.Metadata.MAX_ATTEMPTS + 1) - ); - } - - @ParameterizedTest - @MethodSource("provide_Metadata_getLeaseExpiry_UnhappyPath_args") - void Metadata_getLeaseExpiry_UnhappyPath(int numAttempts) { - // Run the test - assertThrows(CmsEntry.CouldNotGenerateNextLeaseDuration.class, () -> { - CmsEntry.Metadata.getLeaseExpiry(0, numAttempts); - }); - } -} diff --git a/RFS/src/test/java/com/rfs/cms/WorkCoordinatorTest.java b/RFS/src/test/java/com/rfs/cms/WorkCoordinatorTest.java new file mode 100644 index 000000000..a18c4058a --- /dev/null +++ b/RFS/src/test/java/com/rfs/cms/WorkCoordinatorTest.java @@ -0,0 +1,204 @@ +package com.rfs.cms; + + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.opensearch.testcontainers.OpensearchContainer; + +import java.io.IOException; +import java.net.URI; +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Supplier; + +/** + * The contract here is that the first request in will acquire a lease for the duration that was requested. + * + * Once the work is complete, the worker will mark it as such and as long as the workerId matches what was set, + * the work will be marked for completion and no other lease requests will be granted. + * + * When a lease has NOT been acquired, the update request will return a noop. If it was created, + * the expiration period will be equal to the original timestamp that the client sent + the expiration window. + * + * In case there was an expired lease and this worker has acquired the lease, the result will be 'updated'. + * The client will need to retrieve the document to find out what the expiration value was. That means that + * in all non-contentious cases, clients only need to make one call per work item. Multiple calls are only + * required when a lease has expired and a new one is being granted since the worker/client needs to make the + * GET call to find out the new expiration value. + */ +@Slf4j +public class WorkCoordinatorTest { + + final static OpensearchContainer container = + new OpensearchContainer<>("opensearchproject/opensearch:1.3.0"); + public static final String DUMMY_FINISHED_DOC_ID = "dummy_finished_doc"; + private static Supplier httpClientSupplier; + + + @BeforeAll + static void setupOpenSearchContainer() throws Exception { + // Start the container. This step might take some time... + container.start(); + httpClientSupplier = () -> new ApacheHttpClient(URI.create(container.getHttpHostAddress())); + try (var workCoordinator = new OpenSearchWorkCoordinator(httpClientSupplier.get(), + 2, "testWorker")) { + workCoordinator.setup(); + } + } +// +// @BeforeAll +// static void setupOpenSearchContainer() throws Exception { +// httpClientSupplier = () -> new ApacheHttpClient(URI.create("http://localhost:58078")); +// try (var workCoordinator = new OpenSearchWorkCoordinator(httpClientSupplier.get(), +// 3600, "initializer")) { +// try (var httpClient = httpClientSupplier.get()) { +// httpClient.makeJsonRequest("DELETE", OpenSearchWorkCoordinator.INDEX_NAME, null, null); +// } +// workCoordinator.setup(); +// } +// } + + @Test + void testCreateOrUpdateOrReturnAsIsRequest() throws Exception { + var objMapper = new ObjectMapper(); + var docId = "A"; +// +// var response1 = workCoordinator.createOrUpdateLeaseForWorkItem(docId, Duration.ofSeconds(2)); +// Assertions.assertEquals("created", response1.get("result").textValue()); +// var doc1 = client.execute(workCoordinator.makeGetRequest(docId), r -> { +// return objMapper.readTree(r.getEntity().getContent()); +// }); +// Assertions.assertEquals(1, doc1.get("_source").get("numAttempts").longValue()); +// var response2 = client.execute( +// workCoordinator.makeUpdateRequest(docId, "node_1", Instant.now(), 2), +// r -> { +// Assertions.assertEquals(HttpStatus.SC_OK, r.getCode()); +// return objMapper.readTree(r.getEntity().getContent()); +// }); +// var doc2 = client.execute(workCoordinator.makeGetRequest(docId), r -> { +// return objMapper.readTree(r.getEntity().getContent()); +// }); +// Assertions.assertEquals("noop", response2.get("result").textValue(), +// "response that came back was unexpected - document == " + objMapper.writeValueAsString(doc2)); +// Assertions.assertEquals(1, doc2.get("_source").get("numAttempts").longValue()); +// +// Thread.sleep(2500); +// +// var response3 = client.execute( +// workCoordinator.makeUpdateRequest(docId, "node_1", Instant.now(), 2), +// r -> { +// Assertions.assertEquals(HttpStatus.SC_OK, r.getCode()); +// return objMapper.readTree(r.getEntity().getContent()); +// }); +// Assertions.assertEquals("updated", response3.get("result").textValue()); +// var doc3 = client.execute(workCoordinator.makeGetRequest(docId), r -> { +// return objMapper.readTree(r.getEntity().getContent()); +// }); +// Assertions.assertEquals(2, doc3.get("_source").get("numAttempts").longValue()); +// Assertions.assertTrue( +// doc2.get("_source").get("expiration").longValue() < +// doc3.get("_source").get("expiration").longValue()); +// +// var response4 = client.execute( +// workCoordinator.makeCompletionRequest(docId, "node_1", Instant.now()), r -> { +// Assertions.assertEquals(HttpStatus.SC_OK, r.getCode()); +// return objMapper.readTree(r.getEntity().getContent()); +// }); +// var doc4 = client.execute(workCoordinator.makeGetRequest(docId), r -> { +// return objMapper.readTree(r.getEntity().getContent()); +// }); +// Assertions.assertEquals("updated", response4.get("result").textValue()); +// Assertions.assertTrue(doc4.get("_source").get("completedAt").longValue() > 0); +// log.info("doc4="+doc4); + } + + //@Test + public void testAcquireLeaseForQuery() throws Exception { + var objMapper = new ObjectMapper(); + final var NUM_DOCS = 40; + final var MAX_RUNS = 2; + try (var workCoordinator = new OpenSearchWorkCoordinator(httpClientSupplier.get(), + 3600, "docCreatorWorker")) { + Assertions.assertFalse(workCoordinator.workItemsArePending()); + for (var i = 0; i < NUM_DOCS; ++i) { + final var docId = "R" + i; + workCoordinator.createUnassignedWorkItem(docId); + } + Assertions.assertTrue(workCoordinator.workItemsArePending()); + } + + for (int run = 0; run < MAX_RUNS; ++run) { + final var seenWorkerItems = new ConcurrentHashMap(); + var allFutures = new ArrayList>(); + final var expiration = Duration.ofSeconds(5); + var markAsComplete = run + 1 == MAX_RUNS; + for (int i = 0; i < NUM_DOCS; ++i) { + var label = run + "-" + i; + allFutures.add(CompletableFuture.supplyAsync(() -> + getWorkItemAndVerify(label, seenWorkerItems, expiration, markAsComplete))); + } + CompletableFuture.allOf(allFutures.toArray(CompletableFuture[]::new)).join(); + Assertions.assertEquals(NUM_DOCS, seenWorkerItems.size()); + + try (var workCoordinator = new OpenSearchWorkCoordinator(httpClientSupplier.get(), + 3600, "firstPass_NONE")) { + var nextWorkItem = workCoordinator.acquireNextWorkItem(Duration.ofSeconds(2)); + log.error("Next work item picked=" + nextWorkItem); + Assertions.assertNull(nextWorkItem); + } + + Thread.sleep(expiration.multipliedBy(2).toMillis()); + } + try (var workCoordinator = new OpenSearchWorkCoordinator(httpClientSupplier.get(), + 3600, "docCreatorWorker")) { + Assertions.assertFalse(workCoordinator.workItemsArePending()); + } + } + + static AtomicInteger nonce = new AtomicInteger(); + @SneakyThrows + private static String getWorkItemAndVerify(String workerSuffix, ConcurrentHashMap seenWorkerItems, + Duration expirationWindow, boolean markCompleted) { + try (var workCoordinator = new OpenSearchWorkCoordinator(httpClientSupplier.get(), + 3600, "firstPass_"+ workerSuffix)) { + var doneId = DUMMY_FINISHED_DOC_ID + "_" + nonce.incrementAndGet(); + workCoordinator.createOrUpdateLeaseForDocument(doneId, 1); + workCoordinator.completeWorkItem(doneId); + + return workCoordinator.acquireNextWorkItem(expirationWindow).visit( + new IWorkCoordinator.WorkAcquisitionOutcomeVisitor<>() { + @Override + public String onAlreadyCompleted() throws IOException { + throw new IllegalStateException(); + } + + @Override + public String onNoAvailableWorkToBeDone() throws IOException { + throw new IllegalStateException(); + } + + @Override + public String onAcquiredWork(IWorkCoordinator.WorkItemAndDuration workItem) throws IOException { + log.error("Next work item picked=" + workItem); + Assertions.assertNotNull(workItem); + Assertions.assertNotNull(workItem.workItemId); + Assertions.assertTrue(workItem.leaseExpirationTime.isAfter(Instant.now())); + seenWorkerItems.put(workItem.workItemId, workItem.workItemId); + + if (markCompleted) { + workCoordinator.completeWorkItem(workItem.workItemId); + } + return workItem.workItemId; + } + }); + } + } +} diff --git a/RFS/src/test/java/com/rfs/common/LuceneDocumentsReaderTest.java b/RFS/src/test/java/com/rfs/common/LuceneDocumentsReaderTest.java index 2c071083b..587a19805 100644 --- a/RFS/src/test/java/com/rfs/common/LuceneDocumentsReaderTest.java +++ b/RFS/src/test/java/com/rfs/common/LuceneDocumentsReaderTest.java @@ -69,7 +69,8 @@ public class LuceneDocumentsReaderTest { @Test void ReadDocuments_AsExpected() { // Use the TestLuceneDocumentsReader to get the mocked documents - Flux documents = new TestLuceneDocumentsReader(Paths.get("/fake/path")).readDocuments("testIndex", 1); + Flux documents = + new TestLuceneDocumentsReader(Paths.get("/fake/path/testIndex/0")).readDocuments(); // Verify that the results are as expected StepVerifier.create(documents) diff --git a/RFS/src/test/java/com/rfs/framework/SimpleRestoreFromSnapshot_ES_7_10.java b/RFS/src/test/java/com/rfs/framework/SimpleRestoreFromSnapshot_ES_7_10.java index dedc3d8ae..1f4fbb421 100644 --- a/RFS/src/test/java/com/rfs/framework/SimpleRestoreFromSnapshot_ES_7_10.java +++ b/RFS/src/test/java/com/rfs/framework/SimpleRestoreFromSnapshot_ES_7_10.java @@ -57,7 +57,9 @@ public List extractSnapshotIndexData(final String localPath, public void updateTargetCluster(final List indices, final Path unpackedShardDataDir, final OpenSearchClient client) throws Exception { for (final IndexMetadata.Data index : indices) { for (int shardId = 0; shardId < index.getNumberOfShards(); shardId++) { - final var documents = new LuceneDocumentsReader(unpackedShardDataDir).readDocuments(index.getName(), shardId); + final var documents = + new LuceneDocumentsReader(unpackedShardDataDir.resolve(index.getName()).resolve(""+shardId)) + .readDocuments(); final var finalShardId = shardId; new DocumentReindexer(client).reindex(index.getName(), documents) diff --git a/RFS/src/test/java/com/rfs/integration/SnapshotStateTest.java b/RFS/src/test/java/com/rfs/integration/SnapshotStateTest.java index 470de3c5c..0ebb3e303 100644 --- a/RFS/src/test/java/com/rfs/integration/SnapshotStateTest.java +++ b/RFS/src/test/java/com/rfs/integration/SnapshotStateTest.java @@ -31,6 +31,7 @@ /** * Tests focused on setting up different snapshot states and then verifying the behavior of RFS towards the target cluster + * This should move to the CreateSnapshot project */ public class SnapshotStateTest { @@ -43,7 +44,7 @@ public class SnapshotStateTest { @BeforeEach public void setUp() throws Exception { // Start the cluster for testing - cluster = new ElasticsearchContainer(ElasticsearchContainer.Version.V7_10_2); + cluster = new ElasticsearchContainer(ElasticsearchContainer.V7_10_2); cluster.start(); // Configure operations and rfs implementation diff --git a/RFS/src/test/java/com/rfs/worker/DocumentsRunnerTest.java b/RFS/src/test/java/com/rfs/worker/DocumentsRunnerTest.java deleted file mode 100644 index 1f50b176e..000000000 --- a/RFS/src/test/java/com/rfs/worker/DocumentsRunnerTest.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.rfs.worker; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.Mockito.*; - -import java.util.Optional; - -import org.junit.jupiter.api.Test; -import org.mockito.Mockito; - -import com.rfs.cms.CmsClient; -import com.rfs.common.DocumentReindexer; -import com.rfs.common.IndexMetadata; -import com.rfs.common.LuceneDocumentsReader; -import com.rfs.common.RfsException; -import com.rfs.common.ShardMetadata; -import com.rfs.common.SnapshotShardUnpacker; - -public class DocumentsRunnerTest { - - @Test - void run_encountersAnException_asExpected() { - // Setup - GlobalState globalState = Mockito.mock(GlobalState.class); - CmsClient cmsClient = Mockito.mock(CmsClient.class); - String snapshotName = "testSnapshot"; - long maxShardSizeBytes = 50 * 1024 * 1024 * 1024L; - - IndexMetadata.Factory metadataFactory = Mockito.mock(IndexMetadata.Factory.class); - ShardMetadata.Factory shardMetadataFactory = Mockito.mock(ShardMetadata.Factory.class); - SnapshotShardUnpacker.Factory unpackerFactory = Mockito.mock(SnapshotShardUnpacker.Factory.class); - LuceneDocumentsReader reader = Mockito.mock(LuceneDocumentsReader.class); - DocumentReindexer reindexer = Mockito.mock(DocumentReindexer.class); - RfsException testException = new RfsException("Unit test"); - - doThrow(testException).when(cmsClient).getDocumentsEntry(); - when(globalState.getPhase()).thenReturn(GlobalState.Phase.DOCUMENTS_IN_PROGRESS); - - // Run the test - DocumentsRunner testRunner = new DocumentsRunner(globalState, cmsClient, snapshotName, maxShardSizeBytes, metadataFactory, shardMetadataFactory, unpackerFactory, reader, reindexer); - final var e = assertThrows(DocumentsRunner.DocumentsMigrationPhaseFailed.class, () -> testRunner.run()); - - // Verify the results - assertEquals(GlobalState.Phase.DOCUMENTS_IN_PROGRESS, e.phase); - assertEquals(DocumentsStep.GetEntry.class, e.nextStep.getClass()); - assertEquals(Optional.empty(), e.cmsEntry); - assertEquals(testException, e.getCause()); - } -} diff --git a/RFS/src/test/java/com/rfs/worker/DocumentsStepTest.java b/RFS/src/test/java/com/rfs/worker/DocumentsStepTest.java deleted file mode 100644 index c86bca9ac..000000000 --- a/RFS/src/test/java/com/rfs/worker/DocumentsStepTest.java +++ /dev/null @@ -1,709 +0,0 @@ -package com.rfs.worker; - -import java.time.Duration; -import java.time.Instant; -import java.util.Optional; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; -import org.apache.lucene.document.Document; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import org.mockito.Mockito; -import org.mockito.junit.jupiter.MockitoExtension; - -import com.rfs.cms.CmsClient; -import com.rfs.cms.CmsEntry; -import com.rfs.cms.OpenSearchCmsClient; -import com.rfs.common.DocumentReindexer; -import com.rfs.common.IndexMetadata; -import com.rfs.common.LuceneDocumentsReader; -import com.rfs.common.ShardMetadata; -import com.rfs.common.SnapshotRepo; -import com.rfs.common.SnapshotShardUnpacker; -import com.rfs.worker.DocumentsStep.SharedMembers; - -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -import com.rfs.worker.DocumentsStep.MaxAttemptsExceeded; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.argThat; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.*; - - -@ExtendWith(MockitoExtension.class) -public class DocumentsStepTest { - private SharedMembers testMembers; - private SnapshotShardUnpacker unpacker; - - @BeforeEach - void setUp() { - GlobalState globalState = Mockito.mock(GlobalState.class); - CmsClient cmsClient = Mockito.mock(CmsClient.class); - String snapshotName = "test"; - long maxShardSizeBytes = 50 * 1024 * 1024 * 1024L; - - IndexMetadata.Factory metadataFactory = Mockito.mock(IndexMetadata.Factory.class); - ShardMetadata.Factory shardMetadataFactory = Mockito.mock(ShardMetadata.Factory.class); - - unpacker = Mockito.mock(SnapshotShardUnpacker.class); - SnapshotShardUnpacker.Factory unpackerFactory = Mockito.mock(SnapshotShardUnpacker.Factory.class); - lenient().when(unpackerFactory.create(any())).thenReturn(unpacker); - - LuceneDocumentsReader reader = Mockito.mock(LuceneDocumentsReader.class); - DocumentReindexer reindexer = Mockito.mock(DocumentReindexer.class); - testMembers = new SharedMembers(globalState, cmsClient, snapshotName, maxShardSizeBytes, metadataFactory, shardMetadataFactory, unpackerFactory, reader, reindexer); - } - - @Test - void EnterPhase_AsExpected() { - // Run the test - DocumentsStep.EnterPhase testStep = new DocumentsStep.EnterPhase(testMembers); - testStep.run(); - WorkerStep nextStep = testStep.nextStep(); - - // Check the results - Mockito.verify(testMembers.globalState, times(1)).updatePhase(GlobalState.Phase.DOCUMENTS_IN_PROGRESS); - assertEquals(DocumentsStep.GetEntry.class, nextStep.getClass()); - } - - static Stream provideGetEntryArgs() { - return Stream.of( - // There is no CMS entry, so we need to create one - Arguments.of( - Optional.empty(), - DocumentsStep.CreateEntry.class - ), - - // The CMS entry has an expired lease and is under the retry limit, so we try to acquire the lease - Arguments.of( - Optional.of(new CmsEntry.Documents( - CmsEntry.DocumentsStatus.SETUP, - String.valueOf(Instant.now().minus(Duration.ofDays(1)).toEpochMilli()), - CmsEntry.Documents.MAX_ATTEMPTS - 1 - )), - DocumentsStep.AcquireLease.class - ), - - // The CMS entry has an expired lease and is at the retry limit, so we exit as failed - Arguments.of( - Optional.of(new CmsEntry.Documents( - CmsEntry.DocumentsStatus.SETUP, - String.valueOf(Instant.now().minus(Duration.ofDays(1)).toEpochMilli()), - CmsEntry.Documents.MAX_ATTEMPTS - )), - DocumentsStep.ExitPhaseFailed.class - ), - - // The CMS entry has an expired lease and is over the retry limit, so we exit as failed - Arguments.of( - Optional.of(new CmsEntry.Documents( - CmsEntry.DocumentsStatus.SETUP, - String.valueOf(Instant.now().minus(Duration.ofDays(1)).toEpochMilli()), - CmsEntry.Documents.MAX_ATTEMPTS + 1 - )), - DocumentsStep.ExitPhaseFailed.class - ), - - // The CMS entry has valid lease and is under the retry limit, so we back off a bit - Arguments.of( - Optional.of(new CmsEntry.Documents( - CmsEntry.DocumentsStatus.SETUP, - String.valueOf(Instant.now().plus(Duration.ofDays(1)).toEpochMilli()), - CmsEntry.Documents.MAX_ATTEMPTS - 1 - )), - DocumentsStep.RandomWait.class - ), - - // The CMS entry has valid lease and is at the retry limit, so we back off a bit - Arguments.of( - Optional.of(new CmsEntry.Documents( - CmsEntry.DocumentsStatus.SETUP, - String.valueOf(Instant.now().plus(Duration.ofDays(1)).toEpochMilli()), - CmsEntry.Documents.MAX_ATTEMPTS - )), - DocumentsStep.RandomWait.class - ), - - // The CMS entry is marked as in progress, so we try to do some work - Arguments.of( - Optional.of(new CmsEntry.Documents( - CmsEntry.DocumentsStatus.IN_PROGRESS, - String.valueOf(Instant.now().plus(Duration.ofDays(1)).toEpochMilli()), - CmsEntry.Documents.MAX_ATTEMPTS - 1 - )), - DocumentsStep.GetDocumentsToMigrate.class - ), - - // The CMS entry is marked as completed, so we exit as success - Arguments.of( - Optional.of(new CmsEntry.Documents( - CmsEntry.DocumentsStatus.COMPLETED, - String.valueOf(Instant.now().plus(Duration.ofDays(1)).toEpochMilli()), - CmsEntry.Documents.MAX_ATTEMPTS - 1 - )), - DocumentsStep.ExitPhaseSuccess.class - ), - - // The CMS entry is marked as failed, so we exit as failed - Arguments.of( - Optional.of(new CmsEntry.Documents( - CmsEntry.DocumentsStatus.FAILED, - String.valueOf(Instant.now().plus(Duration.ofDays(1)).toEpochMilli()), - CmsEntry.Documents.MAX_ATTEMPTS - 1 - )), - DocumentsStep.ExitPhaseFailed.class - ) - ); - } - - @ParameterizedTest - @MethodSource("provideGetEntryArgs") - void GetEntry_AsExpected(Optional index, Class nextStepClass) { - // Set up the test - Mockito.when(testMembers.cmsClient.getDocumentsEntry()).thenReturn(index); - - // Run the test - DocumentsStep.GetEntry testStep = new DocumentsStep.GetEntry(testMembers); - testStep.run(); - WorkerStep nextStep = testStep.nextStep(); - - // Check the results - Mockito.verify(testMembers.cmsClient, times(1)).getDocumentsEntry(); - assertEquals(nextStepClass, nextStep.getClass()); - } - - static Stream provideCreateEntryArgs() { - return Stream.of( - // We were able to create the CMS entry ourselves, so we have the work lease - Arguments.of( - Optional.of(new CmsEntry.Documents( - CmsEntry.DocumentsStatus.IN_PROGRESS, - String.valueOf(Instant.now().plus(Duration.ofDays(1)).toEpochMilli()), - 1 - )), - DocumentsStep.SetupDocumentsWorkEntries.class - ), - - // We were unable to create the CMS entry ourselves, so we do not have the work lease - Arguments.of(Optional.empty(), DocumentsStep.GetEntry.class) - ); - } - - @ParameterizedTest - @MethodSource("provideCreateEntryArgs") - void CreateEntry_AsExpected(Optional createdEntry, Class nextStepClass) { - // Set up the test - Mockito.when(testMembers.cmsClient.createDocumentsEntry()).thenReturn(createdEntry); - - // Run the test - DocumentsStep.CreateEntry testStep = new DocumentsStep.CreateEntry(testMembers); - testStep.run(); - WorkerStep nextStep = testStep.nextStep(); - - // Check the results - Mockito.verify(testMembers.cmsClient, times(1)).createDocumentsEntry(); - assertEquals(nextStepClass, nextStep.getClass()); - } - - public static class TestAcquireLease extends DocumentsStep.AcquireLease { - public static final int MILLI_SINCE_EPOCH = 42; // Arbitrarily chosen, but predictable - - public TestAcquireLease(SharedMembers members, CmsEntry.Base entry) { - super(members, entry); - } - - @Override - protected long getNowMs() { - return MILLI_SINCE_EPOCH; - } - } - - static Stream provideAcquireLeaseSetupArgs() { - return Stream.of( - // We were able to acquire the lease, and it's on the Document setup step - Arguments.of( - Optional.of(new CmsEntry.Documents( - CmsEntry.DocumentsStatus.SETUP, - String.valueOf(0L), - 1 - )), - Optional.of(new CmsEntry.Documents( - CmsEntry.DocumentsStatus.SETUP, - CmsEntry.Documents.getLeaseExpiry(TestAcquireLease.MILLI_SINCE_EPOCH, 2), - 2 - )), - Optional.of(new CmsEntry.Documents( - CmsEntry.DocumentsStatus.SETUP, - CmsEntry.Documents.getLeaseExpiry(TestAcquireLease.MILLI_SINCE_EPOCH, 2), - 2 - )), - DocumentsStep.SetupDocumentsWorkEntries.class - ), - - // We were unable to acquire the lease - Arguments.of( - Optional.of(new CmsEntry.Documents( - CmsEntry.DocumentsStatus.SETUP, - String.valueOf(0L), - 1 - )), - Optional.of(new CmsEntry.Documents( - CmsEntry.DocumentsStatus.SETUP, - CmsEntry.Documents.getLeaseExpiry(TestAcquireLease.MILLI_SINCE_EPOCH, 2), - 2 - )), - Optional.empty(), - DocumentsStep.RandomWait.class - ) - ); - } - - @ParameterizedTest - @MethodSource("provideAcquireLeaseSetupArgs") - void AcquireLease_Setup_AsExpected( - Optional existingEntry, // The entry we started with, before trying to acquire the lease - Optional updatedEntry, // The entry we try to update to - Optional responseEntry, // The response from the CMS client - Class nextStepClass) { - // Set up the test - testMembers.cmsEntry = existingEntry; - - Mockito.when(testMembers.cmsClient.updateDocumentsEntry( - any(CmsEntry.Documents.class), eq(existingEntry.get()) - )).thenReturn(responseEntry); - - // Run the test - DocumentsStep.AcquireLease testStep = new TestAcquireLease(testMembers, existingEntry.get()); - testStep.run(); - WorkerStep nextStep = testStep.nextStep(); - - // Check the results - Mockito.verify(testMembers.cmsClient, times(1)).updateDocumentsEntry( - updatedEntry.get(), existingEntry.get() - ); - assertEquals(nextStepClass, nextStep.getClass()); - } - - static Stream provideAcquireLeaseWorkArgs() { - return Stream.of( - // We were able to acquire the lease, and it's on a Document Work Item setup - Arguments.of( - Optional.of(new CmsEntry.DocumentsWorkItem( - "index-name", - 1, - CmsEntry.DocumentsWorkItemStatus.NOT_STARTED, - String.valueOf(0L), - 1 - )), - Optional.of(new CmsEntry.DocumentsWorkItem( - "index-name", - 1, - CmsEntry.DocumentsWorkItemStatus.NOT_STARTED, - CmsEntry.DocumentsWorkItem.getLeaseExpiry(TestAcquireLease.MILLI_SINCE_EPOCH, 2), - 2 - )), - Optional.of(new CmsEntry.DocumentsWorkItem( - "index-name", - 1, - CmsEntry.DocumentsWorkItemStatus.NOT_STARTED, - CmsEntry.DocumentsWorkItem.getLeaseExpiry(TestAcquireLease.MILLI_SINCE_EPOCH, 2), - 2 - )), - DocumentsStep.MigrateDocuments.class - ), - - // We were unable to acquire the lease - Arguments.of( - Optional.of(new CmsEntry.DocumentsWorkItem( - "index-name", - 1, - CmsEntry.DocumentsWorkItemStatus.NOT_STARTED, - String.valueOf(0L), - 1 - )), - Optional.of(new CmsEntry.DocumentsWorkItem( - "index-name", - 1, - CmsEntry.DocumentsWorkItemStatus.NOT_STARTED, - CmsEntry.DocumentsWorkItem.getLeaseExpiry(TestAcquireLease.MILLI_SINCE_EPOCH, 2), - 2 - )), - Optional.empty(), - DocumentsStep.RandomWait.class - ) - ); - } - - @ParameterizedTest - @MethodSource("provideAcquireLeaseWorkArgs") - void AcquireLease_Work_AsExpected( - Optional existingEntry, // The entry we started with, before trying to acquire the lease - Optional updatedEntry, // The entry we try to update to - Optional responseEntry, // The response from the CMS client - Class nextStepClass) { - // Set up the test - testMembers.cmsWorkEntry = existingEntry; - - Mockito.when(testMembers.cmsClient.updateDocumentsWorkItem( - any(CmsEntry.DocumentsWorkItem.class), eq(existingEntry.get()) - )).thenReturn(responseEntry); - - // Run the test - DocumentsStep.AcquireLease testStep = new TestAcquireLease(testMembers, existingEntry.get()); - testStep.run(); - WorkerStep nextStep = testStep.nextStep(); - - // Check the results - Mockito.verify(testMembers.cmsClient, times(1)).updateDocumentsWorkItem( - updatedEntry.get(), existingEntry.get() - ); - assertEquals(nextStepClass, nextStep.getClass()); - } - - static Stream provideSetupDocumentsWorkEntriesArgs() { - return Stream.of( - // We were able to mark the setup as completed - Arguments.of( - Optional.of(new CmsEntry.Documents( - CmsEntry.DocumentsStatus.IN_PROGRESS, - String.valueOf(42), - 1 - )), - DocumentsStep.GetDocumentsToMigrate.class - ), - - // We were unable to mark the setup as completed - Arguments.of(Optional.empty(), DocumentsStep.GetEntry.class) - ); - } - - @ParameterizedTest - @MethodSource("provideSetupDocumentsWorkEntriesArgs") - void SetupDocumentsWorkEntries_AsExpected(Optional returnedEntry, Class nextStepClass) { - // Set up the test - SnapshotRepo.Provider repoDataProvider = Mockito.mock(SnapshotRepo.Provider.class); - Mockito.when(testMembers.metadataFactory.getRepoDataProvider()).thenReturn(repoDataProvider); - - SnapshotRepo.Index index1 = Mockito.mock(SnapshotRepo.Index.class); - Mockito.when(index1.getName()).thenReturn("index1"); - SnapshotRepo.Index index2 = Mockito.mock(SnapshotRepo.Index.class); - Mockito.when(index2.getName()).thenReturn("index2"); - Mockito.when(repoDataProvider.getIndicesInSnapshot(testMembers.snapshotName)).thenReturn( - Stream.of(index1, index2).collect(Collectors.toList()) - ); - - IndexMetadata.Data indexMetadata1 = Mockito.mock(IndexMetadata.Data.class); - Mockito.when(indexMetadata1.getName()).thenReturn("index1"); - Mockito.when(indexMetadata1.getNumberOfShards()).thenReturn(1); - Mockito.when(testMembers.metadataFactory.fromRepo(testMembers.snapshotName, "index1")).thenReturn(indexMetadata1); - - IndexMetadata.Data indexMetadata2 = Mockito.mock(IndexMetadata.Data.class); - Mockito.when(indexMetadata2.getName()).thenReturn("index2"); - Mockito.when(indexMetadata2.getNumberOfShards()).thenReturn(2); - Mockito.when(testMembers.metadataFactory.fromRepo(testMembers.snapshotName, "index2")).thenReturn(indexMetadata2); - - var existingEntry = Optional.of(new CmsEntry.Documents( - CmsEntry.DocumentsStatus.SETUP, - String.valueOf(42), - 1 - )); - testMembers.cmsEntry = existingEntry; - - CmsEntry.Documents updatedEntry = new CmsEntry.Documents( - CmsEntry.DocumentsStatus.IN_PROGRESS, - existingEntry.get().leaseExpiry, - existingEntry.get().numAttempts - ); - Mockito.when(testMembers.cmsClient.updateDocumentsEntry( - updatedEntry, existingEntry.get() - )).thenReturn(returnedEntry); - - // Run the test - DocumentsStep.SetupDocumentsWorkEntries testStep = new DocumentsStep.SetupDocumentsWorkEntries(testMembers); - testStep.run(); - WorkerStep nextStep = testStep.nextStep(); - - // Check the results - Mockito.verify(testMembers.globalState, times(1)).updateWorkItem( - argThat(argument -> { - if (!(argument instanceof OpenSearchWorkItem)) { - return false; - } - OpenSearchWorkItem workItem = (OpenSearchWorkItem) argument; - return workItem.indexName.equals(OpenSearchCmsClient.CMS_INDEX_NAME) && - workItem.documentId.equals(OpenSearchCmsClient.CMS_DOCUMENTS_DOC_ID); - }) - ); - Mockito.verify(testMembers.cmsClient, times(1)).createDocumentsWorkItem( - "index1", 0 - ); - Mockito.verify(testMembers.cmsClient, times(1)).createDocumentsWorkItem( - "index2", 0 - ); - Mockito.verify(testMembers.cmsClient, times(1)).createDocumentsWorkItem( - "index2", 1 - ); - - Mockito.verify(testMembers.cmsClient, times(1)).updateDocumentsEntry( - updatedEntry, existingEntry.get() - ); - Mockito.verify(testMembers.globalState, times(1)).updateWorkItem( - null - ); - - assertEquals(nextStepClass, nextStep.getClass()); - } - - static Stream provideGetDocumentsToMigrateArgs() { - return Stream.of( - // There's still work to do - Arguments.of( - Optional.of(new CmsEntry.DocumentsWorkItem("index1", 0, CmsEntry.DocumentsWorkItemStatus.NOT_STARTED, "42", 1)), - DocumentsStep.MigrateDocuments.class - ), - - // There's no more work to do - Arguments.of(Optional.empty(), DocumentsStep.ExitPhaseSuccess.class) - ); - } - - @ParameterizedTest - @MethodSource("provideGetDocumentsToMigrateArgs") - void GetDocumentsToMigrate_AsExpected(Optional workItem, Class nextStepClass) { - // Set up the test - Mockito.when(testMembers.cmsClient.getAvailableDocumentsWorkItem()).thenReturn(workItem); - - // Run the test - DocumentsStep.GetDocumentsToMigrate testStep = new DocumentsStep.GetDocumentsToMigrate(testMembers); - testStep.run(); - WorkerStep nextStep = testStep.nextStep(); - - // Check the results - assertEquals(nextStepClass, nextStep.getClass()); - } - - static Stream provideMigrateDocumentsArgs() { - return Stream.of( - // We have an to migrate and we create it - Arguments.of( - new CmsEntry.DocumentsWorkItem("index1", 0, CmsEntry.DocumentsWorkItemStatus.NOT_STARTED, "42", 1), - new CmsEntry.DocumentsWorkItem("index1", 0, CmsEntry.DocumentsWorkItemStatus.COMPLETED, "42", 1), - new CmsEntry.DocumentsWorkItem("index1", 0, CmsEntry.DocumentsWorkItemStatus.COMPLETED, "42", 1) - ) - ); - } - - @ParameterizedTest - @MethodSource("provideMigrateDocumentsArgs") - void MigrateDocuments_workToDo_AsExpected(CmsEntry.DocumentsWorkItem workItem, CmsEntry.DocumentsWorkItem updatedItem, CmsEntry.DocumentsWorkItem returnedItem) { - // Set up the test - testMembers.cmsWorkEntry = Optional.of(workItem); - - ShardMetadata.Data shardMetadata = Mockito.mock(ShardMetadata.Data.class); - Mockito.when(shardMetadata.getIndexName()).thenReturn(workItem.indexName); - Mockito.when(shardMetadata.getShardId()).thenReturn(workItem.shardId); - Mockito.when(testMembers.shardMetadataFactory.fromRepo(testMembers.snapshotName, workItem.indexName, workItem.shardId)).thenReturn(shardMetadata); - - Flux documents = Mockito.mock(Flux.class); - Mockito.when(testMembers.reader.readDocuments(shardMetadata.getIndexName(), shardMetadata.getShardId())).thenReturn(documents); - - Mockito.when(testMembers.reindexer.reindex(workItem.indexName, documents)).thenReturn(Mono.empty()); - - Mockito.when(testMembers.cmsClient.updateDocumentsWorkItemForceful(updatedItem)).thenReturn(returnedItem); - - // Run the test - DocumentsStep.MigrateDocuments testStep = new DocumentsStep.MigrateDocuments(testMembers); - testStep.run(); - WorkerStep nextStep = testStep.nextStep(); - - // Check the results - Mockito.verify(testMembers.reindexer, times(1)).reindex(workItem.indexName, documents); - Mockito.verify(testMembers.unpackerFactory, times(1)).create(shardMetadata); - Mockito.verify(unpacker, times(1)).close(); - Mockito.verify(testMembers.cmsClient, times(1)).updateDocumentsWorkItemForceful(updatedItem); - assertEquals(DocumentsStep.GetDocumentsToMigrate.class, nextStep.getClass()); - } - - @Test - void MigrateDocuments_failedItem_AsExpected() { - // Set up the test - CmsEntry.DocumentsWorkItem workItem = new CmsEntry.DocumentsWorkItem( - "index1", 0, CmsEntry.DocumentsWorkItemStatus.NOT_STARTED, "42", 1 - ); - testMembers.cmsWorkEntry = Optional.of(workItem); - - CmsEntry.DocumentsWorkItem updatedItem = new CmsEntry.DocumentsWorkItem( - "index1", 0, CmsEntry.DocumentsWorkItemStatus.NOT_STARTED, "42", 2 - ); - - ShardMetadata.Data shardMetadata = Mockito.mock(ShardMetadata.Data.class); - Mockito.when(shardMetadata.getIndexName()).thenReturn(workItem.indexName); - Mockito.when(shardMetadata.getShardId()).thenReturn(workItem.shardId); - Mockito.when(testMembers.shardMetadataFactory.fromRepo(testMembers.snapshotName, workItem.indexName, workItem.shardId)).thenReturn(shardMetadata); - - Flux documents = Mockito.mock(Flux.class); - Mockito.when(testMembers.reader.readDocuments(shardMetadata.getIndexName(), shardMetadata.getShardId())).thenReturn(documents); - - Mockito.doThrow(new RuntimeException("Test exception")).when(testMembers.reindexer).reindex(workItem.indexName, documents); - - // Run the test - DocumentsStep.MigrateDocuments testStep = new DocumentsStep.MigrateDocuments(testMembers); - testStep.run(); - WorkerStep nextStep = testStep.nextStep(); - - // Check the results - Mockito.verify(testMembers.reindexer, times(1)).reindex(workItem.indexName, documents); - Mockito.verify(testMembers.unpackerFactory, times(1)).create(shardMetadata); - Mockito.verify(unpacker, times(1)).close(); - Mockito.verify(testMembers.cmsClient, times(1)).updateDocumentsWorkItem(updatedItem, workItem); - assertEquals(DocumentsStep.GetDocumentsToMigrate.class, nextStep.getClass()); - } - - @Test - void MigrateDocuments_largeShard_AsExpected() { - // Set up the test - CmsEntry.DocumentsWorkItem workItem = new CmsEntry.DocumentsWorkItem( - "index1", 0, CmsEntry.DocumentsWorkItemStatus.NOT_STARTED, "42", 1 - ); - testMembers.cmsWorkEntry = Optional.of(workItem); - - CmsEntry.DocumentsWorkItem updatedItem = new CmsEntry.DocumentsWorkItem( - "index1", 0, CmsEntry.DocumentsWorkItemStatus.NOT_STARTED, "42", 2 - ); - - ShardMetadata.Data shardMetadata = Mockito.mock(ShardMetadata.Data.class); - Mockito.when(shardMetadata.getTotalSizeBytes()).thenReturn(testMembers.maxShardSizeBytes + 1); - Mockito.when(testMembers.shardMetadataFactory.fromRepo(testMembers.snapshotName, workItem.indexName, workItem.shardId)).thenReturn(shardMetadata); - - // Run the test - DocumentsStep.MigrateDocuments testStep = new DocumentsStep.MigrateDocuments(testMembers); - testStep.run(); - WorkerStep nextStep = testStep.nextStep(); - - // Check the results - Mockito.verify(testMembers.cmsClient, times(1)).updateDocumentsWorkItem(updatedItem, workItem); - Mockito.verify(testMembers.unpackerFactory, times(0)).create(shardMetadata); - Mockito.verify(unpacker, times(0)).close(); - assertEquals(DocumentsStep.GetDocumentsToMigrate.class, nextStep.getClass()); - } - - @Test - void MigrateDocuments_exceededAttempts_AsExpected() { - // Set up the test - CmsEntry.DocumentsWorkItem workItem = new CmsEntry.DocumentsWorkItem( - "index1", 0, CmsEntry.DocumentsWorkItemStatus.NOT_STARTED, "42", CmsEntry.DocumentsWorkItem.MAX_ATTEMPTS + 1 - ); - testMembers.cmsWorkEntry = Optional.of(workItem); - - CmsEntry.DocumentsWorkItem updatedItem = new CmsEntry.DocumentsWorkItem( - "index1", 0, CmsEntry.DocumentsWorkItemStatus.FAILED, "42", CmsEntry.DocumentsWorkItem.MAX_ATTEMPTS + 1 - ); - - // Run the test - DocumentsStep.MigrateDocuments testStep = new DocumentsStep.MigrateDocuments(testMembers); - testStep.run(); - WorkerStep nextStep = testStep.nextStep(); - - // Check the results - Mockito.verify(testMembers.cmsClient, times(1)).updateDocumentsWorkItem(updatedItem, workItem); - assertEquals(DocumentsStep.GetDocumentsToMigrate.class, nextStep.getClass()); - } - - public static class TestRandomWait extends DocumentsStep.RandomWait { - public TestRandomWait(SharedMembers members) { - super(members); - } - - @Override - protected void waitABit() { - // do nothing - } - } - - @Test - void RandomWait_AsExpected() { - // Run the test - DocumentsStep.RandomWait testStep = new TestRandomWait(testMembers); - testStep.run(); - WorkerStep nextStep = testStep.nextStep(); - - // Check the results - assertEquals(DocumentsStep.GetEntry.class, nextStep.getClass()); - } - - @Test - void ExitPhaseSuccess_AsExpected() { - // Set up the test - var existingEntry = Optional.of(new CmsEntry.Documents( - CmsEntry.DocumentsStatus.IN_PROGRESS, - String.valueOf(42), - 1 - )); - testMembers.cmsEntry = existingEntry; - - // Run the test - DocumentsStep.ExitPhaseSuccess testStep = new DocumentsStep.ExitPhaseSuccess(testMembers); - testStep.run(); - WorkerStep nextStep = testStep.nextStep(); - - // Check the results - var expectedEntry = new CmsEntry.Documents( - CmsEntry.DocumentsStatus.COMPLETED, - existingEntry.get().leaseExpiry, - existingEntry.get().numAttempts - ); - Mockito.verify(testMembers.cmsClient, times(1)).updateDocumentsEntry( - expectedEntry, existingEntry.get() - ); - - Mockito.verify(testMembers.globalState, times(1)).updatePhase( - GlobalState.Phase.DOCUMENTS_COMPLETED - ); - assertEquals(null, nextStep); - } - - @Test - void ExitPhaseFailed_AsExpected() { - // Set up the test - MaxAttemptsExceeded e = new MaxAttemptsExceeded(); - - var existingEntry = Optional.of(new CmsEntry.Documents( - CmsEntry.DocumentsStatus.SETUP, - String.valueOf(42), - 1 - )); - testMembers.cmsEntry = existingEntry; - - // Run the test - DocumentsStep.ExitPhaseFailed testStep = new DocumentsStep.ExitPhaseFailed(testMembers, e); - testStep.run(); - assertThrows(MaxAttemptsExceeded.class, () -> { - testStep.nextStep(); - }); - - // Check the results - var expectedEntry = new CmsEntry.Documents( - CmsEntry.DocumentsStatus.FAILED, - existingEntry.get().leaseExpiry, - existingEntry.get().numAttempts - ); - Mockito.verify(testMembers.cmsClient, times(1)).updateDocumentsEntry( - expectedEntry, existingEntry.get() - ); - Mockito.verify(testMembers.globalState, times(1)).updatePhase( - GlobalState.Phase.DOCUMENTS_FAILED - ); - } -} diff --git a/RFS/src/test/java/com/rfs/worker/IndexRunnerTest.java b/RFS/src/test/java/com/rfs/worker/IndexRunnerTest.java deleted file mode 100644 index 2ec34bbe3..000000000 --- a/RFS/src/test/java/com/rfs/worker/IndexRunnerTest.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.rfs.worker; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.Mockito.*; - -import java.util.Optional; - -import org.junit.jupiter.api.Test; -import org.mockito.Mockito; - -import com.rfs.cms.CmsClient; -import com.rfs.common.IndexMetadata; -import com.rfs.common.RfsException; -import com.rfs.transformers.Transformer; -import com.rfs.version_os_2_11.IndexCreator_OS_2_11; - -public class IndexRunnerTest { - - @Test - void run_encountersAnException_asExpected() { - // Setup - GlobalState globalState = Mockito.mock(GlobalState.class); - CmsClient cmsClient = Mockito.mock(CmsClient.class); - String snapshotName = "testSnapshot"; - IndexMetadata.Factory metadataFactory = Mockito.mock(IndexMetadata.Factory.class); - IndexCreator_OS_2_11 creator = Mockito.mock(IndexCreator_OS_2_11.class); - Transformer transformer = Mockito.mock(Transformer.class); - RfsException testException = new RfsException("Unit test"); - - doThrow(testException).when(cmsClient).getIndexEntry(); - when(globalState.getPhase()).thenReturn(GlobalState.Phase.INDEX_IN_PROGRESS); - - // Run the test - IndexRunner testRunner = new IndexRunner(globalState, cmsClient, snapshotName, metadataFactory, creator, transformer); - final var e = assertThrows(IndexRunner.IndexMigrationPhaseFailed.class, () -> testRunner.run()); - - // Verify the results - assertEquals(GlobalState.Phase.INDEX_IN_PROGRESS, e.phase); - assertEquals(IndexStep.GetEntry.class, e.nextStep.getClass()); - assertEquals(Optional.empty(), e.cmsEntry); - assertEquals(testException, e.getCause()); - } -} diff --git a/RFS/src/test/java/com/rfs/worker/IndexStepTest.java b/RFS/src/test/java/com/rfs/worker/IndexStepTest.java deleted file mode 100644 index 40f13cb05..000000000 --- a/RFS/src/test/java/com/rfs/worker/IndexStepTest.java +++ /dev/null @@ -1,551 +0,0 @@ -package com.rfs.worker; - -import java.time.Duration; -import java.time.Instant; -import java.util.List; -import java.util.Optional; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import org.mockito.Mockito; -import org.mockito.junit.jupiter.MockitoExtension; - -import com.fasterxml.jackson.databind.node.ObjectNode; -import com.rfs.cms.CmsClient; -import com.rfs.cms.CmsEntry; -import com.rfs.cms.OpenSearchCmsClient; -import com.rfs.common.IndexMetadata; -import com.rfs.common.SnapshotRepo; -import com.rfs.transformers.Transformer; -import com.rfs.version_os_2_11.IndexCreator_OS_2_11; -import com.rfs.worker.IndexStep.SharedMembers; -import com.rfs.worker.IndexStep.MaxAttemptsExceeded; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.argThat; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.*; - -@ExtendWith(MockitoExtension.class) -public class IndexStepTest { - private SharedMembers testMembers; - - @BeforeEach - void setUp() { - GlobalState globalState = Mockito.mock(GlobalState.class); - CmsClient cmsClient = Mockito.mock(CmsClient.class); - String snapshotName = "test"; - - IndexMetadata.Factory metadataFactory = Mockito.mock(IndexMetadata.Factory.class); - IndexCreator_OS_2_11 indexCreator = Mockito.mock(IndexCreator_OS_2_11.class); - Transformer transformer = Mockito.mock(Transformer.class); - testMembers = new SharedMembers(globalState, cmsClient, snapshotName, metadataFactory, indexCreator, transformer); - } - - @Test - void EnterPhase_AsExpected() { - // Run the test - IndexStep.EnterPhase testStep = new IndexStep.EnterPhase(testMembers); - testStep.run(); - WorkerStep nextStep = testStep.nextStep(); - - // Check the results - Mockito.verify(testMembers.globalState, times(1)).updatePhase(GlobalState.Phase.INDEX_IN_PROGRESS); - assertEquals(IndexStep.GetEntry.class, nextStep.getClass()); - } - - static Stream provideGetEntryArgs() { - return Stream.of( - // There is no CMS entry, so we need to create one - Arguments.of( - Optional.empty(), - IndexStep.CreateEntry.class - ), - - // The CMS entry has an expired lease and is under the retry limit, so we try to acquire the lease - Arguments.of( - Optional.of(new CmsEntry.Index( - CmsEntry.IndexStatus.SETUP, - String.valueOf(Instant.now().minus(Duration.ofDays(1)).toEpochMilli()), - CmsEntry.Index.MAX_ATTEMPTS - 1 - )), - IndexStep.AcquireLease.class - ), - - // The CMS entry has an expired lease and is at the retry limit, so we exit as failed - Arguments.of( - Optional.of(new CmsEntry.Index( - CmsEntry.IndexStatus.SETUP, - String.valueOf(Instant.now().minus(Duration.ofDays(1)).toEpochMilli()), - CmsEntry.Index.MAX_ATTEMPTS - )), - IndexStep.ExitPhaseFailed.class - ), - - // The CMS entry has an expired lease and is over the retry limit, so we exit as failed - Arguments.of( - Optional.of(new CmsEntry.Index( - CmsEntry.IndexStatus.SETUP, - String.valueOf(Instant.now().minus(Duration.ofDays(1)).toEpochMilli()), - CmsEntry.Index.MAX_ATTEMPTS + 1 - )), - IndexStep.ExitPhaseFailed.class - ), - - // The CMS entry has valid lease and is under the retry limit, so we back off a bit - Arguments.of( - Optional.of(new CmsEntry.Index( - CmsEntry.IndexStatus.SETUP, - String.valueOf(Instant.now().plus(Duration.ofDays(1)).toEpochMilli()), - CmsEntry.Index.MAX_ATTEMPTS - 1 - )), - IndexStep.RandomWait.class - ), - - // The CMS entry has valid lease and is at the retry limit, so we back off a bit - Arguments.of( - Optional.of(new CmsEntry.Index( - CmsEntry.IndexStatus.SETUP, - String.valueOf(Instant.now().plus(Duration.ofDays(1)).toEpochMilli()), - CmsEntry.Index.MAX_ATTEMPTS - )), - IndexStep.RandomWait.class - ), - - // The CMS entry is marked as in progress, so we try to do some work - Arguments.of( - Optional.of(new CmsEntry.Index( - CmsEntry.IndexStatus.IN_PROGRESS, - String.valueOf(Instant.now().plus(Duration.ofDays(1)).toEpochMilli()), - CmsEntry.Index.MAX_ATTEMPTS - 1 - )), - IndexStep.GetIndicesToMigrate.class - ), - - // The CMS entry is marked as completed, so we exit as success - Arguments.of( - Optional.of(new CmsEntry.Index( - CmsEntry.IndexStatus.COMPLETED, - String.valueOf(Instant.now().plus(Duration.ofDays(1)).toEpochMilli()), - CmsEntry.Index.MAX_ATTEMPTS - 1 - )), - IndexStep.ExitPhaseSuccess.class - ), - - // The CMS entry is marked as failed, so we exit as failed - Arguments.of( - Optional.of(new CmsEntry.Index( - CmsEntry.IndexStatus.FAILED, - String.valueOf(Instant.now().plus(Duration.ofDays(1)).toEpochMilli()), - CmsEntry.Index.MAX_ATTEMPTS - 1 - )), - IndexStep.ExitPhaseFailed.class - ) - ); - } - - @ParameterizedTest - @MethodSource("provideGetEntryArgs") - void GetEntry_AsExpected(Optional index, Class nextStepClass) { - // Set up the test - Mockito.when(testMembers.cmsClient.getIndexEntry()).thenReturn(index); - - // Run the test - IndexStep.GetEntry testStep = new IndexStep.GetEntry(testMembers); - testStep.run(); - WorkerStep nextStep = testStep.nextStep(); - - // Check the results - Mockito.verify(testMembers.cmsClient, times(1)).getIndexEntry(); - assertEquals(nextStepClass, nextStep.getClass()); - } - - static Stream provideCreateEntryArgs() { - return Stream.of( - // We were able to create the CMS entry ourselves, so we have the work lease - Arguments.of( - Optional.of(new CmsEntry.Index( - CmsEntry.IndexStatus.IN_PROGRESS, - String.valueOf(Instant.now().plus(Duration.ofDays(1)).toEpochMilli()), - 1 - )), - IndexStep.SetupIndexWorkEntries.class - ), - - // We were unable to create the CMS entry ourselves, so we do not have the work lease - Arguments.of(Optional.empty(), IndexStep.GetEntry.class) - ); - } - - @ParameterizedTest - @MethodSource("provideCreateEntryArgs") - void CreateEntry_AsExpected(Optional createdEntry, Class nextStepClass) { - // Set up the test - Mockito.when(testMembers.cmsClient.createIndexEntry()).thenReturn(createdEntry); - - // Run the test - IndexStep.CreateEntry testStep = new IndexStep.CreateEntry(testMembers); - testStep.run(); - WorkerStep nextStep = testStep.nextStep(); - - // Check the results - Mockito.verify(testMembers.cmsClient, times(1)).createIndexEntry(); - assertEquals(nextStepClass, nextStep.getClass()); - } - - public static class TestAcquireLease extends IndexStep.AcquireLease { - public static final int milliSinceEpoch = 42; // Arbitrarily chosen, but predictable - - public TestAcquireLease(SharedMembers members) { - super(members); - } - - @Override - protected long getNowMs() { - return milliSinceEpoch; - } - } - - static Stream provideAcquireLeaseArgs() { - return Stream.of( - // We were able to acquire the lease - Arguments.of( - Optional.of(new CmsEntry.Index( - CmsEntry.IndexStatus.SETUP, - String.valueOf(Instant.now().plus(Duration.ofDays(1)).toEpochMilli()), - 1 - )), - IndexStep.SetupIndexWorkEntries.class - ), - - // We were unable to acquire the lease - Arguments.of(Optional.empty(), IndexStep.RandomWait.class) - ); - } - - @ParameterizedTest - @MethodSource("provideAcquireLeaseArgs") - void AcquireLease_AsExpected(Optional updatedEntry, Class nextStepClass) { - // Set up the test - var existingEntry = Optional.of(new CmsEntry.Index( - CmsEntry.IndexStatus.SETUP, - CmsEntry.Index.getLeaseExpiry(0L, CmsEntry.Index.MAX_ATTEMPTS - 1), - CmsEntry.Index.MAX_ATTEMPTS - 1 - )); - testMembers.cmsEntry = existingEntry; - - Mockito.when(testMembers.cmsClient.updateIndexEntry( - any(CmsEntry.Index.class), eq(existingEntry.get()) - )).thenReturn(updatedEntry); - - // Run the test - IndexStep.AcquireLease testStep = new TestAcquireLease(testMembers); - testStep.run(); - WorkerStep nextStep = testStep.nextStep(); - - // Check the results - var expectedEntry = new CmsEntry.Index( - CmsEntry.IndexStatus.SETUP, - CmsEntry.Index.getLeaseExpiry(TestAcquireLease.milliSinceEpoch, CmsEntry.Index.MAX_ATTEMPTS), - CmsEntry.Index.MAX_ATTEMPTS - ); - Mockito.verify(testMembers.cmsClient, times(1)).updateIndexEntry( - expectedEntry, existingEntry.get() - ); - assertEquals(nextStepClass, nextStep.getClass()); - } - - static Stream provideSetupIndexWorkEntriesArgs() { - return Stream.of( - // We were able to mark the setup as completed - Arguments.of( - Optional.of(new CmsEntry.Index( - CmsEntry.IndexStatus.IN_PROGRESS, - String.valueOf(42), - 1 - )), - IndexStep.GetIndicesToMigrate.class - ), - - // We were unable to mark the setup as completed - Arguments.of(Optional.empty(), IndexStep.GetEntry.class) - ); - } - - @ParameterizedTest - @MethodSource("provideSetupIndexWorkEntriesArgs") - void SetupIndexWorkEntries_AsExpected(Optional updatedEntry, Class nextStepClass) { - // Set up the test - var existingEntry = Optional.of(new CmsEntry.Index( - CmsEntry.IndexStatus.SETUP, - String.valueOf(42), - 1 - )); - testMembers.cmsEntry = existingEntry; - - SnapshotRepo.Provider repoDatProvider = Mockito.mock(SnapshotRepo.Provider.class); - Mockito.when(testMembers.metadataFactory.getRepoDataProvider()).thenReturn(repoDatProvider); - - SnapshotRepo.Index index1 = Mockito.mock(SnapshotRepo.Index.class); - Mockito.when(index1.getName()).thenReturn("index1"); - SnapshotRepo.Index index2 = Mockito.mock(SnapshotRepo.Index.class); - Mockito.when(index2.getName()).thenReturn("index2"); - Mockito.when(repoDatProvider.getIndicesInSnapshot(testMembers.snapshotName)).thenReturn( - Stream.of(index1, index2).collect(Collectors.toList()) - ); - - IndexMetadata.Data indexMetadata1 = Mockito.mock(IndexMetadata.Data.class); - Mockito.when(indexMetadata1.getName()).thenReturn("index1"); - Mockito.when(indexMetadata1.getNumberOfShards()).thenReturn(1); - Mockito.when(testMembers.metadataFactory.fromRepo(testMembers.snapshotName, "index1")).thenReturn(indexMetadata1); - - IndexMetadata.Data indexMetadata2 = Mockito.mock(IndexMetadata.Data.class); - Mockito.when(indexMetadata2.getName()).thenReturn("index2"); - Mockito.when(indexMetadata2.getNumberOfShards()).thenReturn(2); - Mockito.when(testMembers.metadataFactory.fromRepo(testMembers.snapshotName, "index2")).thenReturn(indexMetadata2); - - CmsEntry.Index expectedEntry = new CmsEntry.Index( - CmsEntry.IndexStatus.IN_PROGRESS, - existingEntry.get().leaseExpiry, - existingEntry.get().numAttempts - ); - Mockito.when(testMembers.cmsClient.updateIndexEntry( - expectedEntry, existingEntry.get() - )).thenReturn(updatedEntry); - - // Run the test - IndexStep.SetupIndexWorkEntries testStep = new IndexStep.SetupIndexWorkEntries(testMembers); - testStep.run(); - WorkerStep nextStep = testStep.nextStep(); - - // Check the results - Mockito.verify(testMembers.globalState, times(1)).updateWorkItem( - argThat(argument -> { - if (!(argument instanceof OpenSearchWorkItem)) { - return false; - } - OpenSearchWorkItem workItem = (OpenSearchWorkItem) argument; - return workItem.indexName.equals(OpenSearchCmsClient.CMS_INDEX_NAME) && - workItem.documentId.equals(OpenSearchCmsClient.CMS_INDEX_DOC_ID); - }) - ); - Mockito.verify(testMembers.cmsClient, times(1)).createIndexWorkItem( - "index1", 1 - ); - Mockito.verify(testMembers.cmsClient, times(1)).createIndexWorkItem( - "index2", 2 - ); - - Mockito.verify(testMembers.cmsClient, times(1)).updateIndexEntry( - expectedEntry, existingEntry.get() - ); - Mockito.verify(testMembers.globalState, times(1)).updateWorkItem( - null - ); - - assertEquals(nextStepClass, nextStep.getClass()); - } - - static Stream provideGetIndicesToMigrateArgs() { - return Stream.of( - // There's still work to do - Arguments.of( - Stream.of( - new CmsEntry.IndexWorkItem("index1", CmsEntry.IndexWorkItemStatus.NOT_STARTED, 1, 1), - new CmsEntry.IndexWorkItem("index2", CmsEntry.IndexWorkItemStatus.NOT_STARTED, 1, 2) - ).collect(Collectors.toList()), - IndexStep.MigrateIndices.class - ), - - // There's no more work to do - Arguments.of(List.of(), IndexStep.ExitPhaseSuccess.class) - ); - } - - @ParameterizedTest - @MethodSource("provideGetIndicesToMigrateArgs") - void GetIndicesToMigrate_AsExpected(List workItems, Class nextStepClass) { - // Set up the test - Mockito.when(testMembers.cmsClient.getAvailableIndexWorkItems(IndexStep.GetIndicesToMigrate.MAX_WORK_ITEMS)).thenReturn(workItems); - - // Run the test - IndexStep.GetIndicesToMigrate testStep = new IndexStep.GetIndicesToMigrate(testMembers); - testStep.run(); - WorkerStep nextStep = testStep.nextStep(); - - // Check the results - assertEquals(nextStepClass, nextStep.getClass()); - } - - static Stream provideMigrateIndicesArgs() { - return Stream.of( - // We have an to migrate and we create it - Arguments.of( - new CmsEntry.IndexWorkItem("index1", CmsEntry.IndexWorkItemStatus.NOT_STARTED, 1, 1), - Optional.of(Mockito.mock(ObjectNode.class)), - new CmsEntry.IndexWorkItem("index1", CmsEntry.IndexWorkItemStatus.COMPLETED, 1, 1) - ), - - // We have an index to migrate and someone else created it before us - Arguments.of( - new CmsEntry.IndexWorkItem("index2", CmsEntry.IndexWorkItemStatus.NOT_STARTED, 1, 2), - Optional.empty(), - new CmsEntry.IndexWorkItem("index2", CmsEntry.IndexWorkItemStatus.COMPLETED, 1, 2) - ) - ); - } - - @ParameterizedTest - @MethodSource("provideMigrateIndicesArgs") - void MigrateIndices_workToDo_AsExpected(CmsEntry.IndexWorkItem workItem, Optional createResponse, CmsEntry.IndexWorkItem updatedItem) { - // Set up the test - IndexMetadata.Data indexMetadata = Mockito.mock(IndexMetadata.Data.class); - Mockito.when(testMembers.metadataFactory.fromRepo(testMembers.snapshotName, workItem.name)).thenReturn(indexMetadata); - - ObjectNode root = Mockito.mock(ObjectNode.class); - Mockito.when(indexMetadata.toObjectNode()).thenReturn(root); - Mockito.when(indexMetadata.getId()).thenReturn("index-id"); - ObjectNode transformedRoot = Mockito.mock(ObjectNode.class); - Mockito.when(testMembers.transformer.transformIndexMetadata(root)).thenReturn(transformedRoot); - Mockito.when(testMembers.indexCreator.create(transformedRoot, workItem.name, "index-id")).thenReturn(createResponse); - - // Run the test - IndexStep.MigrateIndices testStep = new IndexStep.MigrateIndices(testMembers, List.of(workItem)); - testStep.run(); - WorkerStep nextStep = testStep.nextStep(); - - // Check the results - Mockito.verify(testMembers.cmsClient, times(1)).updateIndexWorkItemForceful(updatedItem); - assertEquals(IndexStep.GetIndicesToMigrate.class, nextStep.getClass()); - } - - @Test - void MigrateIndices_exceededAttempts_AsExpected(){ - // Set up the test - CmsEntry.IndexWorkItem workItem = new CmsEntry.IndexWorkItem("index1", CmsEntry.IndexWorkItemStatus.NOT_STARTED, CmsEntry.IndexWorkItem.ATTEMPTS_SOFT_LIMIT + 1, 1); - CmsEntry.IndexWorkItem updatedItem = new CmsEntry.IndexWorkItem("index1", CmsEntry.IndexWorkItemStatus.FAILED, CmsEntry.IndexWorkItem.ATTEMPTS_SOFT_LIMIT + 1, 1); - - // Run the test - IndexStep.MigrateIndices testStep = new IndexStep.MigrateIndices(testMembers, List.of(workItem)); - testStep.run(); - WorkerStep nextStep = testStep.nextStep(); - - // Check the results - Mockito.verify(testMembers.cmsClient, times(1)).updateIndexWorkItem(updatedItem, workItem); - assertEquals(IndexStep.GetIndicesToMigrate.class, nextStep.getClass()); - } - - @Test - void MigrateIndices_workItemFailed_AsExpected(){ - // Set up the test - CmsEntry.IndexWorkItem workItem = new CmsEntry.IndexWorkItem("index1", CmsEntry.IndexWorkItemStatus.NOT_STARTED, 1, 1); - CmsEntry.IndexWorkItem updatedItem = new CmsEntry.IndexWorkItem("index1", CmsEntry.IndexWorkItemStatus.NOT_STARTED, 2, 1); - doThrow(new RuntimeException("Test exception")).when(testMembers.metadataFactory).fromRepo(testMembers.snapshotName, workItem.name); - - // Run the test - IndexStep.MigrateIndices testStep = new IndexStep.MigrateIndices(testMembers, List.of(workItem)); - testStep.run(); - WorkerStep nextStep = testStep.nextStep(); - - // Check the results - Mockito.verify(testMembers.cmsClient, times(1)).updateIndexWorkItem(updatedItem, workItem); - assertEquals(IndexStep.GetIndicesToMigrate.class, nextStep.getClass()); - } - - public static class TestRandomWait extends IndexStep.RandomWait { - public TestRandomWait(SharedMembers members) { - super(members); - } - - @Override - protected void waitABit() { - // do nothing - } - } - - @Test - void RandomWait_AsExpected() { - // Run the test - IndexStep.RandomWait testStep = new TestRandomWait(testMembers); - testStep.run(); - WorkerStep nextStep = testStep.nextStep(); - - // Check the results - assertEquals(IndexStep.GetEntry.class, nextStep.getClass()); - } - - @Test - void ExitPhaseSuccess_AsExpected() { - // Set up the test - var existingEntry = Optional.of(new CmsEntry.Index( - CmsEntry.IndexStatus.IN_PROGRESS, - String.valueOf(42), - 1 - )); - testMembers.cmsEntry = existingEntry; - - // Run the test - IndexStep.ExitPhaseSuccess testStep = new IndexStep.ExitPhaseSuccess(testMembers); - testStep.run(); - WorkerStep nextStep = testStep.nextStep(); - - // Check the results - var expectedEntry = new CmsEntry.Index( - CmsEntry.IndexStatus.COMPLETED, - existingEntry.get().leaseExpiry, - existingEntry.get().numAttempts - ); - Mockito.verify(testMembers.cmsClient, times(1)).updateIndexEntry( - expectedEntry, existingEntry.get() - ); - - Mockito.verify(testMembers.globalState, times(1)).updatePhase( - GlobalState.Phase.INDEX_COMPLETED - ); - assertEquals(null, nextStep); - } - - @Test - void ExitPhaseFailed_AsExpected() { - // Set up the test - MaxAttemptsExceeded e = new MaxAttemptsExceeded(); - - var existingEntry = Optional.of(new CmsEntry.Index( - CmsEntry.IndexStatus.SETUP, - String.valueOf(42), - 1 - )); - testMembers.cmsEntry = existingEntry; - - // Run the test - IndexStep.ExitPhaseFailed testStep = new IndexStep.ExitPhaseFailed(testMembers, e); - testStep.run(); - assertThrows(MaxAttemptsExceeded.class, () -> { - testStep.nextStep(); - }); - - // Check the results - var expectedEntry = new CmsEntry.Index( - CmsEntry.IndexStatus.FAILED, - existingEntry.get().leaseExpiry, - existingEntry.get().numAttempts - ); - Mockito.verify(testMembers.cmsClient, times(1)).updateIndexEntry( - expectedEntry, existingEntry.get() - ); - Mockito.verify(testMembers.globalState, times(1)).updatePhase( - GlobalState.Phase.INDEX_FAILED - ); - } - -} diff --git a/RFS/src/test/java/com/rfs/worker/MetadataRunnerTest.java b/RFS/src/test/java/com/rfs/worker/MetadataRunnerTest.java deleted file mode 100644 index 16a242dd3..000000000 --- a/RFS/src/test/java/com/rfs/worker/MetadataRunnerTest.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.rfs.worker; - -import static org.junit.Assert.assertThrows; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.Mockito.*; - -import java.util.Optional; - -import org.junit.jupiter.api.Test; -import org.mockito.Mockito; - -import com.rfs.cms.CmsClient; -import com.rfs.common.GlobalMetadata; -import com.rfs.common.RfsException; -import com.rfs.transformers.Transformer; -import com.rfs.version_os_2_11.GlobalMetadataCreator_OS_2_11; - -class MetadataRunnerTest { - - @Test - void run_encountersAnException_asExpected() { - // Setup - GlobalState globalState = Mockito.mock(GlobalState.class); - CmsClient cmsClient = Mockito.mock(CmsClient.class); - String snapshotName = "testSnapshot"; - GlobalMetadata.Factory metadataFactory = Mockito.mock(GlobalMetadata.Factory.class); - GlobalMetadataCreator_OS_2_11 metadataCreator = Mockito.mock(GlobalMetadataCreator_OS_2_11.class); - Transformer transformer = Mockito.mock(Transformer.class); - RfsException testException = new RfsException("Unit test"); - - doThrow(testException).when(cmsClient).getMetadataEntry(); - when(globalState.getPhase()).thenReturn(GlobalState.Phase.METADATA_IN_PROGRESS); - - // Run the test - MetadataRunner testRunner = new MetadataRunner(globalState, cmsClient, snapshotName, metadataFactory, metadataCreator, transformer); - final var e = assertThrows(MetadataRunner.MetadataMigrationPhaseFailed.class, () -> testRunner.run()); - - // Verify the results - assertEquals(GlobalState.Phase.METADATA_IN_PROGRESS, e.phase); - assertEquals(null, e.nextStep); - assertEquals(Optional.empty(), e.cmsEntry); - assertEquals(testException, e.getCause()); - } - -} \ No newline at end of file diff --git a/RFS/src/test/java/com/rfs/worker/MetadataStepTest.java b/RFS/src/test/java/com/rfs/worker/MetadataStepTest.java deleted file mode 100644 index 3d0ec01f0..000000000 --- a/RFS/src/test/java/com/rfs/worker/MetadataStepTest.java +++ /dev/null @@ -1,402 +0,0 @@ -package com.rfs.worker; - -import java.time.Duration; -import java.time.Instant; -import java.util.Optional; -import java.util.stream.Stream; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import org.mockito.Mockito; -import org.mockito.junit.jupiter.MockitoExtension; - -import com.fasterxml.jackson.databind.node.ObjectNode; -import com.rfs.cms.CmsClient; -import com.rfs.cms.CmsEntry; -import com.rfs.cms.OpenSearchCmsClient; -import com.rfs.common.GlobalMetadata; -import com.rfs.transformers.Transformer; -import com.rfs.version_os_2_11.GlobalMetadataCreator_OS_2_11; -import com.rfs.worker.MetadataStep.MaxAttemptsExceeded; -import com.rfs.worker.MetadataStep.SharedMembers; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.argThat; -import static org.mockito.Mockito.*; - - -@ExtendWith(MockitoExtension.class) -public class MetadataStepTest { - private SharedMembers testMembers; - - @BeforeEach - void setUp() { - GlobalState globalState = Mockito.mock(GlobalState.class); - CmsClient cmsClient = Mockito.mock(CmsClient.class); - String snapshotName = "test"; - GlobalMetadata.Factory metadataFactory = Mockito.mock(GlobalMetadata.Factory.class); - GlobalMetadataCreator_OS_2_11 metadataCreator = Mockito.mock(GlobalMetadataCreator_OS_2_11.class); - Transformer transformer = Mockito.mock(Transformer.class); - testMembers = new SharedMembers(globalState, cmsClient, snapshotName, metadataFactory, metadataCreator, transformer); - } - - @Test - void EnterPhase_AsExpected() { - // Run the test - MetadataStep.EnterPhase testStep = new MetadataStep.EnterPhase(testMembers); - testStep.run(); - WorkerStep nextStep = testStep.nextStep(); - - // Check the results - Mockito.verify(testMembers.globalState, times(1)).updatePhase(GlobalState.Phase.METADATA_IN_PROGRESS); - assertEquals(MetadataStep.GetEntry.class, nextStep.getClass()); - } - - static Stream provideGetEntryArgs() { - return Stream.of( - // There is no CMS entry, so we need to create one - Arguments.of( - Optional.empty(), - MetadataStep.CreateEntry.class - ), - - // The CMS entry has an expired lease and is under the retry limit, so we try to acquire the lease - Arguments.of( - Optional.of(new CmsEntry.Metadata( - CmsEntry.MetadataStatus.IN_PROGRESS, - String.valueOf(Instant.now().minus(Duration.ofDays(1)).toEpochMilli()), - CmsEntry.Metadata.MAX_ATTEMPTS - 1 - )), - MetadataStep.AcquireLease.class - ), - - // The CMS entry has an expired lease and is at the retry limit, so we exit as failed - Arguments.of( - Optional.of(new CmsEntry.Metadata( - CmsEntry.MetadataStatus.IN_PROGRESS, - String.valueOf(Instant.now().minus(Duration.ofDays(1)).toEpochMilli()), - CmsEntry.Metadata.MAX_ATTEMPTS - )), - MetadataStep.ExitPhaseFailed.class - ), - - // The CMS entry has an expired lease and is over the retry limit, so we exit as failed - Arguments.of( - Optional.of(new CmsEntry.Metadata( - CmsEntry.MetadataStatus.IN_PROGRESS, - String.valueOf(Instant.now().minus(Duration.ofDays(1)).toEpochMilli()), - CmsEntry.Metadata.MAX_ATTEMPTS + 1 - )), - MetadataStep.ExitPhaseFailed.class - ), - - // The CMS entry has valid lease and is under the retry limit, so we back off a bit - Arguments.of( - Optional.of(new CmsEntry.Metadata( - CmsEntry.MetadataStatus.IN_PROGRESS, - String.valueOf(Instant.now().plus(Duration.ofDays(1)).toEpochMilli()), - CmsEntry.Metadata.MAX_ATTEMPTS - 1 - )), - MetadataStep.RandomWait.class - ), - - // The CMS entry has valid lease and is at the retry limit, so we back off a bit - Arguments.of( - Optional.of(new CmsEntry.Metadata( - CmsEntry.MetadataStatus.IN_PROGRESS, - String.valueOf(Instant.now().plus(Duration.ofDays(1)).toEpochMilli()), - CmsEntry.Metadata.MAX_ATTEMPTS - )), - MetadataStep.RandomWait.class - ), - - // The CMS entry is marked as completed, so we exit as success - Arguments.of( - Optional.of(new CmsEntry.Metadata( - CmsEntry.MetadataStatus.COMPLETED, - String.valueOf(Instant.now().plus(Duration.ofDays(1)).toEpochMilli()), - CmsEntry.Metadata.MAX_ATTEMPTS - 1 - )), - MetadataStep.ExitPhaseSuccess.class - ), - - // The CMS entry is marked as failed, so we exit as faile - Arguments.of( - Optional.of(new CmsEntry.Metadata( - CmsEntry.MetadataStatus.FAILED, - String.valueOf(Instant.now().plus(Duration.ofDays(1)).toEpochMilli()), - CmsEntry.Metadata.MAX_ATTEMPTS - 1 - )), - MetadataStep.ExitPhaseFailed.class - ) - ); - } - - @ParameterizedTest - @MethodSource("provideGetEntryArgs") - void GetEntry_AsExpected(Optional metadata, Class nextStepClass) { - // Set up the test - Mockito.when(testMembers.cmsClient.getMetadataEntry()).thenReturn(metadata); - - // Run the test - MetadataStep.GetEntry testStep = new MetadataStep.GetEntry(testMembers); - testStep.run(); - WorkerStep nextStep = testStep.nextStep(); - - // Check the results - Mockito.verify(testMembers.cmsClient, times(1)).getMetadataEntry(); - assertEquals(nextStepClass, nextStep.getClass()); - } - - static Stream provideCreateEntryArgs() { - return Stream.of( - // We were able to create the CMS entry ourselves, so we have the work lease - Arguments.of( - Optional.of(new CmsEntry.Metadata( - CmsEntry.MetadataStatus.IN_PROGRESS, - String.valueOf(Instant.now().plus(Duration.ofDays(1)).toEpochMilli()), - 1 - )), - MetadataStep.MigrateTemplates.class - ), - - // We were unable to create the CMS entry ourselves, so we do not have the work lease - Arguments.of(Optional.empty(), MetadataStep.GetEntry.class) - ); - } - - @ParameterizedTest - @MethodSource("provideCreateEntryArgs") - void CreateEntry_AsExpected(Optional createdEntry, Class nextStepClass) { - // Set up the test - Mockito.when(testMembers.cmsClient.createMetadataEntry()).thenReturn(createdEntry); - - // Run the test - MetadataStep.CreateEntry testStep = new MetadataStep.CreateEntry(testMembers); - testStep.run(); - WorkerStep nextStep = testStep.nextStep(); - - // Check the results - Mockito.verify(testMembers.cmsClient, times(1)).createMetadataEntry(); - assertEquals(nextStepClass, nextStep.getClass()); - } - - public static class TestAcquireLease extends MetadataStep.AcquireLease { - public static final int milliSinceEpoch = 42; // Arbitrarily chosen, but predictable - - public TestAcquireLease(SharedMembers members) { - super(members); - } - - @Override - protected long getNowMs() { - return milliSinceEpoch; - } - } - - static Stream provideAcquireLeaseArgs() { - return Stream.of( - // We were able to acquire the lease - Arguments.of( - Optional.of(new CmsEntry.Metadata( - CmsEntry.MetadataStatus.IN_PROGRESS, - String.valueOf(Instant.now().plus(Duration.ofDays(1)).toEpochMilli()), - 1 - )), - MetadataStep.MigrateTemplates.class - ), - - // We were unable to acquire the lease - Arguments.of(Optional.empty(), MetadataStep.RandomWait.class) - ); - } - - @ParameterizedTest - @MethodSource("provideAcquireLeaseArgs") - void AcquireLease_AsExpected(Optional updatedEntry, Class nextStepClass) { - // Set up the test - var existingEntry = Optional.of(new CmsEntry.Metadata( - CmsEntry.MetadataStatus.IN_PROGRESS, - CmsEntry.Metadata.getLeaseExpiry(0L, CmsEntry.Metadata.MAX_ATTEMPTS - 1), - CmsEntry.Metadata.MAX_ATTEMPTS - 1 - )); - testMembers.cmsEntry = existingEntry; - - Mockito.when(testMembers.cmsClient.updateMetadataEntry( - any(CmsEntry.Metadata.class), eq(existingEntry.get()) - )).thenReturn(updatedEntry); - - // Run the test - MetadataStep.AcquireLease testStep = new TestAcquireLease(testMembers); - testStep.run(); - WorkerStep nextStep = testStep.nextStep(); - - // Check the results - var expectedEntry = new CmsEntry.Metadata( - CmsEntry.MetadataStatus.IN_PROGRESS, - CmsEntry.Metadata.getLeaseExpiry(TestAcquireLease.milliSinceEpoch, CmsEntry.Metadata.MAX_ATTEMPTS), - CmsEntry.Metadata.MAX_ATTEMPTS - ); - Mockito.verify(testMembers.cmsClient, times(1)).updateMetadataEntry( - expectedEntry, existingEntry.get() - ); - assertEquals(nextStepClass, nextStep.getClass()); - } - - static Stream provideMigrateTemplatesArgs() { - return Stream.of( - // We were able to acquire the lease - Arguments.of( - Optional.of(new CmsEntry.Metadata( - CmsEntry.MetadataStatus.COMPLETED, - String.valueOf(42), - 1 - )), - MetadataStep.ExitPhaseSuccess.class - ), - - // We were unable to acquire the lease - Arguments.of(Optional.empty(), MetadataStep.GetEntry.class) - ); - } - - @ParameterizedTest - @MethodSource("provideMigrateTemplatesArgs") - void MigrateTemplates_AsExpected(Optional updatedEntry, Class nextStepClass) { - // Set up the test - var existingEntry = Optional.of(new CmsEntry.Metadata( - CmsEntry.MetadataStatus.IN_PROGRESS, - String.valueOf(42), - 1 - )); - testMembers.cmsEntry = existingEntry; - - GlobalMetadata.Data testGlobalMetadata = Mockito.mock(GlobalMetadata.Data.class); - ObjectNode testNode = Mockito.mock(ObjectNode.class); - ObjectNode testTransformedNode = Mockito.mock(ObjectNode.class); - Mockito.when(testMembers.metadataFactory.fromRepo(testMembers.snapshotName)).thenReturn(testGlobalMetadata); - Mockito.when(testGlobalMetadata.toObjectNode()).thenReturn(testNode); - Mockito.when(testMembers.transformer.transformGlobalMetadata(testNode)).thenReturn(testTransformedNode); - - var expectedEntry = new CmsEntry.Metadata( - CmsEntry.MetadataStatus.COMPLETED, - existingEntry.get().leaseExpiry, - existingEntry.get().numAttempts - ); - Mockito.when(testMembers.cmsClient.updateMetadataEntry( - eq(expectedEntry), eq(existingEntry.get()) - - )).thenReturn(updatedEntry); - - // Run the test - MetadataStep.MigrateTemplates testStep = new MetadataStep.MigrateTemplates(testMembers); - testStep.run(); - WorkerStep nextStep = testStep.nextStep(); - - // Check the results - Mockito.verify(testMembers.globalState, times(1)).updateWorkItem( - argThat(argument -> { - if (!(argument instanceof OpenSearchWorkItem)) { - return false; - } - OpenSearchWorkItem workItem = (OpenSearchWorkItem) argument; - return workItem.indexName.equals(OpenSearchCmsClient.CMS_INDEX_NAME) && - workItem.documentId.equals(OpenSearchCmsClient.CMS_METADATA_DOC_ID); - }) - ); - Mockito.verify(testMembers.metadataFactory, times(1)).fromRepo( - testMembers.snapshotName - ); - Mockito.verify(testMembers.transformer, times(1)).transformGlobalMetadata( - testNode - ); - Mockito.verify(testMembers.metadataCreator, times(1)).create( - testTransformedNode - ); - Mockito.verify(testMembers.cmsClient, times(1)).updateMetadataEntry( - expectedEntry, existingEntry.get() - ); - Mockito.verify(testMembers.globalState, times(1)).updateWorkItem( - null - ); - - assertEquals(nextStepClass, nextStep.getClass()); - } - - public static class TestRandomWait extends MetadataStep.RandomWait { - public TestRandomWait(SharedMembers members) { - super(members); - } - - @Override - protected void waitABit() { - // do nothing - } - } - - @Test - void RandomWait_AsExpected() { - // Run the test - MetadataStep.RandomWait testStep = new TestRandomWait(testMembers); - testStep.run(); - WorkerStep nextStep = testStep.nextStep(); - - // Check the results - assertEquals(MetadataStep.GetEntry.class, nextStep.getClass()); - } - - @Test - void ExitPhaseSuccess_AsExpected() { - // Run the test - MetadataStep.ExitPhaseSuccess testStep = new MetadataStep.ExitPhaseSuccess(testMembers); - testStep.run(); - WorkerStep nextStep = testStep.nextStep(); - - // Check the results - Mockito.verify(testMembers.globalState, times(1)).updatePhase( - GlobalState.Phase.METADATA_COMPLETED - ); - assertEquals(null, nextStep); - } - - @Test - void ExitPhaseFailed_AsExpected() { - // Set up the test - MaxAttemptsExceeded e = new MaxAttemptsExceeded(); - - var existingEntry = Optional.of(new CmsEntry.Metadata( - CmsEntry.MetadataStatus.IN_PROGRESS, - String.valueOf(42), - 1 - )); - testMembers.cmsEntry = existingEntry; - - // Run the test - MetadataStep.ExitPhaseFailed testStep = new MetadataStep.ExitPhaseFailed(testMembers, e); - testStep.run(); - assertThrows(MaxAttemptsExceeded.class, () -> { - testStep.nextStep(); - }); - - // Check the results - var expectedEntry = new CmsEntry.Metadata( - CmsEntry.MetadataStatus.FAILED, - existingEntry.get().leaseExpiry, - existingEntry.get().numAttempts - ); - Mockito.verify(testMembers.cmsClient, times(1)).updateMetadataEntry( - expectedEntry, existingEntry.get() - ); - Mockito.verify(testMembers.globalState, times(1)).updatePhase( - GlobalState.Phase.METADATA_FAILED - ); - } -} diff --git a/RFS/src/test/java/com/rfs/worker/SnapshotRunnerTest.java b/RFS/src/test/java/com/rfs/worker/SnapshotRunnerTest.java deleted file mode 100644 index 2c8594434..000000000 --- a/RFS/src/test/java/com/rfs/worker/SnapshotRunnerTest.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.rfs.worker; - -import static org.junit.Assert.assertThrows; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.Mockito.*; - -import java.util.Optional; - -import org.junit.jupiter.api.Test; - -import com.rfs.cms.CmsClient; -import com.rfs.common.RfsException; -import com.rfs.common.SnapshotCreator; - -class SnapshotRunnerTest { - - @Test - void run_encountersAnException_asExpected() { - // Setup - String snapshotName = "snapshotName"; - GlobalState globalState = mock(GlobalState.class); - CmsClient cmsClient = mock(CmsClient.class); - SnapshotCreator snapshotCreator = mock(SnapshotCreator.class); - RfsException testException = new RfsException("Unit test"); - - doThrow(testException).when(cmsClient).getSnapshotEntry(snapshotName); - when(globalState.getPhase()).thenReturn(GlobalState.Phase.SNAPSHOT_IN_PROGRESS); - when(snapshotCreator.getSnapshotName()).thenReturn(snapshotName); - - // Run the test - SnapshotRunner testRunner = new SnapshotRunner(globalState, cmsClient, snapshotCreator); - final var e = assertThrows(SnapshotRunner.SnapshotPhaseFailed.class, () -> testRunner.run()); - - // Verify the results - assertEquals(GlobalState.Phase.SNAPSHOT_IN_PROGRESS, e.phase); - assertEquals(null, e.nextStep); - assertEquals(Optional.empty(), e.cmsEntry); - assertEquals(testException, e.getCause()); - } - -} diff --git a/RFS/src/test/java/com/rfs/worker/SnapshotStepTest.java b/RFS/src/test/java/com/rfs/worker/SnapshotStepTest.java deleted file mode 100644 index 5d12cfe78..000000000 --- a/RFS/src/test/java/com/rfs/worker/SnapshotStepTest.java +++ /dev/null @@ -1,213 +0,0 @@ -package com.rfs.worker; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import org.mockito.Mockito; -import org.mockito.junit.jupiter.MockitoExtension; - -import com.rfs.cms.CmsClient; -import com.rfs.cms.CmsEntry; -import com.rfs.cms.CmsEntry.Snapshot; -import com.rfs.cms.CmsEntry.SnapshotStatus; -import com.rfs.common.SnapshotCreator; -import com.rfs.common.SnapshotCreator.SnapshotCreationFailed; -import com.rfs.worker.SnapshotStep.ExitPhaseSnapshotFailed; -import com.rfs.worker.SnapshotStep.ExitPhaseSuccess; -import com.rfs.worker.SnapshotStep.SharedMembers; -import com.rfs.worker.SnapshotStep.WaitForSnapshot; - -import static org.mockito.Mockito.*; - -import java.util.Optional; -import java.util.stream.Stream; - - -@ExtendWith(MockitoExtension.class) -public class SnapshotStepTest { - private SharedMembers testMembers; - - @BeforeEach - void setUp() { - GlobalState globalState = Mockito.mock(GlobalState.class); - CmsClient cmsClient = Mockito.mock(CmsClient.class); - SnapshotCreator snapshotCreator = Mockito.mock(SnapshotCreator.class); - testMembers = new SharedMembers(globalState, cmsClient, snapshotCreator); - } - - static Stream provideEnterPhaseArgs() { - return Stream.of( - Arguments.of(Optional.empty(), SnapshotStep.CreateEntry.class), - Arguments.of(Optional.of(new Snapshot("test", SnapshotStatus.NOT_STARTED)), SnapshotStep.InitiateSnapshot.class), - Arguments.of(Optional.of(new Snapshot("test", SnapshotStatus.IN_PROGRESS)), SnapshotStep.WaitForSnapshot.class) - ); - } - - @ParameterizedTest - @MethodSource("provideEnterPhaseArgs") - void EnterPhase_AsExpected(Optional snapshotEntry, Class expected) { - // Run the test - SnapshotStep.EnterPhase testStep = new SnapshotStep.EnterPhase(testMembers, snapshotEntry); - testStep.run(); - WorkerStep nextStep = testStep.nextStep(); - - // Check the results - Mockito.verify(testMembers.globalState, times(1)).updatePhase(GlobalState.Phase.SNAPSHOT_IN_PROGRESS); - assertEquals(expected, nextStep.getClass()); - } - - @Test - void CreateEntry_AsExpected() { - // Set up the test - when(testMembers.snapshotCreator.getSnapshotName()).thenReturn("test"); - - // Run the test - SnapshotStep.CreateEntry testStep = new SnapshotStep.CreateEntry(testMembers); - testStep.run(); - WorkerStep nextStep = testStep.nextStep(); - - // Check the results - Mockito.verify(testMembers.cmsClient, times(1)).createSnapshotEntry("test"); - assertEquals(SnapshotStep.InitiateSnapshot.class, nextStep.getClass()); - } - - @Test - void InitiateSnapshot_AsExpected() { - // Set up the test - var existingEntry = new CmsEntry.Snapshot( - "test", - SnapshotStatus.NOT_STARTED - ); - testMembers.cmsEntry = Optional.of(existingEntry); - - when(testMembers.snapshotCreator.getSnapshotName()).thenReturn("test"); - - // Run the test - SnapshotStep.InitiateSnapshot testStep = new SnapshotStep.InitiateSnapshot(testMembers); - testStep.run(); - WorkerStep nextStep = testStep.nextStep(); - - // Check the results - Mockito.verify(testMembers.snapshotCreator, times(1)).registerRepo(); - Mockito.verify(testMembers.snapshotCreator, times(1)).createSnapshot(); - - var expectedEntry = new CmsEntry.Snapshot( - "test", - SnapshotStatus.IN_PROGRESS - ); - - Mockito.verify(testMembers.cmsClient, times(1)).updateSnapshotEntry( - expectedEntry, existingEntry - ); - assertEquals(SnapshotStep.WaitForSnapshot.class, nextStep.getClass()); - } - - public static class TestableWaitForSnapshot extends WaitForSnapshot { - public TestableWaitForSnapshot(SharedMembers testMembers) { - super(testMembers); - } - - protected void waitABit() throws InterruptedException { - // Do nothing - } - } - - @Test - void WaitForSnapshot_successful_AsExpected() { - // Set up the test - when(testMembers.snapshotCreator.isSnapshotFinished()) - .thenReturn(false) - .thenReturn(true); - - // Run the test - SnapshotStep.WaitForSnapshot waitPhase = new TestableWaitForSnapshot(testMembers); - waitPhase.run(); - WorkerStep nextStep = waitPhase.nextStep(); - - // Check the results - Mockito.verify(testMembers.snapshotCreator, times(2)).isSnapshotFinished(); - assertEquals(SnapshotStep.ExitPhaseSuccess.class, nextStep.getClass()); - } - - @Test - void WaitForSnapshot_failedSnapshot_AsExpected() { - // Set up the test - when(testMembers.snapshotCreator.isSnapshotFinished()) - .thenReturn(false) - .thenThrow(new SnapshotCreationFailed("test")); - - // Run the test - SnapshotStep.WaitForSnapshot waitPhase = new TestableWaitForSnapshot(testMembers); - waitPhase.run(); - WorkerStep nextStep = waitPhase.nextStep(); - - // Check the results - Mockito.verify(testMembers.snapshotCreator, times(2)).isSnapshotFinished(); - assertEquals(SnapshotStep.ExitPhaseSnapshotFailed.class, nextStep.getClass()); - } - - @Test - void ExitPhaseSuccess_AsExpected() { - // Set up the test - var existingEntry = new CmsEntry.Snapshot( - "test", - SnapshotStatus.IN_PROGRESS - ); - testMembers.cmsEntry = Optional.of(existingEntry); - - when(testMembers.snapshotCreator.getSnapshotName()).thenReturn("test"); - - // Run the test - SnapshotStep.ExitPhaseSuccess exitPhase = new ExitPhaseSuccess(testMembers); - exitPhase.run(); - WorkerStep nextStep = exitPhase.nextStep(); - - // Check the results - var expectedEntry = new CmsEntry.Snapshot( - "test", - SnapshotStatus.COMPLETED - ); - Mockito.verify(testMembers.cmsClient, times(1)).updateSnapshotEntry( - expectedEntry, existingEntry - ); - Mockito.verify(testMembers.globalState, times(1)).updatePhase(GlobalState.Phase.SNAPSHOT_COMPLETED); - assertEquals(null, nextStep); - } - - @Test - void ExitPhaseSnapshotFailed_AsExpected() { - // Set up the test - var existingEntry = new CmsEntry.Snapshot( - "test", - SnapshotStatus.IN_PROGRESS - ); - testMembers.cmsEntry = Optional.of(existingEntry); - - when(testMembers.snapshotCreator.getSnapshotName()).thenReturn("test"); - SnapshotCreationFailed e = new SnapshotCreationFailed("test"); - - // Run the test - SnapshotStep.ExitPhaseSnapshotFailed exitPhase = new ExitPhaseSnapshotFailed(testMembers, e); - exitPhase.run(); - assertThrows(SnapshotCreationFailed.class, () -> { - exitPhase.nextStep(); - }); - - // Check the results - var expectedEntry = new CmsEntry.Snapshot( - "test", - SnapshotStatus.FAILED - ); - Mockito.verify(testMembers.cmsClient, times(1)).updateSnapshotEntry( - expectedEntry, existingEntry - ); - Mockito.verify(testMembers.globalState, times(1)).updatePhase(GlobalState.Phase.SNAPSHOT_FAILED); - } -} diff --git a/RFS/src/test/resources/log4j2-test.xml b/RFS/src/test/resources/log4j2.xml similarity index 69% rename from RFS/src/test/resources/log4j2-test.xml rename to RFS/src/test/resources/log4j2.xml index 2fee6a794..99e1e6614 100644 --- a/RFS/src/test/resources/log4j2-test.xml +++ b/RFS/src/test/resources/log4j2.xml @@ -9,6 +9,12 @@ + + + + + + diff --git a/RFS/src/test/java/com/rfs/framework/ElasticsearchContainer.java b/RFS/src/testFixtures/java/com/rfs/framework/ElasticsearchContainer.java similarity index 70% rename from RFS/src/test/java/com/rfs/framework/ElasticsearchContainer.java rename to RFS/src/testFixtures/java/com/rfs/framework/ElasticsearchContainer.java index 0d1c88679..f0bca8d18 100644 --- a/RFS/src/test/java/com/rfs/framework/ElasticsearchContainer.java +++ b/RFS/src/testFixtures/java/com/rfs/framework/ElasticsearchContainer.java @@ -3,8 +3,7 @@ import java.io.File; import java.time.Duration; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; +import lombok.extern.slf4j.Slf4j; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.utility.DockerImageName; @@ -12,10 +11,11 @@ /** * Containerized version of Elasticsearch cluster */ +@Slf4j public class ElasticsearchContainer implements AutoCloseable { - - private static final Logger logger = LogManager.getLogger(ElasticsearchContainer.class); - private static final String CLUSTER_SNAPSHOT_DIR = "/usr/share/elasticsearch/snapshots"; + public static final String CLUSTER_SNAPSHOT_DIR = "/usr/share/elasticsearch/snapshots"; + public static final Version V7_10_2 = + new Version("docker.elastic.co/elasticsearch/elasticsearch:7.10.2", "7.10.2"); private final GenericContainer container; private final Version version; @@ -31,13 +31,13 @@ public ElasticsearchContainer(final Version version) { } public void copySnapshotData(final String directory) { - logger.info("Copy stuff was called"); + log.info("Copy stuff was called"); try { // Execute command to list all files in the directory final var result = container.execInContainer("sh", "-c", "find " + CLUSTER_SNAPSHOT_DIR + " -type f"); - logger.debug("Process Exit Code: " + result.getExitCode()); - logger.debug("Standard Output: " + result.getStdout()); - logger.debug("Standard Error : " + result.getStderr()); + log.debug("Process Exit Code: " + result.getExitCode()); + log.debug("Standard Output: " + result.getStdout()); + log.debug("Standard Error : " + result.getStderr()); // Process each file and copy it from the container try (final var lines = result.getStdout().lines()) { lines.forEach(fullFilePath -> { @@ -46,7 +46,7 @@ public void copySnapshotData(final String directory) { final var destinationPath = directory + "/" + file; // Make sure the parent directory tree exists before copying new File(destinationPath).getParentFile().mkdirs(); - logger.info("Copying file " + sourcePath + " from container onto " + destinationPath); + log.info("Copying file " + sourcePath + " from container onto " + destinationPath); container.copyFileFromContainer(sourcePath, destinationPath); }); } @@ -56,7 +56,7 @@ public void copySnapshotData(final String directory) { } public void start() { - logger.info("Starting ElasticsearchContainer version:" + version.prettyName); + log.info("Starting ElasticsearchContainer version:" + version.prettyName); container.start(); } @@ -68,17 +68,15 @@ public String getUrl() { @Override public void close() throws Exception { - logger.info("Stopping ElasticsearchContainer version:" + version.prettyName); - logger.debug("Instance logs:\n" + container.getLogs()); + log.info("Stopping ElasticsearchContainer version:" + version.prettyName); + log.debug("Instance logs:\n" + container.getLogs()); container.stop(); } - public static enum Version { - V7_10_2("docker.elastic.co/elasticsearch/elasticsearch:7.10.2", "7.10.2"); - + public static class Version { final String imageName; final String prettyName; - Version(final String imageName, final String prettyName) { + public Version(final String imageName, final String prettyName) { this.imageName = imageName; this.prettyName = prettyName; } diff --git a/RFS/src/test/java/com/rfs/framework/OpenSearchContainer.java b/RFS/src/testFixtures/java/com/rfs/framework/OpenSearchContainer.java similarity index 100% rename from RFS/src/test/java/com/rfs/framework/OpenSearchContainer.java rename to RFS/src/testFixtures/java/com/rfs/framework/OpenSearchContainer.java diff --git a/build.gradle b/build.gradle index 3dc191821..c9fb394f6 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ plugins { task buildDockerImages() { dependsOn(':TrafficCapture:dockerSolution:buildDockerImages') - dependsOn(':RFS:buildDockerImages') + dependsOn(':DocumentsFromSnapshotMigration:buildDockerImages') } subprojects { diff --git a/commonDependencyVersionConstraints/build.gradle b/commonDependencyVersionConstraints/build.gradle index 5dcc8c7d9..405ce863c 100644 --- a/commonDependencyVersionConstraints/build.gradle +++ b/commonDependencyVersionConstraints/build.gradle @@ -51,7 +51,7 @@ dependencies { api group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: jackson api group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-smile', version: jackson - def jupiter = '5.9.3' + def jupiter = '5.10.2' api group: 'org.junit.jupiter', name:'junit-jupiter-api', version: jupiter api group: 'org.junit.jupiter', name:'junit-jupiter-params', version: jupiter api group: 'org.junit.jupiter', name:'junit-jupiter-engine', version: jupiter @@ -93,6 +93,9 @@ dependencies { api group: 'org.testcontainers', name: 'testcontainers', version: testcontainers api group: 'org.testcontainers', name: 'toxiproxy', version: testcontainers + // make sure that this is compatible with the testcontainers version + api group: 'org.opensearch', name: 'opensearch-testcontainers', version: '2.0.1' + def mockito = '5.11.0' api group: 'org.mockito', name:'mockito-core', version: mockito api group: 'org.mockito', name:'mockito-junit-jupiter', version: mockito diff --git a/deployment/cdk/opensearch-service-migration/lib/service-stacks/reindex-from-snapshot-stack.ts b/deployment/cdk/opensearch-service-migration/lib/service-stacks/reindex-from-snapshot-stack.ts index 4b2f934d4..5b3627ac1 100644 --- a/deployment/cdk/opensearch-service-migration/lib/service-stacks/reindex-from-snapshot-stack.ts +++ b/deployment/cdk/opensearch-service-migration/lib/service-stacks/reindex-from-snapshot-stack.ts @@ -72,7 +72,7 @@ export class ReindexFromSnapshotStack extends MigrationServiceCore { this.createService({ serviceName: 'reindex-from-snapshot', taskInstanceCount: 0, - dockerDirectoryPath: join(__dirname, "../../../../../", "RFS/docker"), + dockerDirectoryPath: join(__dirname, "../../../../../", "DocumentsFromSnapshotMigration/docker"), dockerImageCommand: ['/bin/sh', '-c', rfsCommand], securityGroups: securityGroups, taskRolePolicies: servicePolicies,