You are viewing a plain text version of this content. The canonical link for it is here.
Posted to oak-commits@jackrabbit.apache.org by jo...@apache.org on 2023/11/06 22:23:53 UTC
(jackrabbit-oak) branch old-revisions-cleanup created (now 1bc1280a14)
This is an automated email from the ASF dual-hosted git repository.
joscorbe pushed a change to branch old-revisions-cleanup
in repository https://gitbox.apache.org/repos/asf/jackrabbit-oak.git
at 1bc1280a14 Some test fixes and improvements.
This branch includes the following new commits:
new c0c98b7908 Old revisions cleanup (WIP)
new 8e299c1642 More tests added (WIP)
new 1bc1280a14 Some test fixes and improvements.
The 3 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
(jackrabbit-oak) 01/03: Old revisions cleanup (WIP)
Posted by jo...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
joscorbe pushed a commit to branch old-revisions-cleanup
in repository https://gitbox.apache.org/repos/asf/jackrabbit-oak.git
commit c0c98b7908979393a90ceb8afe9cc6f547f4cbaa
Author: Jose Cordero <co...@adobe.com>
AuthorDate: Fri Jun 30 18:58:40 2023 +0200
Old revisions cleanup (WIP)
---
.../jackrabbit/oak/console/GroovyConsole.groovy | 1 +
.../oak/console/commands/RevisionCommand.groovy | 289 ++++++++++++++++
.../console/commands/RevisionCommand.properties | 21 ++
.../jackrabbit/oak/run/RevisionsCommand.java | 150 ++++++++-
oak-store-document/pom.xml | 6 +
.../document/DocumentRevisionCleanupHelper.java | 276 ++++++++++++++++
.../DocumentRevisionCleanupHelperTest.java | 366 +++++++++++++++++++++
7 files changed, 1108 insertions(+), 1 deletion(-)
diff --git a/oak-run/src/main/groovy/org/apache/jackrabbit/oak/console/GroovyConsole.groovy b/oak-run/src/main/groovy/org/apache/jackrabbit/oak/console/GroovyConsole.groovy
index 904ab15a60..6124cc8a16 100644
--- a/oak-run/src/main/groovy/org/apache/jackrabbit/oak/console/GroovyConsole.groovy
+++ b/oak-run/src/main/groovy/org/apache/jackrabbit/oak/console/GroovyConsole.groovy
@@ -124,6 +124,7 @@ class GroovyConsole {
new CheckpointCommand(shell),
new LsCommand(shell),
new PnCommand(shell),
+ new RevisionCommand(shell),
new RefreshCommand(shell),
new RetrieveCommand(shell),
new LuceneCommand(shell),
diff --git a/oak-run/src/main/groovy/org/apache/jackrabbit/oak/console/commands/RevisionCommand.groovy b/oak-run/src/main/groovy/org/apache/jackrabbit/oak/console/commands/RevisionCommand.groovy
new file mode 100644
index 0000000000..8e055d17a9
--- /dev/null
+++ b/oak-run/src/main/groovy/org/apache/jackrabbit/oak/console/commands/RevisionCommand.groovy
@@ -0,0 +1,289 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.jackrabbit.oak.console.commands
+
+
+import groovy.transform.CompileStatic
+import org.apache.jackrabbit.oak.console.ConsoleSession
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore
+import org.apache.jackrabbit.oak.plugins.document.DocumentRevisionCleanupHelper
+import org.apache.jackrabbit.oak.plugins.document.DocumentStore
+import org.apache.jackrabbit.oak.plugins.document.Revision
+import org.apache.jackrabbit.oak.spi.state.NodeStore
+import org.codehaus.groovy.tools.shell.CommandSupport
+import org.codehaus.groovy.tools.shell.Groovysh
+
+import java.util.stream.Collectors
+
+import static java.lang.Integer.getInteger
+
+@CompileStatic
+class RevisionCommand extends CommandSupport {
+ public static final String COMMAND_NAME = 'revision'
+
+ private static final int REVISION_CAP = getInteger("oak.revision.cap", 250)
+
+ private static final String ARGS_SUBCOMMAND_LIST = "list"
+ private static final String ARGS_SUBCOMMAND_INIT = "init"
+ private static final String ARGS_SUBCOMMAND_SHOW = "show"
+ private static final String ARGS_SUBCOMMAND_CLEANUP = "cleanup"
+
+ private static final String ARGS_NO_CAP_RESULTS = "--nocap"
+ private static final String ARGS_NO_CAP_RESULTS_SHORT = "-n"
+ private static final String ARGS_ORDER_DESC = "--desc"
+ private static final String ARGS_ORDER_DESC_SHORT = "-d"
+
+ private static final int ORDER_ASC = 0
+ private static final int ORDER_DESC = 1
+
+ private static int order = ORDER_ASC
+ private static boolean capResults = true
+ private static boolean showPropertiesUse = false
+
+ private DocumentRevisionCleanupHelper cleanupHelper;
+
+ RevisionCommand(Groovysh shell) {
+ super(shell, COMMAND_NAME, 'rev')
+ }
+
+ @Override
+ Object execute(List<String> args) {
+ if (args.isEmpty()) {
+ io.err.println("Missing subcommand: [<list>|<init>|<show>|<cleanup>]")
+ return null
+ }
+
+ parseArgs(args)
+ switch(args[0].toLowerCase()) {
+ case ARGS_SUBCOMMAND_LIST:
+ list()
+ break
+ case ARGS_SUBCOMMAND_INIT:
+ initialize()
+ break
+ case ARGS_SUBCOMMAND_SHOW:
+ show(args.contains("candidates"), args.contains("blocked"))
+ break
+ case ARGS_SUBCOMMAND_CLEANUP:
+ io.err.println("This command is read-only. To execute the cleanup, use 'oak-run revisions' command:")
+ io.err.println(" oak-run revisions {<jdbc-uri> | <mongodb-uri>} cleanup [options]")
+ io.err.println(" Options:")
+ io.err.println(" --path <path> Path to the node to cleanup. This is required.")
+ io.err.println(" --number <number> Number of revisions to cleanup")
+ io.err.println(" --clusterId <clusterId> ClusterId to cleanup")
+ io.err.println(" Example:")
+ io.err.println(" oak-run revisions {<jdbc-uri> | <mongodb-uri>} cleanup --path /content --number 10 --clusterId 1")
+ io.err.println()
+ break
+ default:
+ io.err.println("Unrecognized subcommand: " + args[0])
+ }
+
+ io.out.flush()
+ return null
+ }
+
+ /**
+ * List command.
+ * Prints the list of revisions for the working document.
+ */
+ void list() {
+ int count = 0
+ NavigableMap<Revision, String> allRevisions = cleanupHelper.getAllRevisions();
+
+ for (Map.Entry<Revision, String> revisionEntry : order == ORDER_ASC ? allRevisions.entrySet() : allRevisions.descendingMap().entrySet()) {
+ Revision revision = revisionEntry.getKey()
+ String value = revisionEntry.getValue()
+ io.out.println(revision.toReadableString() + " " + value)
+ count++
+ if (capResults && count >= REVISION_CAP) {
+ io.out.printf("-- Reached revision cap of %d elements. This document has %d revisions --%n", REVISION_CAP, allRevisions.size())
+ break
+ }
+ }
+ }
+
+ /**
+ * Outputs the candidate and blocked revision sets. These sets are populated during a previous execution of
+ * the info() command.
+ * @param args
+ */
+ void show(boolean candidates, boolean blocked) {
+ if (candidates) {
+ SortedMap<Integer, TreeSet<Revision>> candidateRevisionsToClean = cleanupHelper.getCandidateRevisionsToClean()
+ if (candidateRevisionsToClean.isEmpty()) {
+ io.out.println("-- Candidates revision list is empty. Run 'rev init' before --")
+ }
+ for (Map.Entry<Integer, TreeSet<Revision>> clusterEntry : candidateRevisionsToClean) {
+ int count = 0
+ io.out.println("ClusterId " + clusterEntry.key)
+ for (Revision revision : order == ORDER_ASC ? clusterEntry.value : clusterEntry.value.descendingSet()) {
+ io.out.println(" " + revision.toReadableString())
+ count++
+ if (capResults && count >= REVISION_CAP) {
+ io.out.printf("-- Reached revision cap of %d elements. There are %d revisions in the list --%n", REVISION_CAP, clusterEntry.value.size())
+ break
+ }
+ }
+ }
+ }
+
+ if (blocked) {
+ SortedMap<Integer, TreeSet<Revision>> usedRevisionsToKeep = cleanupHelper.getBlockedRevisionsToKeep()
+ if (cleanupHelper.getBlockedRevisionsToKeep().isEmpty()) {
+ io.out.println("-- Blocked revision list is empty. Run 'rev init' before --")
+ }
+ for (Map.Entry<Integer, TreeSet<Revision>> clusterEntry : usedRevisionsToKeep) {
+ int count = 0
+ io.out.println("ClusterId " + clusterEntry.key)
+ for (Revision revision : order == ORDER_ASC ? clusterEntry.value : clusterEntry.value.descendingSet()) {
+ io.out.println(revision.toReadableString())
+ count++
+ if (capResults && count >= REVISION_CAP) {
+ io.out.printf("-- Reached revision cap of %d elements. There are %d revisions in the list --%n", REVISION_CAP, clusterEntry.value.size())
+ break
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Performs the cleanup of a certain number of revisions for the specified clusterId, starting from oldest.
+ * @param numberToCleanup
+ * @param clusterToCleanup
+ */
+ void cleanup(int numberToCleanup, int clusterToCleanup) {
+ io.out.println("This will delete the following revisions and the property values permanently:")
+ SortedMap<Integer, TreeSet<Revision>> candidateRevisionsToClean = cleanupHelper.getCandidateRevisionsToClean()
+ SortedMap<Revision, TreeSet<String>> propertiesModifiedByRevision = cleanupHelper.getPropertiesModifiedByRevision()
+ TreeSet<Revision> revisions = candidateRevisionsToClean.get(clusterToCleanup)
+ if (revisions != null) {
+ int count = 0
+ for (Revision revision : revisions) {
+ io.out.println(revision.toReadableString() + " => " + propertiesModifiedByRevision.get(revision))
+ count++
+ if (count >= numberToCleanup) {
+ break
+ }
+ }
+ } else {
+ io.err.println("Invalid clusterId")
+ return
+ }
+
+ io.out.println("Are you sure to proceed? [y/N]")
+ int confirmation = io.in.read()
+ io.in.readLine()
+ if (confirmation == (int)('y' as char)) {
+ // Start the cleanup
+ int count = 0
+ for (Revision revision : candidateRevisionsToClean.get(clusterToCleanup)) {
+ // Remove the revision from _revisions and all the properties
+ /*UpdateOp update = new UpdateOp(workingDocument.path.toString(), false)
+ update.removeMapEntry("_revisions", revision)
+ for (String property : propertiesModifiedByRevision.get(revision)) {
+ update.removeMapEntry(property, revision)
+ }
+ io.out.println("Executing UpdateOp: " + update)
+ try {
+ documentStore.findAndUpdate(NODES, update)
+ } catch (DocumentStoreException ex) {
+ io.out.println("Operation failed: " + ex)
+ }*/
+
+ count++
+ if (count >= numberToCleanup) {
+ break
+ }
+ }
+ io.out.println("-- Executed " + count + " operations --")
+ }
+ }
+
+ void initialize() {
+ NodeStore nodeStore = getSession().getStore()
+ assert nodeStore instanceof DocumentNodeStore
+ DocumentStore documentStore = nodeStore.getDocumentStore()
+ DocumentNodeStore documentNodeStore = (DocumentNodeStore) nodeStore
+
+ cleanupHelper = new DocumentRevisionCleanupHelper(documentStore, documentNodeStore, session.getWorkingPath())
+
+ cleanupHelper.initializeCleanupProcess()
+
+ SortedMap<Revision, String> allRevisions = cleanupHelper.getAllRevisions()
+ TreeMap<Integer, Integer> revisionsByClusterId = allRevisions.keySet().groupBy { it.clusterId }
+ .collectEntries { clusterId, revisions ->
+ [clusterId, revisions.size()]
+ } as TreeMap<Integer, Integer>
+
+ io.out.println("=== Last Revision by clusterId ===")
+ for (Map.Entry<Integer, Revision> entry : cleanupHelper.getLastRev()) {
+ io.out.printf(" [%d] -> %s%n", entry.key, entry.value.toReadableString())
+ }
+ io.out.println()
+
+ io.out.println("=== Sweep Revision by clusterId ===")
+ for (Map.Entry<Integer, Revision> entry : cleanupHelper.getSweepRev()) {
+ io.out.printf(" [%d] -> %s%n", entry.key, entry.value.toReadableString())
+ }
+ io.out.println()
+
+ io.out.println("=== Total Revisions by clusterId ===")
+ for (Map.Entry<Integer, Integer> entry : revisionsByClusterId) {
+ io.out.printf(" [%d] -> %d revisions%n", entry.key, entry.value)
+ }
+ io.out.println()
+
+ io.out.println("=== Candidates to cleanup ===")
+ cleanupHelper.getCandidateRevisionsToClean().each { clusterId, revisions ->
+ int blocked = cleanupHelper.getBlockedRevisionsToKeep().get(clusterId)?.size() ?: 0
+ io.out.printf("ClusterId [%d] has %d Candidates and %d Blocked%n", clusterId, revisions.size(), blocked)
+ }
+
+ io.out.println("The lists are stored temporarily. You can print them using:")
+ io.out.println(" > rev show candidates")
+ io.out.println(" > rev show blocked")
+ io.out.println()
+ }
+
+ void parseArgs(List<String> args) {
+ // Defaults
+ order = ORDER_ASC
+ capResults = true
+ showPropertiesUse = false
+
+ for (String arg : args) {
+ switch(arg) {
+ case ARGS_NO_CAP_RESULTS:
+ case ARGS_NO_CAP_RESULTS_SHORT:
+ capResults = false
+ break
+ case ARGS_ORDER_DESC:
+ case ARGS_ORDER_DESC_SHORT:
+ order = ORDER_DESC
+ break
+ }
+ }
+ }
+
+ ConsoleSession getSession(){
+ return (ConsoleSession)variables.session
+ }
+}
diff --git a/oak-run/src/main/groovy/org/apache/jackrabbit/oak/console/commands/RevisionCommand.properties b/oak-run/src/main/groovy/org/apache/jackrabbit/oak/console/commands/RevisionCommand.properties
new file mode 100644
index 0000000000..5bc67c2261
--- /dev/null
+++ b/oak-run/src/main/groovy/org/apache/jackrabbit/oak/console/commands/RevisionCommand.properties
@@ -0,0 +1,21 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+command.description=Revision management.
+command.usage=[<list>|<show>|<init>|<cleanup>]
+command.help=Revision management.
diff --git a/oak-run/src/main/java/org/apache/jackrabbit/oak/run/RevisionsCommand.java b/oak-run/src/main/java/org/apache/jackrabbit/oak/run/RevisionsCommand.java
index 21e5aff706..9775dacc63 100644
--- a/oak-run/src/main/java/org/apache/jackrabbit/oak/run/RevisionsCommand.java
+++ b/oak-run/src/main/java/org/apache/jackrabbit/oak/run/RevisionsCommand.java
@@ -23,6 +23,11 @@ import com.google.common.io.Closer;
import java.io.IOException;
import java.util.List;
import java.util.Locale;
+import java.util.Map;
+import java.util.Scanner;
+import java.util.SortedMap;
+import java.util.TreeMap;
+import java.util.TreeSet;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
@@ -32,6 +37,7 @@ import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.stream.Collectors;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.LoggerContext;
@@ -41,9 +47,11 @@ import org.apache.jackrabbit.oak.commons.TimeDurationFormatter;
import org.apache.jackrabbit.oak.plugins.document.ClusterNodeInfoDocument;
import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreBuilder;
+import org.apache.jackrabbit.oak.plugins.document.DocumentRevisionCleanupHelper;
import org.apache.jackrabbit.oak.plugins.document.DocumentStore;
import org.apache.jackrabbit.oak.plugins.document.FormatVersion;
import org.apache.jackrabbit.oak.plugins.document.MissingLastRevSeeker;
+import org.apache.jackrabbit.oak.plugins.document.Revision;
import org.apache.jackrabbit.oak.plugins.document.RevisionContextWrapper;
import org.apache.jackrabbit.oak.plugins.document.SweepHelper;
import org.apache.jackrabbit.oak.plugins.document.VersionGCSupport;
@@ -56,6 +64,7 @@ import org.apache.jackrabbit.oak.spi.blob.MemoryBlobStore;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import static java.lang.Integer.getInteger;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreHelper.createVersionGC;
import static org.apache.jackrabbit.oak.plugins.document.FormatVersion.versionOf;
@@ -71,6 +80,8 @@ public class RevisionsCommand implements Command {
private static final Logger LOG = LoggerFactory.getLogger(RevisionsCommand.class);
+ private static final int REVISION_CAP = getInteger("oak.revision.cap", 250);
+
private static final String USAGE = Joiner.on(System.lineSeparator()).join(
"revisions {<jdbc-uri> | <mongodb-uri>} <sub-command> [options]",
"where sub-command is one of",
@@ -78,7 +89,8 @@ public class RevisionsCommand implements Command {
" any modifications",
" collect perform garbage collection",
" reset clear all persisted metadata",
- " sweep clean up uncommitted changes"
+ " sweep clean up uncommitted changes",
+ " cleanup clean up old/unused revisions"
);
private static final ImmutableList<String> LOGGER_NAMES = ImmutableList.of(
@@ -92,6 +104,7 @@ public class RevisionsCommand implements Command {
static final String CMD_COLLECT = "collect";
static final String CMD_RESET = "reset";
static final String CMD_SWEEP = "sweep";
+ static final String CMD_CLEANUP = "cleanup";
final OptionSpec<?> once;
final OptionSpec<Integer> limit;
@@ -100,6 +113,8 @@ public class RevisionsCommand implements Command {
final OptionSpec<Double> delay;
final OptionSpec<?> continuous;
final OptionSpec<?> verbose;
+ final OptionSpec<String> path;
+ final OptionSpec<Integer> number;
RevisionsOptions(String usage) {
super(usage);
@@ -120,6 +135,11 @@ public class RevisionsCommand implements Command {
.accepts("continuous", "run continuously (collect only)");
verbose = parser
.accepts("verbose", "print INFO messages to the console");
+ path = parser
+ .accepts("path", "path to the document to be cleaned up").withRequiredArg();
+ number = parser
+ .accepts("number", "number of revisions to clean").withRequiredArg()
+ .ofType(Integer.class).defaultsTo(0);
}
public RevisionsOptions parse(String[] args) {
@@ -162,6 +182,14 @@ public class RevisionsCommand implements Command {
boolean isVerbose() {
return options.has(verbose);
}
+
+ String getPath() {
+ return path.value(options);
+ }
+
+ int getNumber() {
+ return number.value(options);
+ }
}
@Override
@@ -180,6 +208,8 @@ public class RevisionsCommand implements Command {
reset(options, closer);
} else if (RevisionsOptions.CMD_SWEEP.equals(subCmd)) {
sweep(options, closer);
+ } else if (RevisionsOptions.CMD_CLEANUP.equals(subCmd)) {
+ cleanup(options, closer);
} else {
System.err.println("unknown revisions command: " + subCmd);
}
@@ -394,6 +424,124 @@ public class RevisionsCommand implements Command {
SweepHelper.sweep(store, new RevisionContextWrapper(ns, clusterId), seeker);
}
+ private void cleanup(RevisionsOptions options, Closer closer) throws IOException {
+ String path = options.getPath();
+ if (path == null || path.isEmpty()) {
+ System.err.println("path option is required for " + RevisionsOptions.CMD_CLEANUP + " command");
+ return;
+ }
+ Integer clusterToCleanup = options.getClusterId();
+ if (clusterToCleanup == null) {
+ System.err.println("clusterId option is required for " + RevisionsOptions.CMD_CLEANUP + " command");
+ return;
+ }
+ Integer numberToCleanup = options.getNumber();
+ if (numberToCleanup == null) {
+ System.err.println("number option is required for " + RevisionsOptions.CMD_CLEANUP + " command");
+ return;
+ }
+
+ DocumentNodeStoreBuilder<?> builder = createDocumentMKBuilder(options, closer);
+ if (builder == null) {
+ System.err.println("revisions mode only available for DocumentNodeStore");
+ return;
+ }
+
+ DocumentStore documentStore = builder.getDocumentStore();
+ builder.setReadOnlyMode();
+ useMemoryBlobStore(builder);
+ DocumentNodeStore documentNodeStore = builder.build();
+ DocumentRevisionCleanupHelper cleanupHelper = new DocumentRevisionCleanupHelper(documentStore, documentNodeStore, path);
+ cleanupHelper.initializeCleanupProcess();
+
+ SortedMap<Revision, String> allRevisions = cleanupHelper.getAllRevisions();
+ SortedMap<Integer, Integer> revisionsByClusterId = new TreeMap<>();
+ for (Revision revision : allRevisions.keySet()) {
+ Integer cid = revision.getClusterId();
+ Integer count = revisionsByClusterId.get(cid);
+ if (count == null) {
+ count = 0;
+ }
+ revisionsByClusterId.put(cid, count + 1);
+ }
+
+ System.out.println("Last Revision by clusterId");
+ for (Map.Entry<Integer, Revision> entry : cleanupHelper.getLastRev().entrySet()) {
+ System.out.printf(" [%d] -> %s%n", entry.getKey(), entry.getValue().toReadableString());
+ }
+
+ System.out.println("Sweep Revision by clusterId");
+ for (Map.Entry<Integer, Revision> entry : cleanupHelper.getSweepRev().entrySet()) {
+ System.out.printf(" [%d] -> %s%n", entry.getKey(), entry.getValue().toReadableString());
+ }
+ System.out.println();
+
+ int count = 0;
+ System.out.println("=== Total Revisions by clusterId ===");
+ for (Map.Entry<Integer, Integer> entry : revisionsByClusterId.entrySet()) {
+ System.out.printf(" [%d] -> %d revisions%n", entry.getKey(), entry.getValue());
+ count++;
+ if (count >= REVISION_CAP) {
+ System.out.printf(" ...%n");
+ break;
+ }
+ }
+
+ for (Map.Entry<Integer, TreeSet<Revision>> entry : cleanupHelper.getCandidateRevisionsToClean().entrySet()) {
+ Integer cid = entry.getKey();
+ TreeSet<Revision> revisions = entry.getValue();
+ int blocked = cleanupHelper.getBlockedRevisionsToKeep().get(cid) != null ? cleanupHelper.getBlockedRevisionsToKeep().get(cid).size() : 0;
+ System.out.printf("ClusterId [%d] has %d Candidates and %d Blocked%n", cid, revisions.size(), blocked);
+ }
+
+ System.out.println("=== Revisions to be cleaned for clusterId " + clusterToCleanup + " ===");
+ TreeSet<Revision> revisionsToClean = cleanupHelper.getCandidateRevisionsToClean().get(clusterToCleanup)
+ .stream().limit(numberToCleanup).collect(Collectors.toCollection(TreeSet::new));
+ if (revisionsToClean.isEmpty()) {
+ System.out.println("No revisions to clean");
+ return;
+ } else {
+ count = 0;
+ for (Revision revision : revisionsToClean) {
+ System.out.printf(" %s%n", revision.toReadableString());
+ count++;
+ if (count >= REVISION_CAP) {
+ System.out.print(" ...%n");
+ System.out.printf(" %s%n", revisionsToClean.last().toReadableString());
+ break;
+ }
+ }
+ }
+
+ Scanner scanner = new Scanner(System.in);
+ System.out.println("The revisions will be deleted permanently. Are you sure to proceed? [y/N]");
+ String confirmation = scanner.nextLine().trim().toLowerCase();
+ if (confirmation.equals("y") || confirmation.equals("yes")) {
+ // Start the cleanup
+ count = 0;
+ for (Revision revision : cleanupHelper.getCandidateRevisionsToClean().get(clusterToCleanup)) {
+ // Remove the revision from _revisions and all the properties
+ /*UpdateOp update = new UpdateOp(workingDocument.path.toString(), false)
+ update.removeMapEntry("_revisions", revision)
+ for (String property : propertiesModifiedByRevision.get(revision)) {
+ update.removeMapEntry(property, revision)
+ }
+ io.out.println("Executing UpdateOp: " + update)
+ try {
+ documentStore.findAndUpdate(NODES, update)
+ } catch (DocumentStoreException ex) {
+ io.out.println("Operation failed: " + ex)
+ }*/
+
+ count++;
+ if (count >= numberToCleanup) {
+ break;
+ }
+ }
+ System.out.println("-- Executed " + count + " operations --");
+ }
+ }
+
private String fmtTimestamp(long ts) {
return timestampToString(ts);
}
diff --git a/oak-store-document/pom.xml b/oak-store-document/pom.xml
index 5063896026..5b582f457b 100644
--- a/oak-store-document/pom.xml
+++ b/oak-store-document/pom.xml
@@ -346,5 +346,11 @@
<version>${testcontainers.version}</version>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>com.fasterxml.jackson.core</groupId>
+ <artifactId>jackson-databind</artifactId>
+ <version>${jackson.version}</version>
+ <scope>test</scope>
+ </dependency>
</dependencies>
</project>
diff --git a/oak-store-document/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentRevisionCleanupHelper.java b/oak-store-document/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentRevisionCleanupHelper.java
new file mode 100644
index 0000000000..1cefd6f8f1
--- /dev/null
+++ b/oak-store-document/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentRevisionCleanupHelper.java
@@ -0,0 +1,276 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.jackrabbit.oak.plugins.document;
+
+import com.google.common.collect.Maps;
+import org.apache.jackrabbit.oak.plugins.document.util.Utils;
+
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.SortedMap;
+import java.util.TreeMap;
+import java.util.TreeSet;
+
+import static org.apache.jackrabbit.oak.plugins.document.Collection.NODES;
+import static org.apache.jackrabbit.oak.plugins.document.NodeDocument.isDeletedEntry;
+
+public class DocumentRevisionCleanupHelper {
+
+ private final DocumentStore documentStore;
+ private final DocumentNodeStore documentNodeStore;
+ private final NodeDocument rootDoc;
+ private final NodeDocument workingDocument;
+
+ private SortedMap<String, SortedMap<Integer, TreeSet<Revision>>> revisionsModifyingProperty;
+ private SortedMap<Revision, TreeSet<String>> propertiesModifiedByRevision;
+ private SortedMap<Integer, TreeSet<Revision>> blockedRevisionsToKeep;
+ private SortedMap<Integer, TreeSet<Revision>> candidateRevisionsToClean;
+
+ /**
+ * Constructor for DocumentRevisionCleanupHelper.
+ * @param documentStore The DocumentStore instance. Must be writable to perform cleanup.
+ * @param documentNodeStore The DocumentNodeStore instance.
+ * @param path The path of the document to clean up.
+ */
+ public DocumentRevisionCleanupHelper(DocumentStore documentStore, DocumentNodeStore documentNodeStore, String path) {
+ this.candidateRevisionsToClean = new TreeMap<>();
+ this.blockedRevisionsToKeep = new TreeMap<>();
+ this.revisionsModifyingProperty = new TreeMap<>();
+ this.propertiesModifiedByRevision = new TreeMap<>(StableRevisionComparator.INSTANCE);
+
+ this.rootDoc = documentStore.find(NODES, Utils.getIdFromPath("/"));
+ String id = Utils.getIdFromPath(path);
+ this.workingDocument = documentStore.find(NODES, id);
+ this.documentStore = documentStore;
+ this.documentNodeStore = documentNodeStore;
+ }
+
+ /**
+ * Performs the full revision cleanup process for the given document for a clusterId.
+ */
+ public void initializeCleanupProcess() {
+ classifyAndMapRevisionsAndProperties();
+ markRevisionsNewerThanThresholdToPreserve(24, ChronoUnit.HOURS);
+ markLastRevisionForEachProperty();
+ markCheckpointRevisionsToPreserve();
+ removeCandidatesInList(blockedRevisionsToKeep);
+ }
+
+ public int executeCleanupProcess(int numberToCleanup, int clusterToCleanup) {
+ return -99;
+ }
+
+ /**
+ * Step 1:
+ * This method processes the revisions of the working document, classifying them into two categories:
+ * candidate revisions that can be cleaned up and used revisions that should be kept. It also creates maps to
+ * track the relationships between revisions and properties modified by them.
+ */
+ protected void classifyAndMapRevisionsAndProperties() {
+ candidateRevisionsToClean = new TreeMap<>();
+ blockedRevisionsToKeep = new TreeMap<>();
+ revisionsModifyingProperty = new TreeMap<>();
+ propertiesModifiedByRevision = new TreeMap<>(StableRevisionComparator.INSTANCE);
+
+ // The first entry in "_deleted" has to be kept, as is when the document was created
+ NavigableMap<Revision, String> deletedRevisions = ((NavigableMap<Revision, String>) workingDocument.get("_deleted"));
+ if (deletedRevisions != null && !deletedRevisions.isEmpty()) {
+ Revision createdRevision = deletedRevisions.descendingMap().lastKey();
+ addCandidateRevisionToClean(createdRevision);
+ addBlockedRevisionToKeep(createdRevision);
+ }
+
+ SortedMap<Revision, String> documentRevisions = getAllRevisions();
+ for (Map.Entry<Revision, String> revisionEntry : documentRevisions.entrySet()) {
+ Revision revision = revisionEntry.getKey();
+ String revisionValue = revisionEntry.getValue();
+
+ // Only check committed revisions (ignore branch commits starting with "c-")
+ if ("c".equals(revisionValue)) {
+ // Candidate to clean up
+ addCandidateRevisionToClean(revision);
+ // Store properties usage
+ mapPropertiesModifiedByThisRevision(revision);
+ }
+ }
+ }
+
+ /**
+ * Step 2:
+ * This method filters out revisions that are newer than a specified time threshold (specified by amount and unit)
+ * from the candidate revisions to be cleaned up, marking them as used revisions that should be kept.
+ * @param amount the amount of time
+ * @param unit the unit of time
+ */
+ protected void markRevisionsNewerThanThresholdToPreserve(long amount, ChronoUnit unit) {
+ long twentyFiveHoursAgoMillis = Instant.now().minus(amount, unit).toEpochMilli();
+ for (TreeSet<Revision> revisionSet : candidateRevisionsToClean.values()) {
+ for (Revision revision : revisionSet) {
+ if (revision.getTimestamp() > twentyFiveHoursAgoMillis) {
+ addBlockedRevisionToKeep(revision);
+ }
+ }
+ }
+ }
+
+ /**
+ * Step 3:
+ * This method processes a set of revisions that modified certain properties and keeps the last revision that
+ * modified each property. This means, the current status of the node will be preserved.
+ */
+ protected void markLastRevisionForEachProperty() {
+ for (SortedMap<Integer, TreeSet<Revision>> revisionsByCluster : revisionsModifyingProperty.values()) {
+ for (TreeSet<Revision> revisions : revisionsByCluster.values()) {
+ Revision lastRevision = revisions.last();
+ addBlockedRevisionToKeep(lastRevision);
+ }
+ }
+ }
+
+ /**
+ * Step 4:
+ * Process a set of revisions that modified certain properties and determine which revisions should be
+ * kept based on the checkpoints.
+ */
+ protected void markCheckpointRevisionsToPreserve() {
+ SortedMap<Revision, Checkpoints.Info> checkpoints = documentNodeStore.getCheckpoints().getCheckpoints();
+ checkpoints.forEach((revision, info) -> {
+ // For each checkpoint, keep the last revision that modified a property prior to checkpoint
+ revisionsModifyingProperty.forEach((propertyName, revisionsByCluster) -> {
+ // Traverse the revisionVector of the checkpoint and find the last revision that modified the property
+ info.getCheckpoint().forEach(revisionToFind -> {
+ TreeSet<Revision> listOfRevisionsForProperty = revisionsByCluster.get(revisionToFind.getClusterId());
+ if (listOfRevisionsForProperty != null) {
+ // If the exact revision exists, keep it. If not, find the previous one that modified that property
+ if (listOfRevisionsForProperty.contains(revisionToFind)) {
+ addBlockedRevisionToKeep(revisionToFind);
+ } else {
+ Revision previousRevision = listOfRevisionsForProperty.descendingSet().ceiling(revisionToFind);
+ if (previousRevision != null) {
+ addBlockedRevisionToKeep(previousRevision);
+ }
+ }
+ }
+ });
+ });
+ });
+ }
+
+ /**
+ * Step 5:
+ * Removes for each clusterId the revisions in the Map, that were blocked in the methods above.
+ */
+ private void removeCandidatesInList(SortedMap<Integer, TreeSet<Revision>> revisions) {
+ revisions.forEach((key, value) -> {
+ if (candidateRevisionsToClean.containsKey(key)) {
+ candidateRevisionsToClean.get(key).removeAll(value);
+ }
+ });
+ }
+
+ /**
+ * This method processes a given revision and identify the properties modified by it within the working document.
+ * It maintains two data structures, propertiesModifiedByRevision and revisionsModifyingProperty, to store
+ * the properties and their associated revisions.
+ * @param revision
+ */
+ private void mapPropertiesModifiedByThisRevision(Revision revision) {
+ for (Map.Entry<String, Object> propertyEntry : workingDocument.entrySet()) {
+ if (Utils.isPropertyName(propertyEntry.getKey()) || isDeletedEntry(propertyEntry.getKey())) {
+ Map<Revision, String> valueMap = (Map) propertyEntry.getValue();
+ if (valueMap.containsKey(revision)) {
+ propertiesModifiedByRevision.computeIfAbsent(revision, key ->
+ new TreeSet<>()).add(propertyEntry.getKey()
+ );
+
+ revisionsModifyingProperty.computeIfAbsent(propertyEntry.getKey(), key ->
+ new TreeMap<>()
+ ).computeIfAbsent(revision.getClusterId(), key ->
+ new TreeSet<>(StableRevisionComparator.INSTANCE)
+ ).add(revision);
+ }
+ }
+ }
+ }
+
+ /**
+ * Adds a revision to the list of candidates to delete.
+ * @param revision
+ */
+ private void addCandidateRevisionToClean(Revision revision) {
+ candidateRevisionsToClean.computeIfAbsent(revision.getClusterId(), key ->
+ new TreeSet<>(StableRevisionComparator.INSTANCE)
+ ).add(revision);
+ }
+
+ /**
+ * Adds a revision to the list of revisions to keep.
+ * @param revision
+ */
+ private void addBlockedRevisionToKeep(Revision revision) {
+ blockedRevisionsToKeep.computeIfAbsent(revision.getClusterId(), key ->
+ new TreeSet<>(StableRevisionComparator.INSTANCE)
+ ).add(revision);
+ }
+
+ /**
+ * Returns the LastRev map from the root document.
+ * @return
+ */
+ public Map<Integer, Revision> getLastRev() {
+ return rootDoc.getLastRev();
+ }
+
+ /**
+ * Returns the SweepRev map from the root document.
+ * @return
+ */
+ public Map<Integer, Revision> getSweepRev() {
+ Map<Integer, Revision> map = Maps.newHashMap();
+ Map<Revision, String> valueMap = (SortedMap<Revision, String>) rootDoc.get("_sweepRev");
+ for (Map.Entry<Revision, String> e : valueMap.entrySet()) {
+ int clusterId = e.getKey().getClusterId();
+ Revision rev = Revision.fromString(e.getValue());
+ map.put(clusterId, rev);
+ }
+ return map;
+ }
+
+ public NavigableMap<Revision, String> getAllRevisions() {
+ return (NavigableMap<Revision, String>) workingDocument.get("_revisions");
+ }
+
+ public SortedMap<String, SortedMap<Integer, TreeSet<Revision>>> getRevisionsModifyingProperty() {
+ return revisionsModifyingProperty;
+ }
+
+ public SortedMap<Revision, TreeSet<String>> getPropertiesModifiedByRevision() {
+ return propertiesModifiedByRevision;
+ }
+
+ public SortedMap<Integer, TreeSet<Revision>> getBlockedRevisionsToKeep() {
+ return blockedRevisionsToKeep;
+ }
+
+ public SortedMap<Integer, TreeSet<Revision>> getCandidateRevisionsToClean() {
+ return candidateRevisionsToClean;
+ }
+}
diff --git a/oak-store-document/src/test/java/org/apache/jackrabbit/oak/plugins/document/DocumentRevisionCleanupHelperTest.java b/oak-store-document/src/test/java/org/apache/jackrabbit/oak/plugins/document/DocumentRevisionCleanupHelperTest.java
new file mode 100644
index 0000000000..01daf58358
--- /dev/null
+++ b/oak-store-document/src/test/java/org/apache/jackrabbit/oak/plugins/document/DocumentRevisionCleanupHelperTest.java
@@ -0,0 +1,366 @@
+package org.apache.jackrabbit.oak.plugins.document;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.google.common.collect.Maps;
+import org.mockito.Mockito;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.IOException;
+import java.util.Map;
+import java.util.Set;
+import java.util.SortedMap;
+import java.util.TreeMap;
+import java.util.TreeSet;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import static org.apache.jackrabbit.oak.plugins.document.Collection.NODES;
+import static org.apache.jackrabbit.oak.plugins.document.Collection.SETTINGS;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+public class DocumentRevisionCleanupHelperTest {
+
+ @Mock
+ DocumentStore documentStore;
+
+ @Mock
+ DocumentNodeStore documentNodeStore;
+
+ @Mock
+ Checkpoints checkpoints;
+
+ @Mock
+ NodeDocument workingDocument;
+
+ DocumentRevisionCleanupHelper documentRevisionCleanupHelper;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ workingDocument = Mockito.mock(NodeDocument.class);
+
+ Mockito.when(documentStore.find(Mockito.eq(NODES), Mockito.anyString())).thenReturn(workingDocument);
+ documentRevisionCleanupHelper = new DocumentRevisionCleanupHelper(documentStore, documentNodeStore, "/");
+ }
+
+ @Test
+ public void testMarkCheckpointRevisionsToPreserveOnePropertyOneCluster() throws IOException {
+ Revision revisionA = Revision.fromString("r100000000-0-1");
+ Revision revisionB = Revision.fromString("r105000000-0-1");
+ Revision revisionC = Revision.fromString("r110000000-0-1");
+ Revision revisionD = Revision.fromString("r115000000-0-1");
+ Revision revisionE = Revision.fromString("r120000000-0-1");
+ Revision revisionF = Revision.fromString("r125000000-0-1");
+
+ Revision checkpoint1 = Revision.fromString("r109000000-0-1");
+ Revision checkpoint2 = Revision.fromString("r119000000-0-1");
+
+ String jsonProperties = "{" +
+ "'prop1': {'" + revisionA + "': 'value1', '" + revisionB + "': 'value2', '" + revisionC + "': 'value3', '" + revisionD + "': 'value4', '" + revisionE + "': 'value5', '" + revisionF + "': 'value6'}, " +
+ "'_revisions': {'" + revisionA + "': 'c', '" + revisionB + "': 'c', '" + revisionC + "': 'c', '" + revisionC + "': 'c', '" + revisionD + "': 'c', '" + revisionE + "': 'c', '" + revisionF + "': 'c'}" +
+ "}";
+ String jsonCheckpoints = "{" +
+ "'" + checkpoint1 + "': {'expires':'200000000','rv':'r109000000-0-1'}," +
+ "'" + checkpoint2 + "': {'expires':'200000000','rv':'r119000000-0-1'}" +
+ "}";
+
+
+ prepareDocumentMock(jsonProperties);
+ prepareCheckpointsMock(jsonCheckpoints);
+
+ documentRevisionCleanupHelper.classifyAndMapRevisionsAndProperties();
+ documentRevisionCleanupHelper.markCheckpointRevisionsToPreserve();
+
+ // The revisions blocked should be:
+ // - r105000000-0-1 (blocked by checkpoint r109000000-0-1)
+ // - r115000000-0-1 (blocked by checkpoint r119000000-0-1)
+
+ assertEquals(Set.of(revisionB, revisionD), documentRevisionCleanupHelper.getBlockedRevisionsToKeep().get(1));
+
+
+ /*assertEquals(Set.of(revisionC), documentRevisionCleanupHelper.getBlockedRevisionsToKeep().get(1));
+ assertEquals(Set.of(revisionA, revisionB, revisionC), documentRevisionCleanupHelper.getCandidateRevisionsToClean().get(1));*/
+ }
+
+ @Test
+ public void testInitializeCleanupProcessMultipleClusters() throws IOException {
+ Revision revisionA = Revision.fromString("r100000000-0-1");
+ Revision revisionB = Revision.fromString("r105000000-0-2");
+ Revision revisionC = Revision.fromString("r110000000-0-3");
+ Revision revisionD = Revision.fromString("r115000000-0-1");
+ Revision revisionE = Revision.fromString("r120000000-0-2");
+ Revision revisionF = Revision.fromString("r125000000-0-3");
+
+ Revision checkpoint1 = Revision.fromString("r109000000-0-1");
+ Revision checkpoint2 = Revision.fromString("r119000000-0-1");
+
+ String jsonProperties = "{" +
+ "'prop1': {'" + revisionA + "': 'value1', '" + revisionB + "': 'value2', '" + revisionC + "': 'value3', '" + revisionD + "': 'value4', '" + revisionE + "': 'value5', '" + revisionF + "': 'value6'}, " +
+ "'_revisions': {'" + revisionA + "': 'c', '" + revisionB + "': 'c', '" + revisionC + "': 'c', '" + revisionC + "': 'c', '" + revisionD + "': 'c', '" + revisionE + "': 'c', '" + revisionF + "': 'c'}" +
+ "}";
+ String jsonCheckpoints = "{" +
+ "'" + checkpoint1 + "': {'expires':'200000000','rv':'r109000000-0-1,r109000000-0-1'}," +
+ "'" + checkpoint2 + "': {'expires':'200000000','rv':'r119000000-0-1,r109000000-0-1'}" +
+ "}";
+
+
+ prepareDocumentMock(jsonProperties);
+ prepareCheckpointsMock(jsonCheckpoints);
+
+ documentRevisionCleanupHelper.initializeCleanupProcess();
+
+ // The revisions blocked should be:
+ //
+
+ assertEquals(Set.of(revisionC), documentRevisionCleanupHelper.getBlockedRevisionsToKeep().get(1));
+ assertEquals(Set.of(revisionA, revisionB, revisionC), documentRevisionCleanupHelper.getCandidateRevisionsToClean().get(1));
+ }
+
+ @Test
+ public void testLastRevisionIsBlocked() throws IOException {
+ Revision revisionA = new Revision(111111111L, 0, 1);
+ Revision revisionB = new Revision(222222222L, 0, 1);
+ Revision revisionC = new Revision(333333333L, 0, 1);
+
+ String jsonProperties = "{" +
+ "'prop1': {'" + revisionA + "': 'value1', '" + revisionB + "': 'value2', '" + revisionC + "': 'value3'}, " +
+ "'_revisions': {'" + revisionA + "': 'c', '" + revisionB + "': 'c', '" + revisionC + "': 'c'}" +
+ "}";
+ prepareDocumentMock(jsonProperties);
+
+ documentRevisionCleanupHelper.classifyAndMapRevisionsAndProperties();
+ documentRevisionCleanupHelper.markLastRevisionForEachProperty();
+
+ assertFalse(documentRevisionCleanupHelper.getBlockedRevisionsToKeep().get(1).contains(revisionA));
+ assertFalse(documentRevisionCleanupHelper.getBlockedRevisionsToKeep().get(1).contains(revisionB));
+ assertEquals(Set.of(revisionC), documentRevisionCleanupHelper.getBlockedRevisionsToKeep().get(1));
+ assertEquals(Set.of(revisionA, revisionB, revisionC), documentRevisionCleanupHelper.getCandidateRevisionsToClean().get(1));
+ }
+
+ @Test
+ public void testFirstDeletedRevisionIsBlocked() throws IOException {
+ Revision revisionA = new Revision(111111111L, 0, 1);
+ Revision revisionB = new Revision(222222222L, 0, 1);
+ Revision revisionC = new Revision(333333333L, 0, 1);
+
+ String jsonProperties = "{" +
+ "'_deleted': {'" + revisionA + "': 'false', '" + revisionB + "': 'true', '" + revisionC + "': 'false'}," +
+ "'_revisions': {'" + revisionA + "': 'c', '" + revisionB + "': 'c', '" + revisionC + "': 'c'}" +
+ "}";
+ prepareDocumentMock(jsonProperties);
+
+ documentRevisionCleanupHelper.classifyAndMapRevisionsAndProperties();
+
+ assertTrue(documentRevisionCleanupHelper.getBlockedRevisionsToKeep().get(1).contains(revisionA));
+ assertEquals(Set.of(revisionA, revisionB, revisionC), documentRevisionCleanupHelper.getCandidateRevisionsToClean().get(1));
+ }
+
+ @Test
+ public void testClassifyAndMapRevisionsAndPropertiesWithDeleted() throws IOException {
+ Revision revisionA = new Revision(111111111L, 0, 1);
+ Revision revisionB = new Revision(222222222L, 0, 1);
+ Revision revisionC = new Revision(333333333L, 0, 1);
+
+ String jsonProperties = "{" +
+ "'prop1': {'" + revisionA + "': 'value1', '" + revisionB + "': 'value2', '" + revisionC + "': 'value3'}, " +
+ "'prop2': {'" + revisionB + "': 'value4'}, " +
+ "'_deleted': {'" + revisionA + "': 'false'}," +
+ "'_revisions': {'" + revisionA + "': 'c', '" + revisionB + "': 'c', '" + revisionC + "': 'c'}" +
+ "}";
+ prepareDocumentMock(jsonProperties);
+
+ documentRevisionCleanupHelper.classifyAndMapRevisionsAndProperties();
+
+ SortedMap<Revision, TreeSet<String>> propertiesModifiedByRevision = documentRevisionCleanupHelper.getPropertiesModifiedByRevision();
+ assertEquals(Set.of("prop1", "_deleted"), propertiesModifiedByRevision.get(revisionA));
+ assertEquals(Set.of("prop1", "prop2"), propertiesModifiedByRevision.get(revisionB));
+ assertEquals(Set.of("prop1"), propertiesModifiedByRevision.get(revisionC));
+
+ SortedMap<String, SortedMap<Integer, TreeSet<Revision>>> revisionsModifyingProperty = documentRevisionCleanupHelper.getRevisionsModifyingProperty();
+ assertEquals(Set.of(revisionA, revisionB, revisionC), revisionsModifyingProperty.get("prop1").get(1));
+ assertEquals(Set.of(revisionB), revisionsModifyingProperty.get("prop2").get(1));
+ assertEquals(Set.of(revisionA), revisionsModifyingProperty.get("_deleted").get(1));
+ }
+
+ @Test
+ public void testClassifyAndMapRevisionsMultipleDeleted() throws IOException {
+ Revision revisionA = new Revision(111111111L, 0, 1);
+ Revision revisionB = new Revision(222222222L, 0, 1);
+ Revision revisionC = new Revision(333333333L, 0, 1);
+
+ String jsonProperties = "{" +
+ "'_deleted': {'" + revisionA + "': 'false', '" + revisionB + "': 'true', '" + revisionC + "': 'false'}," +
+ "'_revisions': {'" + revisionA + "': 'c', '" + revisionB + "': 'c', '" + revisionC + "': 'c'}" +
+ "}";
+ prepareDocumentMock(jsonProperties);
+
+ documentRevisionCleanupHelper.classifyAndMapRevisionsAndProperties();
+
+ SortedMap<Revision, TreeSet<String>> propertiesModifiedByRevision = documentRevisionCleanupHelper.getPropertiesModifiedByRevision();
+ assertEquals(Set.of("_deleted"), propertiesModifiedByRevision.get(revisionA));
+ assertEquals(Set.of("_deleted"), propertiesModifiedByRevision.get(revisionB));
+ assertEquals(Set.of("_deleted"), propertiesModifiedByRevision.get(revisionC));
+
+ SortedMap<String, SortedMap<Integer, TreeSet<Revision>>> revisionsModifyingProperty = documentRevisionCleanupHelper.getRevisionsModifyingProperty();
+ assertEquals(Set.of(revisionA, revisionB, revisionC), revisionsModifyingProperty.get("_deleted").get(1));
+ }
+
+ @Test
+ public void testClassifyAndMapRevisionsAndPropertiesWithoutDeleted() throws IOException {
+ Revision revisionA = new Revision(111111111L, 0, 1);
+ Revision revisionB = new Revision(222222222L, 0, 1);
+ Revision revisionC = new Revision(333333333L, 0, 1);
+
+ String jsonProperties = "{" +
+ "'prop1': {'" + revisionA + "': 'value1', '" + revisionB + "': 'value2', '" + revisionC + "': 'value3'}, " +
+ "'prop2': {'" + revisionB + "': 'value4'}, " +
+ "'_revisions': {'" + revisionA + "': 'c', '" + revisionB + "': 'c', '" + revisionC + "': 'c'}" +
+ "}";
+ prepareDocumentMock(jsonProperties);
+
+ documentRevisionCleanupHelper.classifyAndMapRevisionsAndProperties();
+
+ SortedMap<Revision, TreeSet<String>> propertiesModifiedByRevision = documentRevisionCleanupHelper.getPropertiesModifiedByRevision();
+ assertEquals(Set.of("prop1"), propertiesModifiedByRevision.get(revisionA));
+ assertEquals(Set.of("prop1", "prop2"), propertiesModifiedByRevision.get(revisionB));
+ assertEquals(Set.of("prop1"), propertiesModifiedByRevision.get(revisionC));
+
+ SortedMap<String, SortedMap<Integer, TreeSet<Revision>>> revisionsModifyingProperty = documentRevisionCleanupHelper.getRevisionsModifyingProperty();
+ assertEquals(Set.of(revisionA, revisionB, revisionC), revisionsModifyingProperty.get("prop1").get(1));
+ assertEquals(Set.of(revisionB), revisionsModifyingProperty.get("prop2").get(1));
+ }
+
+ @Test
+ public void testClassifyAndMapRevisionsAndPropertiesNotCommitted() throws IOException {
+ Revision revisionA = new Revision(111111111L, 0, 1);
+ Revision revisionB = new Revision(222222222L, 0, 1);
+ Revision revisionC = new Revision(333333333L, 0, 1);
+ Revision revisionD = new Revision(444444444L, 0, 1);
+
+ String jsonProperties = "{" +
+ "'prop1': {'" + revisionA + "': 'value1', '" + revisionB + "': 'value2', '" + revisionC + "': 'value3', '" + revisionD + "': 'value5'}, " +
+ "'prop2': {'" + revisionB + "': 'value4', '" + revisionD + "': 'value5'}, " +
+ "'_revisions': {'" + revisionA + "': 'c', '" + revisionB + "': 'c', '" + revisionC + "': 'c', '" + revisionD + "': 'nc'}" +
+ "}";
+ prepareDocumentMock(jsonProperties);
+
+ documentRevisionCleanupHelper.classifyAndMapRevisionsAndProperties();
+
+ // Modifications done in revisionD should be ignored, as it is not committed
+ SortedMap<Revision, TreeSet<String>> propertiesModifiedByRevision = documentRevisionCleanupHelper.getPropertiesModifiedByRevision();
+ assertEquals(Set.of("prop1"), propertiesModifiedByRevision.get(revisionA));
+ assertEquals(Set.of("prop1", "prop2"), propertiesModifiedByRevision.get(revisionB));
+ assertEquals(Set.of("prop1"), propertiesModifiedByRevision.get(revisionC));
+ assertNull(propertiesModifiedByRevision.get(revisionD));
+
+ SortedMap<String, SortedMap<Integer, TreeSet<Revision>>> revisionsModifyingProperty = documentRevisionCleanupHelper.getRevisionsModifyingProperty();
+ assertEquals(Set.of(revisionA, revisionB, revisionC), revisionsModifyingProperty.get("prop1").get(1));
+ assertEquals(Set.of(revisionB), revisionsModifyingProperty.get("prop2").get(1));
+ }
+
+ @Test
+ public void testClassifyAndMapRevisionsAndPropertiesDifferentClusters() throws IOException {
+ Revision revisionA = new Revision(111111111L, 0, 1);
+ Revision revisionB = new Revision(222222222L, 0, 2);
+ Revision revisionC = new Revision(333333333L, 0, 3);
+ Revision revisionD = new Revision(444444444L, 0, 1);
+
+ String jsonProperties = "{" +
+ "'prop1': {'" + revisionA + "': 'value1', '" + revisionB + "': 'value2', '" + revisionC + "': 'value3', '" + revisionD + "': 'value5'}, " +
+ "'prop2': {'" + revisionB + "': 'value4', '" + revisionD + "': 'value5'}, " +
+ "'_revisions': {'" + revisionA + "': 'c', '" + revisionB + "': 'c', '" + revisionC + "': 'c', '" + revisionD + "': 'c'}" +
+ "}";
+ prepareDocumentMock(jsonProperties);
+
+ documentRevisionCleanupHelper.classifyAndMapRevisionsAndProperties();
+
+ SortedMap<Revision, TreeSet<String>> propertiesModifiedByRevision = documentRevisionCleanupHelper.getPropertiesModifiedByRevision();
+ assertEquals(Set.of("prop1"), propertiesModifiedByRevision.get(revisionA));
+ assertEquals(Set.of("prop1", "prop2"), propertiesModifiedByRevision.get(revisionB));
+ assertEquals(Set.of("prop1"), propertiesModifiedByRevision.get(revisionC));
+ assertEquals(Set.of("prop1", "prop2"), propertiesModifiedByRevision.get(revisionD));
+
+ SortedMap<String, SortedMap<Integer, TreeSet<Revision>>> revisionsModifyingProperty = documentRevisionCleanupHelper.getRevisionsModifyingProperty();
+ assertEquals(Set.of(revisionA, revisionD), revisionsModifyingProperty.get("prop1").get(1));
+ assertEquals(Set.of(revisionB), revisionsModifyingProperty.get("prop1").get(2));
+ assertEquals(Set.of(revisionC), revisionsModifyingProperty.get("prop1").get(3));
+ assertEquals(Set.of(revisionD), revisionsModifyingProperty.get("prop2").get(1));
+ assertEquals(Set.of(revisionB), revisionsModifyingProperty.get("prop2").get(2));
+ assertNull(revisionsModifyingProperty.get("prop2").get(3));
+ }
+
+ private void prepareDocumentMock(String jsonProperties) throws IOException {
+ String json = jsonProperties.replace("'", "\"");
+
+ ObjectMapper objectMapper = new ObjectMapper();
+ Map<String, Map<String, String>> data = objectMapper.readValue(json, new TypeReference<>() {});
+
+ SortedMap<String, Object> entries = new TreeMap<>();
+ SortedMap<Revision, String> allRevisions = new TreeMap<>(StableRevisionComparator.INSTANCE);
+ for (Map.Entry<String, Map<String, String>> entry : data.entrySet()) {
+ String property = entry.getKey();
+ Map<String, String> revisions = entry.getValue();
+
+ SortedMap<Revision, String> sortedRevisions = new TreeMap<>(StableRevisionComparator.INSTANCE);
+ for (Map.Entry<String, String> revisionEntry : revisions.entrySet()) {
+ String revisionStr = revisionEntry.getKey();
+ String value = revisionEntry.getValue();
+
+ String[] parts = revisionStr.split("-");
+ // The timestamp part of the revision string (first part) is parsed as hexadecimal (radix 16)
+ long timestamp = Long.parseLong(parts[0].substring(1), 16);
+ int counter = Integer.parseInt(parts[1]);
+ int clusterId = Integer.parseInt(parts[2]);
+
+ Revision revision = new Revision(timestamp, counter, clusterId);
+ sortedRevisions.put(revision, value);
+
+ // Add all revisions to the "_revisions" list
+ allRevisions.put(revision, value);
+ }
+ entries.put(property, sortedRevisions);
+ }
+
+ Mockito.when(workingDocument.entrySet()).thenReturn(entries.entrySet());
+ Mockito.when(workingDocument.get("_deleted")).thenReturn(entries.get("_deleted"));
+ Mockito.when(workingDocument.get("_revisions")).thenReturn(allRevisions);
+ }
+
+ private void prepareCheckpointsMock(String jsonCheckpoints) throws IOException {
+ String json = jsonCheckpoints.replace("'", "\"");
+
+ ObjectMapper objectMapper = new ObjectMapper();
+ Map<String, Map<String, Object>> data = objectMapper.readValue(json, new TypeReference<>() {});
+
+ SortedMap<Revision, Checkpoints.Info> checkpoints = new TreeMap<>(StableRevisionComparator.REVERSE);
+ for (Map.Entry<String, Map<String, Object>> entry : data.entrySet()) {
+ String checkpointStr = entry.getKey();
+ Map<String, Object> checkpointData = entry.getValue();
+
+ String[] parts = checkpointStr.split("-");
+ long timestamp = Long.parseLong(parts[0].substring(1), 16);
+ int counter = Integer.parseInt(parts[1]);
+ int clusterId = Integer.parseInt(parts[2]);
+
+ Revision checkpoint = new Revision(timestamp, counter, clusterId);
+ String checkpointDataJson = objectMapper.writeValueAsString(checkpointData);
+
+ Checkpoints.Info info = Checkpoints.Info.fromString(checkpointDataJson);
+ checkpoints.put(checkpoint, info);
+ }
+
+ Mockito.when(documentNodeStore.getCheckpoints()).thenReturn(this.checkpoints);
+ Mockito.when(documentNodeStore.getCheckpoints().getCheckpoints()).thenReturn(checkpoints);
+
+ /*Document cdoc = SETTINGS.newDocument(documentStore);
+ Mockito.when(documentNodeStore.getCheckpoints()).thenReturn(this.checkpoints);
+ Mockito.when(documentStore.find(Collection.SETTINGS, "checkpoint", 0)).thenReturn(cdoc);*/
+ }
+}
\ No newline at end of file
(jackrabbit-oak) 03/03: Some test fixes and improvements.
Posted by jo...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
joscorbe pushed a commit to branch old-revisions-cleanup
in repository https://gitbox.apache.org/repos/asf/jackrabbit-oak.git
commit 1bc1280a14ebea4f975cff3016f7ba5b17067422
Author: Jose Cordero <co...@adobe.com>
AuthorDate: Mon Nov 6 23:23:04 2023 +0100
Some test fixes and improvements.
---
.../document/DocumentRevisionCleanupHelper.java | 40 ++++---
.../DocumentRevisionCleanupHelperTest.java | 126 +++++++++++++++------
2 files changed, 117 insertions(+), 49 deletions(-)
diff --git a/oak-store-document/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentRevisionCleanupHelper.java b/oak-store-document/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentRevisionCleanupHelper.java
index 77c96ea465..efb5f7a31a 100644
--- a/oak-store-document/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentRevisionCleanupHelper.java
+++ b/oak-store-document/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentRevisionCleanupHelper.java
@@ -39,7 +39,8 @@ public class DocumentRevisionCleanupHelper {
private final NodeDocument rootDoc;
private final NodeDocument workingDocument;
- private SortedMap<String, SortedMap<Integer, TreeSet<Revision>>> revisionsModifyingProperty;
+ private SortedMap<String, SortedMap<Integer, TreeSet<Revision>>> revisionsModifyingPropertyByCluster;
+ private SortedMap<String, TreeSet<Revision>> revisionsModifyingProperty;
private SortedMap<Revision, TreeSet<String>> propertiesModifiedByRevision;
private SortedMap<Integer, TreeSet<Revision>> blockedRevisionsToKeep;
private SortedMap<Integer, TreeSet<Revision>> candidateRevisionsToClean;
@@ -53,6 +54,7 @@ public class DocumentRevisionCleanupHelper {
public DocumentRevisionCleanupHelper(DocumentStore documentStore, DocumentNodeStore documentNodeStore, String path) {
this.candidateRevisionsToClean = new TreeMap<>();
this.blockedRevisionsToKeep = new TreeMap<>();
+ this.revisionsModifyingPropertyByCluster = new TreeMap<>();
this.revisionsModifyingProperty = new TreeMap<>();
this.propertiesModifiedByRevision = new TreeMap<>(StableRevisionComparator.INSTANCE);
@@ -87,6 +89,7 @@ public class DocumentRevisionCleanupHelper {
protected void classifyAndMapRevisionsAndProperties() {
candidateRevisionsToClean = new TreeMap<>();
blockedRevisionsToKeep = new TreeMap<>();
+ revisionsModifyingPropertyByCluster = new TreeMap<>();
revisionsModifyingProperty = new TreeMap<>();
propertiesModifiedByRevision = new TreeMap<>(StableRevisionComparator.INSTANCE);
@@ -137,10 +140,10 @@ public class DocumentRevisionCleanupHelper {
/**
* Step 3:
* This method processes a set of revisions that modified certain properties and keeps the last revision that
- * modified each property. This means, the current status of the node will be preserved.
+ * modified each property. This means, the current status of the node will be preserved, for each clusterId.
*/
protected void markLastRevisionForEachProperty() {
- for (SortedMap<Integer, TreeSet<Revision>> revisionsByCluster : revisionsModifyingProperty.values()) {
+ for (SortedMap<Integer, TreeSet<Revision>> revisionsByCluster : revisionsModifyingPropertyByCluster.values()) {
for (TreeSet<Revision> revisions : revisionsByCluster.values()) {
Revision lastRevision = revisions.last();
addBlockedRevisionToKeep(lastRevision);
@@ -157,19 +160,16 @@ public class DocumentRevisionCleanupHelper {
SortedMap<Revision, Checkpoints.Info> checkpoints = documentNodeStore.getCheckpoints().getCheckpoints();
checkpoints.forEach((revision, info) -> {
// For each checkpoint, keep the last revision that modified a property prior to checkpoint
- revisionsModifyingProperty.forEach((propertyName, revisionsByCluster) -> {
+ revisionsModifyingProperty.forEach((propertyName, revisionsSet) -> {
// Traverse the revisionVector of the checkpoint and find the last revision that modified the property
info.getCheckpoint().forEach(revisionToFind -> {
- TreeSet<Revision> listOfRevisionsForProperty = revisionsByCluster.get(revisionToFind.getClusterId());
- if (listOfRevisionsForProperty != null) {
- // If the exact revision exists, keep it. If not, find the previous one that modified that property
- if (listOfRevisionsForProperty.contains(revisionToFind)) {
- addBlockedRevisionToKeep(revisionToFind);
- } else {
- Revision previousRevision = listOfRevisionsForProperty.descendingSet().ceiling(revisionToFind);
- if (previousRevision != null) {
- addBlockedRevisionToKeep(previousRevision);
- }
+ // If the exact revision exists, keep it. If not, find the previous one that modified that property
+ if (revisionsSet.contains(revisionToFind)) {
+ addBlockedRevisionToKeep(revisionToFind);
+ } else {
+ Revision previousRevision = revisionsSet.descendingSet().ceiling(revisionToFind);
+ if (previousRevision != null) {
+ addBlockedRevisionToKeep(previousRevision);
}
}
});
@@ -204,11 +204,15 @@ public class DocumentRevisionCleanupHelper {
new TreeSet<>()).add(propertyEntry.getKey()
);
- revisionsModifyingProperty.computeIfAbsent(propertyEntry.getKey(), key ->
+ revisionsModifyingPropertyByCluster.computeIfAbsent(propertyEntry.getKey(), key ->
new TreeMap<>()
).computeIfAbsent(revision.getClusterId(), key ->
new TreeSet<>(StableRevisionComparator.INSTANCE)
).add(revision);
+
+ revisionsModifyingProperty.computeIfAbsent(propertyEntry.getKey(), key ->
+ new TreeSet<>(StableRevisionComparator.INSTANCE)
+ ).add(revision);
}
}
}
@@ -261,7 +265,11 @@ public class DocumentRevisionCleanupHelper {
return (NavigableMap<Revision, String>) workingDocument.get("_revisions");
}
- public SortedMap<String, SortedMap<Integer, TreeSet<Revision>>> getRevisionsModifyingProperty() {
+ public SortedMap<String, SortedMap<Integer, TreeSet<Revision>>> getRevisionsModifyingPropertyByCluster() {
+ return revisionsModifyingPropertyByCluster;
+ }
+
+ public SortedMap<String, TreeSet<Revision>> getRevisionsModifyingProperty() {
return revisionsModifyingProperty;
}
diff --git a/oak-store-document/src/test/java/org/apache/jackrabbit/oak/plugins/document/DocumentRevisionCleanupHelperTest.java b/oak-store-document/src/test/java/org/apache/jackrabbit/oak/plugins/document/DocumentRevisionCleanupHelperTest.java
index add00b08f5..b56df5ae2c 100644
--- a/oak-store-document/src/test/java/org/apache/jackrabbit/oak/plugins/document/DocumentRevisionCleanupHelperTest.java
+++ b/oak-store-document/src/test/java/org/apache/jackrabbit/oak/plugins/document/DocumentRevisionCleanupHelperTest.java
@@ -152,38 +152,38 @@ public class DocumentRevisionCleanupHelperTest {
// Some initial revisions (0-2)
revs.add(Revision.fromString("r100000000-0-1"));
- revs.add(Revision.fromString("r100000000-0-2"));
- revs.add(Revision.fromString("r100000000-0-3"));
+ revs.add(Revision.fromString("r100000001-0-2"));
+ revs.add(Revision.fromString("r100000002-0-3"));
// Blocked by first checkpoint r109000000 (3-5)
- revs.add(Revision.fromString("r106000000-0-1"));
- revs.add(Revision.fromString("r105000000-0-2"));
- revs.add(Revision.fromString("r108000000-0-3"));
+ revs.add(Revision.fromString("r106000003-0-1"));
+ revs.add(Revision.fromString("r107000004-0-2"));
+ revs.add(Revision.fromString("r108000005-0-3"));
// Some unblocked revisions in middle (6-10)
- revs.add(Revision.fromString("r110000000-0-3"));
- revs.add(Revision.fromString("r110000000-1-3"));
- revs.add(Revision.fromString("r114000000-0-2"));
- revs.add(Revision.fromString("r115000000-0-1"));
- revs.add(Revision.fromString("r118000000-0-1"));
+ revs.add(Revision.fromString("r110000006-0-3"));
+ revs.add(Revision.fromString("r110000006-1-3"));
+ revs.add(Revision.fromString("r114000007-0-2"));
+ revs.add(Revision.fromString("r115000008-0-1"));
+ revs.add(Revision.fromString("r118000009-0-1"));
// Blocked by second checkpoint r118000000 (11-13)
- revs.add(Revision.fromString("r118000000-1-1"));
- revs.add(Revision.fromString("r118000000-0-2"));
- revs.add(Revision.fromString("r118000000-0-3"));
+ revs.add(Revision.fromString("r118000009-1-1"));
+ revs.add(Revision.fromString("r118000010-0-2"));
+ revs.add(Revision.fromString("r118000011-0-3"));
// Some more unblocked revisions (14-19)
- revs.add(Revision.fromString("r120000000-0-1"));
- revs.add(Revision.fromString("r122000000-0-2"));
- revs.add(Revision.fromString("r122000000-1-2"));
- revs.add(Revision.fromString("r123000000-0-2"));
- revs.add(Revision.fromString("r125000000-0-3"));
- revs.add(Revision.fromString("r130000000-0-1"));
+ revs.add(Revision.fromString("r120000012-0-1"));
+ revs.add(Revision.fromString("r122000013-0-2"));
+ revs.add(Revision.fromString("r122000013-1-2"));
+ revs.add(Revision.fromString("r123000014-0-2"));
+ revs.add(Revision.fromString("r125000015-0-3"));
+ revs.add(Revision.fromString("r130000016-0-1"));
// Last revision (20-22)
- revs.add(Revision.fromString("r130000000-1-1"));
- revs.add(Revision.fromString("r130000000-0-2"));
- revs.add(Revision.fromString("r130000000-0-3"));
+ revs.add(Revision.fromString("r130000016-1-1"));
+ revs.add(Revision.fromString("r130000017-0-2"));
+ revs.add(Revision.fromString("r130000018-0-3"));
// Checkpoint revisions
Revision checkpoint1 = Revision.fromString("r109000000-0-1");
@@ -207,20 +207,19 @@ public class DocumentRevisionCleanupHelperTest {
jsonBuilder.append(jsonPropBuilder).append(", ").append(jsonRevisionsBuilder).append("}");
String jsonCheckpoints = "{" +
- "'" + checkpoint1 + "': {'expires':'200000000','rv':'r109000000-0-1,r109000000-0-2,r109000000-0-3'}," +
- "'" + checkpoint2 + "': {'expires':'200000000','rv':'r118000000-1-1,r118000000-0-2,r118000000-0-3'}" +
+ "'" + checkpoint1 + "': {'expires':'200000000','rv':'r106500003-0-1,r107500004-0-2,r108500005-0-3'}," +
+ "'" + checkpoint2 + "': {'expires':'200000000','rv':'r118000009-1-1,r118000010-0-2,r118000011-0-3'}" +
"}";
-
prepareDocumentMock(jsonBuilder.toString());
prepareCheckpointsMock(jsonCheckpoints);
documentRevisionCleanupHelper.initializeCleanupProcess();
// The revisions blocked should be:
- // - r106000000-0-1, r118000000-0-2, r108000000-0-3 (referenced by checkpoint 1)
- // - r118000000-1-1, r118000000-0-2, r118000000-0-3 (referenced by checkpoint 2)
- // - r130000000-1-1, r130000000-0-2, r130000000-0-3 (last revision)
+ // - r106000003-0-3, r118000004-0-2, r108000005-0-3 (referenced by checkpoint 1)
+ // - r118000009-1-1, r118000010-0-2, r118000011-0-3 (referenced by checkpoint 2)
+ // - r130000016-1-1, r130000017-0-2, r130000018-0-3 (last revision)
assertEquals(Set.of(revs.get(3), revs.get(11), revs.get(20)), documentRevisionCleanupHelper.getBlockedRevisionsToKeep().get(1));
assertEquals(Set.of(revs.get(4), revs.get(12), revs.get(21)), documentRevisionCleanupHelper.getBlockedRevisionsToKeep().get(2));
assertEquals(Set.of(revs.get(5), revs.get(13), revs.get(22)), documentRevisionCleanupHelper.getBlockedRevisionsToKeep().get(3));
@@ -235,6 +234,67 @@ public class DocumentRevisionCleanupHelperTest {
assertTrue(Collections.disjoint(documentRevisionCleanupHelper.getBlockedRevisionsToKeep().get(3), documentRevisionCleanupHelper.getCandidateRevisionsToClean().get(3)));
}
+ @Test
+ public void testCheckpointedRevisionFallback() throws IOException {
+ List<Revision> revs = new ArrayList<>();
+
+ revs.add(Revision.fromString("r100000000-0-1"));
+ revs.add(Revision.fromString("r100000001-0-2"));
+ revs.add(Revision.fromString("r100000002-0-3"));
+
+ revs.add(Revision.fromString("r101100003-0-1"));
+ revs.add(Revision.fromString("r101200004-0-2"));
+ revs.add(Revision.fromString("r101300005-0-3"));
+
+ revs.add(Revision.fromString("r102000006-0-1"));
+ revs.add(Revision.fromString("r102000007-0-3"));
+
+ revs.add(Revision.fromString("r103000008-0-1"));
+ revs.add(Revision.fromString("r103000009-0-2"));
+ revs.add(Revision.fromString("r103000010-0-3"));
+
+ // Checkpoint
+ Revision checkpoint1 = Revision.fromString("r102000000-0-1");
+
+ StringBuilder jsonPropBuilder = new StringBuilder("'prop1': {");
+ StringBuilder jsonRevisionsBuilder = new StringBuilder("'_revisions': {");
+
+ for (int i = 0; i < revs.size(); i++) {
+ jsonPropBuilder.append("'").append(revs.get(i)).append("': 'value").append(i).append("'");
+ jsonRevisionsBuilder.append("'").append(revs.get(i)).append("': 'c'");
+ if (i < revs.size() - 1) {
+ jsonPropBuilder.append(", ");
+ jsonRevisionsBuilder.append(", ");
+ }
+ }
+
+ jsonPropBuilder.append("}");
+ jsonRevisionsBuilder.append("}");
+ StringBuilder jsonBuilder = new StringBuilder("{");
+ jsonBuilder.append(jsonPropBuilder).append(", ").append(jsonRevisionsBuilder).append("}");
+
+ String jsonCheckpoints = "{" +
+ "'" + checkpoint1 + "': {'expires':'200000000','rv':'r102000007-0-1,r102000000-0-2,r102000008-0-3'}" +
+ "}";
+
+ prepareDocumentMock(jsonBuilder.toString());
+ prepareCheckpointsMock(jsonCheckpoints);
+
+ documentRevisionCleanupHelper.initializeCleanupProcess();
+
+ // The revisions blocked should be:
+ // - r103000008-0-1, r103000009-0-2, r103000010-0-3 (last revisions)
+ // - r102000006-0-1, r101300005-0-3, r102000007-0-3 (referenced by checkpoint for clusters 1, 2 and 3 respectively)
+ assertEquals(Set.of(revs.get(6), revs.get(8)), documentRevisionCleanupHelper.getBlockedRevisionsToKeep().get(1));
+ assertEquals(Set.of(revs.get(9)), documentRevisionCleanupHelper.getBlockedRevisionsToKeep().get(2));
+ assertEquals(Set.of(revs.get(5), revs.get(7), revs.get(10)), documentRevisionCleanupHelper.getBlockedRevisionsToKeep().get(3));
+
+ // Rest of revisions are candidates to clean
+ assertEquals(Set.of(revs.get(0), revs.get(3)), documentRevisionCleanupHelper.getCandidateRevisionsToClean().get(1));
+ assertEquals(Set.of(revs.get(1), revs.get(4)), documentRevisionCleanupHelper.getCandidateRevisionsToClean().get(2));
+ assertEquals(Set.of(revs.get(2)), documentRevisionCleanupHelper.getCandidateRevisionsToClean().get(3));
+ }
+
@Test
public void testLastRevisionIsBlocked() throws IOException {
Revision revisionA = new Revision(111111111L, 0, 1);
@@ -295,7 +355,7 @@ public class DocumentRevisionCleanupHelperTest {
assertEquals(Set.of("prop1", "prop2"), propertiesModifiedByRevision.get(revisionB));
assertEquals(Set.of("prop1"), propertiesModifiedByRevision.get(revisionC));
- SortedMap<String, SortedMap<Integer, TreeSet<Revision>>> revisionsModifyingProperty = documentRevisionCleanupHelper.getRevisionsModifyingProperty();
+ SortedMap<String, SortedMap<Integer, TreeSet<Revision>>> revisionsModifyingProperty = documentRevisionCleanupHelper.getRevisionsModifyingPropertyByCluster();
assertEquals(Set.of(revisionA, revisionB, revisionC), revisionsModifyingProperty.get("prop1").get(1));
assertEquals(Set.of(revisionB), revisionsModifyingProperty.get("prop2").get(1));
assertEquals(Set.of(revisionA), revisionsModifyingProperty.get("_deleted").get(1));
@@ -320,7 +380,7 @@ public class DocumentRevisionCleanupHelperTest {
assertEquals(Set.of("_deleted"), propertiesModifiedByRevision.get(revisionB));
assertEquals(Set.of("_deleted"), propertiesModifiedByRevision.get(revisionC));
- SortedMap<String, SortedMap<Integer, TreeSet<Revision>>> revisionsModifyingProperty = documentRevisionCleanupHelper.getRevisionsModifyingProperty();
+ SortedMap<String, SortedMap<Integer, TreeSet<Revision>>> revisionsModifyingProperty = documentRevisionCleanupHelper.getRevisionsModifyingPropertyByCluster();
assertEquals(Set.of(revisionA, revisionB, revisionC), revisionsModifyingProperty.get("_deleted").get(1));
}
@@ -344,7 +404,7 @@ public class DocumentRevisionCleanupHelperTest {
assertEquals(Set.of("prop1", "prop2"), propertiesModifiedByRevision.get(revisionB));
assertEquals(Set.of("prop1"), propertiesModifiedByRevision.get(revisionC));
- SortedMap<String, SortedMap<Integer, TreeSet<Revision>>> revisionsModifyingProperty = documentRevisionCleanupHelper.getRevisionsModifyingProperty();
+ SortedMap<String, SortedMap<Integer, TreeSet<Revision>>> revisionsModifyingProperty = documentRevisionCleanupHelper.getRevisionsModifyingPropertyByCluster();
assertEquals(Set.of(revisionA, revisionB, revisionC), revisionsModifyingProperty.get("prop1").get(1));
assertEquals(Set.of(revisionB), revisionsModifyingProperty.get("prop2").get(1));
}
@@ -372,7 +432,7 @@ public class DocumentRevisionCleanupHelperTest {
assertEquals(Set.of("prop1"), propertiesModifiedByRevision.get(revisionC));
assertNull(propertiesModifiedByRevision.get(revisionD));
- SortedMap<String, SortedMap<Integer, TreeSet<Revision>>> revisionsModifyingProperty = documentRevisionCleanupHelper.getRevisionsModifyingProperty();
+ SortedMap<String, SortedMap<Integer, TreeSet<Revision>>> revisionsModifyingProperty = documentRevisionCleanupHelper.getRevisionsModifyingPropertyByCluster();
assertEquals(Set.of(revisionA, revisionB, revisionC), revisionsModifyingProperty.get("prop1").get(1));
assertEquals(Set.of(revisionB), revisionsModifyingProperty.get("prop2").get(1));
}
@@ -399,7 +459,7 @@ public class DocumentRevisionCleanupHelperTest {
assertEquals(Set.of("prop1"), propertiesModifiedByRevision.get(revisionC));
assertEquals(Set.of("prop1", "prop2"), propertiesModifiedByRevision.get(revisionD));
- SortedMap<String, SortedMap<Integer, TreeSet<Revision>>> revisionsModifyingProperty = documentRevisionCleanupHelper.getRevisionsModifyingProperty();
+ SortedMap<String, SortedMap<Integer, TreeSet<Revision>>> revisionsModifyingProperty = documentRevisionCleanupHelper.getRevisionsModifyingPropertyByCluster();
assertEquals(Set.of(revisionA, revisionD), revisionsModifyingProperty.get("prop1").get(1));
assertEquals(Set.of(revisionB), revisionsModifyingProperty.get("prop1").get(2));
assertEquals(Set.of(revisionC), revisionsModifyingProperty.get("prop1").get(3));
(jackrabbit-oak) 02/03: More tests added (WIP)
Posted by jo...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
joscorbe pushed a commit to branch old-revisions-cleanup
in repository https://gitbox.apache.org/repos/asf/jackrabbit-oak.git
commit 8e299c164244f9ebbc43eec406bfc697d1320e4e
Author: Jose Cordero <co...@adobe.com>
AuthorDate: Fri Nov 3 15:33:01 2023 +0100
More tests added (WIP)
---
.../document/DocumentRevisionCleanupHelper.java | 11 +-
.../DocumentRevisionCleanupHelperTest.java | 164 +++++++++++++++++----
2 files changed, 145 insertions(+), 30 deletions(-)
diff --git a/oak-store-document/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentRevisionCleanupHelper.java b/oak-store-document/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentRevisionCleanupHelper.java
index 1cefd6f8f1..77c96ea465 100644
--- a/oak-store-document/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentRevisionCleanupHelper.java
+++ b/oak-store-document/src/main/java/org/apache/jackrabbit/oak/plugins/document/DocumentRevisionCleanupHelper.java
@@ -93,8 +93,10 @@ public class DocumentRevisionCleanupHelper {
// The first entry in "_deleted" has to be kept, as is when the document was created
NavigableMap<Revision, String> deletedRevisions = ((NavigableMap<Revision, String>) workingDocument.get("_deleted"));
if (deletedRevisions != null && !deletedRevisions.isEmpty()) {
+ // TODO: This is just a check to ensure the order is the expected
+ assert(deletedRevisions.descendingMap().lastKey().getTimestamp() <= deletedRevisions.descendingMap().firstKey().getTimestamp());
+
Revision createdRevision = deletedRevisions.descendingMap().lastKey();
- addCandidateRevisionToClean(createdRevision);
addBlockedRevisionToKeep(createdRevision);
}
@@ -121,10 +123,11 @@ public class DocumentRevisionCleanupHelper {
* @param unit the unit of time
*/
protected void markRevisionsNewerThanThresholdToPreserve(long amount, ChronoUnit unit) {
- long twentyFiveHoursAgoMillis = Instant.now().minus(amount, unit).toEpochMilli();
+ // TODO: Should we add a buffer here? Maybe 1 minute or even 1 hour?
+ long thresholdToPreserve = Instant.now().minus(amount, unit).toEpochMilli();
for (TreeSet<Revision> revisionSet : candidateRevisionsToClean.values()) {
for (Revision revision : revisionSet) {
- if (revision.getTimestamp() > twentyFiveHoursAgoMillis) {
+ if (revision.getTimestamp() > thresholdToPreserve) {
addBlockedRevisionToKeep(revision);
}
}
@@ -178,7 +181,7 @@ public class DocumentRevisionCleanupHelper {
* Step 5:
* Removes for each clusterId the revisions in the Map, that were blocked in the methods above.
*/
- private void removeCandidatesInList(SortedMap<Integer, TreeSet<Revision>> revisions) {
+ protected void removeCandidatesInList(SortedMap<Integer, TreeSet<Revision>> revisions) {
revisions.forEach((key, value) -> {
if (candidateRevisionsToClean.containsKey(key)) {
candidateRevisionsToClean.get(key).removeAll(value);
diff --git a/oak-store-document/src/test/java/org/apache/jackrabbit/oak/plugins/document/DocumentRevisionCleanupHelperTest.java b/oak-store-document/src/test/java/org/apache/jackrabbit/oak/plugins/document/DocumentRevisionCleanupHelperTest.java
index 01daf58358..add00b08f5 100644
--- a/oak-store-document/src/test/java/org/apache/jackrabbit/oak/plugins/document/DocumentRevisionCleanupHelperTest.java
+++ b/oak-store-document/src/test/java/org/apache/jackrabbit/oak/plugins/document/DocumentRevisionCleanupHelperTest.java
@@ -1,25 +1,29 @@
package org.apache.jackrabbit.oak.plugins.document;
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.core.type.TypeReference;
-import com.google.common.collect.Maps;
-import org.mockito.Mockito;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
+import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import java.io.IOException;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.TreeSet;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import static org.apache.jackrabbit.oak.plugins.document.Collection.NODES;
-import static org.apache.jackrabbit.oak.plugins.document.Collection.SETTINGS;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
@@ -77,50 +81,158 @@ public class DocumentRevisionCleanupHelperTest {
documentRevisionCleanupHelper.classifyAndMapRevisionsAndProperties();
documentRevisionCleanupHelper.markCheckpointRevisionsToPreserve();
+ documentRevisionCleanupHelper.removeCandidatesInList(documentRevisionCleanupHelper.getBlockedRevisionsToKeep());
// The revisions blocked should be:
// - r105000000-0-1 (blocked by checkpoint r109000000-0-1)
// - r115000000-0-1 (blocked by checkpoint r119000000-0-1)
-
assertEquals(Set.of(revisionB, revisionD), documentRevisionCleanupHelper.getBlockedRevisionsToKeep().get(1));
+ assertEquals(Set.of(revisionA, revisionC, revisionE, revisionF), documentRevisionCleanupHelper.getCandidateRevisionsToClean().get(1));
+ }
+
+ @Test
+ public void testRecentRevisionsArePreserved() throws IOException {
+ StringBuilder jsonPropBuilder = new StringBuilder("'prop1': {");
+ StringBuilder jsonRevisionsBuilder = new StringBuilder("'_revisions': {");
+
+ Instant currentTime = Instant.now();
+ List<Revision> revisions = new ArrayList<>();
+ for (int i = 29; i >= 0; i--) {
+ long timestamp = currentTime.minus(i, ChronoUnit.HOURS).toEpochMilli();
+ Revision revision = new Revision(timestamp, 0, 1);
+ revisions.add(revision);
+
+ jsonPropBuilder.append("'").append(revision).append("': 'value").append(i).append("', ");
+ jsonRevisionsBuilder.append("'").append(revision).append("': 'c', ");
+ }
+
+ // 23 hours and 59 minutes ago -> Should be preserved
+ long timestamp = currentTime.minus(23, ChronoUnit.HOURS).minus(59, ChronoUnit.MINUTES).toEpochMilli();
+ Revision revision = new Revision(timestamp, 0, 1);
+ revisions.add(revision); // revisions[30]
+ jsonPropBuilder.append("'").append(revision).append("': 'value").append(30).append("', ");
+ jsonRevisionsBuilder.append("'").append(revision).append("': 'c', ");
+
+ // 24 hours and 1 minute ago -> Should be candidate to clean
+ timestamp = currentTime.minus(24, ChronoUnit.HOURS).minus(1, ChronoUnit.MINUTES).toEpochMilli();
+ revision = new Revision(timestamp, 0, 1);
+ revisions.add(revision); // revisions[31]
+ jsonPropBuilder.append("'").append(revision).append("': 'value").append(30).append("'");
+ jsonRevisionsBuilder.append("'").append(revision).append("': 'c'");
+
+ jsonPropBuilder.append("}");
+ jsonRevisionsBuilder.append("}");
+ StringBuilder jsonBuilder = new StringBuilder("{");
+ jsonBuilder.append(jsonPropBuilder).append(", ").append(jsonRevisionsBuilder).append("}");
+
+ // revisions.forEach(rev -> System.out.println(rev.toReadableString()));
+
+ prepareDocumentMock(jsonBuilder.toString());
+
+ documentRevisionCleanupHelper.classifyAndMapRevisionsAndProperties();
+ documentRevisionCleanupHelper.markRevisionsNewerThanThresholdToPreserve(24, ChronoUnit.HOURS);
+ documentRevisionCleanupHelper.removeCandidatesInList(documentRevisionCleanupHelper.getBlockedRevisionsToKeep());
+
+ // The candidate revisions should be the 6 oldest ones (current time -29, -28, -27, -26, -25, -24)
+ // and the one created 24 hours and 1 minute ago
+ Set<Revision> expectedCandidateRevisions = Stream.concat(
+ revisions.subList(0, 6).stream(), Stream.of(revisions.get(31))
+ ).collect(Collectors.toSet());
+ // Blocked revisions are all the 24 revisions created in the last 24 hours, and the one created 23 hours and 59 minutes ago
+ Set<Revision> expectedBlockedRevisions = revisions.subList(6, 31).stream().collect(Collectors.toSet());
- /*assertEquals(Set.of(revisionC), documentRevisionCleanupHelper.getBlockedRevisionsToKeep().get(1));
- assertEquals(Set.of(revisionA, revisionB, revisionC), documentRevisionCleanupHelper.getCandidateRevisionsToClean().get(1));*/
+ assertEquals(expectedCandidateRevisions, documentRevisionCleanupHelper.getCandidateRevisionsToClean().get(1));
+ assertEquals(expectedBlockedRevisions, documentRevisionCleanupHelper.getBlockedRevisionsToKeep().get(1));
}
@Test
public void testInitializeCleanupProcessMultipleClusters() throws IOException {
- Revision revisionA = Revision.fromString("r100000000-0-1");
- Revision revisionB = Revision.fromString("r105000000-0-2");
- Revision revisionC = Revision.fromString("r110000000-0-3");
- Revision revisionD = Revision.fromString("r115000000-0-1");
- Revision revisionE = Revision.fromString("r120000000-0-2");
- Revision revisionF = Revision.fromString("r125000000-0-3");
-
+ List<Revision> revs = new ArrayList<>();
+
+ // Some initial revisions (0-2)
+ revs.add(Revision.fromString("r100000000-0-1"));
+ revs.add(Revision.fromString("r100000000-0-2"));
+ revs.add(Revision.fromString("r100000000-0-3"));
+
+ // Blocked by first checkpoint r109000000 (3-5)
+ revs.add(Revision.fromString("r106000000-0-1"));
+ revs.add(Revision.fromString("r105000000-0-2"));
+ revs.add(Revision.fromString("r108000000-0-3"));
+
+ // Some unblocked revisions in middle (6-10)
+ revs.add(Revision.fromString("r110000000-0-3"));
+ revs.add(Revision.fromString("r110000000-1-3"));
+ revs.add(Revision.fromString("r114000000-0-2"));
+ revs.add(Revision.fromString("r115000000-0-1"));
+ revs.add(Revision.fromString("r118000000-0-1"));
+
+ // Blocked by second checkpoint r118000000 (11-13)
+ revs.add(Revision.fromString("r118000000-1-1"));
+ revs.add(Revision.fromString("r118000000-0-2"));
+ revs.add(Revision.fromString("r118000000-0-3"));
+
+ // Some more unblocked revisions (14-19)
+ revs.add(Revision.fromString("r120000000-0-1"));
+ revs.add(Revision.fromString("r122000000-0-2"));
+ revs.add(Revision.fromString("r122000000-1-2"));
+ revs.add(Revision.fromString("r123000000-0-2"));
+ revs.add(Revision.fromString("r125000000-0-3"));
+ revs.add(Revision.fromString("r130000000-0-1"));
+
+ // Last revision (20-22)
+ revs.add(Revision.fromString("r130000000-1-1"));
+ revs.add(Revision.fromString("r130000000-0-2"));
+ revs.add(Revision.fromString("r130000000-0-3"));
+
+ // Checkpoint revisions
Revision checkpoint1 = Revision.fromString("r109000000-0-1");
Revision checkpoint2 = Revision.fromString("r119000000-0-1");
- String jsonProperties = "{" +
- "'prop1': {'" + revisionA + "': 'value1', '" + revisionB + "': 'value2', '" + revisionC + "': 'value3', '" + revisionD + "': 'value4', '" + revisionE + "': 'value5', '" + revisionF + "': 'value6'}, " +
- "'_revisions': {'" + revisionA + "': 'c', '" + revisionB + "': 'c', '" + revisionC + "': 'c', '" + revisionC + "': 'c', '" + revisionD + "': 'c', '" + revisionE + "': 'c', '" + revisionF + "': 'c'}" +
- "}";
+ StringBuilder jsonPropBuilder = new StringBuilder("'prop1': {");
+ StringBuilder jsonRevisionsBuilder = new StringBuilder("'_revisions': {");
+
+ for (int i = 0; i < revs.size(); i++) {
+ jsonPropBuilder.append("'").append(revs.get(i)).append("': 'value").append(i).append("'");
+ jsonRevisionsBuilder.append("'").append(revs.get(i)).append("': 'c'");
+ if (i < revs.size() - 1) {
+ jsonPropBuilder.append(", ");
+ jsonRevisionsBuilder.append(", ");
+ }
+ }
+
+ jsonPropBuilder.append("}");
+ jsonRevisionsBuilder.append("}");
+ StringBuilder jsonBuilder = new StringBuilder("{");
+ jsonBuilder.append(jsonPropBuilder).append(", ").append(jsonRevisionsBuilder).append("}");
+
String jsonCheckpoints = "{" +
- "'" + checkpoint1 + "': {'expires':'200000000','rv':'r109000000-0-1,r109000000-0-1'}," +
- "'" + checkpoint2 + "': {'expires':'200000000','rv':'r119000000-0-1,r109000000-0-1'}" +
+ "'" + checkpoint1 + "': {'expires':'200000000','rv':'r109000000-0-1,r109000000-0-2,r109000000-0-3'}," +
+ "'" + checkpoint2 + "': {'expires':'200000000','rv':'r118000000-1-1,r118000000-0-2,r118000000-0-3'}" +
"}";
- prepareDocumentMock(jsonProperties);
+ prepareDocumentMock(jsonBuilder.toString());
prepareCheckpointsMock(jsonCheckpoints);
documentRevisionCleanupHelper.initializeCleanupProcess();
// The revisions blocked should be:
- //
-
- assertEquals(Set.of(revisionC), documentRevisionCleanupHelper.getBlockedRevisionsToKeep().get(1));
- assertEquals(Set.of(revisionA, revisionB, revisionC), documentRevisionCleanupHelper.getCandidateRevisionsToClean().get(1));
+ // - r106000000-0-1, r118000000-0-2, r108000000-0-3 (referenced by checkpoint 1)
+ // - r118000000-1-1, r118000000-0-2, r118000000-0-3 (referenced by checkpoint 2)
+ // - r130000000-1-1, r130000000-0-2, r130000000-0-3 (last revision)
+ assertEquals(Set.of(revs.get(3), revs.get(11), revs.get(20)), documentRevisionCleanupHelper.getBlockedRevisionsToKeep().get(1));
+ assertEquals(Set.of(revs.get(4), revs.get(12), revs.get(21)), documentRevisionCleanupHelper.getBlockedRevisionsToKeep().get(2));
+ assertEquals(Set.of(revs.get(5), revs.get(13), revs.get(22)), documentRevisionCleanupHelper.getBlockedRevisionsToKeep().get(3));
+
+ // Rest of revisions are candidates to clean
+ assertEquals(Set.of(revs.get(0), revs.get(9), revs.get(10), revs.get(14), revs.get(19)), documentRevisionCleanupHelper.getCandidateRevisionsToClean().get(1));
+ assertEquals(Set.of(revs.get(1), revs.get(8), revs.get(15), revs.get(16), revs.get(17)), documentRevisionCleanupHelper.getCandidateRevisionsToClean().get(2));
+ assertEquals(Set.of(revs.get(2), revs.get(6), revs.get(7), revs.get(18)), documentRevisionCleanupHelper.getCandidateRevisionsToClean().get(3));
+
+ assertTrue(Collections.disjoint(documentRevisionCleanupHelper.getBlockedRevisionsToKeep().get(1), documentRevisionCleanupHelper.getCandidateRevisionsToClean().get(1)));
+ assertTrue(Collections.disjoint(documentRevisionCleanupHelper.getBlockedRevisionsToKeep().get(2), documentRevisionCleanupHelper.getCandidateRevisionsToClean().get(2)));
+ assertTrue(Collections.disjoint(documentRevisionCleanupHelper.getBlockedRevisionsToKeep().get(3), documentRevisionCleanupHelper.getCandidateRevisionsToClean().get(3)));
}
@Test